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