comment
[conkeror.git] / modules / minibuffer-completion.js
blob80bb3409a2fe3b84f3809e775b25ee920e7ba338
1 /**
2  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
3  * (C) Copyright 2008 Nelson Elhage
4  * (C) Copyright 2010 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 in_module(null);
15 require("minibuffer.js");
17 /**
18  * Generic completer function factory.
19  *
20  * Keyword arguments:
21  * - $completions: Either a visit function or an array.  If a function, the
22  *   function's argument is a function which pushes argument into the
23  *   completions array.  Otherwise, it uses the provided array.
24  * - $get_value: TODO.
25  * - $get_string: TODO. Optional, default: identity function.
26  * - $get_description: TODO: Optional, default: function returning "".
27  * - $get_icon: optional. returns an icon for the completions line.
28  *
29  * TODO: Exactly what does this function return and how do you use it?
30  */
31 define_keywords("$completions", "$get_string", "$get_description",
32                 "$get_value", "$get_icon");
33 function all_word_completer () {
34     keywords(arguments,
35              $get_description = function (x) "",
36              $get_string = function (x) x,
37              $get_icon = null);
38     var completions = arguments.$completions;
39     var get_string = arguments.$get_string;
40     var get_description = arguments.$get_description;
41     var get_value = arguments.$get_value;
42     var get_icon = arguments.$get_icon;
43     var arr;
44     var completer = function (input, pos, conservative) {
45         if (input.length == 0 && conservative)
46             return undefined;
47         var words = input.toLowerCase().split(" ");
48         var data = arr.filter(function (x) {
49                 var s = get_string(x);
50                 var d = get_description(x);
51                 for (var i = 0; i < words.length; ++i) {
52                     if (s.toLowerCase().indexOf(words[i]) == -1 &&
53                         d.toLowerCase().indexOf(words[i]) == -1)
54                     {
55                         return false;
56                     }
57                 }
58                 return true;
59             });
60         return {count: data.length,
61                 index_of:  function (x) data.indexOf(x),
62                 get_string: function (i) get_string(data[i]),
63                 get_description : function (i) get_description(data[i]),
64                 get_input_state: function (i) [get_string(data[i])],
65                 get_value: function (i) (get_value ? get_value(data[i]) : data[i]),
66                 get_icon: function (i) (get_icon ? get_icon(data[i]) : null)
67                };
68     };
69     completer.refresh = function () {
70         if (typeof completions == "function") {
71             arr = [];
72             completions(function (x) { arr.push(x); });
73         } else
74             arr = completions;
75     };
76     completer.refresh();
77     return completer;
80 function get_common_prefix_length (a, b, len) {
81     var lim;
82     if (len != null && len < a.length)
83         lim = len;
84     else
85         lim = a.length;
86     if (b < lim)
87         lim = b;
88     var i;
89     for (i = 0; i < lim && a[i] == b[i]; ++i);
90     return i;
93 function get_partial_completion_input_state (x, prefix_end, suffix_begin, orig_str) {
94     if (suffix_begin < orig_str.length) {
95         if (orig_str[suffix_begin] == " ")
96             suffix_begin++;
97         let sel = x.length + prefix_end + 1;
98         return [orig_str.substring(0, prefix_end) + x + " " + orig_str.substring(suffix_begin),
99                 sel, sel];
100     } else {
101         let sel = x.length + prefix_end;
102         return [orig_str.substring(0, prefix_end) + x, sel, sel];
103     }
106 function prefix_completer () {
107     keywords(arguments,
108              $get_description = function (x) "",
109              $get_string = function (x) x,
110              $get_icon = null);
111     var completions = arguments.$completions;
112     var get_string = arguments.$get_string;
113     var get_description = arguments.$get_description;
114     var get_value = arguments.$get_value;
115     var get_icon = arguments.$get_icon;
116     var arr;
117     if (typeof completions == "function") {
118         arr = [];
119         completions(function (x) { arr.push(x); });
120     } else
121         arr = completions.slice();
122     arr.sort(function (a,b) {
123             a = get_string(a);
124             b = get_string(b);
125             if (a < b)
126                 return -1;
127             if (a > b)
128                 return 1;
129             return 0;
130         });
131     return function (input, pos, conservative) {
132         var common_prefix = null;
133         if (pos == 0 && conservative)
134             return undefined;
135         var input_prefix = input.substring(0,pos);
136         var default_completion = null;
137         var i = 0;
138         var data = arr.filter(function (x) {
139             var s = get_string(x);
140             if (s == input) {
141                 default_completion = i;
142                 var retval = true;
143             } else
144                 retval = (s.length >= pos && s.substring(0,pos) == input_prefix);
145             if (retval)
146                 ++i;
147             return retval;
148         });
149         if (data.length > 0) {
150             let a = get_string(data[0]);
151             let b = get_string(data[data.length - 1]);
152             let i = get_common_prefix_length(a, b);
153             if (i > pos) {
154                 common_prefix = a.substring(0,i);
155                 if (!default_completion) {
156                     for (let j = 0; j < data.length; ++j) {
157                         if (get_string(data[j]) == common_prefix) {
158                             default_completion = j;
159                             break;
160                         }
161                     }
162                 }
163             }
164         }
165         return {count:data.length,
166                 index_of:  function (x) data.indexOf(x),
167                 get_string: function (i) get_string(data[i]),
168                 get_description: function (i) get_description(data[i]),
169                 get_input_state: function (i) get_partial_completion_input_state(get_string(data[i]), 0, pos, input),
170                 get_value: function (i) (get_value ? get_value(data[i]) : data[i]),
171                 get_icon: function (i) (get_icon ? get_icon(data[i]) : null),
172                 get common_prefix_input_state () {
173                     return common_prefix && get_partial_completion_input_state(common_prefix, 0, pos, input);
174                 },
175                 default_completion: default_completion
176                };
177     };
180 function javascript_completer (buffer) {
181     var window = buffer.window;
183     return function (input, pos, conservative) {
184         // Derived from Vimperator JavaScript completion
185         if (pos == 0 && conservative)
186             return undefined;
187         var str = input.substr(0, pos);
188         var matches = str.match(/^(.*?)(\s*\.\s*)?(\w*)$/);
189         var filter = matches[3] || "";
190         var start = matches[1].length - 1;
191         var offset = matches[1] ? matches[1].length : 0;
192         offset += matches[2] ? matches[2].length : 0;
194         if (matches[2]) {
195             let brackets = 0, parentheses = 0;
196         outer:
197             for (; start >= 0; start--) {
198                 switch (matches[1][start]) {
199                 case ";":
200                 case "{":
201                     break outer;
203                 case "]":
204                     brackets--;
205                     break;
206                 case "[":
207                     brackets++;
208                     break;
209                 case ")":
210                     parentheses--;
211                     break;
212                 case "(":
213                     parentheses++;
214                     break;
215                 }
216                 if (brackets > 0 || parentheses > 0)
217                     break outer;
218             }
219         }
221         var objects = [];
222         var source_obj ;
223         var data = [];
224         var common_prefix_len = null;
225         var common_prefix = null;
227         function add_completion (str, desc) {
228             if (common_prefix != null)
229                 common_prefix_len = get_common_prefix_length(common_prefix, str, common_prefix_len);
230             else
231                 common_prefix = str;
232             data.push([str,desc]);
233         }
234         if (matches[1].substr(start+1)) {
235             try {
236                 source_obj = eval(matches[1].substr(start+1));
237             } catch (e) {}
238         } else {
239             source_obj = conkeror;
240             if ("window".substring(0,filter.length) == filter)
241                 add_completion("window", "object");
242             if ("buffer".substring(0,filter.length) == filter)
243                 add_completion("buffer", "object");
244         }
246         if (source_obj != null) {
247             try {
248                 for (let i in source_obj) {
249                     if (i.substring(0,filter.length) != filter)
250                         continue;
251                     let type, description;
252                     try {
253                         type = typeof(source_obj[i]);
254                     } catch (e) { type = "unknown type"; }
255                     if (type == "number" || type == "string" || type == "boolean") {
256                         description = type + ": " + source_obj[i];
257                     } else
258                         description = type;
259                     add_completion(i, description);
260                 }
261             } catch (e) {}
262         }
263         if (common_prefix != null && common_prefix_len > 0)
264             common_prefix = common_prefix.substr(0, common_prefix_len);
265         else if (common_prefix_len != null)
266             common_prefix = null;
267         return {count:data.length,
268                 get_string: function (i) data[i][0],
269                 get_description: function (i) data[i][1],
270                 get_input_state: function (i) get_partial_completion_input_state(data[i][0], offset, pos, input),
271                 get common_prefix_input_state  () {
272                     return common_prefix && get_partial_completion_input_state(common_prefix, offset, pos, input);
273                 }
274                };
275     }
279 function merge_completers (completers) {
280     if(completers.length == 0)
281         return null;
282     return function (input, pos, conservative) {
283         var results = [];
284         var count = 0;
285         for (let i = 0; i < completers.length; ++i) {
286             let r = yield completers[i](input, pos, conservative);
287             if (r != null && (r.count > 0 || "get_match_required" in r)) {
288                 results.push(r);
289                 count += r.count;
290             }
291         }
292         function forward (name) {
293             return function () {
294                 var args = Array.prototype.slice.call(arguments);
295                 var i = args.shift();
296                 for (var j=0; j < results.length; j++) {
297                     var r = results[j];
298                     if (i < r.count) {
299                         if (name in r && r[name] != null) {
300                             args.unshift(i);
301                             return r[name].apply(this, args);
302                         } else {
303                             return null;
304                         }
305                     }
306                     i -= r.count;
307                 }
308                 return null;
309             }
310         }
311         function combine_or (name) {
312             return function() {
313                 var b = false;
314                 for (var j=0; j < results.length; j++) {
315                     var r = results[j];
316                     if (name in r && r[name] != null) {
317                         b = b || r[name].apply(this, arguments);
318                     }
319                 }
320                 return b;
321             }
322         }
323         yield co_return({count: count,
324                          get_string: forward('get_string'),
325                          get_description: forward('get_description'),
326                          get_input_state: forward('get_input_state'),
327                          get_icon: forward('get_icon'),
328                          destroy: forward('destroy'),
329                          get_match_required: combine_or('get_match_required')
330                         });
331     };
334 function nest_completions (completions, prefix, suffix) {
335     if (prefix == null)
336         prefix = "";
337     if (suffix == null)
338         suffix = "";
339     function nest (x) {
340         let [s, a, b] = x;
341         if (a == null)
342             a = s.length;
343         if (b == null)
344             b = s.length;
345         return [prefix + s + suffix, a + prefix.length, b + prefix.length];
346     }
347     return {
348         __proto__: completions,
349         get_input_state: function (i) nest(completions.get_input_state(i)),
350         get common_prefix_input_state () {
351             let x = completions.common_prefix_input_state;
352             if (x)
353                 return nest(x);
354             return null;
355         }
356     };
361  * Generic simple completer for associative arrays.
363  * - `options' is an associative array, where the key represents a string that the
364  *   user can complete to.
365  * - `query' is the string to show in the minibuffer.
366  */
367 function completer_with_mappings (options, query) {
368     var completer = all_word_completer(
369         $completions = function (push) {
370             for (var i in options)
371                 push(i);
372         });
373     yield co_return(
374         yield get_recent_conkeror_window().minibuffer.read(
375             $prompt = query,
376             $completer = completer
377         )
378     );
381 provide("minibuffer-completion");