Fix #8049 - language server hover and editor hover (not supported for gtkview / rooview)
[roobuilder] / src / Palete / LanguageClientVala.vala
1
2 namespace Palete {
3         public class LanguageClientVala : LanguageClient {
4                 int countdown = 0;
5                 protected bool initialized = false;
6                 bool sent_shutdown = false;
7                 uint change_queue_id = 0;
8                 
9                         
10                 private bool _closed = false;
11                 private bool closed {
12                         get { return this._closed ; } 
13                         set {
14                                 GLib.debug("closed has been set? to %s" , value ? "TRUE" : "FALSE" );
15                                 this._closed = value;
16                         }
17                 }
18                 private GLib.SubprocessLauncher launcher = null;
19                 private GLib.Subprocess? subprocess = null;
20                 private IOStream? subprocess_stream = null;
21             public Jsonrpc.Client? jsonrpc_client = null;
22                 
23                 Gee.ArrayList<JsRender.JsRender> open_files;
24                 private JsRender.JsRender? _change_queue_file = null;
25                 private string change_queue_file_source = "";
26                 
27                 JsRender.JsRender? change_queue_file {
28                         set {
29                                 this.change_queue_file_source = value == null ? "" : value.toSource();
30                                 this._change_queue_file = value;
31                         } 
32                         get {
33                                 return this._change_queue_file;
34                         } 
35                 }
36                 void startServer()
37                 {
38                         var exe = GLib.Environment.find_program_in_path( "vala-language-server");
39                         if (exe == null) {
40                                 GLib.warning("could not find vala-language-server");
41                                  
42                                 return;
43                         }
44                         this.initProcess(exe);
45                 }
46                 
47                 
48                 public LanguageClientVala(Project.Project project)
49                 {
50                         // extend versions will proably call initialize to start and connect to server.
51                         base(project);
52
53                         this.change_queue_id = GLib.Timeout.add_seconds(1, () => {
54                                 if (this.change_queue_file == null) {
55                                         return true;
56                                 }
57                                 if (this.getting_diagnostics) {
58                                         return true;
59                                 }
60                                 this.countdown--;
61
62                         
63                                 if (this.countdown < 0){
64                                         this.document_change_force.begin(this.change_queue_file,  this.change_queue_file_source, (o, res) => {
65                                                 this.document_change_force.end(res);
66                                         });
67                                         this.change_queue_file = null;
68                                            
69                                 }
70                                 return true;
71                         });
72                         this.startServer();
73
74                 }
75                 
76                  
77                 public bool initProcess(string process_path)
78                 {
79                         this.onClose();
80                         this.log(LanguageClientAction.LAUNCH, process_path);
81                         GLib.debug("Launching %s", process_path);
82                         this.launcher = new GLib.SubprocessLauncher (SubprocessFlags.STDIN_PIPE | SubprocessFlags.STDOUT_PIPE);
83                         var env = GLib.Environ.get();
84                         env += "G_MESSAGES_DEBUG=all";
85
86                         this.launcher.set_environ(env);
87                         var logpath = GLib.Environment.get_home_dir() + "/.cache/vala-language-server";
88                         
89                         if (!GLib.FileUtils.test(logpath, GLib.FileTest.IS_DIR)) {
90                                 Posix.mkdir(logpath, 0700);
91                         }
92                         // not very reliable..
93                         //this.launcher.set_stderr_file_path( 
94                         //      logpath + "/" + 
95                         //      (new GLib.DateTime.now_local()).format("%Y-%m-%d") + ".log"
96                         //);
97                         //GLib.debug("log lang server to %s", logpath + "/" + 
98                         //      (new GLib.DateTime.now_local()).format("%Y-%m-%d") + ".log");
99
100                         try {
101
102                                 
103                                 this.subprocess = launcher.spawnv ({ process_path , "2>" , "/tmp/vala-language-server.log" });
104                                 
105                                 this.subprocess.wait_async.begin( null, ( obj,res ) => {
106                                         try {
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);           
111                                         }
112                                         this.log(LanguageClientAction.EXIT, "process ended");
113                                         GLib.debug("Subprocess ended %s", process_path);
114                                         this.onClose();
115
116                                 });
117                                 var input_stream = this.subprocess.get_stdout_pipe ();
118                                 var output_stream = this.subprocess.get_stdin_pipe ();
119  
120                                 if (input_stream is GLib.UnixInputStream && output_stream is GLib.UnixOutputStream) {
121                                         // set nonblocking
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)) 
124                                          {
125                                                 GLib.debug("could not set pipes to nonblocking");
126                                                 this.onClose();
127                                             return false;
128                                     }
129                             }
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);   
135                                 this.onClose();
136                                 return false;
137                 }
138             return true;
139         }
140         bool in_close = false;
141                 public override void client_accepted (Jsonrpc.Client client) 
142                 {
143                         if (this.jsonrpc_client == null) {
144                                 this.jsonrpc_client = client;
145                                 
146                                 GLib.debug("client accepted connection - calling init server");
147                                 this.log(LanguageClientAction.ACCEPT, "client accepted");
148
149                                 this.jsonrpc_client.notification.connect((method, paramz) => {
150                                         this.onNotification(method, paramz);
151                                 });
152                                  
153                                 this.jsonrpc_client.failed.connect(() => {
154                                         this.log(LanguageClientAction.ERROR_RPC, "client failed");
155                                         GLib.debug("language server server has failed");                                        
156                                         this.onClose();
157                                         
158
159                                 });
160
161                                 this.initialize_server ();
162                         } 
163                                          
164                          
165                 }
166                 
167                 
168                 public override   void  initialize_server()   {
169                         try {
170                                 Variant? return_value;
171                                     this.jsonrpc_client.call (
172                                     "initialize",
173                                     this.buildDict (
174                                         processId: new Variant.int32 ((int32) Posix.getpid ()),
175                                         rootPath: new Variant.string (this.project.path),
176                                         rootUri: new Variant.string (File.new_for_path (this.project.path).get_uri ())
177                                     ),
178                                     null,
179                                     out return_value
180                                 );
181                                 GLib.debug ("LS replied with %s", Json.to_string (Json.gvariant_serialize (return_value), true));
182                                 this.open_files = new Gee.ArrayList<JsRender.JsRender>((a,b) => {
183                                         return a.path == b.path;
184                                 });
185                                 this.initialized = true;
186                                 this.getting_diagnostics = false;
187                                 return;
188                         } catch (GLib.Error e) {
189                                 GLib.debug ("LS replied with error %s", e.message);
190                                 this.onClose();
191                         }
192                         
193                 }
194                 void onClose()
195                 {
196                         if (this.in_close) {
197                                 return;
198                         }
199                         if (this.launcher == null) {
200                                 return;
201                         }
202                         this.getting_diagnostics = false;
203                         this.in_close = true;
204                         GLib.debug("onClose called");
205                         
206                         if (this.jsonrpc_client != null) {
207                                 try {
208                                         this.jsonrpc_client.close();
209                                 } catch (GLib.Error e) {
210                                         GLib.debug("rpc Error close error %s", e.message);      
211                                 }               
212                         }
213                         if (this.subprocess_stream != null) {
214                                 try {
215                                         this.subprocess_stream.close();
216                                 } catch (GLib.Error e) {
217                                         GLib.debug("stream Error close  %s", e.message);        
218                                 }               
219                         }
220                         if (this.subprocess != null) {
221                                 this.subprocess.force_exit();
222                         }
223                         if (this.launcher != null) {
224                                 this.launcher.close();
225                         }
226                         
227                         this.launcher = null;
228                         this.subprocess = null;
229                         this.jsonrpc_client = null;
230                         this.closed = true;             
231                         this.in_close = false;
232                 }
233         
234                 public async void restartServer()
235                 {
236                         this.startServer();
237                          
238                 }
239         
240                 public bool isReady()
241                 {
242                         if (this.closed) {
243                                 this.log(LanguageClientAction.RESTART,"closed is set - restarting");
244                                 GLib.debug("server stopped = restarting");
245                                 this.initialized = false;
246                                 this.closed = false;
247                                 GLib.MainLoop loop = new GLib.MainLoop ();
248                                 this.restartServer.begin ((obj, async_res) => {
249                                         this.restartServer.end(async_res);
250                                         loop.quit ();
251                                 });
252                                 return false; // can't do an operation yet?
253                                  
254                         }
255                         
256                         if (!this.initialized) {
257                                 GLib.debug("Server has not been initialized");
258                                 return false;
259                         }
260                         if (this.sent_shutdown) {
261                                 GLib.debug("Server has been started its shutting down process");
262                                 return false;
263                         }
264                         // restart server..
265                 
266                         
267                         
268                         return true;
269                 }
270         
271                 public void onNotification(string method, Variant? return_value)
272                 {
273                         switch (method) {
274                                 case "textDocument/publishDiagnostics":
275                                         //GLib.debug("got notification %s : %s",  method , Json.to_string (Json.gvariant_serialize (return_value), true));
276                                          
277                                         GLib.Idle.add(() => {
278                                                 this.onDiagnostic(return_value);
279                                                 return false;
280                                         });
281                                         return;
282                                 default: 
283                                         break;
284                                  
285                         }
286                         GLib.debug("got notification %s : %s",  method , Json.to_string (Json.gvariant_serialize (return_value), true));
287                         
288                 }
289                 
290                 bool getting_diagnostics = false;
291                 /***
292                 
293                 */
294                 public void onDiagnostic(Variant? return_value) 
295                 {
296                         //GLib.debug ("LS replied with %s", Json.to_string (Json.gvariant_serialize (return_value), true));                                     
297                         var dg = Json.gobject_deserialize (typeof (Lsp.Diagnostics), Json.gvariant_serialize (return_value)) as Lsp.Diagnostics; 
298                         GLib.debug("got diag for %s", dg.filename);
299                         this.log(LanguageClientAction.DIAG, dg.filename);
300                         if (this.project.path == dg.filename) {
301                                 this.getting_diagnostics = false;
302                                 this.log(LanguageClientAction.DIAG_END, "diagnostics done");
303                                 return;
304                         
305                         }
306                         this.getting_diagnostics =true;
307                         var f = this.project.getByPath(dg.filename);
308                         if (f == null) {
309                                 //GLib.debug("no file %s", dg.uri);
310                                 //this.project.updateErrorsforFile(null);
311                                 return;
312                         }
313                         //GLib.debug("got Diagnostics for %s", f.path);
314                         f.updateErrors( dg.diagnostics );
315                          
316                         
317                 }
318                 
319                 public override void document_open (JsRender.JsRender file)  
320                 {
321                         if (!this.isReady()) {
322                                 return;
323                         }
324                         if (this.open_files.contains(file)) {
325                                 return;
326                         }
327                         this.open_files.add(file);
328                         
329                         
330                         GLib.debug ("LS sent open");                     
331                         try {
332                                 this.jsonrpc_client.send_notification (
333                                         "textDocument/didOpen",
334                                         this.buildDict (
335                                                 textDocument : this.buildDict (
336                                                         uri: new Variant.string (file.to_url()),
337                                                         languageId :  new Variant.string (file.language_id()),
338                                                         version :  new GLib.Variant.uint64 ( (uint64) file.version),
339                                                         text : new Variant.string (file.toSource())
340                                                 )
341                                         ),
342                                         null
343                                 );
344                                 this.log(LanguageClientAction.OPEN, file.path);
345                         } catch( GLib.Error  e) {
346                                 this.log(LanguageClientAction.ERROR_RPC, e.message);
347                                 this.onClose();
348                                 GLib.debug ("LS sent open err %s", e.message);
349                         }
350
351                 }
352                 
353                 public override  async void document_save (JsRender.JsRender file)  
354         {
355                         if (!this.isReady()) {
356                                 return;
357                         }
358                         // save only really flags the file on the server - to actually force a change update - we need to 
359                         // flag it as changed.
360                         yield this.document_change_force(file, file.toSource());
361                         
362                         this.change_queue_file = null;
363                         GLib.debug ("LS send save");
364                          try {
365                          
366                                 var args = this.buildDict (  
367                                         textDocument : this.buildDict (    ///TextDocumentItem;
368                                                 uri: new GLib.Variant.string (file.to_url()),
369                                                 version :  new GLib.Variant.uint64 ( (uint64) file.version)
370                                         )
371                                 );
372                          
373                                 //GLib.debug ("textDocument/save send with %s", Json.to_string (Json.gvariant_serialize (args), true));                                 
374                         
375                          
376                          
377                                   yield this.jsonrpc_client.send_notification_async  (
378                                         "textDocument/didSave",
379                                         args,
380                                         null 
381                                 );
382                                 this.log(LanguageClientAction.SAVE, file.path);
383                         } catch( GLib.Error  e) {
384                                 this.log(LanguageClientAction.ERROR_RPC, e.message);
385                                 GLib.debug ("LS   save err %s", e.message);
386                                 this.onClose();
387                         }
388
389          
390         }
391                 public override  void document_close (JsRender.JsRender file) 
392         {
393                         if (!this.isReady()) {
394                                 return;
395                         }
396                         this.change_queue_file = null;
397                         
398                         if (this.open_files.contains(file)) {
399                                 this.open_files.remove(file);
400                         }
401                         this.log(LanguageClientAction.CLOSE, file.path);
402                         GLib.debug ("LS send close");
403                         try {
404                                   this.jsonrpc_client.send_notification  (
405                                         "textDocument/didChange",
406                                         this.buildDict (  
407                                                 textDocument : this.buildDict (    ///TextDocumentItem;
408                                                         uri: new GLib.Variant.string (file.to_url())
409                                                         
410                                                 )
411                                         ),
412                                         null  
413                                 );
414                         } catch( GLib.Error  e) {
415                                 this.log(LanguageClientAction.ERROR_RPC, e.message);
416                                 GLib.debug ("LS close err %s", e.message);
417                                 this.onClose();
418                         }
419
420          
421         }
422         
423          
424                 public override void document_change (JsRender.JsRender file )    
425                 {
426                         if (this.change_queue_file != null && this.change_queue_file.path != file.path) {
427                                 this.document_change_force.begin(this.change_queue_file, this.change_queue_file_source, (o, res) => {
428                                         this.document_change_force.end(res);
429                                 });
430                         }
431                         
432                         this.countdown = 3;
433                         this.change_queue_file = file;
434                          
435                         
436
437                 }
438         
439
440                 public override async void document_change_force (JsRender.JsRender file, string contents)  
441         {
442                         
443                         
444                         if (!this.isReady()) {
445                                 return;
446                         }
447                         this.countdown = 9; // not really relivant..
448                         this.change_queue_file = null; // this is more important..
449                         
450                     if (!this.open_files.contains(file)) {
451                                  this.document_open(file);
452                         }  
453                         
454                         GLib.debug ("LS send change %s rev %d", file.path, file.version);
455                         var ar = new Json.Array();
456                         var obj = new Json.Object();
457                         obj.set_string_member("text", contents);
458                         ar.add_object_element(obj);
459                         var node = new Json.Node(Json.NodeType.ARRAY);
460                         node.set_array(ar);
461                         this.log(LanguageClientAction.CHANGE, file.path);
462                          try {
463                                 yield this.jsonrpc_client.send_notification_async (
464                                         "textDocument/didChange",
465                                         this.buildDict (  
466                                                 textDocument : this.buildDict (    ///TextDocumentItem;
467                                                         uri: new GLib.Variant.string (file.to_url()),
468                                                         version :  new GLib.Variant.uint64 ( (uint64) file.version) 
469                                                 ),
470                                                 contentChanges : Json.gvariant_deserialize (node, null)
471                                                 
472                                         ),
473                                         null 
474                                 );
475                         } catch( GLib.Error  e) {
476                                 this.log(LanguageClientAction.ERROR_RPC, e.message);
477                                 GLib.debug ("LS change err %s", e.message);
478                                 this.onClose();
479                         }
480
481          
482         }
483         // called by close window (on last window)...
484                 public override  void exit () throws GLib.Error 
485                 {
486                         if (!this.isReady()) {
487                         
488                                 return;
489                         }
490                         this.log(LanguageClientAction.TERM, "SEND exit");
491                  
492                           this.jsonrpc_client.send_notification (
493                                 "exit",
494                                 null,
495                                 null 
496                         );
497                         this.onClose();
498
499                 }
500                 // not used currently..
501                 public override async void shutdown () throws GLib.Error 
502                 {
503                         if (!this.isReady()) {
504                                 return;
505                         }
506                         this.log(LanguageClientAction.TERM, "SEND shutodwn");
507                         this.sent_shutdown  = true;
508                         Variant? return_value;
509                         yield this.jsonrpc_client.call_async (
510                                 "shutdown",
511                                 null,
512                                 null,
513                                 out return_value
514                         );
515                         GLib.debug ("LS replied with %s", Json.to_string (Json.gvariant_serialize (return_value), true));               
516                 }
517                 //public async  ??/symbol (string symbol) throws GLib.Error {
518                 
519                 // and now for the important styff..
520                 
521                 /*
522                 
523                 @triggerType 1 = typing or ctl-spac, 2 = tiggercharactres?  3= inside completion?
524                 */
525                  public override async Lsp.CompletionList?  completion(JsRender.JsRender file, int line, int offset , int triggerType = 1) throws GLib.Error 
526                  {
527                         /* partial_result_token ,  work_done_token   context = null) */
528                         GLib.debug("%s get completion %s @ %d:%d", this.get_type().name(),  file.relpath, line, offset);
529                         
530                         var ret = new Lsp.CompletionList();     
531                         
532                     if (!this.isReady()) {
533                         GLib.debug("completion - language server not ready");
534                                 return ret;
535                         }
536                         // make sure completion has the latest info..
537                         //if (this.change_queue_file != null && this.change_queue_file.path != file.path) {
538                         //      this.document_change_real(this.change_queue_file, this.change_queue_file_source);
539                         //      this.change_queue_file != null;
540                         //}
541                         this.log(LanguageClientAction.COMPLETE, "SEND complete  %s @ %d:%d".printf(file.relpath, line, offset) );
542                         
543                         Variant? return_value;
544                         
545                         var args = this.buildDict (  
546                                         context : this.buildDict (    ///CompletionContext;
547                                                 triggerKind: new GLib.Variant.int32 (triggerType) 
548                                         //      triggerCharacter :  new GLib.Variant.string ("")
549                                         ),
550                                         textDocument : this.buildDict (    ///TextDocumentItem;
551                                                 uri: new GLib.Variant.string (file.to_url()),
552                                                 version :  new GLib.Variant.uint64 ( (uint64) file.version) 
553                                         ), 
554                                         position :  this.buildDict ( 
555                                                 line :  new GLib.Variant.uint64 ( (uint) line) ,
556                                                 character :  new GLib.Variant.uint64 ( uint.max(0,  (offset -1))) 
557                                         )
558                                 );
559                          
560                         GLib.debug ("textDocument/completion send with %s", Json.to_string (Json.gvariant_serialize (args), true));                                     
561                         
562                         yield this.jsonrpc_client.call_async (
563                                 "textDocument/completion",
564                                 args,
565                                 null,
566                                 out return_value
567                         );
568                         
569                         
570                         //GLib.debug ("LS replied with %s", Json.to_string (Json.gvariant_serialize (return_value), true));                                     
571                         var json = Json.gvariant_serialize (return_value);
572
573
574                         if (json.get_node_type() == Json.NodeType.OBJECT) {
575                                 ret = Json.gobject_deserialize (typeof (Lsp.CompletionList), json) as Lsp.CompletionList; 
576                                 this.log(LanguageClientAction.COMPLETE_REPLY, "GOT complete  %d items".printf(ret.items.size) );
577                                 GLib.debug ("LS replied with Object");
578                                 return ret;
579                         }  
580
581                         if (json.get_node_type() != Json.NodeType.ARRAY) {
582                                 GLib.debug ("LS replied with %s", Json.to_string (Json.gvariant_serialize (return_value), true));                                       
583                                 this.log(LanguageClientAction.ERROR_REPLY, "GOT something else??");
584                                 return ret;
585                         
586                         }
587                         var ar = json.get_array();                      
588                         
589                         for(var i = 0; i < ar.get_length(); i++ ) {
590                                 var add= Json.gobject_deserialize ( typeof (Lsp.CompletionItem),  ar.get_element(i)) as Lsp.CompletionItem;
591                                 ret.items.add( add);
592                                          
593                         }
594                         this.log(LanguageClientAction.COMPLETE_REPLY, "GOT array %d items".printf(ret.items.size) );
595                         GLib.debug ("LS replied with Array");
596                         return ret;
597                 
598
599                 }
600                 
601                 
602                 //CompletionListInfo.itmems.parse_varient  or CompletionListInfo.parsevarient
603                 public override async  Lsp.Hover hover (JsRender.JsRender file, int line, int offset) throws GLib.Error 
604                  {
605                         /* partial_result_token ,  work_done_token   context = null) */
606                         GLib.debug("get syntax %s", file.relpath);
607                         var ret = new Lsp.Hover();      
608                         //ret = null;
609                     if (!this.isReady()) {
610                                 return ret;
611                         }
612                         Variant? return_value;
613                         yield this.jsonrpc_client.call_async (
614                                 "textDocument/hover",
615                                 this.buildDict (  
616                                          
617                                         textDocument : this.buildDict (    ///TextDocumentItem;
618                                                 uri: new GLib.Variant.string (file.to_url()),
619                                                 version :  new GLib.Variant.uint64 ( (uint64) file.version) 
620                                         ),
621                                         position :  this.buildDict ( 
622                                                 line :  new GLib.Variant.uint64 ( (uint) line) ,
623                                                 character :  new GLib.Variant.uint64 ( uint.max(0,  (offset -1))) 
624                                         )
625                                          
626                                 ),
627                                 null,
628                                 out return_value
629                         );
630                         
631                         
632                         GLib.debug ("LS replied with %s", Json.to_string (Json.gvariant_serialize (return_value), true));                                       
633                         var json = Json.gvariant_serialize (return_value);
634                         ret =  Json.gobject_deserialize ( typeof (Lsp.Hover),  json) as Lsp.Hover; 
635                         
636                         return ret;
637                         
638                 
639
640                 }
641                 
642                 
643                 
644                 //CompletionListInfo.itmems.parse_varient  or CompletionListInfo.parsevarient
645                 public override async Gee.ArrayList<Lsp.DocumentSymbol> syntax (JsRender.JsRender file) throws GLib.Error 
646                  {
647                         /* partial_result_token ,  work_done_token   context = null) */
648                         GLib.debug("get syntax %s", file.relpath);
649                         var ret = new Gee.ArrayList<Lsp.DocumentSymbol>();      
650                         //ret = null;
651                     if (!this.isReady()) {
652                                 return ret;
653                         }
654                         Variant? return_value;
655                         yield this.jsonrpc_client.call_async (
656                                 "textDocument/documentSymbol",
657                                 this.buildDict (  
658                                          
659                                         textDocument : this.buildDict (    ///TextDocumentItem;
660                                                 uri: new GLib.Variant.string (file.to_url()),
661                                                 version :  new GLib.Variant.uint64 ( (uint64) file.version) 
662                                         ) 
663                                          
664                                 ),
665                                 null,
666                                 out return_value
667                         );
668                         
669                         
670                         GLib.debug ("LS replied with %s", Json.to_string (Json.gvariant_serialize (return_value), true));                                       
671                         var json = Json.gvariant_serialize (return_value);
672                          
673                          
674
675                         var ar = json.get_array();
676                         for(var i = 0; i < ar.get_length(); i++ ) {
677                                 var add= Json.gobject_deserialize ( typeof (Lsp.DocumentSymbol),  ar.get_element(i)) as Lsp.DocumentSymbol;
678                                 ret.add( add);
679                                          
680                         }
681                                 return ret ;
682                         
683                 
684
685                 }
686                 
687         }
688         
689 }