Fix #8047 - debugging language server and fix completion handling
[roobuilder] / src / Palete / CompletionProvider.vala
1  
2 //using Gtk;
3
4 // not sure why - but extending Gtk.SourceCompletionProvider seems to give an error..
5 namespace Palete {
6
7     public class CompletionProvider : Object, GtkSource.CompletionProvider
8     {
9                 public JsRender.JsRender file {
10                         get { return this.editor.file; }
11                         private set {}
12                 }
13                 public Editor editor; 
14                 //public WindowState windowstate;
15                 public CompletionModel model;
16                 global::Gtk.StringFilter? filter = null;
17
18                 public CompletionProvider(Editor editor)
19                 {
20                     this.editor  = editor;
21                  
22                    // this.windowstate = null; // not ready until the UI is built.
23                     
24                 }
25
26                 public string get_name ()
27                 {
28                   return  "roojsbuilder";
29                 }
30
31                 public int get_priority (GtkSource.CompletionContext context)
32                 {
33                   return 200;
34                 }
35                 
36                 
37                 public bool is_trigger(global::Gtk.TextIter  iter, unichar ch)
38                 {
39                         if (this.in_populate || ch == 32 || ch == 10) {
40                                 return false;
41                         }
42                         if (this.editor.buffer.el.iter_has_context_class(iter, "comment") ||
43                                 this.editor.buffer.el.iter_has_context_class(iter, "string")
44                         ) { 
45                                 return false;
46                         }
47                         var back = iter.copy();
48                         back.backward_char();
49                         
50                         // what's the character at the iter?
51                         var str = back.get_text(iter);
52                         
53                         GLib.debug("Previos char to trigger is '%s;", str);
54                         
55                         
56                         return true;
57                 }
58                 
59                 public  void activate (GtkSource.CompletionContext context, GtkSource.CompletionProposal proposal)
60                 {
61                         GLib.debug("compelte activate");
62                         
63                         var  p = (CompletionProposal) proposal;
64                         GLib.debug("lsp says use %s", p.ci.insertText);
65                         
66                         
67                         global::Gtk.TextMark end_mark = null;
68                         global::Gtk.TextIter begin, end;
69
70                         if (!context.get_bounds(out begin, out end)) {
71                                 return;
72                         }  
73                         var buffer = begin.get_buffer();
74                 
75                         var  word = p.label;
76                         if (p.ci.kind == Lsp.CompletionItemKind.Method || p.ci.kind == Lsp.CompletionItemKind.Function) {
77                                 var bits = p.text.split("(");
78                                 var abits = bits[1].split(")");
79                                 var args = abits[0].split(",");
80                                 
81                                 word += "(";
82                                 for(var i = 0 ; i < args.length; i++) {
83                                         word += i > 0 ? ", " : " ";
84                                         var wbit = args[i].strip().split(" ");
85                                         if (wbit.length < 2) {
86                                                 word += wbit[0];
87                                                 continue;
88                                         }
89                                         var ty = wbit[wbit.length - 2];
90                                         ty = ty.has_suffix("?") ? "?" : "";  
91                                         word += ty + wbit[wbit.length-1]; // property type..?
92                                 }
93                                 word += args.length > 0 ? " )" : ")";
94                         }
95                         
96                         var len = -1;
97                         
98
99                         /* If the insertion cursor is within a word and the trailing characters
100                          * of the word match the suffix of the proposal, then limit how much
101                          * text we insert so that the word is completed properly.
102                          */
103                         if (!end.ends_line() &&
104                                 !end.get_char().isspace() &&
105                                 !end.ends_word ())
106                         {
107                                 var word_end = end;
108
109                                 if (word_end.forward_word_end ()) {
110                                         var text = end.get_slice(word_end);
111                                         if (text.length > word.length) {
112                                                 return;
113                                         }
114                                         if (word.has_suffix (text)) {
115                                                 //g_assert (strlen (word) >= strlen (text));
116                                                 len = word.length - text.length;
117                                                 end_mark = buffer.create_mark (null, word_end, false); 
118                                         }
119                                 }
120                         }
121
122                         buffer.begin_user_action();
123                         buffer.delete (ref begin, ref end);
124                         buffer.insert ( ref begin, word, len);
125                         buffer.end_user_action ();
126
127                         if (end_mark != null)
128                         {
129                                 buffer.get_iter_at_mark(out end, end_mark);
130                                 buffer.select_range(end,  end);
131                                 buffer.delete_mark(end_mark);
132                         }
133                 
134
135                 }
136
137
138                 public  void display (GtkSource.CompletionContext context, GtkSource.CompletionProposal proposal, GtkSource.CompletionCell cell)
139                 {
140                         //GLib.debug("compelte display");
141                         var col = cell.get_column();
142                         
143                         var p = (CompletionProposal) proposal;
144                         switch(col) {
145                                 case GtkSource.CompletionColumn.TYPED_TEXT:
146                                         cell.set_text(p.label);
147                                         break;
148                                 case GtkSource.CompletionColumn.ICON:
149 //cell.set_icon_name("lang-define-symbolic");return;
150 //cell.set_icon_name("lang-include-symbolic");return;
151 //cell.set_icon_name("lang-typedef-symbolic");return;
152 //cell.set_icon_name("lang-union-symbolic");return;                      
153                                         switch (p.ci.kind) {
154                                         
155                                                 case    Lsp.CompletionItemKind.Text: cell.set_icon_name("completion-snippet-symbolic");return;
156                                                 case    Lsp.CompletionItemKind.Method: cell.set_icon_name("lang-method-symbolic");return;
157                                                 case    Lsp.CompletionItemKind.Function: cell.set_icon_name("lang-function-symbolic");return;
158                                                 case    Lsp.CompletionItemKind.Constructor: cell.set_icon_name("lang-method-symbolic");return;
159                                                 case    Lsp.CompletionItemKind.Field: cell.set_icon_name("lang-struct-field-symbolic");return;
160                                                 case    Lsp.CompletionItemKind.Variable: cell.set_icon_name("lang-variable-symbolic");return;
161                                                 case    Lsp.CompletionItemKind.Class: cell.set_icon_name("lang-class-symbolic");return;
162                                                 case    Lsp.CompletionItemKind.Interface: cell.set_icon_name("lang-class-symbolic");return;
163                                                 case    Lsp.CompletionItemKind.Module: cell.set_icon_name("lang-namespace-symbolic");return;
164                                                 case    Lsp.CompletionItemKind.Property:cell.set_icon_name("lang-struct-field-symbolic");return;
165                                                 case    Lsp.CompletionItemKind.Unit: cell.set_icon_name("lang-variable-symbolic");return;
166                                                 case    Lsp.CompletionItemKind.Value: cell.set_icon_name("lang-variable-symbolic");return;
167                                                 case    Lsp.CompletionItemKind.Enum: cell.set_icon_name("lang-enum-symbolic");return;
168                                                 case    Lsp.CompletionItemKind.Keyword: cell.set_icon_name("completion-word-symbolic");return;
169                                                 case    Lsp.CompletionItemKind.Snippet: cell.set_icon_name("completion-snippet-symbolic");return;
170
171                                                 case    Lsp.CompletionItemKind.Color: cell.set_icon_name("lang-typedef-symbolic");return;
172                                                 case    Lsp.CompletionItemKind.File:cell.set_icon_name("lang-typedef-symbolic");return;
173                                                 case    Lsp.CompletionItemKind.Reference: cell.set_icon_name("lang-typedef-symbolic");return;
174                                                 case    Lsp.CompletionItemKind.Folder:cell.set_icon_name("lang-typedef-symbolic");return;
175                                                 case    Lsp.CompletionItemKind.EnumMember: cell.set_icon_name("lang-typedef-symbolic");return;
176                                                 case    Lsp.CompletionItemKind.Constant:cell.set_icon_name("lang-typedef-symbolic");return;
177                                                 case    Lsp.CompletionItemKind.Struct: cell.set_icon_name("lang-struct-symbolic");return;
178                                                 case    Lsp.CompletionItemKind.Event:cell.set_icon_name("lang-typedef-symbolic");return;
179                                                 case    Lsp.CompletionItemKind.Operator:cell.set_icon_name("lang-typedef-symbolic");return;
180                                                 case    Lsp.CompletionItemKind.TypeParameter:cell.set_icon_name("lang-typedef-symbolic");return;
181                                                 default:
182
183
184
185                                         
186                                                         cell.set_icon_name("completion-snippet-symbolic");
187                                                         return;
188                                                 }
189                                                 
190                                         
191                                 case  GtkSource.CompletionColumn.COMMENT:
192                                         cell.set_text(p.text);
193                                         break;
194                                 case GtkSource.CompletionColumn.DETAILS:
195                                         if (p.ci.documentation != null) {
196                                                 cell.set_text(p.ci.documentation.value);
197                                                 return;
198                                         }
199                                 
200                                         cell.set_text(p.text);
201                                         break;
202                                 default:
203                                         cell.set_text(null);
204                                         break;
205                         }       
206                 }
207
208                 bool in_populate = false;
209          
210                 internal  async GLib.ListModel populate_async (GtkSource.CompletionContext context, GLib.Cancellable? cancellable) 
211                 {
212                         GLib.debug("pupoulate async");
213                         var ret = new GLib.ListStore(typeof(CompletionProposal));
214                         
215                         if (this.in_populate) {
216                                 GLib.debug("pupoulate async  - skipped waiting for reply");
217                                 return ret;
218                         }
219                         this.in_populate = true;
220
221                         global::Gtk.TextIter begin, end;
222                         Lsp.CompletionList res;
223                         if (context.get_bounds (out begin, out end)) {
224                                 var line = end.get_line();
225                                 var offset =  end.get_line_offset();
226                                 if (this.editor.prop != null) {
227                                 //      tried line -1 (does not work)
228                                         GLib.debug("node pad = '%s' %d", this.editor.node.node_pad, this.editor.node.node_pad.length);
229                                         
230                                         line += this.editor.prop.start_line ; 
231                                         // this is based on Gtk using tabs (hence 1/2 chars);
232                                         offset += this.editor.node.node_pad.length;
233                                         // javascript listeners are indented 2 more spaces.
234                                         if (this.editor.prop.ptype == JsRender.NodePropType.LISTENER) {
235                                                 offset += 2;
236                                         }
237                                 } 
238                                 //  this should not really be slow, as it's a quick repsonse
239                                 yield this.file.getLanguageServer().document_change_force(this.file, this.editor.tempFileContents());                           
240                                 try {
241                                         GLib.debug("sending request to language server %s", this.file.getLanguageServer().get_type().name());
242                                         
243                                         res = yield this.file.getLanguageServer().completion(this.file, line, offset, 1);
244                                 } catch (GLib.Error e) {
245                                         GLib.debug("got error %s", e.message);
246                                         this.in_populate = false;
247                                         return ret;
248                                 }
249                                 
250                         } else {
251                                 this.in_populate = false;
252                                 return ret;
253                         }
254                         
255                         GLib.debug("pupoulate async  - got reply");
256                         this.model = new CompletionModel(this, context, res, cancellable); 
257                         var word = context.get_word();
258                         
259                         var lc = end.copy();
260                         lc.backward_char();
261                         var lchar = lc.get_text(end);
262                         
263                         
264                         GLib.debug("Context word is %s / '%s' , %d", word, lchar, (int)word.length);
265                         if (word.length < 1 && lchar != ".") {
266                                 word = " "; // this should filter out everything, and prevent it displaying 
267                         }
268                         
269                         var expression = new global::Gtk.PropertyExpression(typeof(CompletionProposal), null, "label");
270                         this.filter = new global::Gtk.StringFilter(expression);
271                         this.filter.set_search( word);
272                         var  filter_model = new global::Gtk.FilterListModel(this.model, this.filter); 
273                         filter.match_mode = global::Gtk.StringFilterMatchMode.PREFIX;
274                         filter_model.set_incremental(true);
275                         this.in_populate = false;
276                         return filter_model; 
277                         
278                          
279                         
280                 }
281
282                 internal  void refilter (GtkSource.CompletionContext context, GLib.ListModel in_model)
283                 {
284  
285                         //GLib.debug("pupoulate refilter");
286                         if (this.filter == null) {
287                                 return;
288                         }
289
290                         var word = context.get_word();
291                         this.filter.set_search(word);
292                  
293                 
294                 }
295
296
297 /*
298                 public bool activate_proposal (GtkSource.CompletionProposal proposal, TextIter iter)
299                 {
300                         var istart = iter;
301                         istart.backward_find_char(is_space, null);
302                         istart.forward_char();
303
304                 //    var search = iter.get_text(istart);           
305                 
306                         var buffer = iter.get_buffer();
307                         buffer.delete(ref istart, ref iter);
308                         buffer.insert(ref istart, proposal.get_text(), -1);
309                 
310                         return true;
311                 }
312   
313          
314
315                 private bool is_space(unichar space){
316                         return space.isspace() || space.to_string() == "";
317                 }
318                 */
319                  
320         }
321         public class CompletionModel : Object, GLib.ListModel 
322         {
323                 CompletionProvider provider;
324                 Gee.ArrayList<CompletionProposal> items;
325                 string search;
326                 int minimum_word_size = 2;
327                 
328                 public Cancellable? cancellable;
329                 
330                 public CompletionModel(CompletionProvider provider, GtkSource.CompletionContext context, Lsp.CompletionList? res, Cancellable? cancellable)
331                 {
332                         this.provider = provider;
333                         this.cancellable = cancellable;
334                         this.items = new Gee.ArrayList<CompletionProposal>();
335                         
336                         var word = context.get_word();
337                         GLib.debug("looking for %s", word);
338                         this.search = word;
339                         if (res != null) {
340                                 foreach(var comp in res.items) {
341                                          if (comp.label == "_") { // skip '_'
342                                                 continue;
343                                         }
344                                         GLib.debug("got suggestion %s", comp.label);
345                                         this.items.add(new CompletionProposal(comp));   
346                                         
347                                 }
348                         }
349                     GLib.debug("GOT %d results\n", (int) items.size); 
350                         // WHY TWICE?
351                     if (this.items.size < this.minimum_word_size) {
352                                 return;
353                     }
354                 
355                     items.sort((a, b) => {
356                             return ((string)(a.label)).collate((string)(b.label));
357                     });
358                 
359                 }
360                 
361                  
362                 
363                 public GLib.Object? get_item (uint pos)
364                 {
365                         return (Object) this.items.get((int) pos);
366                 }
367                 public GLib.Type  get_item_type ()
368                 {
369                         return typeof(GtkSource.CompletionProposal);
370                 }
371                 public   uint get_n_items () 
372                 {
373                         return this.items.size;
374                 }
375                 public bool can_filter (string word) 
376                 {
377                         if (word == null || word[0] == 0) {
378                                 return false;
379                         }
380  
381                         if (word.length < this.minimum_word_size) {
382                                 return false;
383                         }
384
385                         /* If the new word starts with our initial word, then we can simply
386                          * refilter outside this model using a GtkFilterListModel.
387                          */
388                          
389                          return word.has_prefix(this.search); 
390                 }
391                 public void  cancel ()
392                 {
393                         if (this.cancellable != null) {
394                                 this.cancellable.cancel();
395                         }
396                 }
397
398
399                 
400         }
401         public class CompletionProposal : Object, GtkSource.CompletionProposal 
402         {
403                 
404                 public string label { get; set; default = ""; }
405                 
406                 public string text  { get; set; default = ""; }
407                 public string info  { get; set; default = ""; }
408                 
409                 public Lsp.CompletionItem ci;
410                 
411                 public CompletionProposal(Lsp.CompletionItem ci) //string label, string text, string info)
412                 {
413                         
414                         this.ci = ci;
415                         this.text = ci.detail == null ? "" : ci.detail ;
416                         this.label = ci.label;
417                         this.info = ci.documentation == null ? "": ci.documentation.value;
418                         //GLib.debug("SET: detail =%s, label = %s; info =%s", ci.detail, ci.label, "to long..");
419                 }
420                 
421         }
422
423
424