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