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 }