completions_tree_view.setTree: fix trivial typo bug
[conkeror.git] / modules / minibuffer-read.js
blob3f934e3581c9d25a9adb9d589c81906ce85075af
1 /**
2  * (C) Copyright 2007-2010 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 in_module(null);
11 define_variable("default_minibuffer_auto_complete_delay", 150,
12     "Delay (in milliseconds) after the most recent key-stroke "+
13     "before auto-completing.");
15 define_variable("minibuffer_auto_complete_preferences", {});
17 define_variable("minibuffer_auto_complete_default", false,
18     "Boolean specifying whether to auto-complete by default. "+
19     "The user variable `minibuffer_auto_complete_preferences' "+
20     "overrides this.");
22 var minibuffer_history_data = {};
24 /* FIXME: These should possibly be saved to disk somewhere */
25 define_variable("minibuffer_history_max_items", 100,
26     "Maximum number of minibuffer history entries stored. Older "+
27     "history entries are truncated after this limit is reached.");
30 /* The parameter `args' specifies the arguments.  In addition, the
31  * arguments for basic_minibuffer_state are also allowed.
32  *
33  * history:           [optional] specifies a string to identify the history list to use
34  *
35  * completer
36  *
37  * match_required
38  *
39  * default_completion  only used if match_required is set to true
40  *
41  * $valiator          [optional]
42  *          specifies a function
43  */
44 define_keywords("$keymap", "$history", "$validator",
45                 "$completer", "$match_required", "$default_completion",
46                 "$auto_complete", "$auto_complete_initial", "$auto_complete_conservative",
47                 "$auto_complete_delay",
48                 "$space_completes");
49 /* FIXME: support completing in another thread */
50 function text_entry_minibuffer_state (minibuffer, continuation) {
51     keywords(arguments, $keymap = minibuffer_keymap);
53     basic_minibuffer_state.call(this, minibuffer, forward_keywords(arguments));
55     this.continuation = continuation;
56     if (arguments.$history) {
57         this.history = minibuffer_history_data[arguments.$history] =
58             minibuffer_history_data[arguments.$history] || [];
59         this.history_index = -1;
60         this.saved_last_history_entry = null;
61     }
63     this.validator = arguments.$validator;
65     if (arguments.$completer != null) {
66         this.completer = arguments.$completer;
67         let auto = arguments.$auto_complete;
68         while (typeof(auto) == "string")
69             auto = minibuffer_auto_complete_preferences[auto];
70         if (auto == null)
71             auto = minibuffer_auto_complete_default;
72         this.auto_complete = auto;
73         this.auto_complete_initial = !!arguments.$auto_complete_initial;
74         this.auto_complete_conservative = !!arguments.$auto_complete_conservative;
75         let delay = arguments.$auto_complete_delay;
76         if (delay == null)
77             delay = default_minibuffer_auto_complete_delay;
78         this.auto_complete_delay = delay;
79         this.completions = null;
80         this.completions_valid = false;
81         this.space_completes = !!arguments.$space_completes;
82         if (this.space_completes)
83             this.keymaps.push(minibuffer_space_completion_keymap);
84         this.completions_timer_ID = null;
85         this.completions_display_element = null;
86         this.selected_completion_index = -1;
87         this.match_required  = !!arguments.$match_required;
88         this.match_required_default = this.match_required;
89         if (this.match_required)
90             this.default_completion = arguments.$default_completion;
91     }
94 function completions_tree_view (minibuffer_state) {
95     this.minibuffer_state = minibuffer_state;
98 var atom_service = Cc["@mozilla.org/atom-service;1"].getService(Ci.nsIAtomService);
100 completions_tree_view.prototype = {
101     constructor: completions_tree_view,
102     get rowCount () {
103         var c = this.minibuffer_state.completions;
104         if (!c)
105             return 0;
106         return c.count;
107     },
108     getCellText: function (row, column) {
109         var c = this.minibuffer_state.completions;
110         if (row >= c.count)
111             return null;
112         if (column.index == 0)
113             return c.get_string(row);
114         if (c.get_description)
115             return c.get_description(row);
116         return "";
117     },
118     setTree: function (treebox) { this.treeBox = treebox; },
119     isContainer: function (row) { return false; },
120     isSeparator: function (row) { return false; },
121     isSorted: function () { return false; },
122     getLevel: function (row) { return 0; },
123     getImageSrc: function (row, col) { return null; },
124     getRowProperties: function (row, props) {},
125     getCellProperties: function (row, col, props) {
126         if (col.index == 0)
127             props.AppendElement(atom_service.getAtom("completion-string"));
128         else
129             props.AppendElement(atom_service.getAtom("completion-description"));
130     },
131     getColumnProperties: function (colid, col, props) {}
134 // inherit from basic_minibuffer_state
135 text_entry_minibuffer_state.prototype = {
136     constructor: text_entry_minibuffer_state,
137     __proto__: basic_minibuffer_state.prototype,
138     load: function () {
139         basic_minibuffer_state.prototype.load.call(this);
140         var window = this.minibuffer.window;
141         if (this.completer) {
142             // Create completion display element if needed
143             if (!this.completion_element) {
144                 /* FIXME: maybe use the dom_generator */
145                 var tree = create_XUL(window, "tree");
146                 var s = this;
147                 tree.addEventListener("select", function () {
148                         s.selected_completion_index = s.completions_display_element.currentIndex;
149                         s.handle_completion_selected();
150                     }, true);
151                 tree.setAttribute("class", "completions");
153                 tree.setAttribute("rows", "8");
155                 tree.setAttribute("collapsed", "true");
157                 tree.setAttribute("hidecolumnpicker", "true");
158                 tree.setAttribute("hideheader", "true");
160                 var treecols = create_XUL(window, "treecols");
161                 tree.appendChild(treecols);
162                 var treecol = create_XUL(window, "treecol");
163                 treecol.setAttribute("flex", "1");
164                 treecols.appendChild(treecol);
165                 treecol = create_XUL(window, "treecol");
166                 treecol.setAttribute("flex", "1");
167                 treecols.appendChild(treecol);
168                 tree.appendChild(create_XUL(window, "treechildren"));
170                 this.minibuffer.insert_before(tree);
171                 tree.view = new completions_tree_view(this);
172                 this.completions_display_element = tree;
174                 /* This is the initial loading of this minibuffer
175                  * state.  If this.complete_initial is true, generate
176                  * completions. */
177                 if (this.auto_complete_initial)
178                     this.handle_input();
179             }
180             this.update_completions_display();
181         }
182     },
184     unload: function () {
185         if (this.completions_display_element)
186             this.completions_display_element.setAttribute("collapsed", "true");
187         basic_minibuffer_state.prototype.unload.call(this);
188     },
190     destroy: function () {
191         if (this.completions != null && this.completions.destroy)
192             this.completions.destroy();
193         delete this.completions;
194         if (this.completions_cont)
195             this.completions_cont.throw(abort());
196         delete this.completions_cont;
198         var el = this.completions_display_element;
199         if (el) {
200             el.parentNode.removeChild(el);
201             this.completions_display_element = null;
202         }
203         if (this.continuation)
204             this.continuation.throw(abort());
205         basic_minibuffer_state.prototype.destroy.call(this);
206     },
208     handle_input: function () {
209         if (!this.completer) return;
211         this.completions_valid = false;
213         if (!this.auto_complete) return;
215         var s = this;
216         var window = this.minibuffer.window;
218         if (this.auto_complete_delay > 0) {
219             if (this.completions_timer_ID != null)
220                 window.clearTimeout(this.completions_timer_ID);
221             this.completions_timer_ID = window.setTimeout(
222                 function () {
223                     s.completions_timer_ID = null;
224                     s.update_completions(true /* auto */, true /* update completions display */);
225                 }, this.auto_complete_delay);
226             return;
227         }
228         s.update_completions(true /* auto */, true /* update completions display */);
229     },
231     update_completions_display: function () {
232         var m = this.minibuffer;
233         if (m.current_state == this) {
234             if (this.completions && this.completions.count > 0) {
235                 this.completions_display_element.view = this.completions_display_element.view;
236                 this.completions_display_element.setAttribute("collapsed", "false");
238                 this.completions_display_element.currentIndex = this.selected_completion_index;
239                 this.completions_display_element.treeBoxObject.scrollToRow(this.selected_completion_index);
240             } else {
241                 this.completions_display_element.setAttribute("collapsed", "true");
242             }
243         }
244     },
246     /* If auto is true, this update is due to auto completion, rather
247      * than specifically requested. */
248     update_completions: function (auto, update_display) {
249         var window = this.minibuffer.window;
250         if (this.completions_timer_ID != null) {
251             window.clearTimeout(this.completions_timer_ID);
252             this.completions_timer_ID = null;
253         }
255         let m = this.minibuffer;
257         if (this.completions_cont) {
258             this.completions_cont.throw(abort());
259             this.completions_cont = null;
260         }
262         let c = this.completer(m._input_text, m._selection_start,
263                                auto && this.auto_complete_conservative);
265         if (is_coroutine(c)) {
266             let s = this;
267             let already_done = false;
268             this.completions_cont = co_call(function () {
269                 var x;
270                 try {
271                     x = yield c;
272                 } catch (e) {
273                     handle_interactive_error(window, e);
274                 } finally {
275                     s.completions_cont = null;
276                     already_done = true;
277                 }
278                 s.update_completions_done(x, update_display);
279             }());
281             // In case the completer actually already finished
282             if (already_done)
283                 this.completions_cont = null;
284             return;
285         } else
286             this.update_completions_done(c, update_display);
287     },
289     update_completions_done: function (c, update_display) {
290         /* The completer should return undefined if completion was not
291          * attempted due to auto being true.  Otherwise, it can return
292          * null to indicate no completions. */
293         if (this.completions != null && this.completions.destroy)
294             this.completions.destroy();
296         this.completions = c;
297         this.completions_valid = true;
298         this.applied_common_prefix = false;
300         if (c && ("get_match_required" in c))
301             this.match_required = c.get_match_required();
302         if (this.match_required == null)
303             this.match_required = this.match_required_default;
305         let i = -1;
306         if (c && c.count > 0) {
307             if (this.match_required) {
308                 if (c.count == 1)
309                     i = 0;
310                 else if (c.default_completion != null)
311                     i = c.default_completion;
312                 else if (this.default_completion && this.completions.index_of)
313                     i = this.completions.index_of(this.default_completion);
314             }
315             this.selected_completion_index = i;
316         }
318         if (update_display)
319             this.update_completions_display();
320     },
322     select_completion: function (i) {
323         this.selected_completion_index = i;
324         this.completions_display_element.currentIndex = i;
325         if (i >= 0)
326             this.completions_display_element.treeBoxObject.ensureRowIsVisible(i);
327         this.handle_completion_selected();
328     },
330     handle_completion_selected: function () {
331         /**
332          * When a completion is selected, apply it to the input text
333          * if a match is not "required"; otherwise, the completion is
334          * only displayed.
335          */
336         var i = this.selected_completion_index;
337         var m = this.minibuffer;
338         var c = this.completions;
340         if (this.completions_valid && c && !this.match_required && i >= 0 && i < c.count)
341             m.set_input_state(c.get_input_state(i));
342     }
345 function minibuffer_complete (window, count) {
346     var m = window.minibuffer;
347     var s = m.current_state;
348     if (!(s instanceof text_entry_minibuffer_state))
349         throw new Error("Invalid minibuffer state");
350     if (!s.completer)
351         return;
352     var just_completed_manually = false;
353     if (!s.completions_valid || s.completions === undefined) {
354         if (s.completions_timer_ID == null)
355             just_completed_manually = true;
356         //XXX: may need to use ignore_input_events here
357         s.update_completions(false /* not auto */, true /* update completions display */);
359         // If the completer is a coroutine, nothing we can do here
360         if (!s.completions_valid)
361             return;
362     }
364     var c = s.completions;
366     if (!c || c.count == 0)
367         return;
369     var e = s.completions_display_element;
370     var new_index = -1;
372     let common_prefix;
374     if (count == 1 && !s.applied_common_prefix && (common_prefix = c.common_prefix_input_state)) {
375         //XXX: may need to use ignore_input_events here
376         m.set_input_state(common_prefix);
377         s.applied_common_prefix = true;
378     } else if (!just_completed_manually) {
379         if (e.currentIndex != -1) {
380             new_index = (e.currentIndex + count) % c.count;
381             if (new_index < 0)
382                 new_index += c.count;
383         } else {
384             new_index = (count - 1) % c.count;
385             if (new_index < 0)
386                 new_index += c.count;
387         }
388     }
390     if (new_index != -1) {
391        try {
392             m.ignore_input_events = true;
393             s.select_completion(new_index);
394         } finally {
395             m.ignore_input_events = false;
396         }
397     }
399 interactive("minibuffer-complete", null,
400     function (I) { minibuffer_complete(I.window, I.p); });
401 interactive("minibuffer-complete-previous", null,
402     function (I) { minibuffer_complete(I.window, -I.p); });
404 function exit_minibuffer (window) {
405     var m = window.minibuffer;
406     var s = m.current_state;
407     if (!(s instanceof text_entry_minibuffer_state))
408         throw new Error("Invalid minibuffer state");
410     var val = m._input_text;
412     if (s.validator != null && !s.validator(val, m))
413         return;
415     var match = null;
417     if (s.completer && s.match_required) {
418         if (!s.completions_valid || s.completions === undefined)
419             s.update_completions(false /* not conservative */, false /* don't update */);
421         let c = s.completions;
422         let i = s.selected_completion_index;
423         if (c != null && i >= 0 && i < c.count) {
424             if (c.get_value != null)
425                 match = c.get_value(i);
426             else
427                 match = c.get_string(i);
428         } else {
429             m.message("No match");
430             return;
431         }
432     }
434     if (s.history) {
435         s.history.push(val);
436         if (s.history.length > minibuffer_history_max_items)
437             s.history.splice(0, s.history.length - minibuffer_history_max_items);
438     }
439     var cont = s.continuation;
440     delete s.continuation;
441     m.pop_state();
442     if (cont) {
443         if (s.match_required)
444             cont(match);
445         else
446             cont(val);
447     }
449 interactive("exit-minibuffer", null,
450     function (I) { exit_minibuffer(I.window); });
452 function minibuffer_history_next (window, count) {
453     var m = window.minibuffer;
454     var s = m.current_state;
455     if (!(s instanceof text_entry_minibuffer_state))
456         throw new Error("Invalid minibuffer state");
457     if (!s.history || s.history.length == 0)
458         throw interactive_error("No history available.");
459     if (count == 0)
460         return;
461     var index = s.history_index;
462     if (count > 0 && index == -1)
463         throw interactive_error("End of history; no next item");
464     else if (count < 0 && index == 0)
465         throw interactive_error("Beginning of history; no preceding item");
466     if (index == -1) {
467         s.saved_last_history_entry = m._input_text;
468         index = s.history.length + count;
469     } else
470         index = index + count;
472     if (index < 0)
473         index = 0;
475     m._restore_normal_state();
476     if (index >= s.history.length) {
477         index = -1;
478         m._input_text = s.saved_last_history_entry;
479     } else {
480         m._input_text = s.history[index];
481     }
482     s.history_index = index;
483     m._set_selection();
484     s.handle_input();
486 interactive("minibuffer-history-next", null,
487     function (I) { minibuffer_history_next(I.window, I.p); });
488 interactive("minibuffer-history-previous", null,
489     function (I) { minibuffer_history_next(I.window, -I.p); });
491 // Define the asynchronous minibuffer.read function
492 minibuffer.prototype.read = function () {
493     var s = new text_entry_minibuffer_state(this, (yield CONTINUATION), forward_keywords(arguments));
494     this.push_state(s);
495     var result = yield SUSPEND;
496     yield co_return(result);
499 minibuffer.prototype.read_command = function () {
500     keywords(
501         arguments,
502         $prompt = "Command", $history = "command",
503         $completer = prefix_completer(
504             $completions = function (visitor) interactive_commands.for_each_value(visitor),
505             $get_string = function (x) x.name,
506             $get_description = function (x) x.shortdoc || "",
507             $get_value = function (x) x.name),
508         $match_required,
509         $space_completes);
510     var result = yield this.read(forward_keywords(arguments));
511     yield co_return(result);
514 minibuffer.prototype.read_user_variable = function () {
515     keywords(
516         arguments,
517         $prompt = "User variable", $history = "user_variable",
518         $completer = prefix_completer(
519             $completions = function (visitor) {
520                 for (var i in user_variables) visitor(i);
521             },
522             $get_string = function (x) x,
523             $get_description = function (x) user_variables[x].shortdoc || "",
524             $get_value = function (x) x),
525         $match_required,
526         $space_completes);
527     var result = yield this.read(forward_keywords(arguments));
528     yield co_return(result);
531 minibuffer.prototype.read_preference = function () {
532     keywords(arguments,
533              $prompt = "Preference:", $history = "preference",
534              $completer = prefix_completer(
535                  $completions = preferences.getBranch(null).getChildList("", {}),
536                  $get_description = function (pref) {
537                      let default_value = get_default_pref(pref);
538                      let value = get_pref(pref);
539                      if (value == default_value)
540                          value = null;
541                      let type;
542                      switch (preferences.getBranch(null).getPrefType(pref)) {
543                      case Ci.nsIPrefBranch.PREF_STRING:
544                          type = "string";
545                          break;
546                      case Ci.nsIPrefBranch.PREF_INT:
547                          type = "int";
548                          break;
549                      case Ci.nsIPrefBranch.PREF_BOOL:
550                          type = "boolean";
551                          break;
552                      }
553                      let out = type + ":";
554                      if (value != null)
555                          out += " " + pretty_print_value(value);
556                      if (default_value != null)
557                          out += " (" + pretty_print_value(default_value) + ")";
558                      return out;
559                  }),
560              $match_required,
561              $space_completes);
562     var result = yield this.read(forward_keywords(arguments));
563     yield co_return(result);
566 provide("minibuffer-read");