1 module nvimhost.plugin; 2 3 enum Exec { 4 // Synchronous execution 5 Sync, 6 // Asynchronous execution means that Nvim won't wait for a response. 7 Async, 8 // Asynchronous Thread, is same as Async except it spawns a new Thread for each call. 9 AsyncThread 10 }; 11 12 alias Sync = Exec.Sync; 13 alias Async = Exec.Async; 14 alias AsyncThread = Exec.AsyncThread; 15 16 /** 17 NvimFunc is synchronous by default. 18 */ 19 struct NvimFunc { 20 string name; 21 Exec exec = Sync; 22 } 23 24 /** 25 Host a D plugin, by template instanciating a Struct on Nvim. The main thread (NvimClient) 26 provides a socket to call Nvim API functions, and there's a second thread, which is 27 exclusively used for exchanging call messages between the Plugin functions, commands, and Nvim. 28 */ 29 struct NvimPlugin(Struct) { 30 import nvimhost.client; 31 import nvimhost.api; 32 import std.traits; 33 import std.meta : ApplyLeft, Filter; 34 import std.variant; 35 import std.typecons; 36 import std.socket; 37 import std.experimental.logger; 38 import core.thread; 39 import std.concurrency : thisTid, Tid, send, receiveOnly; 40 import std.conv; 41 import std.uni : toLower; 42 import vibe.core.net; 43 import eventcore.driver : IOMode; 44 import nvimhost.util : genSrcAddr; 45 import std.path; 46 import std.file; 47 48 public: 49 // Encapsulate Nvim API to facilitate for final consumers of this Class. 50 auto nvim = NvimAPI(); 51 // Template consumer class. 52 Struct stc; 53 54 private: 55 56 // The client instance will run in the main thread. 57 NvimClient c = void; 58 59 // Nvim callback socket to handle rpcnotify and rpcrequest. 60 TCPConnection conn; 61 NetworkAddress netAddr; 62 string srcAddr; 63 immutable bufferSize = 4096; 64 static ubyte[bufferSize] cbBuffer; 65 66 // Neovim cb channel ID 67 ulong chId; 68 69 // Main thread id 70 Tid mTid; 71 // Current supported return types 72 enum string[string] retTypes = [ 73 "string" : "string", "bool" : "bool", "int" : "int", "double" : "double", "string[]" 74 : "string[]", "bool[]" : "bool[]", "int[]" : "int[]", "double[]" 75 : "double[]", "void" : "int", "immutable(char)[]" 76 : "immutable(char)[]", "immutable(char)[][]" : "immutable(char)[][]" 77 ]; 78 79 static string retTypesToString() { 80 string res; 81 foreach (k, v; retTypes) { 82 res ~= v ~ " "; 83 } 84 return res; 85 } 86 87 /** 88 Thread responsible for receiving nvim requests and notifications. 89 Both requests and notifications are handled immediately. This thread is exclusive 90 for this client, messages might queue up in the buffer size for now. 91 Messages can come out of order. 92 */ 93 void cbThread() { 94 import msgpack; 95 import std.uuid; 96 97 srcAddr = genSrcAddr(); 98 auto netSrcAddr = NetworkAddress(new UnixAddress(srcAddr)); 99 100 auto unixAddr = new UnixAddress(c.nvimAddr); 101 assert(c.nvimAddr != c.nvimAddr.init); 102 netAddr = NetworkAddress(unixAddr); 103 104 trace(c.logEnabled, "cbThread opening TCP connection"); 105 conn = connectTCP(netAddr, netSrcAddr); 106 conn.keepAlive(true); 107 trace(c.logEnabled, "cbThread connected"); 108 109 requestChannelId(); 110 tracef(c.logEnabled, "cbThread ready, chid %d", chId); 111 mTid.send("connected"); 112 113 ubyte[] data; 114 while (true) { 115 // async conn 116 data = rcvdCbSocket(); 117 if (data) { 118 tracef(c.logEnabled, "%( %x%)", data); 119 auto msg = c.inspectMsgKind(data); 120 final switch (msg.kind) { 121 case MsgKind.request: 122 tracef(c.logEnabled,"received request id %s, method %s, args %s", 123 msg.id, msg.method, msg.args); 124 try { 125 tracef(c.logEnabled, "msg args: %s", msg.args); 126 auto decoded = c.decodeMethod(msg.method); 127 if (decoded.type == MethodType.NvimFunc) { 128 auto ret = dispatchMethod(decoded.name, msg.args); 129 tracef(c.logEnabled, "ret: %s", ret); 130 outer: 131 switch (ret.type.toString) { 132 static foreach (k, v; retTypes) { 133 case k: 134 mixin(v ~ " t;"); 135 auto packed = pack(MsgKind.response, 136 msg.id, null, ret.get!(typeof(t))); 137 tracef(c.logEnabled, "replying with %(%x %)", packed); 138 conn.write(packed); 139 break outer; 140 } 141 default: 142 throw new Exception("This return type " ~ ret.type.toString 143 ~ " is not supported. Plese, use any of these for now: " 144 ~ retTypesToString); 145 } 146 } 147 } catch (Exception e) { 148 errorf(e.msg); 149 auto packed = pack(MsgKind.response, msg.id, e.msg, null); 150 conn.write(packed); 151 } 152 break; 153 case MsgKind.response: 154 tracef(c.logEnabled, 155 "received response id %s, method %s, args %s", 156 msg.id, msg.method, msg.args); 157 break; 158 case MsgKind.notify: 159 tracef(c.logEnabled, 160 "received notify, method %s, args %s", msg.method, msg.args); 161 auto decoded = c.decodeMethod(msg.method); 162 try { 163 if (decoded.type == MethodType.NvimFunc) { 164 auto ret = dispatchMethod(decoded.name, msg.args); 165 } 166 } catch (Exception e) { 167 errorf(e.msg); 168 c.call!(void)("nvim_err_writeln", e.msg); 169 } 170 // TODO implement option to spawn a new thread. 171 break; 172 } 173 } 174 } 175 } 176 177 /** 178 Runtime dispatch method from an RPC request/notify. 179 */ 180 Variant dispatchMethod(string methodName, Variant[] args) { 181 import std.traits : ReturnType, Parameters; 182 tracef(c.logEnabled, "dispatchMethod methodName: %s args: %s", methodName, args); 183 switch (methodName) { 184 default: 185 throw new Exception("Unknown function: " ~ methodName); 186 static foreach (name; AllPluginFuncs!Struct) { 187 static foreach (attr; __traits(getAttributes, __traits(getMember, Struct, name))) { 188 static if (is(typeof(attr) == NvimFunc)) { 189 case attr.name: 190 { 191 try { 192 Parameters!(mixin("Struct." ~ name)) tup; 193 if (tup.length != args.length) { 194 enum errMsg = "Wrong number of function params. Expected: " ~ Parameters!(__traits(getMember, Struct, name)).stringof; 195 throw new Exception(errMsg); 196 } 197 static foreach (i; 0 .. tup.length) { 198 tup[i] = args[i].get!(typeof(tup[i])); 199 } 200 static if (ReturnType!(mixin("Struct." ~ name)).stringof !in retTypes) { 201 import std.array; 202 static assert(false, "This return type " ~ ReturnType!(mixin("Struct." ~ name)).stringof ~ " is not supported yet, please use any of these instead for now: " ~ NvimPlugin.retTypesToString()); 203 } 204 static if (is(ReturnType!(mixin("Struct." ~ name)) == void)) { 205 static if (attr.exec == Sync) { 206 static assert(false, "Sync NvimFunc can't return void."); 207 } 208 mixin("stc." ~ name ~ "(tup); return Variant(0);"); 209 } else { 210 mixin("return Variant(stc." ~ name ~ "(tup));"); 211 } 212 } catch(VariantException e) { 213 enum errMsg = "Wrong function argument types. Expected: " ~ Parameters!(__traits(getMember, Struct, name)).stringof; 214 errorf(errMsg ~ "." ~ e.msg ~ "." ~ "Variant args: %s", args); 215 throw new Exception(errMsg); 216 } 217 } 218 } 219 } 220 } 221 } 222 } 223 224 enum isPluginMethod(T, string name) = __traits(compiles, { 225 static foreach (attr; __traits(getAttributes, __traits(getMember, T, name))) { 226 static if (is(typeof(attr) == NvimFunc)) { 227 enum found = true; 228 } 229 } 230 static if (found) { 231 auto x = found; 232 } 233 }); 234 235 alias AllPluginFuncs(T) = Filter!(ApplyLeft!(isPluginMethod, T), __traits(allMembers, T)); 236 237 /** 238 Async connection read 239 */ 240 ubyte[] rcvdCbSocket() { 241 size_t nBytes = bufferSize; 242 ubyte[] data; 243 do { 244 nBytes = conn.read(cbBuffer, IOMode.once); 245 tracef("Received nBytes %d", nBytes); 246 data ~= cbBuffer[0 .. nBytes]; 247 } 248 while (nBytes >= bufferSize); 249 return data; 250 } 251 252 /** 253 Identify the callback channel ID of this client. 254 */ 255 ulong requestChannelId() { 256 import msgpack; 257 import std.string : format; 258 259 if (!chId) { 260 auto myT = tuple(); 261 auto s = Msg!(myT.Types)(MsgKind.request, 0, "nvim_get_api_info", myT); 262 auto packed = pack(s); 263 264 ubyte[] data; 265 conn.write(packed, IOMode.once); 266 data = rcvdCbSocket(); 267 268 auto unpacker = StreamingUnpacker(cast(ubyte[]) null); 269 unpacker.feed(data); 270 unpacker.execute(); 271 foreach (unpacked; unpacker.purge()) { 272 if (unpacked.type == Value.Type.array) { 273 foreach (item; unpacked.via.array) { 274 // first element in this array has to be the chid 275 if (item.type == Value.Type.unsigned) { 276 chId = item.via.uinteger; 277 return chId; 278 } 279 } 280 } 281 } 282 throw new Exception("nvim_get_api has changed."); 283 } 284 return chId; 285 } 286 287 void ensureDirCreated(string filePath) { 288 import std.range; 289 290 auto folderPath = filePath.expandTilde.split(dirSeparator)[0 .. $ - 1]; 291 mkdirRecurse(folderPath.join(dirSeparator)); 292 } 293 294 public: 295 296 this(string pluginBinPath, string outManifestFilePath) { 297 c = nvim.getClient(); 298 c.enableLog(); 299 300 ensureDirCreated(outManifestFilePath); 301 genManifest!Struct(pluginBinPath, expandTilde(outManifestFilePath)); 302 303 try { 304 c.connect(); 305 stc = Struct(nvim); 306 307 mTid = thisTid; 308 auto tcb = new Thread(&cbThread); 309 tcb.isDaemon(true); 310 tcb.start(); 311 312 trace(c.logEnabled, "Waiting for cbThread message"); 313 receiveOnly!string(); 314 trace(c.logEnabled, "Received cbThread message"); 315 316 immutable string loadedName = Struct.stringof.toLower; 317 logf(c.logEnabled, "Setting g:" ~ loadedName ~ "_channel" ~ "=" ~ to!string(chId)); 318 c.call!(void)("nvim_command", "let g:" ~ loadedName ~ "_channel" ~ "=" ~ to!string(chId)); 319 trace(c.logEnabled, "Plugin " ~ loadedName ~ " is ready!"); 320 } catch(NvimListenAddressException e) { 321 import std.stdio : writeln; 322 import core.stdc.stdlib; 323 324 writeln(e.msg); 325 error(c.logEnabled, e.msg); 326 writeln("The manifest file has been written to " ~ expandTilde(outManifestFilePath)); 327 exit(1); 328 } 329 } 330 331 ~this() { 332 close(); 333 } 334 335 /** 336 Release resources. 337 */ 338 void close() { 339 import std.file : exists, remove; 340 if (netAddr.family == AddressFamily.UNIX) { 341 conn.close(); 342 } 343 if (srcAddr.length && exists(srcAddr)) { 344 remove(srcAddr); 345 } 346 } 347 348 /** 349 Used for keeping the main and callback daemonized thread open. 350 You should only call keepRunning if you don't have an inifinite loop in your void main(), for example: 351 352 void main(){ 353 auto p = new NvimPlugin!(PluginStruct)(); 354 scope(exit) p.keepRunning(); 355 } 356 */ 357 void keepRunning() { 358 while (true) { 359 Thread.sleep(60.seconds); 360 } 361 } 362 363 /** 364 Generate plugin manifest, the plugin hosted name is the class name lowered case. 365 The binary path of the plugin application should be in your path. autoCmdPattern 366 is used for registring an auto command for this plugin, by default, it doesn't 367 restrict any pattern at all. As soon as the plugin is registered, all the functions 368 and commands will be lazy loaded, and the binary will be executed as soon as your 369 make a first call from Nvim. 370 */ 371 void genManifest(Struct)(string binExecutable, string outputFile, string autoCmdPattern = "*") { 372 import std.stdio : writeln, File; 373 import std.string : format; 374 375 immutable string loadedName = Struct.stringof.toLower; 376 // 1 -> loadedName 377 // 2 -> binExecutablePath 378 // 3 -> autoCmdPattern 379 auto manifestExpr = ` 380 " This file has been autogenerated by '%2$s' 381 " Don't edit this file manually 382 383 if exists('g:loaded_%1$s') 384 finish 385 endif 386 let g:loaded_%1$s = 1 387 388 function! F%1$s(host) 389 if executable('%2$s') != 1 390 echoerr "Executable '%2$s' not found in PATH." 391 endif 392 393 let g:job_%1$s = jobstart(['%2$s']) 394 395 " make sure the plugin host is ready and double check rpc channel id 396 let g:%1$s_channel = 0 397 for count in range(0, 100) 398 if g:%1$s_channel != 0 399 break 400 endif 401 sleep 1m 402 endfor 403 if g:%1$s_channel == 0 404 echoerr "Failed to initialize %1$s" 405 endif 406 407 return g:%1$s_channel 408 endfunction 409 410 call remote#host#Register('%1$s', '%3$s', function('F%1$s'))`; 411 412 auto registerHead = ` 413 call remote#host#RegisterPlugin('%1$s', '%1$sPlugin', [`; 414 auto registerFunc = ` 415 \ {'type': 'function', 'name': '%1$s', 'sync': %2$d, 'opts': {}},`; 416 immutable string registerTail = ` 417 \ ])`; 418 string[] lines; 419 420 lines ~= manifestExpr.format(loadedName, binExecutable, autoCmdPattern); 421 lines ~= registerHead.format(loadedName); 422 423 static foreach (name; __traits(allMembers, Struct)) { 424 static foreach (attr; __traits(getAttributes, __traits(getMember, Struct, name))) { 425 static if (is(typeof(attr) == NvimFunc)) { 426 static if (attr.exec == Async) { 427 lines ~= registerFunc.format(attr.name, 0); 428 } else static if (attr.exec == Sync) { 429 lines ~= registerFunc.format(attr.name, 1); 430 } else static if (attr.exec == AsyncThread) { 431 static assert("AsyncThread is not supported yet."); 432 } 433 } 434 } 435 } 436 437 if (lines.length < 3) { 438 throw new Exception("At least one decorated @NvimFunc is required."); 439 } 440 441 lines ~= registerTail; 442 443 auto f = File(outputFile.expandTilde, "w"); 444 foreach (item; lines) { 445 f.write(item); 446 } 447 } 448 }