Add webjump integration with OpenSearch search engines
[conkeror.git] / modules / minibuffer-read.js
blob2de54b497f9f3007088a68f173473bbdc02e9b3e
2 define_variable("default_minibuffer_auto_complete_delay", 150,
3                      "Delay (in milliseconds) after the most recent key stroke before auto-completing.");
5 define_variable("minibuffer_auto_complete_preferences", {});
7 define_variable("minibuffer_auto_complete_default", false, "Boolean specifying whether to auto-complete by default.\nThe user variable `minibuffer_auto_complete_preferences' overrides this.");
9 var minibuffer_history_data = new string_hashmap();
11 /* FIXME: These should possibly be saved to disk somewhere */
12 define_variable("minibuffer_history_max_items", 100, "Maximum number of minibuffer history entries stored.\nOlder history entries are truncated after this limit is reached.");
15 /* The parameter `args' specifies the arguments.  In addition, the
16  * arguments for basic_minibuffer_state are also allowed.
17  *
18  * history:           [optional] specifies a string to identify the history list to use
19  *
20  * completer
21  *
22  * match_required
23  *
24  * default_completion  only used if match_required is set to true
25  *
26  * $valiator          [optional]
27  *          specifies a function
28  */
29 define_keywords("$history", "$validator",
31                 "$completer", "$match_required", "$default_completion",
32                 "$auto_complete", "$auto_complete_initial", "$auto_complete_conservative",
33                 "$auto_complete_delay",
34                 "$space_completes");
35 /* FIXME: support completing in another thread */
36 function text_entry_minibuffer_state(continuation) {
37     keywords(arguments);
39     basic_minibuffer_state.call(this, forward_keywords(arguments));
40     this.keymap = minibuffer_keymap;
42     this.continuation = continuation;
43     if (arguments.$history)
44     {
45         this.history = minibuffer_history_data.get_put_default(arguments.$history, []);
46         this.history_index = this.history.length;
47     }
49     this.validator = arguments.$validator;
51     if (arguments.$completer != null)
52     {
53         this.completer = arguments.$completer;
54         let auto = arguments.$auto_complete;
55         while (typeof(auto) == "string")
56             auto = minibuffer_auto_complete_preferences[auto];
57         if (auto == null)
58             auto = minibuffer_auto_complete_default;
59         this.auto_complete = auto;
60         this.auto_complete_initial = !!arguments.$auto_complete_initial;
61         this.auto_complete_conservative = !!arguments.$auto_complete_conservative;
62         let delay = arguments.$auto_complete_delay;
63         if (delay == null)
64             delay = default_minibuffer_auto_complete_delay;
65         this.auto_complete_delay = delay;
66         this.completions = null;
67         this.completions_valid = false;
68         this.space_completes = !!arguments.$space_completes;
69         this.completions_timer_ID = null;
70         this.completions_display_element = null;
71         this.selected_completion_index = -1;
72         this.match_required  = !!arguments.$match_required;
73         if (this.match_required)
74             this.default_completion = arguments.$default_completion;
75     }
78 function completions_tree_view(minibuffer_state)
80     this.minibuffer_state = minibuffer_state;
83 var atom_service = Cc["@mozilla.org/atom-service;1"].getService(Ci.nsIAtomService);
85 completions_tree_view.prototype = {
86     get rowCount () {
87         var c = this.minibuffer_state.completions;
88         if (!c)
89             return 0;
90         return c.count;
91     },
92     getCellText : function(row,column){
93         var c = this.minibuffer_state.completions;
94         if (row >= c.count)
95             return null;
96         if (column.index == 0)
97             return c.get_string(row);
98         if (c.get_description)
99             return c.get_description(row);
100         return "";
101     },
102     setTree : function(treebox){ this.treebox = treebox; },
103     isContainer: function(row){ return false; },
104     isSeparator: function(row){ return false; },
105     isSorted: function(){ return false; },
106     getLevel: function(row){ return 0; },
107     getImageSrc: function(row,col){ return null; },
108     getRowProperties: function(row,props){},
109     getCellProperties: function(row,col,props){
110         if (col.index == 0)
111             props.AppendElement(atom_service.getAtom("completion-string"));
112         else
113             props.AppendElement(atom_service.getAtom("completion-description"));
114     },
115     getColumnProperties: function(colid,col,props){}
118 // inherit from basic_minibuffer_state
119 text_entry_minibuffer_state.prototype = {
120     __proto__: basic_minibuffer_state.prototype,
121     load : function (window) {
122         this.window = window;
123         if (this.completer) {
124             // Create completion display element if needed
125             if (!this.completion_element)
126             {
127                 /* FIXME: maybe use the dom_generator */
128                 var tree = create_XUL(window, "tree");
129                 var s = this;
130                 tree.addEventListener("select", function () {
131                         s.selected_completion_index = s.completions_display_element.currentIndex;
132                         s.handle_completion_selected();
133                     }, true, false);
134                 tree.setAttribute("class", "completions");
136                 tree.setAttribute("rows", "8");
138                 tree.setAttribute("collapsed", "true");
140                 tree.setAttribute("hidecolumnpicker", "true");
141                 tree.setAttribute("hideheader", "true");
143                 var treecols = create_XUL(window, "treecols");
144                 tree.appendChild(treecols);
145                 var treecol = create_XUL(window, "treecol");
146                 treecol.setAttribute("flex", "1");
147                 treecols.appendChild(treecol);
148                 treecol = create_XUL(window, "treecol");
149                 treecol.setAttribute("flex", "1");
150                 treecols.appendChild(treecol);
151                 tree.appendChild(create_XUL(window, "treechildren"));
153                 window.minibuffer.insert_before(tree);
154                 tree.view = new completions_tree_view(this);
155                 this.completions_display_element = tree;
157                 /* This is the initial loading of this minibuffer
158                  * state.  If this.complete_initial is true, generate
159                  * completions. */
160                 if (this.auto_complete_initial)
161                     this.handle_input();
162             }
164             this.update_completions_display();
165         }
166     },
168     unload : function (window) {
169         if (this.completions_display_element)
170             this.completions_display_element.setAttribute("collapsed", "true");
171     },
173     destroy : function (window) {
174         if (this.completions != null && this.completions.destroy)
175             this.completions.destroy();
176         delete this.completions;
177         if (this.completions_cont)
178             this.completions_cont.throw(abort());
179         delete this.completions_cont;
181         var el = this.completions_display_element;
182         if (el)
183         {
184             el.parentNode.removeChild(el);
185             this.completions_display_element = null;
186         }
187         if (this.continuation)
188             this.continuation.throw(abort());
189     },
191     handle_input : function () {
192         if (!this.completer) return;
194         this.completions_valid = false;
196         if (!this.auto_complete) return;
198         var s = this;
200         if (this.auto_complete_delay > 0) {
201             if (this.completions_timer_ID != null)
202                 this.window.clearTimeout(this.completions_timer_ID);
203             this.completions_timer_ID = this.window.setTimeout(
204                 function () {
205                     s.completions_timer_ID = null;
206                     s.update_completions(true /* auto */, true /* update completions display */);
207                 }, this.auto_complete_delay);
208             return;
209         }
211         s.update_completions(true /* auto */, true /* update completions display */);
212     },
214     ran_minibuffer_command : function () {
215         this.handle_input();
216     },
218     update_completions_display : function () {
220         var m = this.window.minibuffer;
222         if (m.current_state == this)
223         {
224             if (this.completions && this.completions.count > 0)
225             {
226                 this.completions_display_element.view = this.completions_display_element.view;
227                 this.completions_display_element.setAttribute("collapsed", "false");
229                 this.completions_display_element.currentIndex = this.selected_completion_index;
230                 this.completions_display_element.treeBoxObject.scrollToRow(this.selected_completion_index);
231             } else {
232                 this.completions_display_element.setAttribute("collapsed", "true");
233             }
234         }
235     },
237     /* If auto is true, this update is due to auto completion, rather
238      * than specifically requested. */
239     update_completions : function (auto, update_display) {
241         if (this.completions_timer_ID != null) {
242             this.window.clearTimeout(this.completions_timer_ID);
243             this.completions_timer_ID = null;
244         }
246         let m = this.window.minibuffer;
248         if (this.completions_cont) {
249             this.completions_cont.throw(abort());
250             this.completions_cont = null;
251         }
253         let c = this.completer(m._input_text, m._selection_start,
254                                auto && this.auto_complete_conservative);
256         if (is_coroutine(c)) {
257             let s = this;
258             let already_done = false;
259             this.completions_cont = co_call(function () {
260                 var x;
261                 try {
262                     x = yield c;
263                 } finally {
264                     s.completions_cont = null;
265                     already_done = true;
266                 }
267                 s.update_completions_done(x, update_display);
268             }());
270             // In case the completer actually already finished
271             if (already_done)
272                 this.completions_cont = null;
273             return;
274         } else
275             this.update_completions_done(c, update_display);
276     },
278     update_completions_done : function update_completions_done(c, update_display) {
280         /* The completer should return undefined if completion was not
281          * attempted due to auto being true.  Otherwise, it can return
282          * null to indicate no completions. */
283         if (this.completions != null && this.completions.destroy)
284             this.completions.destroy();
286         this.completions = c;
287         this.completions_valid = true;
288         this.applied_common_prefix = false;
290         let i = -1;
291         if (c && c.count > 0) {
292             if (this.match_required) {
293                 if (c.count == 1)
294                     i = 0;
295                 else if (c.default_completion != null)
296                     i = c.default_completion;
297                 else if (this.default_completion && this.completions.index_of)
298                     i = this.completions.index_of(this.default_completion);
299             }
300             this.selected_completion_index = i;
301         }
303         if (update_display)
304             this.update_completions_display();
305     },
307     select_completion : function (i) {
308         this.selected_completion_index = i;
309         this.completions_display_element.currentIndex = i;
310         if (i >= 0)
311             this.completions_display_element.treeBoxObject.ensureRowIsVisible(i);
312         this.handle_completion_selected();
313     },
315     handle_completion_selected : function () {
316         /**
317          * When a completion is selected, apply it to the input text
318          * if a match is not "required"; otherwise, the completion is
319          * only displayed.
320          */
321         var i = this.selected_completion_index;
322         var m = this.window.minibuffer;
323         var c = this.completions;
325         if (this.completions_valid && c && !this.match_required && i >= 0 && i < c.count)
326         {
327             m.set_input_state(c.get_input_state(i));
328         }
329     }
332 function minibuffer_complete(window, count)
334     var m = window.minibuffer;
335     var s = m.current_state;
336     if (!(s instanceof text_entry_minibuffer_state))
337         throw new Error("Invalid minibuffer state");
338     if (!s.completer)
339         return;
340     var just_completed_manually = false;
341     if (!s.completions_valid || s.completions === undefined) {
342         if (s.completions_timer_ID == null)
343             just_completed_manually = true;
344         s.update_completions(false /* not auto */, true /* update completions display */);
346         // If the completer is a coroutine, nothing we can do here
347         if (!s.completions_valid)
348             return;
349     }
351     var c = s.completions;
353     if (!c || c.count == 0)
354         return;
356     var e = s.completions_display_element;
357     var new_index = -1;
359     let common_prefix;
361     if (count == 1 && !s.applied_common_prefix && (common_prefix = c.common_prefix_input_state))
362     {
363         m.set_input_state(common_prefix);
364         s.applied_common_prefix = true;
365     } else if (!just_completed_manually) {
366         if (e.currentIndex != -1)
367         {
368             new_index = (e.currentIndex + count) % c.count;
369             if (new_index < 0)
370                 new_index += c.count;
371         } else {
372             new_index = (count - 1) % c.count;
373             if (new_index < 0)
374                 new_index += c.count;
375         }
376     }
378     if (new_index != -1)
379         s.select_completion(new_index);
381 interactive("minibuffer-complete", function (I) {minibuffer_complete(I.window, I.p);});
382 interactive("minibuffer-complete-previous", function (I) {minibuffer_complete(I.window, -I.p);});
384 function exit_minibuffer(window)
386     var m = window.minibuffer;
387     var s = m.current_state;
388     if (!(s instanceof text_entry_minibuffer_state))
389         throw new Error("Invalid minibuffer state");
391     var val = m._input_text;
393     if (s.validator != null && !s.validator(val, m))
394         return;
396     var match = null;
398     if (s.completer && s.match_required) {
399         if (!s.completions_valid || s.completions === undefined)
400             s.update_completions(false /* not conservative */, false /* don't update */);
402         let c = s.completions;
403         let i = s.selected_completion_index;
404         if (c != null && i >= 0 && i < c.count) {
405             if (c.get_value != null)
406                 match = c.get_value(i);
407             else
408                 match = c.get_string(i);
409         } else {
410             m.message("No match");
411             return;
412         }
413     }
415     if (s.history)
416     {
417         s.history.push(val);
418         if (s.history.length > minibuffer_history_max_items)
419             s.history.splice(0, s.history.length - minibuffer_history_max_items);
420     }
421     var cont = s.continuation;
422     delete s.continuation;
423     m.pop_state();
424     if (cont) {
425         if (s.match_required)
426             cont(match);
427         else
428             cont(val);
429     }
431 interactive("exit-minibuffer", function (I) {exit_minibuffer(I.window);});
433 function minibuffer_history_next (window, count)
435     var m = window.minibuffer;
436     var s = m.current_state;
437     if (!(s instanceof text_entry_minibuffer_state))
438         throw new Error("Invalid minibuffer state");
439     if (!s.history || s.history.length == 0)
440         return;
441     m._restore_normal_state();
442     var index = s.history_index + count;
443     if (index < 0)
444         index = 0;
445     if (index >= s.history.length)
446         index = s.history.length - 1;
447     s.history_index = index;
448     m._input_text = s.history[index];
449     m._set_selection();
451 interactive("minibuffer-history-next", function (I) {minibuffer_history_next(I.window, I.p);});
452 interactive("minibuffer-history-previous", function (I) {minibuffer_history_next(I.window, -I.p);});
454 // Define the asynchronous minibuffer.read function
455 minibuffer.prototype.read = function () {
456     var s = new text_entry_minibuffer_state((yield CONTINUATION), forward_keywords(arguments));
457     this.push_state(s);
458     var result = yield SUSPEND;
459     yield co_return(result);
462 minibuffer.prototype.read_command = function () {
463     keywords(arguments);
464     var completer = prefix_completer(
465         $completions = function (visitor) interactive_commands.for_each_value(visitor),
466         $get_string = function (x) x.name,
467         $get_description = function (x) x.shortdoc || "",
468         $get_value = function (x) x.name
469     );
471     var result = yield this.read($prompt = "Command", $history = "command",
472         forward_keywords(arguments),
473         $completer = completer,
474         $match_required = true);
475     yield co_return(result);
478 minibuffer.prototype.read_user_variable = function () {
479     keywords(arguments);
480     var completer = prefix_completer(
481         $completions = function (visitor) user_variables.for_each(visitor),
482         $get_string = function (x) x,
483         $get_description = function (x) user_variables.get(x).shortdoc || "",
484         $get_value = function (x) x
485     );
487     var result = yield this.read($prompt = "User variable", $history = "user_variable",
488         forward_keywords(arguments),
489         $completer = completer,
490         $match_required = true);
491     yield co_return(result);