whitespace
[conkeror.git] / modules / minibuffer-read.js
blob28775497cdad1e164cf2500e4070420b7bb978c7
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.");
29 define_variable("minibuffer_completion_rows", 8,
30     "Number of minibuffer completions to display at one time.");
32 var atom_service = Cc["@mozilla.org/atom-service;1"].getService(Ci.nsIAtomService);
34 function completions_tree_view (minibuffer_state) {
35     this.minibuffer_state = minibuffer_state;
37 completions_tree_view.prototype = {
38     constructor: completions_tree_view,
39     QueryInterface: XPCOMUtils.generateQI([Ci.nsITreeView]),
40     get rowCount () {
41         var c = this.minibuffer_state.completions;
42         if (!c)
43             return 0;
44         return c.count;
45     },
46     getCellText: function (row,column) {
47         var c = this.minibuffer_state.completions;
48         if (row >= c.count)
49             return null;
50         if (column.index == 0)
51             return c.get_string(row);
52         if (c.get_description)
53             return c.get_description(row);
54         return "";
55     },
56     setTree: function (treebox) { this.treeBox = treebox; },
57     isContainer: function (row) { return false; },
58     isSeparator: function (row) { return false; },
59     isSorted: function () { return false; },
60     getLevel: function (row) { return 0; },
61     getImageSrc: function (row, col) {
62         var c = this.minibuffer_state.completions;
63         if (this.minibuffer_state.enable_icons &&
64             c.get_icon && col.index == 0)
65         {
66             return c.get_icon(row);
67         }
68         return null;
69     },
70     getRowProperties: function (row, props) {},
71     getCellProperties: function (row, col, props) {
72         if (col.index == 0)
73             props.AppendElement(atom_service.getAtom("completion-string"));
74         else
75             props.AppendElement(atom_service.getAtom("completion-description"));
76     },
77     getColumnProperties: function (colid, col, props) {}
81 /* The parameter `args' specifies the arguments.  In addition, the
82  * arguments for basic_minibuffer_state are also allowed.
83  *
84  * history:           [optional] specifies a string to identify the history list to use
85  *
86  * completer
87  *
88  * match_required
89  *
90  * default_completion  only used if match_required is set to true
91  *
92  * $valiator          [optional]
93  *          specifies a function
94  */
95 define_keywords("$keymap", "$history", "$validator",
96                 "$completer", "$match_required", "$default_completion",
97                 "$auto_complete", "$auto_complete_initial", "$auto_complete_conservative",
98                 "$auto_complete_delay", "$enable_icons",
99                 "$space_completes");
100 /* FIXME: support completing in another thread */
101 function text_entry_minibuffer_state (minibuffer, continuation) {
102     keywords(arguments, $keymap = minibuffer_keymap,
103              $enable_icons = false);
105     basic_minibuffer_state.call(this, minibuffer, forward_keywords(arguments));
107     this.continuation = continuation;
108     if (arguments.$history) {
109         this.history = minibuffer_history_data[arguments.$history] =
110             minibuffer_history_data[arguments.$history] || [];
111         this.history_index = -1;
112         this.saved_last_history_entry = null;
113     }
115     this.validator = arguments.$validator;
117     if (arguments.$completer != null) {
118         this.completer = arguments.$completer;
119         let auto = arguments.$auto_complete;
120         while (typeof(auto) == "string")
121             auto = minibuffer_auto_complete_preferences[auto];
122         if (auto == null)
123             auto = minibuffer_auto_complete_default;
124         this.auto_complete = auto;
125         this.auto_complete_initial = !!arguments.$auto_complete_initial;
126         this.auto_complete_conservative = !!arguments.$auto_complete_conservative;
127         let delay = arguments.$auto_complete_delay;
128         if (delay == null)
129             delay = default_minibuffer_auto_complete_delay;
130         this.auto_complete_delay = delay;
131         this.completions = null;
132         this.completions_valid = false;
133         this.space_completes = !!arguments.$space_completes;
134         if (this.space_completes)
135             this.keymaps.push(minibuffer_space_completion_keymap);
136         this.completions_timer_ID = null;
137         this.completions_display_element = null;
138         this.selected_completion_index = -1;
139         this.match_required  = !!arguments.$match_required;
140         this.match_required_default = this.match_required;
141         if (this.match_required)
142             this.default_completion = arguments.$default_completion;
143         this.enable_icons = arguments.$enable_icons;
144     }
146 text_entry_minibuffer_state.prototype = {
147     constructor: text_entry_minibuffer_state,
148     __proto__: basic_minibuffer_state.prototype,
149     load: function () {
150         basic_minibuffer_state.prototype.load.call(this);
151         var window = this.minibuffer.window;
152         if (this.completer) {
153             // Create completion display element if needed
154             if (!this.completion_element) {
155                 /* FIXME: maybe use the dom_generator */
156                 var tree = create_XUL(window, "tree");
157                 var s = this;
158                 tree.addEventListener("select", function () {
159                         s.selected_completion_index = s.completions_display_element.currentIndex;
160                         s.handle_completion_selected();
161                     }, true);
162                 tree.setAttribute("class", "completions");
164                 tree.setAttribute("rows", minibuffer_completion_rows);
166                 tree.setAttribute("collapsed", "true");
168                 tree.setAttribute("hidecolumnpicker", "true");
169                 tree.setAttribute("hideheader", "true");
170                 if (this.enable_icons)
171                     tree.setAttribute("hasicons", "true");
173                 var treecols = create_XUL(window, "treecols");
174                 tree.appendChild(treecols);
175                 var treecol = create_XUL(window, "treecol");
176                 treecol.setAttribute("flex", "1");
177                 treecols.appendChild(treecol);
178                 treecol = create_XUL(window, "treecol");
179                 treecol.setAttribute("flex", "1");
180                 treecols.appendChild(treecol);
181                 tree.appendChild(create_XUL(window, "treechildren"));
183                 this.minibuffer.insert_before(tree);
184                 tree.view = new completions_tree_view(this);
185                 this.completions_display_element = tree;
187                 /* This is the initial loading of this minibuffer
188                  * state.  If this.complete_initial is true, generate
189                  * completions. */
190                 if (this.auto_complete_initial)
191                     this.handle_input();
192             }
193             this.update_completions_display();
194         }
195     },
197     unload: function () {
198         if (this.completions_display_element)
199             this.completions_display_element.setAttribute("collapsed", "true");
200         basic_minibuffer_state.prototype.unload.call(this);
201     },
203     destroy: function () {
204         if (this.completions != null && this.completions.destroy)
205             this.completions.destroy();
206         delete this.completions;
207         if (this.completions_cont)
208             this.completions_cont.throw(abort());
209         delete this.completions_cont;
211         var el = this.completions_display_element;
212         if (el) {
213             el.parentNode.removeChild(el);
214             this.completions_display_element = null;
215         }
216         if (this.continuation)
217             this.continuation.throw(abort());
218         basic_minibuffer_state.prototype.destroy.call(this);
219     },
221     handle_input: function () {
222         if (!this.completer) return;
224         this.completions_valid = false;
226         if (!this.auto_complete) return;
228         var s = this;
229         var window = this.minibuffer.window;
231         if (this.auto_complete_delay > 0) {
232             if (this.completions_timer_ID != null)
233                 window.clearTimeout(this.completions_timer_ID);
234             this.completions_timer_ID = window.setTimeout(
235                 function () {
236                     s.completions_timer_ID = null;
237                     s.update_completions(true /* auto */, true /* update completions display */);
238                 }, this.auto_complete_delay);
239             return;
240         }
241         s.update_completions(true /* auto */, true /* update completions display */);
242     },
244     update_completions_display: function () {
245         var m = this.minibuffer;
246         if (m.current_state == this) {
247             if (this.completions && this.completions.count > 0) {
248                 this.completions_display_element.view = this.completions_display_element.view;
249                 this.completions_display_element.setAttribute("collapsed", "false");
250                 this.completions_display_element.currentIndex = this.selected_completion_index;
251                 var max_display = this.completions_display_element.treeBoxObject.getPageLength();
252                 var mid_point = Math.floor(max_display / 2);
253                 if (this.completions.count - this.selected_completion_index <= mid_point)
254                     var pos = this.completions.count - max_display;
255                 else
256                     pos = Math.max(0, this.selected_completion_index - mid_point);
257                 this.completions_display_element.treeBoxObject.scrollToRow(pos);
258             } else {
259                 this.completions_display_element.setAttribute("collapsed", "true");
260             }
261         }
262     },
264     /* If auto is true, this update is due to auto completion, rather
265      * than specifically requested. */
266     update_completions: function (auto, update_display) {
267         var window = this.minibuffer.window;
268         if (this.completions_timer_ID != null) {
269             window.clearTimeout(this.completions_timer_ID);
270             this.completions_timer_ID = null;
271         }
273         let m = this.minibuffer;
275         if (this.completions_cont) {
276             this.completions_cont.throw(abort());
277             this.completions_cont = null;
278         }
280         let c = this.completer(m._input_text, m._selection_start,
281                                auto && this.auto_complete_conservative);
283         if (is_coroutine(c)) {
284             let s = this;
285             let already_done = false;
286             this.completions_cont = co_call(function () {
287                 var x;
288                 try {
289                     x = yield c;
290                 } catch (e) {
291                     handle_interactive_error(window, e);
292                 } finally {
293                     s.completions_cont = null;
294                     already_done = true;
295                 }
296                 s.update_completions_done(x, update_display);
297             }());
299             // In case the completer actually already finished
300             if (already_done)
301                 this.completions_cont = null;
302             return;
303         } else
304             this.update_completions_done(c, update_display);
305     },
307     update_completions_done: function (c, update_display) {
308         /* The completer should return undefined if completion was not
309          * attempted due to auto being true.  Otherwise, it can return
310          * null to indicate no completions. */
311         if (this.completions != null && this.completions.destroy)
312             this.completions.destroy();
314         this.completions = c;
315         this.completions_valid = true;
316         this.applied_common_prefix = false;
318         if (c && ("get_match_required" in c))
319             this.match_required = c.get_match_required();
320         if (this.match_required == null)
321             this.match_required = this.match_required_default;
323         let i = -1;
324         if (c && c.count > 0) {
325             if (this.match_required) {
326                 if (c.count == 1)
327                     i = 0;
328                 else if (c.default_completion != null)
329                     i = c.default_completion;
330                 else if (this.default_completion && this.completions.index_of)
331                     i = this.completions.index_of(this.default_completion);
332             }
333             this.selected_completion_index = i;
334         }
336         if (update_display)
337             this.update_completions_display();
338     },
340     select_completion: function (i) {
341         this.selected_completion_index = i;
342         this.completions_display_element.currentIndex = i;
343         if (i >= 0)
344             this.completions_display_element.treeBoxObject.ensureRowIsVisible(i);
345         this.handle_completion_selected();
346     },
348     handle_completion_selected: function () {
349         /**
350          * When a completion is selected, apply it to the input text
351          * if a match is not "required"; otherwise, the completion is
352          * only displayed.
353          */
354         var i = this.selected_completion_index;
355         var m = this.minibuffer;
356         var c = this.completions;
358         if (this.completions_valid && c && !this.match_required && i >= 0 && i < c.count)
359             m.set_input_state(c.get_input_state(i));
360     }
363 function minibuffer_complete (window, count) {
364     var m = window.minibuffer;
365     var s = m.current_state;
366     if (!(s instanceof text_entry_minibuffer_state))
367         throw new Error("Invalid minibuffer state");
368     if (!s.completer)
369         return;
370     var just_completed_manually = false;
371     if (!s.completions_valid || s.completions === undefined) {
372         if (s.completions_timer_ID == null)
373             just_completed_manually = true;
374         //XXX: may need to use ignore_input_events here
375         s.update_completions(false /* not auto */, true /* update completions display */);
377         // If the completer is a coroutine, nothing we can do here
378         if (!s.completions_valid)
379             return;
380     }
382     var c = s.completions;
384     if (!c || c.count == 0)
385         return;
387     var e = s.completions_display_element;
388     var new_index = -1;
390     let common_prefix;
392     if (count == 1 && !s.applied_common_prefix && (common_prefix = c.common_prefix_input_state)) {
393         //XXX: may need to use ignore_input_events here
394         m.set_input_state(common_prefix);
395         s.applied_common_prefix = true;
396     } else if (!just_completed_manually) {
397         if (e.currentIndex != -1) {
398             new_index = (e.currentIndex + count) % c.count;
399             if (new_index < 0)
400                 new_index += c.count;
401         } else {
402             new_index = (count - 1) % c.count;
403             if (new_index < 0)
404                 new_index += c.count;
405         }
406     }
408     if (new_index != -1) {
409        try {
410             m.ignore_input_events = true;
411             s.select_completion(new_index);
412         } finally {
413             m.ignore_input_events = false;
414         }
415     }
417 interactive("minibuffer-complete", null,
418     function (I) { minibuffer_complete(I.window, I.p); });
419 interactive("minibuffer-complete-previous", null,
420     function (I) { minibuffer_complete(I.window, -I.p); });
422 function exit_minibuffer (window) {
423     var m = window.minibuffer;
424     var s = m.current_state;
425     if (!(s instanceof text_entry_minibuffer_state))
426         throw new Error("Invalid minibuffer state");
428     var val = m._input_text;
430     if (s.validator != null && !s.validator(val, m))
431         return;
433     var match = null;
435     if (s.completer && s.match_required) {
436         if (!s.completions_valid || s.completions === undefined)
437             s.update_completions(false /* not conservative */, false /* don't update */);
439         let c = s.completions;
440         let i = s.selected_completion_index;
441         if (c != null && i >= 0 && i < c.count) {
442             if (c.get_value != null)
443                 match = c.get_value(i);
444             else
445                 match = c.get_string(i);
446         } else {
447             m.message("No match");
448             return;
449         }
450     }
452     if (s.history) {
453         s.history.push(val);
454         if (s.history.length > minibuffer_history_max_items)
455             s.history.splice(0, s.history.length - minibuffer_history_max_items);
456     }
457     var cont = s.continuation;
458     delete s.continuation;
459     m.pop_state();
460     if (cont) {
461         if (s.match_required)
462             cont(match);
463         else
464             cont(val);
465     }
467 interactive("exit-minibuffer", null,
468     function (I) { exit_minibuffer(I.window); });
470 function minibuffer_history_next (window, count) {
471     var m = window.minibuffer;
472     var s = m.current_state;
473     if (!(s instanceof text_entry_minibuffer_state))
474         throw new Error("Invalid minibuffer state");
475     if (!s.history || s.history.length == 0)
476         throw interactive_error("No history available.");
477     if (count == 0)
478         return;
479     var index = s.history_index;
480     if (count > 0 && index == -1)
481         throw interactive_error("End of history; no next item");
482     else if (count < 0 && index == 0)
483         throw interactive_error("Beginning of history; no preceding item");
484     if (index == -1) {
485         s.saved_last_history_entry = m._input_text;
486         index = s.history.length + count;
487     } else
488         index = index + count;
490     if (index < 0)
491         index = 0;
493     m._restore_normal_state();
494     if (index >= s.history.length) {
495         index = -1;
496         m._input_text = s.saved_last_history_entry;
497     } else {
498         m._input_text = s.history[index];
499     }
500     s.history_index = index;
501     m._set_selection();
502     s.handle_input();
504 interactive("minibuffer-history-next", null,
505     function (I) { minibuffer_history_next(I.window, I.p); });
506 interactive("minibuffer-history-previous", null,
507     function (I) { minibuffer_history_next(I.window, -I.p); });
509 // Define the asynchronous minibuffer.read function
510 minibuffer.prototype.read = function () {
511     var s = new text_entry_minibuffer_state(this, (yield CONTINUATION), forward_keywords(arguments));
512     this.push_state(s);
513     var result = yield SUSPEND;
514     yield co_return(result);
517 minibuffer.prototype.read_command = function () {
518     keywords(
519         arguments,
520         $prompt = "Command", $history = "command",
521         $completer = prefix_completer(
522             $completions = function (visitor) interactive_commands.for_each_value(visitor),
523             $get_string = function (x) x.name,
524             $get_description = function (x) x.shortdoc || "",
525             $get_value = function (x) x.name),
526         $match_required,
527         $space_completes);
528     var result = yield this.read(forward_keywords(arguments));
529     yield co_return(result);
532 minibuffer.prototype.read_user_variable = function () {
533     keywords(
534         arguments,
535         $prompt = "User variable", $history = "user_variable",
536         $completer = prefix_completer(
537             $completions = function (visitor) {
538                 for (var i in user_variables) visitor(i);
539             },
540             $get_string = function (x) x,
541             $get_description = function (x) user_variables[x].shortdoc || "",
542             $get_value = function (x) x),
543         $match_required,
544         $space_completes);
545     var result = yield this.read(forward_keywords(arguments));
546     yield co_return(result);
549 minibuffer.prototype.read_preference = function () {
550     keywords(arguments,
551              $prompt = "Preference:", $history = "preference",
552              $completer = prefix_completer(
553                  $completions = preferences.getBranch(null).getChildList("", {}),
554                  $get_description = function (pref) {
555                      let default_value = get_default_pref(pref);
556                      let value = get_pref(pref);
557                      if (value == default_value)
558                          value = null;
559                      let type;
560                      switch (preferences.getBranch(null).getPrefType(pref)) {
561                      case Ci.nsIPrefBranch.PREF_STRING:
562                          type = "string";
563                          break;
564                      case Ci.nsIPrefBranch.PREF_INT:
565                          type = "int";
566                          break;
567                      case Ci.nsIPrefBranch.PREF_BOOL:
568                          type = "boolean";
569                          break;
570                      }
571                      let out = type + ":";
572                      if (value != null)
573                          out += " " + pretty_print_value(value);
574                      if (default_value != null)
575                          out += " (" + pretty_print_value(default_value) + ")";
576                      return out;
577                  }),
578              $match_required,
579              $space_completes);
580     var result = yield this.read(forward_keywords(arguments));
581     yield co_return(result);
584 provide("minibuffer-read");