refactor completers
[conkeror.git] / modules / completers.js
blob6ff3a6e930caeaacb93434974d06546f7a990020
1 /**
2  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
3  * (C) Copyright 2008 Nelson Elhage
4  * (C) Copyright 2010,2012 John J. Foerch
5  *
6  * Portions of this file (the JavaScript completer) were derived from Vimperator,
7  * (C) Copyright 2006-2007 Martin Stubenschrott.
8  *
9  * Use, modification, and distribution are subject to the terms specified in the
10  * COPYING file.
11 **/
13 function completions (completer, data) {
14     this.completer = completer;
15     if (data) {
16         this.data = data;
17         this.count = data.length;
18     }
20 completions.prototype = {
21     constructor: completions,
22     toString: function () "#<completions>",
23     completer: null,
24     data: null,
25     count: null,
26     destroy: function () {},
27     index_of: function (x) {
28         return this.data.indexOf(x);
29     },
30     get_string: function (i) {
31         return this.completer.get_string(this.data[i]);
32     },
33     get_input_state: function (i) {
34         return [this.get_string(i)];
35     },
36     get_description: function (i) {
37         return this.completer.get_description(this.data[i]);
38     },
39     get_icon: function (i) {
40         if (this.completer.get_icon)
41             return this.completer.get_icon(this.data[i]);
42         else
43             return null;
44     },
45     get_value: function (i) {
46         if (this.completer.get_value)
47             return this.completer.get_value(this.data[i]);
48         else
49             return this.data[i];
50     }
54 define_keywords("$completions", "$get_string", "$get_description",
55                 "$get_icon", "$get_value");
56 function completer () {
57     keywords(arguments,
58              $completions = [],
59              $get_string = identity,
60              $get_description = constantly(""),
61              $get_icon = null,
62              $get_value = null);
63     this.completions_src = arguments.$completions;
64     this.get_string = arguments.$get_string;
65     this.get_description = arguments.$get_description;
66     this.get_icon = arguments.$get_icon;
67     this.get_value = arguments.$get_value;
68     this.refresh();
70 completer.prototype = {
71     constructor: completer,
72     toString: function () "#<completer>",
73     completions_src: null,
74     completions: null,
75     get_string: null,
76     get_description: null,
77     get_icon: null,
78     get_value: null,
79     complete: function (input, pos) {},
80     refresh: function () {
81         if (typeof this.completions_src == "function") {
82             var completions = [];
83             this.completions_src(function (x) { completions.push(x); });
84             this.completions = completions;
85         } else if (this.completions_src)
86             this.completions = this.completions_src.slice();
87     }
92  * All Word Completer
93  */
95 function all_word_completer () {
96     keywords(arguments);
97     completer.call(this, forward_keywords(arguments));
99 all_word_completer.prototype = {
100     constructor: all_word_completer,
101     __proto__: completer.prototype,
102     toString: function () "#<all_word_completer>",
103     complete: function (input, pos) {
104         var words = input.toLowerCase().split(" ");
105         var nwords = words.length;
106         var c = this;
107         var narrowed = this.completions.filter(function (x) {
108                 var s = c.get_string(x);
109                 var d = c.get_description(x);
110                 for (var i = 0; i < nwords; ++i) {
111                     if (s.toLowerCase().indexOf(words[i]) == -1 &&
112                         d.toLowerCase().indexOf(words[i]) == -1)
113                     {
114                         return false;
115                     }
116                 }
117                 return true;
118             });
119         return new completions(this, narrowed);
120     }
125  * Prefix Completer
126  */
128 function prefix_completions (completer, data, default_completion,
129                              offset, pos, input, common_prefix)
131     completions.call(this, completer, data);
132     this.default_completion = default_completion;
133     this.common_prefix = common_prefix;
134     this.offset = offset || 0;
135     this.pos = pos;
136     this.input = input;
138 prefix_completions.prototype = {
139     constructor: prefix_completions,
140     __proto__: completions.prototype,
141     toString: function () "#<prefix_completions>",
142     default_completion: null,
143     common_prefix: null,
144     offset: null,
145     pos: null,
146     input: null,
147     get_partial_completion_input_state: function (x, prefix_end, suffix_begin, orig_str) {
148         if (suffix_begin < orig_str.length) {
149             if (orig_str[suffix_begin] == " ")
150                 suffix_begin++;
151             var sel = x.length + prefix_end + 1;
152             return [orig_str.substring(0, prefix_end) + x + " " +
153                     orig_str.substring(suffix_begin),
154                     sel, sel];
155         } else {
156             sel = x.length + prefix_end;
157             return [orig_str.substring(0, prefix_end) + x, sel, sel];
158         }
159     },
160     get_input_state: function (i) {
161         return this.get_partial_completion_input_state(
162             this.get_string(i), 0, this.pos, this.input)
163     },
164     get common_prefix_input_state () { //used by minibuffer-read
165         return (this.common_prefix &&
166                 this.get_partial_completion_input_state(this.common_prefix,
167                                                         this.offset, this.pos,
168                                                         this.input));
169     }
172 function prefix_completer () {
173     keywords(arguments);
174     completer.call(this, forward_keywords(arguments));
176 prefix_completer.prototype = {
177     constructor: prefix_completer,
178     __proto__: completer.prototype,
179     toString: function () "#<prefix_completer>",
180     complete: function (input, pos) {
181         var common_prefix = null;
182         var input_prefix = input.substring(0, pos);
183         var default_completion = null;
184         var i = 0;
185         var c = this;
186         var narrowed = this.completions.filter(function (x) {
187                 var s = c.get_string(x);
188                 if (s == input) {
189                     default_completion = i;
190                     var retval = true;
191                 } else
192                     retval = (s.substring(0, pos) == input_prefix);
193                 if (retval)
194                     ++i;
195                 return retval;
196             });
197         var nnarrowed = narrowed.length;
198         if (nnarrowed > 0) {
199             var a = this.get_string(narrowed[0]);
200             var b = this.get_string(narrowed[nnarrowed - 1]);
201             let i = common_prefix_length(a, b);
202             if (i > pos) {
203                 common_prefix = a.substring(0, i);
204                 if (! default_completion) {
205                     for (var j = 0; j < nnarrowed; ++j) {
206                         if (this.get_string(narrowed[j]) == common_prefix) {
207                             default_completion = j;
208                             break;
209                         }
210                     }
211                 }
212             }
213         }
214         return new prefix_completions(this, narrowed, default_completion,
215                                       null, pos, input, common_prefix);
216     },
217     refresh: function () {
218         completer.prototype.refresh.call(this);
219         var c = this;
220         this.completions.sort(function (a, b) {
221                 a = c.get_string(a);
222                 b = c.get_string(b);
223                 if (a < b)
224                     return -1;
225                 if (a > b)
226                     return 1;
227                 return 0;
228             });
229     }
234  * Javascript Completer
235  */
237 function javascript_completer (scope) {
238     prefix_completer.call(this,
239                           $get_string = first,
240                           $get_description = second);
241     this.scope = scope;
243 javascript_completer.prototype = {
244     constructor: javascript_completer,
245     __proto__: prefix_completer.prototype,
246     toString: function () "#<javascript_completer>",
247     scope: null,
248     complete: function (input, pos) {
249         var str = input.substr(0, pos);
250         var matches = str.match(/^(.*?)(\s*\.\s*)?(\w*)$/);
251         var filter = matches[3] || "";
252         var start = matches[1].length - 1;
253         var offset = matches[1] ? matches[1].length : 0;
254         offset += matches[2] ? matches[2].length : 0;
256         if (matches[2]) {
257             let brackets = 0, parentheses = 0;
258         outer:
259             for (; start >= 0; start--) {
260                 switch (matches[1][start]) {
261                 case ";":
262                 case "{":
263                     break outer;
264                 case "]":
265                     brackets--;
266                     break;
267                 case "[":
268                     brackets++;
269                     break;
270                 case ")":
271                     parentheses--;
272                     break;
273                 case "(":
274                     parentheses++;
275                     break;
276                 }
277                 if (brackets > 0 || parentheses > 0)
278                     break outer;
279             }
280         }
282         var objects = [];
283         var narrowed = [];
284         var common_prefix_len = null;
285         var common_prefix = null;
286         var c = this;
288         function add_completion (str, desc) {
289             if (common_prefix != null)
290                 common_prefix_len = common_prefix_length(common_prefix, str, common_prefix_len);
291             else
292                 common_prefix = str;
293             narrowed.push([str, desc]);
294         }
295         if (matches[1].substr(start+1)) {
296             try {
297                 var source_obj = eval(matches[1].substr(start+1));
298             } catch (e) {}
299         } else {
300             source_obj = this.scope;
301         }
302         if (source_obj != null) {
303             try {
304                 for (let i in source_obj) {
305                     if (i.substring(0, filter.length) != filter)
306                         continue;
307                     try {
308                         var type = typeof source_obj[i];
309                     } catch (e) {
310                         type = "unknown type";
311                     }
312                     if (type == "number" || type == "string" || type == "boolean") {
313                         var description = type + ": " + source_obj[i];
314                     } else
315                         description = type;
316                     add_completion(i, description);
317                 }
318             } catch (e) {}
319         }
320         if (common_prefix != null && common_prefix_len > 0)
321             common_prefix = common_prefix.substr(0, common_prefix_len);
322         else if (common_prefix_len != null)
323             common_prefix = null;
324         return new prefix_completions(this, narrowed, null, offset, pos, input, common_prefix);
325     },
326     refresh: function () {}
331  * Merged Completer (combinator)
332  */
334 function merged_completions (results, count) {
335     completions.call(this, null);
336     this.results = results;
337     this.nresults = results.length;
338     this.count = count;
340 merged_completions.prototype = {
341     constructor: merged_completions,
342     __proto__: completions.prototype,
343     toString: function () "#<merged_completions>",
344     results: null,
345     nresults: 0,
346     forward: function (name, i) {
347         for (var j = 0; j < this.nresults; ++j) {
348             var r = this.results[j];
349             if (i < r.count)
350                 return r[name](i);
351             i -= r.count;
352         }
353         return null;
354     },
355     destroy: function () {
356         for (var j = 0; j < this.nresults; ++j) {
357             this.results[j].destroy();
358         }
359     },
360     //XXX: index_of: function (x) { },
361     get_string: function (i) {
362         return this.forward("get_string", i);
363     },
364     get_input_state: function (i) {
365         return this.forward("get_input_state", i);
366     },
367     get_description: function (i) {
368         return this.forward("get_description", i);
369     },
370     get_icon: function (i) {
371         return this.forward("get_icon", i);
372     },
373     get_value: function (i) {
374         return this.forward("get_value", i);
375     }
378 function merged_completer (completers) {
379     this.completers = completers;
380     this.ncompleters = completers.length;
381     completer.call(this);
383 merged_completer.prototype = {
384     constructor: merged_completer,
385     __proto__: completer.prototype,
386     toString: function () "#<merged_completer>",
387     completers: null,
388     ncompleters: 0,
389     complete: function (input, pos) {
390         var merged_results = [];
391         var count = 0;
392         for (let i = 0; i < this.ncompleters; ++i) {
393             let r = yield this.completers[i].complete(input, pos);
394             if (r != null && r.count > 0) {
395                 merged_results.push(r);
396                 count += r.count;
397             }
398         }
399         yield co_return(new merged_completions(merged_results, count));
400     },
401     refresh: function () {
402         for (var i = 0; i < this.ncompleters; ++i) {
403             this.completers[i].refresh();
404         }
405     },
406     get require_match () {
407         for (var i = 0; i < this.ncompleters; ++i) {
408             var r = this.completers[i];
409             if (r.require_match)
410                 return r.require_match;
411         }
412         return false;
413     }
418  * Nest Completions
420  *   Adjust input_state of a prefix_completions object for prefix and/or
421  * suffix strings.
422  */
424 function nest_completions (completions_o, prefix, suffix) {
425     if (prefix == null)
426         prefix = "";
427     if (suffix == null)
428         suffix = "";
429     function nest (x) {
430         let [s, a, b] = x;
431         if (a == null)
432             a = s.length;
433         if (b == null)
434             b = s.length;
435         return [prefix + s + suffix, a + prefix.length, b + prefix.length];
436     }
437     return {
438         __proto__: completions_o,
439         get_input_state: function (i) {
440             var x = completions_o.get_input_state(i);
441             return nest(x);
442         },
443         get common_prefix_input_state () {
444             let x = completions_o.common_prefix_input_state;
445             if (x)
446                 return nest(x);
447             return null;
448         }
449     };
453 provide("completers");