X-Git-Url: http://git.roojs.org/?a=blobdiff_plain;f=src%2FPalete%2FLanguageClientVala.vala;h=f49bbd8b28fbd4803741f348844417eb5f176b5a;hb=HEAD;hp=5a9327f06416096d1fdd64417481118c140183f7;hpb=e83f8479386654b6d22c7b4e4d7e8b0484df3132;p=roobuilder diff --git a/src/Palete/LanguageClientVala.vala b/src/Palete/LanguageClientVala.vala index 5a9327f06..f49bbd8b2 100644 --- a/src/Palete/LanguageClientVala.vala +++ b/src/Palete/LanguageClientVala.vala @@ -1,18 +1,205 @@ 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.initProcess("/usr/bin/vala-language-server"); + 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() { + + + public override void initialize_server() { try { Variant? return_value; this.jsonrpc_client.call ( @@ -20,19 +207,670 @@ namespace Palete { 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 ()) + 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 override 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()) { + GLib.debug("docsymbols not ready"); + return ret; + } + if (this.getting_symbols) { + GLib.debug("docsymbols currently 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) { + GLib.debug("docsymbols call id does not match %d %d" ,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; + } + } + + + + + + + } \ No newline at end of file