define load_paths user variable in conkeror.js
[conkeror/arlinius.git] / modules / minibuffer-read.js
blob6d96cb4fdb938266eef36c2970b4f55c263aea80
1 /**
2  * (C) Copyright 2007-2009 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 = new string_hashmap();
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.");
28 /* The parameter `args' specifies the arguments.  In addition, the
29  * arguments for basic_minibuffer_state are also allowed.
30  *
31  * history:           [optional] specifies a string to identify the history list to use
32  *
33  * completer
34  *
35  * match_required
36  *
37  * default_completion  only used if match_required is set to true
38  *
39  * $valiator          [optional]
40  *          specifies a function
41  */
42 define_keywords("$keymap", "$history", "$validator",
43                 "$completer", "$match_required", "$default_completion",
44                 "$auto_complete", "$auto_complete_initial", "$auto_complete_conservative",
45                 "$auto_complete_delay",
46                 "$space_completes");
47 /* FIXME: support completing in another thread */
48 function text_entry_minibuffer_state (window, continuation) {
49     keywords(arguments, $keymap = minibuffer_keymap);
51     basic_minibuffer_state.call(this, window, forward_keywords(arguments));
53     this.continuation = continuation;
54     if (arguments.$history) {
55         this.history = minibuffer_history_data.get_put_default(arguments.$history, []);
56         this.history_index = -1;
57         this.saved_last_history_entry = null;
58     }
60     this.validator = arguments.$validator;
62     if (arguments.$completer != null) {
63         this.completer = arguments.$completer;
64         let auto = arguments.$auto_complete;
65         while (typeof(auto) == "string")
66             auto = minibuffer_auto_complete_preferences[auto];
67         if (auto == null)
68             auto = minibuffer_auto_complete_default;
69         this.auto_complete = auto;
70         this.auto_complete_initial = !!arguments.$auto_complete_initial;
71         this.auto_complete_conservative = !!arguments.$auto_complete_conservative;
72         let delay = arguments.$auto_complete_delay;
73         if (delay == null)
74             delay = default_minibuffer_auto_complete_delay;
75         this.auto_complete_delay = delay;
76         this.completions = null;
77         this.completions_valid = false;
78         this.space_completes = !!arguments.$space_completes;
79         this.completions_timer_ID = null;
80         this.completions_display_element = null;
81         this.selected_completion_index = -1;
82         this.match_required  = !!arguments.$match_required;
83         this.match_required_default = this.match_required;
84         if (this.match_required)
85             this.default_completion = arguments.$default_completion;
86     }
89 function completions_tree_view (minibuffer_state) {
90     this.minibuffer_state = minibuffer_state;
93 var atom_service = Cc["@mozilla.org/atom-service;1"].getService(Ci.nsIAtomService);
95 completions_tree_view.prototype = {
96     get rowCount () {
97         var c = this.minibuffer_state.completions;
98         if (!c)
99             return 0;
100         return c.count;
101     },
102     getCellText : function (row,column) {
103         var c = this.minibuffer_state.completions;
104         if (row >= c.count)
105             return null;
106         if (column.index == 0)
107             return c.get_string(row);
108         if (c.get_description)
109             return c.get_description(row);
110         return "";
111     },
112     setTree : function (treebox) { this.treebox = treebox; },
113     isContainer: function (row) { return false; },
114     isSeparator: function (row) { return false; },
115     isSorted: function () { return false; },
116     getLevel: function (row) { return 0; },
117     getImageSrc: function (row, col) { return null; },
118     getRowProperties: function (row, props) {},
119     getCellProperties: function (row, col, props) {
120         if (col.index == 0)
121             props.AppendElement(atom_service.getAtom("completion-string"));
122         else
123             props.AppendElement(atom_service.getAtom("completion-description"));
124     },
125     getColumnProperties: function (colid, col, props) {}
128 // inherit from basic_minibuffer_state
129 text_entry_minibuffer_state.prototype = {
130     __proto__: basic_minibuffer_state.prototype,
131     load : function (window) {
132         basic_minibuffer_state.prototype.load.call(this, window);
133         this.window = window;
134         if (this.completer) {
135             // Create completion display element if needed
136             if (!this.completion_element) {
137                 /* FIXME: maybe use the dom_generator */
138                 var tree = create_XUL(window, "tree");
139                 var s = this;
140                 tree.addEventListener("select", function () {
141                         s.selected_completion_index = s.completions_display_element.currentIndex;
142                         s.handle_completion_selected();
143                     }, true);
144                 tree.setAttribute("class", "completions");
146                 tree.setAttribute("rows", "8");
148                 tree.setAttribute("collapsed", "true");
150                 tree.setAttribute("hidecolumnpicker", "true");
151                 tree.setAttribute("hideheader", "true");
153                 var treecols = create_XUL(window, "treecols");
154                 tree.appendChild(treecols);
155                 var treecol = create_XUL(window, "treecol");
156                 treecol.setAttribute("flex", "1");
157                 treecols.appendChild(treecol);
158                 treecol = create_XUL(window, "treecol");
159                 treecol.setAttribute("flex", "1");
160                 treecols.appendChild(treecol);
161                 tree.appendChild(create_XUL(window, "treechildren"));
163                 window.minibuffer.insert_before(tree);
164                 tree.view = new completions_tree_view(this);
165                 this.completions_display_element = tree;
167                 /* This is the initial loading of this minibuffer
168                  * state.  If this.complete_initial is true, generate
169                  * completions. */
170                 if (this.auto_complete_initial)
171                     this.handle_input();
172             }
174             this.update_completions_display();
175         }
176     },
178     unload : function (window) {
179         if (this.completions_display_element)
180             this.completions_display_element.setAttribute("collapsed", "true");
181         basic_minibuffer_state.prototype.unload.call(this, window);
182     },
184     destroy : function (window) {
185         if (this.completions != null && this.completions.destroy)
186             this.completions.destroy();
187         delete this.completions;
188         if (this.completions_cont)
189             this.completions_cont.throw(abort());
190         delete this.completions_cont;
192         var el = this.completions_display_element;
193         if (el) {
194             el.parentNode.removeChild(el);
195             this.completions_display_element = null;
196         }
197         if (this.continuation)
198             this.continuation.throw(abort());
199         basic_minibuffer_state.prototype.destroy.call(this, window);
200     },
202     handle_input : function () {
203         if (!this.completer) return;
205         this.completions_valid = false;
207         if (!this.auto_complete) return;
209         var s = this;
211         if (this.auto_complete_delay > 0) {
212             if (this.completions_timer_ID != null)
213                 this.window.clearTimeout(this.completions_timer_ID);
214             this.completions_timer_ID = this.window.setTimeout(
215                 function () {
216                     s.completions_timer_ID = null;
217                     s.update_completions(true /* auto */, true /* update completions display */);
218                 }, this.auto_complete_delay);
219             return;
220         }
222         s.update_completions(true /* auto */, true /* update completions display */);
223     },
225     ran_minibuffer_command : function () {
226         this.handle_input();
227     },
229     update_completions_display : function () {
231         var m = this.window.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) {
250         if (this.completions_timer_ID != null) {
251             this.window.clearTimeout(this.completions_timer_ID);
252             this.completions_timer_ID = null;
253         }
255         let m = this.window.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(m.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 update_completions_done (c, update_display) {
291         /* The completer should return undefined if completion was not
292          * attempted due to auto being true.  Otherwise, it can return
293          * null to indicate no completions. */
294         if (this.completions != null && this.completions.destroy)
295             this.completions.destroy();
297         this.completions = c;
298         this.completions_valid = true;
299         this.applied_common_prefix = false;
301         if (c && ("get_match_required" in c))
302             this.match_required = c.get_match_required();
303         if (this.match_required == null)
304             this.match_required = this.match_required_default;
306         let i = -1;
307         if (c && c.count > 0) {
308             if (this.match_required) {
309                 if (c.count == 1)
310                     i = 0;
311                 else if (c.default_completion != null)
312                     i = c.default_completion;
313                 else if (this.default_completion && this.completions.index_of)
314                     i = this.completions.index_of(this.default_completion);
315             }
316             this.selected_completion_index = i;
317         }
319         if (update_display)
320             this.update_completions_display();
321     },
323     select_completion : function (i) {
324         this.selected_completion_index = i;
325         this.completions_display_element.currentIndex = i;
326         if (i >= 0)
327             this.completions_display_element.treeBoxObject.ensureRowIsVisible(i);
328         this.handle_completion_selected();
329     },
331     handle_completion_selected : function () {
332         /**
333          * When a completion is selected, apply it to the input text
334          * if a match is not "required"; otherwise, the completion is
335          * only displayed.
336          */
337         var i = this.selected_completion_index;
338         var m = this.window.minibuffer;
339         var c = this.completions;
341         if (this.completions_valid && c && !this.match_required && i >= 0 && i < c.count)
342         {
343             m.set_input_state(c.get_input_state(i));
344         }
345     }
348 function minibuffer_complete (window, count) {
349     var m = window.minibuffer;
350     var s = m.current_state;
351     if (!(s instanceof text_entry_minibuffer_state))
352         throw new Error("Invalid minibuffer state");
353     if (!s.completer)
354         return;
355     var just_completed_manually = false;
356     if (!s.completions_valid || s.completions === undefined) {
357         if (s.completions_timer_ID == null)
358             just_completed_manually = true;
359         s.update_completions(false /* not auto */, true /* update completions display */);
361         // If the completer is a coroutine, nothing we can do here
362         if (!s.completions_valid)
363             return;
364     }
366     var c = s.completions;
368     if (!c || c.count == 0)
369         return;
371     var e = s.completions_display_element;
372     var new_index = -1;
374     let common_prefix;
376     if (count == 1 && !s.applied_common_prefix && (common_prefix = c.common_prefix_input_state)) {
377         m.set_input_state(common_prefix);
378         s.applied_common_prefix = true;
379     } else if (!just_completed_manually) {
380         if (e.currentIndex != -1) {
381             new_index = (e.currentIndex + count) % c.count;
382             if (new_index < 0)
383                 new_index += c.count;
384         } else {
385             new_index = (count - 1) % c.count;
386             if (new_index < 0)
387                 new_index += c.count;
388         }
389     }
391     if (new_index != -1)
392         s.select_completion(new_index);
394 interactive("minibuffer-complete", null,
395     function (I) { minibuffer_complete(I.window, I.p); });
396 interactive("minibuffer-complete-previous", null,
397     function (I) { minibuffer_complete(I.window, -I.p); });
399 function exit_minibuffer (window) {
400     var m = window.minibuffer;
401     var s = m.current_state;
402     if (!(s instanceof text_entry_minibuffer_state))
403         throw new Error("Invalid minibuffer state");
405     var val = m._input_text;
407     if (s.validator != null && !s.validator(val, m))
408         return;
410     var match = null;
412     if (s.completer && s.match_required) {
413         if (!s.completions_valid || s.completions === undefined)
414             s.update_completions(false /* not conservative */, false /* don't update */);
416         let c = s.completions;
417         let i = s.selected_completion_index;
418         if (c != null && i >= 0 && i < c.count) {
419             if (c.get_value != null)
420                 match = c.get_value(i);
421             else
422                 match = c.get_string(i);
423         } else {
424             m.message("No match");
425             return;
426         }
427     }
429     if (s.history) {
430         s.history.push(val);
431         if (s.history.length > minibuffer_history_max_items)
432             s.history.splice(0, s.history.length - minibuffer_history_max_items);
433     }
434     var cont = s.continuation;
435     delete s.continuation;
436     m.pop_state();
437     if (cont) {
438         if (s.match_required)
439             cont(match);
440         else
441             cont(val);
442     }
444 interactive("exit-minibuffer", null,
445     function (I) { exit_minibuffer(I.window); });
447 function minibuffer_history_next (window, count) {
448     var m = window.minibuffer;
449     var s = m.current_state;
450     if (!(s instanceof text_entry_minibuffer_state))
451         throw new Error("Invalid minibuffer state");
452     if (!s.history || s.history.length == 0)
453         throw interactive_error("No history available.");
454     if (count == 0)
455         return;
456     var index = s.history_index;
457     if (count > 0 && index == -1)
458         throw interactive_error("End of history; no next item");
459     else if (count < 0 && index == 0) {
460         throw interactive_error("Beginning of history; no preceding item");
461     }
462     if (index == -1) {
463         s.saved_last_history_entry = m._input_text;
464         index = s.history.length + count;
465     } else
466         index = index + count;
468     if (index < 0)
469         index = 0;
471     m._restore_normal_state();
472     if (index >= s.history.length) {
473         index = -1;
474         m._input_text = s.saved_last_history_entry;
475     } else {
476         m._input_text = s.history[index];
477     }
478     s.history_index = index;
479     m._set_selection();
480     s.handle_input();
482 interactive("minibuffer-history-next", null,
483     function (I) { minibuffer_history_next(I.window, I.p); });
484 interactive("minibuffer-history-previous", null,
485     function (I) { minibuffer_history_next(I.window, -I.p); });
487 // Define the asynchronous minibuffer.read function
488 minibuffer.prototype.read = function () {
489     var s = new text_entry_minibuffer_state(this.window, (yield CONTINUATION), forward_keywords(arguments));
490     this.push_state(s);
491     var result = yield SUSPEND;
492     yield co_return(result);
495 minibuffer.prototype.read_command = function () {
496     keywords(
497         arguments,
498         $prompt = "Command", $history = "command",
499         $completer = prefix_completer(
500             $completions = function (visitor) interactive_commands.for_each_value(visitor),
501             $get_string = function (x) x.name,
502             $get_description = function (x) x.shortdoc || "",
503             $get_value = function (x) x.name),
504         $match_required = true);
505     var result = yield this.read(forward_keywords(arguments));
506     yield co_return(result);
509 minibuffer.prototype.read_user_variable = function () {
510     keywords(
511         arguments,
512         $prompt = "User variable", $history = "user_variable",
513         $completer = prefix_completer(
514             $completions = function (visitor) {
515                 for (var i in user_variables) visitor(i);
516             },
517             $get_string = function (x) x,
518             $get_description = function (x) user_variables[x].shortdoc || "",
519             $get_value = function (x) x),
520         $match_required = true);
521     var result = yield this.read(forward_keywords(arguments));
522     yield co_return(result);
525 minibuffer.prototype.read_preference = function minibuffer__read_preference () {
526     keywords(arguments,
527              $prompt = "Preference:", $history = "preference",
528              $completer = prefix_completer(
529                  $completions = preferences.getBranch(null).getChildList("", {}),
530                  $get_description = function (pref) {
531                      let default_value = get_default_pref(pref);
532                      let value = get_pref(pref);
533                      if (value == default_value)
534                          value = null;
535                      let type;
536                      switch (preferences.getBranch(null).getPrefType(pref)) {
537                      case Ci.nsIPrefBranch.PREF_STRING:
538                          type = "string";
539                          break;
540                      case Ci.nsIPrefBranch.PREF_INT:
541                          type = "int";
542                          break;
543                      case Ci.nsIPrefBranch.PREF_BOOL:
544                          type = "boolean";
545                          break;
546                      }
547                      let out = type + ":";
548                      if (value != null)
549                          out += " " + pretty_print_value(value);
550                      if (default_value != null)
551                          out += " (" + pretty_print_value(default_value) + ")";
552                      return out;
553                  }),
554              $match_required = true);
555     var result = yield this.read(forward_keywords(arguments));
556     yield co_return(result);