namespace Palete { public class LanguageClientVala : LanguageClient { protected bool initialized = false; bool sent_shutdown = false; uint change_queue_id = 0; private bool _closed = false; private bool closed { get { return this._closed ; } set { GLib.debug("closed has been set? to %s" , value ? "TRUE" : "FALSE" ); this._closed = value; } } private GLib.SubprocessLauncher launcher = null; private GLib.Subprocess? subprocess = null; private IOStream? subprocess_stream = null; public Jsonrpc.Client? jsonrpc_client = null; int countdown = 0; Gee.ArrayList open_files; private JsRender.JsRender? _change_queue_file = null; int doc_countdown = 0; private string change_queue_file_source = ""; private JsRender.JsRender? doc_queue_file = null; JsRender.JsRender? change_queue_file { set { this.change_queue_file_source = value == null ? "" : value.toSource(); this._change_queue_file = value; } get { return this._change_queue_file; } } void startServer() { var exe = GLib.Environment.find_program_in_path( "vala-language-server"); if (exe == null) { GLib.warning("could not find vala-language-server"); return; } this.initProcess(exe); } public LanguageClientVala(Project.Project project) { // extend versions will proably call initialize to start and connect to server. base(project); if (this.change_queue_id == 0 ) { this.change_queue_id = GLib.Timeout.add(500, () => { this.run_change_queue(); return true; }); } this.startServer(); } void run_change_queue() { if (this.change_queue_file == null) { return ; } if (this.countdown < -1) { return; } if (this.getting_diagnostics) { return; } this.countdown--; if (this.countdown < 0){ this.document_change_force.begin(this.change_queue_file, this.change_queue_file_source, (o, res) => { this.document_change_force.end(res); }); this.change_queue_file = null; } return ; } async int queuer(int cnt) { SourceFunc cb = this.queuer.callback; GLib.Timeout.add(500, () => { GLib.Idle.add((owned) cb); return false; }); yield; return cnt; } static int doc_queue_id = 0; public bool initProcess(string process_path) { this.onClose(); this.log(LanguageClientAction.LAUNCH, process_path); GLib.debug("Launching %s", process_path); this.launcher = new GLib.SubprocessLauncher (SubprocessFlags.STDIN_PIPE | SubprocessFlags.STDOUT_PIPE); var env = GLib.Environ.get(); env += "G_MESSAGES_DEBUG=all"; this.launcher.set_environ(env); var logpath = GLib.Environment.get_home_dir() + "/.cache/vala-language-server"; if (!GLib.FileUtils.test(logpath, GLib.FileTest.IS_DIR)) { Posix.mkdir(logpath, 0700); } // not very reliable.. //this.launcher.set_stderr_file_path( // logpath + "/" + // (new GLib.DateTime.now_local()).format("%Y-%m-%d") + ".log" //); //GLib.debug("log lang server to %s", logpath + "/" + // (new GLib.DateTime.now_local()).format("%Y-%m-%d") + ".log"); try { this.subprocess = launcher.spawnv ({ process_path , "2>" , "/tmp/vala-language-server.log" }); this.subprocess.wait_async.begin( null, ( obj,res ) => { try { this.subprocess.wait_async.end(res); } catch (GLib.Error e) { this.log(LanguageClientAction.ERROR_START, e.message); GLib.debug("subprocess startup error %s", e.message); } this.log(LanguageClientAction.EXIT, "process ended"); GLib.debug("Subprocess ended %s", process_path); this.onClose(); }); var input_stream = this.subprocess.get_stdout_pipe (); var output_stream = this.subprocess.get_stdin_pipe (); if (input_stream is GLib.UnixInputStream && output_stream is GLib.UnixOutputStream) { // set nonblocking if (!GLib.Unix.set_fd_nonblocking(((GLib.UnixInputStream)input_stream).fd, true) || !GLib.Unix.set_fd_nonblocking (((GLib.UnixOutputStream)output_stream).fd, true)) { GLib.debug("could not set pipes to nonblocking"); this.onClose(); return false; } } this.subprocess_stream = new GLib.SimpleIOStream (input_stream, output_stream); this.accept_io_stream ( this.subprocess_stream); } catch (GLib.Error e) { this.log(LanguageClientAction.ERROR_START, e.message); GLib.debug("subprocess startup error %s", e.message); this.onClose(); return false; } return true; } bool in_close = false; public override void client_accepted (Jsonrpc.Client client) { if (this.jsonrpc_client == null) { this.jsonrpc_client = client; GLib.debug("client accepted connection - calling init server"); this.log(LanguageClientAction.ACCEPT, "client accepted"); this.jsonrpc_client.notification.connect((method, paramz) => { this.onNotification(method, paramz); }); this.jsonrpc_client.failed.connect(() => { this.log(LanguageClientAction.ERROR_RPC, "client failed"); GLib.debug("language server server has failed"); this.onClose(); }); this.initialize_server (); } } public override void initialize_server() { try { Variant? return_value; this.jsonrpc_client.call ( "initialize", this.buildDict ( processId: new Variant.int32 ((int32) Posix.getpid ()), rootPath: new Variant.string (this.project.path), rootUri: new Variant.string (File.new_for_path (this.project.path).get_uri ()), capabilities : this.buildDict ( textDocument: this.buildDict ( documentSymbol : this.buildDict ( hierarchicalDocumentSymbolSupport : new Variant.boolean (true) ) ) ) ), null, out return_value ); GLib.debug ("LS replied with %s", Json.to_string (Json.gvariant_serialize (return_value), true)); this.open_files = new Gee.ArrayList((a,b) => { return a.path == b.path; }); this.initialized = true; this.getting_diagnostics = false; return; } catch (GLib.Error e) { GLib.debug ("LS replied with error %s", e.message); this.onClose(); } } void onClose() { if (this.in_close) { return; } if (this.launcher == null) { return; } this.getting_diagnostics = false; this.in_close = true; GLib.debug("onClose called"); if (this.jsonrpc_client != null) { try { this.jsonrpc_client.close(); } catch (GLib.Error e) { GLib.debug("rpc Error close error %s", e.message); } } if (this.subprocess_stream != null) { try { this.subprocess_stream.close(); } catch (GLib.Error e) { GLib.debug("stream Error close %s", e.message); } } if (this.subprocess != null) { this.subprocess.force_exit(); } if (this.launcher != null) { this.launcher.close(); } this.launcher = null; this.subprocess = null; this.jsonrpc_client = null; this.closed = true; this.in_close = false; } public async void restartServer() { this.startServer(); } public bool isReady() { if (this.closed) { this.log(LanguageClientAction.RESTART,"closed is set - restarting"); GLib.debug("server stopped = restarting"); this.initialized = false; this.closed = false; GLib.MainLoop loop = new GLib.MainLoop (); this.restartServer.begin ((obj, async_res) => { this.restartServer.end(async_res); loop.quit (); }); return false; // can't do an operation yet? } if (!this.initialized) { GLib.debug("Server has not been initialized"); return false; } if (this.sent_shutdown) { GLib.debug("Server has been started its shutting down process"); return false; } // restart server.. return true; } public void onNotification(string method, Variant? return_value) { switch (method) { case "textDocument/publishDiagnostics": //GLib.debug("got notification %s : %s", method , Json.to_string (Json.gvariant_serialize (return_value), true)); GLib.Idle.add(() => { this.onDiagnostic(return_value); return false; }); return; default: break; } GLib.debug("got notification %s : %s", method , Json.to_string (Json.gvariant_serialize (return_value), true)); } bool getting_diagnostics = false; /*** */ public void onDiagnostic(Variant? return_value) { //GLib.debug ("LS replied with %s", Json.to_string (Json.gvariant_serialize (return_value), true)); var dg = Json.gobject_deserialize (typeof (Lsp.Diagnostics), Json.gvariant_serialize (return_value)) as Lsp.Diagnostics; GLib.debug("got diag for %s", dg.filename); this.log(LanguageClientAction.DIAG, dg.filename); if (this.project.path == dg.filename) { this.getting_diagnostics = false; this.log(LanguageClientAction.DIAG_END, "diagnostics done"); return; } this.getting_diagnostics =true; var f = this.project.getByPath(dg.filename); if (f == null) { //GLib.debug("no file %s", dg.uri); //this.project.updateErrorsforFile(null); return; } //GLib.debug("got Diagnostics for %s", f.path); f.updateErrors( dg.diagnostics ); } public override void document_open (JsRender.JsRender file) { if (!this.isReady()) { return; } if (this.open_files.contains(file)) { return; } this.open_files.add(file); GLib.debug ("LS sent open"); try { this.jsonrpc_client.send_notification ( "textDocument/didOpen", this.buildDict ( textDocument : this.buildDict ( uri: new Variant.string (file.to_url()), languageId : new Variant.string (file.language_id()), version : new GLib.Variant.uint64 ( (uint64) file.version), text : new Variant.string (file.toSource()) ) ), null ); this.log(LanguageClientAction.OPEN, file.path); } catch( GLib.Error e) { this.log(LanguageClientAction.ERROR_RPC, e.message); this.onClose(); GLib.debug ("LS sent open err %s", e.message); } } public override async void document_save (JsRender.JsRender file) { if (!this.isReady()) { return; } // save only really flags the file on the server - to actually force a change update - we need to // flag it as changed. yield this.document_change_force(file, file.toSource()); this.change_queue_file = null; GLib.debug ("LS send save"); try { var args = this.buildDict ( textDocument : this.buildDict ( ///TextDocumentItem; uri: new GLib.Variant.string (file.to_url()), version : new GLib.Variant.uint64 ( (uint64) file.version) ) ); //GLib.debug ("textDocument/save send with %s", Json.to_string (Json.gvariant_serialize (args), true)); yield this.jsonrpc_client.send_notification_async ( "textDocument/didSave", args, null ); this.log(LanguageClientAction.SAVE, file.path); } catch( GLib.Error e) { this.log(LanguageClientAction.ERROR_RPC, e.message); GLib.debug ("LS save err %s", e.message); this.onClose(); } } public override void document_close (JsRender.JsRender file) { if (!this.isReady()) { return; } this.change_queue_file = null; if (this.open_files.contains(file)) { this.open_files.remove(file); } this.log(LanguageClientAction.CLOSE, file.path); GLib.debug ("LS send close"); try { this.jsonrpc_client.send_notification ( "textDocument/didChange", this.buildDict ( textDocument : this.buildDict ( ///TextDocumentItem; uri: new GLib.Variant.string (file.to_url()) ) ), null ); } catch( GLib.Error e) { this.log(LanguageClientAction.ERROR_RPC, e.message); GLib.debug ("LS close err %s", e.message); this.onClose(); } } public override void document_change (JsRender.JsRender file ) { if (this.change_queue_file != null && this.change_queue_file.path != file.path) { this.document_change_force.begin(this.change_queue_file, this.change_queue_file_source, (o, res) => { this.document_change_force.end(res); }); } this.countdown = 2; this.change_queue_file = file; } public override async void document_change_force (JsRender.JsRender file, string contents) { if (!this.isReady()) { return; } this.countdown = -2; // not really relivant.. this.change_queue_file = null; // this is more important.. if (!this.open_files.contains(file)) { this.document_open(file); } GLib.debug ("LS send change %s rev %d", file.path, file.version); var ar = new Json.Array(); var obj = new Json.Object(); obj.set_string_member("text", contents); ar.add_object_element(obj); var node = new Json.Node(Json.NodeType.ARRAY); node.set_array(ar); this.log(LanguageClientAction.CHANGE, file.path); try { yield this.jsonrpc_client.send_notification_async ( "textDocument/didChange", this.buildDict ( textDocument : this.buildDict ( ///TextDocumentItem; uri: new GLib.Variant.string (file.to_url()), version : new GLib.Variant.uint64 ( (uint64) file.version) ), contentChanges : Json.gvariant_deserialize (node, null) ), null ); } catch( GLib.Error e) { this.log(LanguageClientAction.ERROR_RPC, e.message); GLib.debug ("LS change err %s", e.message); this.onClose(); } } // called by close window (on last window)... public override void exit () throws GLib.Error { if (!this.isReady()) { return; } this.log(LanguageClientAction.TERM, "SEND exit"); this.jsonrpc_client.send_notification ( "exit", null, null ); this.onClose(); } // not used currently.. public override async void shutdown () throws GLib.Error { if (!this.isReady()) { return; } this.log(LanguageClientAction.TERM, "SEND shutodwn"); this.sent_shutdown = true; Variant? return_value; yield this.jsonrpc_client.call_async ( "shutdown", null, null, out return_value ); GLib.debug ("LS replied with %s", Json.to_string (Json.gvariant_serialize (return_value), true)); } //public async ??/symbol (string symbol) throws GLib.Error { // and now for the important styff.. /* @triggerType 1 = typing or ctl-spac, 2 = tiggercharactres? 3= inside completion? */ public override async Lsp.CompletionList? completion(JsRender.JsRender file, int line, int offset , int triggerType = 1) throws GLib.Error { /* partial_result_token , work_done_token context = null) */ GLib.debug("%s get completion %s @ %d:%d", this.get_type().name(), file.relpath, line, offset); var ret = new Lsp.CompletionList(); if (!this.isReady()) { GLib.debug("completion - language server not ready"); return ret; } // make sure completion has the latest info.. //if (this.change_queue_file != null && this.change_queue_file.path != file.path) { // this.document_change_real(this.change_queue_file, this.change_queue_file_source); // this.change_queue_file != null; //} this.log(LanguageClientAction.COMPLETE, "SEND complete %s @ %d:%d".printf(file.relpath, line, offset) ); Variant? return_value; var args = this.buildDict ( context : this.buildDict ( ///CompletionContext; triggerKind: new GLib.Variant.int32 (triggerType) // triggerCharacter : new GLib.Variant.string ("") ), textDocument : this.buildDict ( ///TextDocumentItem; uri: new GLib.Variant.string (file.to_url()), version : new GLib.Variant.uint64 ( (uint64) file.version) ), position : this.buildDict ( line : new GLib.Variant.uint64 ( (uint) line) , character : new GLib.Variant.uint64 ( uint.max(0, (offset -1))) ) ); GLib.debug ("textDocument/completion send with %s", Json.to_string (Json.gvariant_serialize (args), true)); yield this.jsonrpc_client.call_async ( "textDocument/completion", args, null, out return_value ); //GLib.debug ("LS replied with %s", Json.to_string (Json.gvariant_serialize (return_value), true)); var json = Json.gvariant_serialize (return_value); if (json.get_node_type() == Json.NodeType.OBJECT) { ret = Json.gobject_deserialize (typeof (Lsp.CompletionList), json) as Lsp.CompletionList; this.log(LanguageClientAction.COMPLETE_REPLY, "GOT complete %d items".printf(ret.items.size) ); GLib.debug ("LS replied with Object"); return ret; } if (json.get_node_type() != Json.NodeType.ARRAY) { GLib.debug ("LS replied with %s", Json.to_string (Json.gvariant_serialize (return_value), true)); this.log(LanguageClientAction.ERROR_REPLY, "GOT something else??"); return ret; } var ar = json.get_array(); for(var i = 0; i < ar.get_length(); i++ ) { var add= Json.gobject_deserialize ( typeof (Lsp.CompletionItem), ar.get_element(i)) as Lsp.CompletionItem; ret.items.add( add); } this.log(LanguageClientAction.COMPLETE_REPLY, "GOT array %d items".printf(ret.items.size) ); GLib.debug ("LS replied with Array"); return ret; } static int hover_call_count = 1; bool getting_hover = false; //CompletionListInfo.itmems.parse_varient or CompletionListInfo.parsevarient public override async Lsp.Hover hover (JsRender.JsRender file, int line, int offset) throws GLib.Error { /* partial_result_token , work_done_token context = null) */ //GLib.debug("get hover %s %d %d", file.relpath, (int)line, (int)offset); var ret = new Lsp.Hover(); //ret = null; if (!this.isReady()) { return ret; } if (this.getting_hover) { return ret; } hover_call_count++; var call_id = yield this.queuer(hover_call_count); //GLib.debug("end hover call=%d count=%d", call_id, hover_call_count); if (call_id != hover_call_count) { //GLib.debug("get hover CANCELLED %s %d %d", file.relpath, (int)line, (int)offset); return ret; } //GLib.debug("get hover RUN %s %d %d", file.relpath, (int)line, (int)offset); this.getting_hover = true; Variant? return_value; try { yield this.jsonrpc_client.call_async ( "textDocument/hover", this.buildDict ( textDocument : this.buildDict ( ///TextDocumentItem; uri: new GLib.Variant.string (file.to_url()), version : new GLib.Variant.uint64 ( (uint64) file.version) ), position : this.buildDict ( line : new GLib.Variant.uint64 ( (uint) line) , character : new GLib.Variant.uint64 ( uint.max(0, (offset -1))) ) ), null, out return_value ); } catch(GLib.Error e) { this.getting_hover = false; throw e; } this.getting_hover = false; GLib.debug ("LS hover replied with %s", Json.to_string (Json.gvariant_serialize (return_value), true)); if (return_value == null) { return ret; } var json = Json.gvariant_serialize (return_value); if (json.get_node_type() != Json.NodeType.OBJECT) { return ret; } ret = Json.gobject_deserialize ( typeof (Lsp.Hover), json) as Lsp.Hover; return ret; } static int doc_symbol_queue_call_count = 1; public override void queueDocumentSymbols (JsRender.JsRender file) { this.documentSymbols.begin(file, (o, res) => { var ret = documentSymbols.end(res); file.navigation_tree_updated(ret); }); } bool getting_symbols = false; public override async Gee.ArrayList documentSymbols (JsRender.JsRender file) throws GLib.Error { /* partial_result_token , work_done_token context = null) */ GLib.debug("get documentSymbols %s", file.relpath); var ret = new Gee.ArrayList(); //ret = null; if (!this.isReady()) { return ret; } if (this.getting_symbols) { return ret; } doc_symbol_queue_call_count++; var call_id = yield this.queuer(doc_symbol_queue_call_count); if (call_id != doc_symbol_queue_call_count) { return ret; } this.getting_symbols = true; Variant? return_value; try { yield this.jsonrpc_client.call_async ( "textDocument/documentSymbol", this.buildDict ( textDocument : this.buildDict ( ///TextDocumentItem; uri: new GLib.Variant.string (file.to_url()), version : new GLib.Variant.uint64 ( (uint64) file.version) ) ), null, out return_value ); } catch(Error e) { this.getting_symbols = false; throw e; } this.getting_symbols = false; GLib.debug ("LS replied with %s", Json.to_string (Json.gvariant_serialize (return_value), true)); var json = Json.gvariant_serialize (return_value); var ar = json.get_array(); GLib.debug ("LS replied with %D items", ar.get_length()); for(var i = 0; i < ar.get_length(); i++ ) { var add= Json.gobject_deserialize ( typeof (Lsp.DocumentSymbol), ar.get_element(i)) as Lsp.DocumentSymbol; ret.add( add); } return ret ; } // cant seem to get this to show anything!! public override async Gee.ArrayList signatureHelp (JsRender.JsRender file, int line, int offset) throws GLib.Error { /* partial_result_token , work_done_token context = null) */ GLib.debug("get signatureHelp %s, %d, %d", file.relpath, line, offset); var ret = new Gee.ArrayList(); //ret = null; if (!this.isReady()) { return ret; } Variant? return_value; yield this.jsonrpc_client.call_async ( "textDocument/signatureHelp", this.buildDict ( textDocument : this.buildDict ( ///TextDocumentItem; uri: new GLib.Variant.string (file.to_url()) ), position : this.buildDict ( line : new GLib.Variant.uint64 ( (uint) line) , character : new GLib.Variant.uint64 ( uint.max(0, (offset -1))) ) ), null, out return_value ); GLib.debug ("LS replied with %s", Json.to_string (Json.gvariant_serialize (return_value), true)); var json = Json.gvariant_serialize (return_value); if (json.get_node_type() != Json.NodeType.ARRAY) { return ret; } var ar = json.get_array(); GLib.debug ("LS replied with %D items", ar.get_length()); for(var i = 0; i < ar.get_length(); i++ ) { var add= Json.gobject_deserialize ( typeof (Lsp.SignatureInformation), ar.get_element(i)) as Lsp.SignatureInformation; ret.add( add); } return ret ; } // ok for general symbol search, not much details though. public override async Gee.ArrayList symbol (string sym) throws GLib.Error { /* partial_result_token , work_done_token context = null) */ GLib.debug("get symbol %s,", sym); var ret = new Gee.ArrayList(); //ret = null; if (!this.isReady()) { return ret; } Variant? return_value; yield this.jsonrpc_client.call_async ( "workspace/symbol", this.buildDict ( query : new GLib.Variant.string (sym) ), null, out return_value ); GLib.debug ("LS replied with %s", Json.to_string (Json.gvariant_serialize (return_value), true)); return ret; } } }