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