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