Minor updates to new-tabs.js.
[conkeror.git] / modules / minibuffer-read.js
blob743181f116260a60d0b0adbe736e34d663c042ee
1 /**
2  * (C) Copyright 2007 John J. Foerch
3  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
4  *
5  * Use, modification, and distribution are subject to the terms specified in the
6  * COPYING file.
7 **/
9 define_variable("default_minibuffer_auto_complete_delay", 150,
10                      "Delay (in milliseconds) after the most recent key stroke before auto-completing.");
12 define_variable("minibuffer_auto_complete_preferences", {});
14 define_variable("minibuffer_auto_complete_default", false, "Boolean specifying whether to auto-complete by default.\nThe user variable `minibuffer_auto_complete_preferences' overrides this.");
16 var minibuffer_history_data = new string_hashmap();
18 /* FIXME: These should possibly be saved to disk somewhere */
19 define_variable("minibuffer_history_max_items", 100, "Maximum number of minibuffer history entries stored.\nOlder history entries are truncated after this limit is reached.");
22 /* The parameter `args' specifies the arguments.  In addition, the
23  * arguments for basic_minibuffer_state are also allowed.
24  *
25  * history:           [optional] specifies a string to identify the history list to use
26  *
27  * completer
28  *
29  * match_required
30  *
31  * default_completion  only used if match_required is set to true
32  *
33  * $valiator          [optional]
34  *          specifies a function
35  */
36 define_keywords("$history", "$validator",
38                 "$completer", "$match_required", "$default_completion",
39                 "$auto_complete", "$auto_complete_initial", "$auto_complete_conservative",
40                 "$auto_complete_delay",
41                 "$space_completes");
42 /* FIXME: support completing in another thread */
43 function text_entry_minibuffer_state(continuation) {
44     keywords(arguments);
46     basic_minibuffer_state.call(this, forward_keywords(arguments));
47     this.keymap = minibuffer_keymap;
49     this.continuation = continuation;
50     if (arguments.$history)
51     {
52         this.history = minibuffer_history_data.get_put_default(arguments.$history, []);
53         this.history_index = -1;
54         this.saved_last_history_entry = null;
55     }
57     this.validator = arguments.$validator;
59     if (arguments.$completer != null)
60     {
61         this.completer = arguments.$completer;
62         let auto = arguments.$auto_complete;
63         while (typeof(auto) == "string")
64             auto = minibuffer_auto_complete_preferences[auto];
65         if (auto == null)
66             auto = minibuffer_auto_complete_default;
67         this.auto_complete = auto;
68         this.auto_complete_initial = !!arguments.$auto_complete_initial;
69         this.auto_complete_conservative = !!arguments.$auto_complete_conservative;
70         let delay = arguments.$auto_complete_delay;
71         if (delay == null)
72             delay = default_minibuffer_auto_complete_delay;
73         this.auto_complete_delay = delay;
74         this.completions = null;
75         this.completions_valid = false;
76         this.space_completes = !!arguments.$space_completes;
77         this.completions_timer_ID = null;
78         this.completions_display_element = null;
79         this.selected_completion_index = -1;
80         this.match_required  = !!arguments.$match_required;
81         if (this.match_required)
82             this.default_completion = arguments.$default_completion;
83     }
86 function completions_tree_view(minibuffer_state)
88     this.minibuffer_state = minibuffer_state;
91 var atom_service = Cc["@mozilla.org/atom-service;1"].getService(Ci.nsIAtomService);
93 completions_tree_view.prototype = {
94     get rowCount () {
95         var c = this.minibuffer_state.completions;
96         if (!c)
97             return 0;
98         return c.count;
99     },
100     getCellText : function(row,column){
101         var c = this.minibuffer_state.completions;
102         if (row >= c.count)
103             return null;
104         if (column.index == 0)
105             return c.get_string(row);
106         if (c.get_description)
107             return c.get_description(row);
108         return "";
109     },
110     setTree : function(treebox){ this.treebox = treebox; },
111     isContainer: function(row){ return false; },
112     isSeparator: function(row){ return false; },
113     isSorted: function(){ return false; },
114     getLevel: function(row){ return 0; },
115     getImageSrc: function(row,col){ return null; },
116     getRowProperties: function(row,props){},
117     getCellProperties: function(row,col,props){
118         if (col.index == 0)
119             props.AppendElement(atom_service.getAtom("completion-string"));
120         else
121             props.AppendElement(atom_service.getAtom("completion-description"));
122     },
123     getColumnProperties: function(colid,col,props){}
126 // inherit from basic_minibuffer_state
127 text_entry_minibuffer_state.prototype = {
128     __proto__: basic_minibuffer_state.prototype,
129     load : function (window) {
130         this.window = window;
131         if (this.completer) {
132             // Create completion display element if needed
133             if (!this.completion_element)
134             {
135                 /* FIXME: maybe use the dom_generator */
136                 var tree = create_XUL(window, "tree");
137                 var s = this;
138                 tree.addEventListener("select", function () {
139                         s.selected_completion_index = s.completions_display_element.currentIndex;
140                         s.handle_completion_selected();
141                     }, true, false);
142                 tree.setAttribute("class", "completions");
144                 tree.setAttribute("rows", "8");
146                 tree.setAttribute("collapsed", "true");
148                 tree.setAttribute("hidecolumnpicker", "true");
149                 tree.setAttribute("hideheader", "true");
151                 var treecols = create_XUL(window, "treecols");
152                 tree.appendChild(treecols);
153                 var treecol = create_XUL(window, "treecol");
154                 treecol.setAttribute("flex", "1");
155                 treecols.appendChild(treecol);
156                 treecol = create_XUL(window, "treecol");
157                 treecol.setAttribute("flex", "1");
158                 treecols.appendChild(treecol);
159                 tree.appendChild(create_XUL(window, "treechildren"));
161                 window.minibuffer.insert_before(tree);
162                 tree.view = new completions_tree_view(this);
163                 this.completions_display_element = tree;
165                 /* This is the initial loading of this minibuffer
166                  * state.  If this.complete_initial is true, generate
167                  * completions. */
168                 if (this.auto_complete_initial)
169                     this.handle_input();
170             }
172             this.update_completions_display();
173         }
174     },
176     unload : function (window) {
177         if (this.completions_display_element)
178             this.completions_display_element.setAttribute("collapsed", "true");
179     },
181     destroy : function (window) {
182         if (this.completions != null && this.completions.destroy)
183             this.completions.destroy();
184         delete this.completions;
185         if (this.completions_cont)
186             this.completions_cont.throw(abort());
187         delete this.completions_cont;
189         var el = this.completions_display_element;
190         if (el)
191         {
192             el.parentNode.removeChild(el);
193             this.completions_display_element = null;
194         }
195         if (this.continuation)
196             this.continuation.throw(abort());
197     },
199     handle_input : function () {
200         if (!this.completer) return;
202         this.completions_valid = false;
204         if (!this.auto_complete) return;
206         var s = this;
208         if (this.auto_complete_delay > 0) {
209             if (this.completions_timer_ID != null)
210                 this.window.clearTimeout(this.completions_timer_ID);
211             this.completions_timer_ID = this.window.setTimeout(
212                 function () {
213                     s.completions_timer_ID = null;
214                     s.update_completions(true /* auto */, true /* update completions display */);
215                 }, this.auto_complete_delay);
216             return;
217         }
219         s.update_completions(true /* auto */, true /* update completions display */);
220     },
222     ran_minibuffer_command : function () {
223         this.handle_input();
224     },
226     update_completions_display : function () {
228         var m = this.window.minibuffer;
230         if (m.current_state == this)
231         {
232             if (this.completions && this.completions.count > 0)
233             {
234                 this.completions_display_element.view = this.completions_display_element.view;
235                 this.completions_display_element.setAttribute("collapsed", "false");
237                 this.completions_display_element.currentIndex = this.selected_completion_index;
238                 this.completions_display_element.treeBoxObject.scrollToRow(this.selected_completion_index);
239             } else {
240                 this.completions_display_element.setAttribute("collapsed", "true");
241             }
242         }
243     },
245     /* If auto is true, this update is due to auto completion, rather
246      * than specifically requested. */
247     update_completions : function (auto, update_display) {
249         if (this.completions_timer_ID != null) {
250             this.window.clearTimeout(this.completions_timer_ID);
251             this.completions_timer_ID = null;
252         }
254         let m = this.window.minibuffer;
256         if (this.completions_cont) {
257             this.completions_cont.throw(abort());
258             this.completions_cont = null;
259         }
261         let c = this.completer(m._input_text, m._selection_start,
262                                auto && this.auto_complete_conservative);
264         if (is_coroutine(c)) {
265             let s = this;
266             let already_done = false;
267             this.completions_cont = co_call(function () {
268                 var x;
269                 try {
270                     x = yield c;
271                 } finally {
272                     s.completions_cont = null;
273                     already_done = true;
274                 }
275                 s.update_completions_done(x, update_display);
276             }());
278             // In case the completer actually already finished
279             if (already_done)
280                 this.completions_cont = null;
281             return;
282         } else
283             this.update_completions_done(c, update_display);
284     },
286     update_completions_done : function update_completions_done(c, update_display) {
288         /* The completer should return undefined if completion was not
289          * attempted due to auto being true.  Otherwise, it can return
290          * null to indicate no completions. */
291         if (this.completions != null && this.completions.destroy)
292             this.completions.destroy();
294         this.completions = c;
295         this.completions_valid = true;
296         this.applied_common_prefix = false;
298         let i = -1;
299         if (c && c.count > 0) {
300             if (this.match_required) {
301                 if (c.count == 1)
302                     i = 0;
303                 else if (c.default_completion != null)
304                     i = c.default_completion;
305                 else if (this.default_completion && this.completions.index_of)
306                     i = this.completions.index_of(this.default_completion);
307             }
308             this.selected_completion_index = i;
309         }
311         if (update_display)
312             this.update_completions_display();
313     },
315     select_completion : function (i) {
316         this.selected_completion_index = i;
317         this.completions_display_element.currentIndex = i;
318         if (i >= 0)
319             this.completions_display_element.treeBoxObject.ensureRowIsVisible(i);
320         this.handle_completion_selected();
321     },
323     handle_completion_selected : function () {
324         /**
325          * When a completion is selected, apply it to the input text
326          * if a match is not "required"; otherwise, the completion is
327          * only displayed.
328          */
329         var i = this.selected_completion_index;
330         var m = this.window.minibuffer;
331         var c = this.completions;
333         if (this.completions_valid && c && !this.match_required && i >= 0 && i < c.count)
334         {
335             m.set_input_state(c.get_input_state(i));
336         }
337     }
340 function minibuffer_complete(window, count)
342     var m = window.minibuffer;
343     var s = m.current_state;
344     if (!(s instanceof text_entry_minibuffer_state))
345         throw new Error("Invalid minibuffer state");
346     if (!s.completer)
347         return;
348     var just_completed_manually = false;
349     if (!s.completions_valid || s.completions === undefined) {
350         if (s.completions_timer_ID == null)
351             just_completed_manually = true;
352         s.update_completions(false /* not auto */, true /* update completions display */);
354         // If the completer is a coroutine, nothing we can do here
355         if (!s.completions_valid)
356             return;
357     }
359     var c = s.completions;
361     if (!c || c.count == 0)
362         return;
364     var e = s.completions_display_element;
365     var new_index = -1;
367     let common_prefix;
369     if (count == 1 && !s.applied_common_prefix && (common_prefix = c.common_prefix_input_state))
370     {
371         m.set_input_state(common_prefix);
372         s.applied_common_prefix = true;
373     } else if (!just_completed_manually) {
374         if (e.currentIndex != -1)
375         {
376             new_index = (e.currentIndex + count) % c.count;
377             if (new_index < 0)
378                 new_index += c.count;
379         } else {
380             new_index = (count - 1) % c.count;
381             if (new_index < 0)
382                 new_index += c.count;
383         }
384     }
386     if (new_index != -1)
387         s.select_completion(new_index);
389 interactive("minibuffer-complete", null, function (I) {minibuffer_complete(I.window, I.p);});
390 interactive("minibuffer-complete-previous", null, function (I) {minibuffer_complete(I.window, -I.p);});
392 function exit_minibuffer(window)
394     var m = window.minibuffer;
395     var s = m.current_state;
396     if (!(s instanceof text_entry_minibuffer_state))
397         throw new Error("Invalid minibuffer state");
399     var val = m._input_text;
401     if (s.validator != null && !s.validator(val, m))
402         return;
404     var match = null;
406     if (s.completer && s.match_required) {
407         if (!s.completions_valid || s.completions === undefined)
408             s.update_completions(false /* not conservative */, false /* don't update */);
410         let c = s.completions;
411         let i = s.selected_completion_index;
412         if (c != null && i >= 0 && i < c.count) {
413             if (c.get_value != null)
414                 match = c.get_value(i);
415             else
416                 match = c.get_string(i);
417         } else {
418             m.message("No match");
419             return;
420         }
421     }
423     if (s.history)
424     {
425         s.history.push(val);
426         if (s.history.length > minibuffer_history_max_items)
427             s.history.splice(0, s.history.length - minibuffer_history_max_items);
428     }
429     var cont = s.continuation;
430     delete s.continuation;
431     m.pop_state();
432     if (cont) {
433         if (s.match_required)
434             cont(match);
435         else
436             cont(val);
437     }
439 interactive("exit-minibuffer", null, function (I) {exit_minibuffer(I.window);});
441 function minibuffer_history_next (window, count)
443     var m = window.minibuffer;
444     var s = m.current_state;
445     if (!(s instanceof text_entry_minibuffer_state))
446         throw new Error("Invalid minibuffer state");
447     if (!s.history || s.history.length == 0)
448         throw interactive_error("No history available.");
449     if (count == 0)
450         return;
451     var index = s.history_index;
452     if (count > 0 && index == -1)
453         throw interactive_error("End of history; no next item");
454     else if (count < 0 && index == 0) {
455         throw interactive_error("Beginning of history; no preceding item");
456     }
457     if (index == -1) {
458         s.saved_last_history_entry = m._input_text;
459         index = s.history.length + count;
460     } else
461         index = index + count;
463     if (index < 0)
464         index = 0;
466     m._restore_normal_state();
467     if (index >= s.history.length) {
468         index = -1;
469         m._input_text = s.saved_last_history_entry;
470     } else {
471         m._input_text = s.history[index];
472     }
473     s.history_index = index;
474     m._set_selection();
475     s.handle_input();
477 interactive("minibuffer-history-next", null, function (I) {minibuffer_history_next(I.window, I.p);});
478 interactive("minibuffer-history-previous", null, function (I) {minibuffer_history_next(I.window, -I.p);});
480 // Define the asynchronous minibuffer.read function
481 minibuffer.prototype.read = function () {
482     var s = new text_entry_minibuffer_state((yield CONTINUATION), forward_keywords(arguments));
483     this.push_state(s);
484     var result = yield SUSPEND;
485     yield co_return(result);
488 minibuffer.prototype.read_command = function () {
489     keywords(
490         arguments,
491         $prompt = "Command", $history = "command",
492         $completer = prefix_completer(
493             $completions = function (visitor) interactive_commands.for_each_value(visitor),
494             $get_string = function (x) x.name,
495             $get_description = function (x) x.shortdoc || "",
496             $get_value = function (x) x.name),
497         $match_required = true);
498     var result = yield this.read(forward_keywords(arguments));
499     yield co_return(result);
502 minibuffer.prototype.read_user_variable = function () {
503     keywords(
504         arguments,
505         $prompt = "User variable", $history = "user_variable",
506         $completer = prefix_completer(
507             $completions = function (visitor) user_variables.for_each(visitor),
508             $get_string = function (x) x,
509             $get_description = function (x) user_variables.get(x).shortdoc || "",
510             $get_value = function (x) x),
511         $match_required = true);
512     var result = yield this.read(forward_keywords(arguments));
513     yield co_return(result);
516 minibuffer.prototype.read_preference = function minibuffer__read_preference () {
517     keywords(arguments,
518              $prompt = "Preference:", $history = "preference",
519              $completer = prefix_completer(
520                  $completions = preferences.getBranch(null).getChildList("", {}),
521                  $get_description = function (pref) {
522                      let default_value = get_default_pref(pref);
523                      let value = get_pref(pref);
524                      if (value == default_value)
525                          value = null;
526                      let type;
527                      switch (preferences.getBranch(null).getPrefType(pref)) {
528                      case Ci.nsIPrefBranch.PREF_STRING:
529                          type = "string";
530                          break;
531                      case Ci.nsIPrefBranch.PREF_INT:
532                          type = "int";
533                          break;
534                      case Ci.nsIPrefBranch.PREF_BOOL:
535                          type = "boolean";
536                          break;
537                      }
538                      let out = type + ":";
539                      if (value != null)
540                          out += " " + pretty_print_value(value);
541                      if (default_value != null)
542                          out += " (" + pretty_print_value(default_value) + ")";
543                      return out;
544                  }),
545              $match_required = true);
546     var result = yield this.read(forward_keywords(arguments));
547     yield co_return(result);