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