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