buffer.js: Properly fix lack of current buffer after removing a buffer
[conkeror.git] / modules / minibuffer-read.js
blobe79f1d7b429153ec136d701a426e00f67e38817c
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             var a = atom_service.getAtom("completion-string");
72         else
73             a = atom_service.getAtom("completion-description");
74         if (props)
75             props.AppendElement(a);
76         return a;
77     },
78     getColumnProperties: function (colid, col, props) {}
82 /* The parameter `args' specifies the arguments.  In addition, the
83  * arguments for basic_minibuffer_state are also allowed.
84  *
85  * history:           [optional] specifies a string to identify the history list to use
86  *
87  * completer
88  *
89  * match_required
90  *
91  * default_completion  only used if match_required is set to true
92  *
93  * $valiator          [optional]
94  *          specifies a function
95  */
96 define_keywords("$keymap", "$history", "$validator",
97                 "$completer", "$match_required", "$default_completion",
98                 "$auto_complete", "$auto_complete_initial", "$auto_complete_conservative",
99                 "$auto_complete_delay", "$enable_icons",
100                 "$space_completes");
101 /* FIXME: support completing in another thread */
102 function text_entry_minibuffer_state (minibuffer) {
103     keywords(arguments, $keymap = minibuffer_keymap,
104              $enable_icons = false);
106     basic_minibuffer_state.call(this, minibuffer, forward_keywords(arguments));
108     let deferred = Promise.defer();
109     this.deferred = deferred;
110     this.promise = make_simple_cancelable(deferred);
112     if (arguments.$history) {
113         this.history = minibuffer_history_data[arguments.$history] =
114             minibuffer_history_data[arguments.$history] || [];
115         this.history_index = -1;
116         this.saved_last_history_entry = null;
117     }
119     this.validator = arguments.$validator;
121     if (arguments.$completer != null) {
122         this.completer = arguments.$completer;
123         let auto = arguments.$auto_complete;
124         while (typeof auto == "string")
125             auto = minibuffer_auto_complete_preferences[auto];
126         if (auto == null)
127             auto = minibuffer_auto_complete_default;
128         this.auto_complete = auto;
129         this.auto_complete_initial = !!arguments.$auto_complete_initial;
130         this.auto_complete_conservative = !!arguments.$auto_complete_conservative;
131         let delay = arguments.$auto_complete_delay;
132         if (delay == null)
133             delay = default_minibuffer_auto_complete_delay;
134         this.auto_complete_delay = delay;
135         this.completions = null;
136         this.completions_valid = false;
137         this.space_completes = !!arguments.$space_completes;
138         if (this.space_completes)
139             this.keymaps.push(minibuffer_space_completion_keymap);
140         this.completions_timer_ID = null;
141         this.completions_display_element = null;
142         this.selected_completion_index = -1;
143         this.match_required  = !!arguments.$match_required;
144         this.match_required_default = this.match_required;
145         if (this.match_required)
146             this.default_completion = arguments.$default_completion;
147         this.enable_icons = arguments.$enable_icons;
148     }
150 text_entry_minibuffer_state.prototype = {
151     constructor: text_entry_minibuffer_state,
152     __proto__: basic_minibuffer_state.prototype,
153     load: function () {
154         basic_minibuffer_state.prototype.load.call(this);
155         var window = this.minibuffer.window;
156         if (this.completer) {
157             // Create completion display element if needed
158             if (! this.completion_element) {
159                 /* FIXME: maybe use the dom_generator */
160                 var tree = create_XUL(window, "tree");
161                 var s = this;
162                 tree.addEventListener("select", function () {
163                         s.selected_completion_index = s.completions_display_element.currentIndex;
164                         s.handle_completion_selected();
165                     }, true);
166                 tree.setAttribute("class", "completions");
168                 tree.setAttribute("rows", minibuffer_completion_rows);
170                 tree.setAttribute("collapsed", "true");
172                 tree.setAttribute("hidecolumnpicker", "true");
173                 tree.setAttribute("hideheader", "true");
174                 if (this.enable_icons)
175                     tree.setAttribute("hasicons", "true");
177                 var treecols = create_XUL(window, "treecols");
178                 tree.appendChild(treecols);
179                 var treecol = create_XUL(window, "treecol");
180                 treecol.setAttribute("flex", "1");
181                 treecols.appendChild(treecol);
182                 treecol = create_XUL(window, "treecol");
183                 treecol.setAttribute("flex", "1");
184                 treecols.appendChild(treecol);
185                 tree.appendChild(create_XUL(window, "treechildren"));
187                 this.minibuffer.insert_before(tree);
188                 tree.view = new completions_tree_view(this);
189                 this.completions_display_element = tree;
191                 // This is the initial loading of this minibuffer state.
192                 // If this.auto_complete_initial is true, generate
193                 // completions.
194                 if (this.auto_complete_initial)
195                     this.handle_input();
196             }
197             this.update_completions_display();
198         }
199     },
201     unload: function () {
202         if (this.completions_display_element)
203             this.completions_display_element.setAttribute("collapsed", "true");
204         basic_minibuffer_state.prototype.unload.call(this);
205     },
207     destroy: function () {
208         if (this.completions != null && this.completions.destroy)
209             this.completions.destroy();
210         delete this.completions;
211         if (this.completions_cont)
212             this.completions_cont.cancel();
213         delete this.completions_cont;
215         var el = this.completions_display_element;
216         if (el) {
217             el.parentNode.removeChild(el);
218             this.completions_display_element = null;
219         }
220         if (this.promise)
221             this.promise.cancel();
222         basic_minibuffer_state.prototype.destroy.call(this);
223     },
225     handle_input: function () {
226         if (! this.completer)
227             return;
228         this.completions_valid = false;
229         if (! this.auto_complete)
230             return;
231         var s = this;
232         var window = this.minibuffer.window;
233         if (this.auto_complete_delay > 0) {
234             if (this.completions_timer_ID != null)
235                 window.clearTimeout(this.completions_timer_ID);
236             this.completions_timer_ID = window.setTimeout(
237                 function () {
238                     s.completions_timer_ID = null;
239                     s.update_completions(true /* auto */, true /* update completions display */);
240                 }, this.auto_complete_delay);
241             return;
242         }
243         s.update_completions(true /* auto */, true /* update completions display */);
244     },
246     update_completions_display: function () {
247         var m = this.minibuffer;
248         if (m.current_state == this) {
249             if (this.completions && this.completions.count > 0) {
250                 this.completions_display_element.view = this.completions_display_element.view;
251                 this.completions_display_element.setAttribute("collapsed", "false");
252                 this.completions_display_element.currentIndex = this.selected_completion_index;
253                 var max_display = this.completions_display_element.treeBoxObject.getPageLength();
254                 var mid_point = Math.floor(max_display / 2);
255                 if (this.completions.count - this.selected_completion_index <= mid_point)
256                     var pos = this.completions.count - max_display;
257                 else
258                     pos = Math.max(0, this.selected_completion_index - mid_point);
259                 this.completions_display_element.treeBoxObject.scrollToRow(pos);
260             } else {
261                 this.completions_display_element.setAttribute("collapsed", "true");
262             }
263         }
264     },
266     /* If auto is true, this update is due to auto completion, rather
267      * than specifically requested. */
268     update_completions: function (auto, update_display) {
269         var window = this.minibuffer.window;
270         if (this.completions_timer_ID != null) {
271             window.clearTimeout(this.completions_timer_ID);
272             this.completions_timer_ID = null;
273         }
274         var m = this.minibuffer;
275         if (this.completions_cont) {
276             this.completions_cont.cancel();
277             this.completions_cont = null;
278         }
279         let c = this.completer(m._input_text, m._selection_start,
280                                auto && this.auto_complete_conservative);
281         if (is_coroutine(c)) {
282             var s = this;
283             var already_done = false;
284             this.completions_cont = spawn(function () {
285                 try {
286                     var 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             }());
295             // In case the completer actually already finished
296             if (already_done)
297                 this.completions_cont = null;
298         } else
299             this.update_completions_done(c, update_display);
300     },
302     update_completions_done: function (c, update_display) {
303         /* The completer should return undefined if completion was not
304          * attempted due to auto being true.  Otherwise, it can return
305          * null to indicate no completions. */
306         if (this.completions != null && this.completions.destroy)
307             this.completions.destroy();
309         this.completions = c;
310         this.completions_valid = true;
311         this.applied_common_prefix = false;
313         if (c && ("get_match_required" in c))
314             this.match_required = c.get_match_required();
315         if (this.match_required == null)
316             this.match_required = this.match_required_default;
318         if (c && c.count > 0) {
319             var i = -1;
320             if (this.match_required) {
321                 if (c.count == 1)
322                     i = 0;
323                 else if (c.default_completion != null)
324                     i = c.default_completion;
325                 else if (this.default_completion && this.completions.index_of)
326                     i = this.completions.index_of(this.default_completion);
327             }
328             this.selected_completion_index = i;
329         }
331         if (update_display)
332             this.update_completions_display();
333     },
335     select_completion: function (i) {
336         this.selected_completion_index = i;
337         this.completions_display_element.currentIndex = i;
338         if (i >= 0)
339             this.completions_display_element.treeBoxObject.ensureRowIsVisible(i);
340         this.handle_completion_selected();
341     },
343     handle_completion_selected: function () {
344         /**
345          * When a completion is selected, apply it to the input text
346          * if a match is not "required"; otherwise, the completion is
347          * only displayed.
348          */
349         var i = this.selected_completion_index;
350         var m = this.minibuffer;
351         var c = this.completions;
353         if (this.completions_valid && c && !this.match_required && i >= 0 && i < c.count)
354             m.set_input_state(c.get_input_state(i));
355     }
358 function minibuffer_complete (window, count) {
359     var m = window.minibuffer;
360     var s = m.current_state;
361     if (! (s instanceof text_entry_minibuffer_state))
362         throw new Error("Invalid minibuffer state");
363     if (! s.completer)
364         return;
365     var just_completed_manually = false;
366     if (! s.completions_valid || s.completions === undefined) {
367         if (s.completions_timer_ID == null)
368             just_completed_manually = true;
369         //XXX: may need to use ignore_input_events here
370         s.update_completions(false /* not auto */, true /* update completions display */);
372         // If the completer is a coroutine, nothing we can do here
373         if (! s.completions_valid)
374             return;
375     }
377     var c = s.completions;
379     if (! c || c.count == 0)
380         return;
382     var e = s.completions_display_element;
383     var new_index = -1;
384     var common_prefix;
386     if (count == 1 && ! s.applied_common_prefix &&
387         (common_prefix = c.common_prefix_input_state))
388     {
389         //XXX: may need to use ignore_input_events here
390         m.set_input_state(common_prefix);
391         s.applied_common_prefix = true;
392     } else if (!just_completed_manually) {
393         if (e.currentIndex != -1) {
394             new_index = (e.currentIndex + count) % c.count;
395             if (new_index < 0)
396                 new_index += c.count;
397         } else {
398             new_index = (count - 1) % c.count;
399             if (new_index < 0)
400                 new_index += c.count;
401         }
402     }
404     if (new_index != -1) {
405        try {
406             m.ignore_input_events = true;
407             s.select_completion(new_index);
408         } finally {
409             m.ignore_input_events = false;
410         }
411     }
413 interactive("minibuffer-complete", null,
414     function (I) { minibuffer_complete(I.window, I.p); });
415 interactive("minibuffer-complete-previous", null,
416     function (I) { minibuffer_complete(I.window, -I.p); });
418 function exit_minibuffer (window) {
419     var m = window.minibuffer;
420     var s = m.current_state;
421     if (! (s instanceof text_entry_minibuffer_state))
422         throw new Error("Invalid minibuffer state");
424     var val = m._input_text;
426     if (s.validator != null && ! s.validator(val, m))
427         return;
429     var match = null;
431     if (s.completer && s.match_required) {
432         if (! s.completions_valid || s.completions === undefined)
433             s.update_completions(false /* not conservative */, false /* don't update */);
435         let c = s.completions;
436         let i = s.selected_completion_index;
437         if (c != null && i >= 0 && i < c.count) {
438             if (c.get_value != null)
439                 match = c.get_value(i);
440             else
441                 match = c.get_string(i);
442         } else {
443             m.message("No match");
444             return;
445         }
446     }
448     if (s.history) {
449         s.history.push(val);
450         if (s.history.length > minibuffer_history_max_items)
451             s.history.splice(0, s.history.length - minibuffer_history_max_items);
452     }
453     var deferred = s.deferred;
454     if (deferred) {
455         if (s.match_required) {
456             deferred.resolve(match);
457         }
458         else {
459             deferred.resolve(val);
460         }
461     }
462     m.pop_state();
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, forward_keywords(arguments));
509     this.push_state(s);
510     yield co_return(yield s.promise);
513 minibuffer.prototype.read_command = function () {
514     keywords(
515         arguments,
516         $prompt = "Command", $history = "command",
517         $completer = prefix_completer(
518             $completions = function (visitor) {
519                 for (let [k,v] in Iterator(interactive_commands)) {
520                     visitor(v);
521                 }
522             },
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);
585 define_keywords("$object");
586 minibuffer.prototype.read_object_property = function () {
587     keywords(arguments,
588              $prompt = "Property:");
589     var o = arguments.$object || {};
590     var result = yield this.read(
591         $prompt = arguments.$prompt,
592         $completer = new prefix_completer(
593             $completions = function (push) {
594                 for (var i in o)
595                     push(i);
596             }),
597         $match_required,
598         $space_completes);
599     yield co_return(result);
603 provide("minibuffer-read");