2 generic interface to language server
3 ?? first of will be for vala... but later?
4 based on gvls-client-jsonrpc (loosly)
5 and vala-language-server
12 public enum LanguageClientAction {
35 public abstract class LanguageClient : Jsonrpc.Server {
37 public Project.Project project;
38 private GLib.SubprocessLauncher launcher = null;
39 private GLib.Subprocess? subprocess = null;
40 private IOStream? subprocess_stream = null;
41 public Jsonrpc.Client? jsonrpc_client = null;
43 Gee.ArrayList<JsRender.JsRender> open_files;
44 private JsRender.JsRender? _change_queue_file = null;
45 private string change_queue_file_source = "";
47 JsRender.JsRender? change_queue_file {
49 this.change_queue_file_source = value == null ? "" : value.toSource();
50 this._change_queue_file = value;
53 return this._change_queue_file;
57 uint change_queue_id = 0;
59 protected bool initialized = false;
60 bool sent_shutdown = false;
61 private bool _closed = false;
63 get { return this._closed ; }
65 GLib.debug("closed has been set? to %s" , value ? "TRUE" : "FALSE" );
70 public signal void log(LanguageClientAction action, string message);
73 protected LanguageClient(Project.Project project)
75 // extend versions will proably call initialize to start and connect to server.
76 this.project = project;
77 this.open_files = new Gee.ArrayList<JsRender.JsRender>();
78 this.change_queue_id = GLib.Timeout.add_seconds(1, () => {
79 if (this.change_queue_file == null) {
83 if (this.countdown < 0){
84 this.document_change_real(this.change_queue_file, this.change_queue_file_source);
85 this.change_queue_file = null;
93 public bool initProcess(string process_path)
96 this.log(LanguageClientAction.LAUNCH, process_path);
97 GLib.debug("Launching %s", process_path);
98 this.launcher = new GLib.SubprocessLauncher (SubprocessFlags.STDIN_PIPE | SubprocessFlags.STDOUT_PIPE);
99 this.launcher.set_environ(GLib.Environ.get());
103 this.subprocess = launcher.spawnv ({ process_path });
105 this.subprocess.wait_async.begin( null, ( obj,res ) => {
107 this.subprocess.wait_async.end(res);
108 } catch (GLib.Error e) {
109 this.log(LanguageClientAction.ERROR_START, e.message);
110 GLib.debug("subprocess startup error %s", e.message);
112 this.log(LanguageClientAction.EXIT, "process ended");
113 GLib.debug("Subprocess ended %s", process_path);
117 var input_stream = this.subprocess.get_stdout_pipe ();
118 var output_stream = this.subprocess.get_stdin_pipe ();
120 if (input_stream is GLib.UnixInputStream && output_stream is GLib.UnixOutputStream) {
122 if (!GLib.Unix.set_fd_nonblocking(((GLib.UnixInputStream)input_stream).fd, true)
123 || !GLib.Unix.set_fd_nonblocking (((GLib.UnixOutputStream)output_stream).fd, true))
125 GLib.debug("could not set pipes to nonblocking");
130 this.subprocess_stream = new GLib.SimpleIOStream (input_stream, output_stream);
131 this.accept_io_stream ( this.subprocess_stream);
132 } catch (GLib.Error e) {
133 this.log(LanguageClientAction.ERROR_START, e.message);
134 GLib.debug("subprocess startup error %s", e.message);
140 bool in_close = false;
141 protected void onClose()
146 if (this.launcher == null) {
149 this.in_close = true;
150 GLib.debug("onClose called");
152 if (this.jsonrpc_client != null) {
154 this.jsonrpc_client.close();
155 } catch (GLib.Error e) {
156 GLib.debug("rpc Error close error %s", e.message);
159 if (this.subprocess_stream != null) {
161 this.subprocess_stream.close();
162 } catch (GLib.Error e) {
163 GLib.debug("stream Error close %s", e.message);
166 if (this.subprocess != null) {
167 this.subprocess.force_exit();
169 if (this.launcher != null) {
170 this.launcher.close();
173 this.launcher = null;
174 this.subprocess = null;
175 this.jsonrpc_client = null;
177 this.in_close = false;
180 utility method to build variant based queries
182 public Variant buildDict (...) {
183 var builder = new GLib.VariantBuilder (new GLib.VariantType ("a{sv}"));
186 string? key = l.arg ();
190 Variant val = l.arg ();
191 builder.add ("{sv}", key, val);
193 return builder.end ();
196 public override void client_accepted (Jsonrpc.Client client)
198 if (this.jsonrpc_client == null) {
199 this.jsonrpc_client = client;
201 GLib.debug("client accepted connection - calling init server");
202 this.log(LanguageClientAction.ACCEPT, "client accepted");
204 this.jsonrpc_client.notification.connect((method, paramz) => {
205 this.onNotification(method, paramz);
208 this.jsonrpc_client.failed.connect(() => {
209 this.log(LanguageClientAction.ERROR_RPC, "client failed");
212 GLib.debug("language server server has failed");
215 this.initialize_server ();
220 public bool isReady()
223 this.log(LanguageClientAction.RESTART,"closed is set - restarting");
224 GLib.debug("server stopped = restarting");
225 this.initialized = false;
228 foreach(var f in this.open_files) {
229 this.document_open(f);
231 return false; // can't do an operation yet?
235 if (!this.initialized) {
236 GLib.debug("Server has not been initialized");
239 if (this.sent_shutdown) {
240 GLib.debug("Server has been started its shutting down process");
251 public abstract void initialize_server();
252 public abstract void startServer();
253 //public abstract void initialize_server() ;
258 public void onNotification(string method, Variant? return_value)
261 case "textDocument/publishDiagnostics":
262 this.onDiagnostic(return_value);
268 GLib.debug("got notification %s : %s", method , Json.to_string (Json.gvariant_serialize (return_value), true));
275 public void onDiagnostic(Variant? return_value)
278 var dg = Json.gobject_deserialize (typeof (Lsp.Diagnostics), Json.gvariant_serialize (return_value)) as Lsp.Diagnostics;
279 this.log(LanguageClientAction.DIAG, dg.filename);
280 var f = this.project.getByPath(dg.filename);
282 //GLib.debug("no file %s", dg.uri);
283 this.project.updateErrorsforFile(null);
286 foreach(var v in f.errorsByType.values) {
289 foreach(var diag in dg.diagnostics) {
290 var ce = new CompileError.new_from_diagnostic(f, diag);
291 if (!f.errorsByType.has_key(ce.category)) {
292 f.errorsByType.set(ce.category, new GLib.ListStore(typeof(CompileError)));
294 f.errorsByType.get(ce.category).append(ce);
296 f.project.updateErrorsforFile(f);
300 public void document_open (JsRender.JsRender file)
302 if (!this.isReady()) {
305 if (!this.open_files.contains(file)) {
306 this.open_files.add(file);
309 GLib.debug ("LS sent open");
311 this.jsonrpc_client.send_notification (
312 "textDocument/didOpen",
314 textDocument : this.buildDict (
315 uri: new Variant.string (file.to_url()),
316 languageId : new Variant.string (file.language_id()),
317 version : new GLib.Variant.uint64 ( (uint64) file.version),
318 text : new Variant.string (file.toSource())
323 this.log(LanguageClientAction.OPEN, file.path);
324 } catch( GLib.Error e) {
325 this.log(LanguageClientAction.ERROR_RPC, e.message);
327 GLib.debug ("LS sent open err %s", e.message);
332 public void document_save (JsRender.JsRender file)
334 if (!this.isReady()) {
337 this.change_queue_file = null;
338 GLib.debug ("LS send save");
340 this.jsonrpc_client.send_notification (
341 "textDocument/didChange",
343 textDocument : this.buildDict ( ///TextDocumentItem;
344 uri: new GLib.Variant.string (file.to_url()),
345 version : new GLib.Variant.uint64 ( (uint64) file.version)
350 this.log(LanguageClientAction.SAVE, file.path);
351 } catch( GLib.Error e) {
352 this.log(LanguageClientAction.ERROR_RPC, e.message);
353 GLib.debug ("LS save err %s", e.message);
359 public void document_close (JsRender.JsRender file)
361 if (!this.isReady()) {
364 this.change_queue_file = null;
366 if (this.open_files.contains(file)) {
367 this.open_files.remove(file);
369 this.log(LanguageClientAction.CLOSE, file.path);
370 GLib.debug ("LS send close");
372 this.jsonrpc_client.send_notification (
373 "textDocument/didChange",
375 textDocument : this.buildDict ( ///TextDocumentItem;
376 uri: new GLib.Variant.string (file.to_url())
382 } catch( GLib.Error e) {
383 this.log(LanguageClientAction.ERROR_RPC, e.message);
384 GLib.debug ("LS close err %s", e.message);
392 public void document_change (JsRender.JsRender file )
394 if (this.change_queue_file != null && this.change_queue_file.path != file.path) {
395 this.document_change_real(this.change_queue_file, this.change_queue_file_source);
399 this.change_queue_file = file;
406 public void document_change_real (JsRender.JsRender file, string contents)
408 if (!this.isReady()) {
413 GLib.debug ("LS send change");
414 var ar = new Json.Array();
415 var obj = new Json.Object();
416 obj.set_string_member("text", contents);
417 ar.add_object_element(obj);
418 var node = new Json.Node(Json.NodeType.ARRAY);
420 this.log(LanguageClientAction.CHANGE, file.path);
422 this.jsonrpc_client.send_notification (
423 "textDocument/didChange",
425 textDocument : this.buildDict ( ///TextDocumentItem;
426 uri: new GLib.Variant.string (file.to_url()),
427 version : new GLib.Variant.uint64 ( (uint64) file.version)
429 contentChanges : Json.gvariant_deserialize (node, null)
434 } catch( GLib.Error e) {
435 this.log(LanguageClientAction.ERROR_RPC, e.message);
436 GLib.debug ("LS change err %s", e.message);
442 // called by close window (on last window)...
443 public void exit () throws GLib.Error
445 if (!this.isReady()) {
449 this.log(LanguageClientAction.TERM, "SEND exit");
451 this.jsonrpc_client.send_notification (
459 // not used currently..
460 public async void shutdown () throws GLib.Error
462 if (!this.isReady()) {
465 this.log(LanguageClientAction.TERM, "SEND shutodwn");
466 this.sent_shutdown = true;
467 Variant? return_value;
468 yield this.jsonrpc_client.call_async (
474 GLib.debug ("LS replied with %s", Json.to_string (Json.gvariant_serialize (return_value), true));
476 //public async ??/symbol (string symbol) throws GLib.Error {
478 // and now for the important styff..
482 @triggerType 1 = typing or ctl-spac, 2 = tiggercharactres? 3= inside completion?
484 public async Lsp.CompletionList? completion(JsRender.JsRender file, int line, int offset , int triggerType = 1) throws GLib.Error
486 /* partial_result_token , work_done_token context = null) */
487 GLib.debug("get completion %s @ %d:%d", file.relpath, line, offset);
489 var ret = new Lsp.CompletionList();
491 if (!this.isReady()) {
492 GLib.debug("completion - language server not ready");
495 // make sure completion has the latest info..
496 //if (this.change_queue_file != null && this.change_queue_file.path != file.path) {
497 // this.document_change_real(this.change_queue_file, this.change_queue_file_source);
498 // this.change_queue_file != null;
500 this.log(LanguageClientAction.COMPLETE, "SEND complete %s @ %d:%d".printf(file.relpath, line, offset) );
502 Variant? return_value;
504 var args = this.buildDict (
505 context : this.buildDict ( ///CompletionContext;
506 triggerKind: new GLib.Variant.int32 (triggerType)
507 // triggerCharacter : new GLib.Variant.string ("")
509 textDocument : this.buildDict ( ///TextDocumentItem;
510 uri: new GLib.Variant.string (file.to_url()),
511 version : new GLib.Variant.uint64 ( (uint64) file.version)
513 position : this.buildDict (
514 line : new GLib.Variant.uint64 ( (uint) line) ,
515 character : new GLib.Variant.uint64 ( uint.max(0, (offset -1)))
519 GLib.debug ("textDocument/completion send with %s", Json.to_string (Json.gvariant_serialize (args), true));
521 yield this.jsonrpc_client.call_async (
522 "textDocument/completion",
529 //GLib.debug ("LS replied with %s", Json.to_string (Json.gvariant_serialize (return_value), true));
530 var json = Json.gvariant_serialize (return_value);
533 if (json.get_node_type() == Json.NodeType.OBJECT) {
534 ret = Json.gobject_deserialize (typeof (Lsp.CompletionList), json) as Lsp.CompletionList;
535 this.log(LanguageClientAction.COMPLETE_REPLY, "GOT complete %d items".printf(ret.items.size) );
536 GLib.debug ("LS replied with Object");
540 if (json.get_node_type() != Json.NodeType.ARRAY) {
541 GLib.debug ("LS replied with %s", Json.to_string (Json.gvariant_serialize (return_value), true));
542 this.log(LanguageClientAction.ERROR_REPLY, "GOT something else??");
546 var ar = json.get_array();
548 for(var i = 0; i < ar.get_length(); i++ ) {
549 var add= Json.gobject_deserialize ( typeof (Lsp.CompletionItem), ar.get_element(i)) as Lsp.CompletionItem;
553 this.log(LanguageClientAction.COMPLETE_REPLY, "GOT array %d items".printf(ret.items.size) );
554 GLib.debug ("LS replied with Array");
559 //CompletionListInfo.itmems.parse_varient or CompletionListInfo.parsevarient
560 public async Gee.ArrayList<Lsp.DocumentSymbol> syntax (JsRender.JsRender file) throws GLib.Error
562 /* partial_result_token , work_done_token context = null) */
563 GLib.debug("get syntax %s", file.relpath);
564 var ret = new Gee.ArrayList<Lsp.DocumentSymbol>();
566 if (!this.isReady()) {
569 Variant? return_value;
570 yield this.jsonrpc_client.call_async (
571 "textDocument/documentSymbol",
574 textDocument : this.buildDict ( ///TextDocumentItem;
575 uri: new GLib.Variant.string (file.to_url()),
576 version : new GLib.Variant.uint64 ( (uint64) file.version)
585 GLib.debug ("LS replied with %s", Json.to_string (Json.gvariant_serialize (return_value), true));
586 var json = Json.gvariant_serialize (return_value);
590 var ar = json.get_array();
591 for(var i = 0; i < ar.get_length(); i++ ) {
592 var add= Json.gobject_deserialize ( typeof (Lsp.DocumentSymbol), ar.get_element(i)) as Lsp.DocumentSymbol;