spawn-process: Better error information in case of timeout
[conkeror.git] / modules / minibuffer-read.js
blob65bf77c3943acdd5fe0e717a41e67bc717f3cb3e
1 /**
2  * (C) Copyright 2007-2010,2012 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         return c.get_description(row);
51     },
52     setTree: function (treebox) { this.treeBox = treebox; },
53     isContainer: function (row) { return false; },
54     isSeparator: function (row) { return false; },
55     isSorted: function () { return false; },
56     getLevel: function (row) { return 0; },
57     getImageSrc: function (row, col) {
58         var c = this.minibuffer_state.completions;
59         if (this.minibuffer_state.enable_icons &&
60             col.index == 0)
61         {
62             return c.get_icon(row);
63         }
64         return null;
65     },
66     getRowProperties: function (row, props) {},
67     getCellProperties: function (row, col, props) {
68         if (col.index == 0)
69             var a = atom_service.getAtom("completion-string");
70         else
71             a = atom_service.getAtom("completion-description");
72         if (props)
73             props.AppendElement(a);
74         return a;
75     },
76     getColumnProperties: function (colid, col, props) {}
80 /* The parameter `args' specifies the arguments.  In addition, the
81  * arguments for basic_minibuffer_state are also allowed.
82  *
83  * history:           [optional] specifies a string to identify the history list to use
84  *
85  * completer
86  *
87  * require_match
88  *
89  * default_completion  only used if require_match is set to true
90  *
91  * $valiator          [optional]
92  *          specifies a function
93  */
94 define_keywords("$keymap", "$history", "$validator",
95                 "$completer", "$require_match", "$default_completion",
96                 "$auto_complete", "$auto_complete_initial", "$auto_complete_conservative",
97                 "$auto_complete_delay", "$enable_icons",
98                 "$space_completes");
99 /* FIXME: support completing in another thread */
100 function text_entry_minibuffer_state (minibuffer) {
101     keywords(arguments, $keymap = minibuffer_keymap,
102              $enable_icons = false);
104     basic_minibuffer_state.call(this, minibuffer, forward_keywords(arguments));
106     let deferred = Promise.defer();
107     this.deferred = deferred;
108     this.promise = make_simple_cancelable(deferred);
110     if (arguments.$history) {
111         this.history = minibuffer_history_data[arguments.$history] =
112             minibuffer_history_data[arguments.$history] || [];
113         this.history_index = -1;
114         this.saved_last_history_entry = null;
115     }
117     this.validator = arguments.$validator;
119     if (arguments.$completer != null) {
120         this.completer = arguments.$completer;
121         let auto = arguments.$auto_complete;
122         while (typeof auto == "string")
123             auto = minibuffer_auto_complete_preferences[auto];
124         if (auto == null)
125             auto = minibuffer_auto_complete_default;
126         this.auto_complete = auto;
127         this.auto_complete_initial = !!arguments.$auto_complete_initial;
128         this.auto_complete_conservative = !!arguments.$auto_complete_conservative;
129         let delay = arguments.$auto_complete_delay;
130         if (delay == null)
131             delay = default_minibuffer_auto_complete_delay;
132         this.auto_complete_delay = delay;
133         this.completions = null;
134         this.completions_valid = false;
135         this.space_completes = !!arguments.$space_completes;
136         if (this.space_completes)
137             this.keymaps.push(minibuffer_space_completion_keymap);
138         this.completions_timer_ID = null;
139         this.completions_display_element = null;
140         this.selected_completion_index = -1;
141         this.require_match = !!arguments.$require_match;
142         this.require_match_default = this.require_match;
143         if (this.require_match)
144             this.default_completion = arguments.$default_completion;
145         this.enable_icons = arguments.$enable_icons;
146     }
148 text_entry_minibuffer_state.prototype = {
149     constructor: text_entry_minibuffer_state,
150     __proto__: basic_minibuffer_state.prototype,
151     load: function () {
152         basic_minibuffer_state.prototype.load.call(this);
153         var window = this.minibuffer.window;
154         if (this.completer) {
155             // Create completion display element if needed
156             if (! this.completion_element) {
157                 /* FIXME: maybe use the dom_generator */
158                 var tree = create_XUL(window, "tree");
159                 var s = this;
160                 tree.addEventListener("select", function () {
161                         s.selected_completion_index = s.completions_display_element.currentIndex;
162                         s.handle_completion_selected();
163                     }, true);
164                 tree.setAttribute("class", "completions");
166                 tree.setAttribute("rows", minibuffer_completion_rows);
168                 tree.setAttribute("collapsed", "true");
170                 tree.setAttribute("hidecolumnpicker", "true");
171                 tree.setAttribute("hideheader", "true");
172                 if (this.enable_icons)
173                     tree.setAttribute("hasicons", "true");
175                 var treecols = create_XUL(window, "treecols");
176                 tree.appendChild(treecols);
177                 var treecol = create_XUL(window, "treecol");
178                 treecol.setAttribute("flex", "1");
179                 treecols.appendChild(treecol);
180                 treecol = create_XUL(window, "treecol");
181                 treecol.setAttribute("flex", "1");
182                 treecols.appendChild(treecol);
183                 tree.appendChild(create_XUL(window, "treechildren"));
185                 this.minibuffer.insert_before(tree);
186                 tree.view = new completions_tree_view(this);
187                 this.completions_display_element = tree;
189                 // This is the initial loading of this minibuffer state.
190                 // If this.auto_complete_initial is true, generate
191                 // completions.
192                 if (this.auto_complete_initial)
193                     this.handle_input();
194             }
195             this.update_completions_display();
196         }
197     },
199     unload: function () {
200         if (this.completions_display_element)
201             this.completions_display_element.setAttribute("collapsed", "true");
202         basic_minibuffer_state.prototype.unload.call(this);
203     },
205     destroy: function () {
206         if (this.completions)
207             this.completions.destroy();
208         delete this.completions;
209         if (this.completions_cont)
210             this.completions_cont.cancel();
211         delete this.completions_cont;
213         var el = this.completions_display_element;
214         if (el) {
215             el.parentNode.removeChild(el);
216             this.completions_display_element = null;
217         }
218         if (this.promise)
219             this.promise.cancel();
220         basic_minibuffer_state.prototype.destroy.call(this);
221     },
223     handle_input: function () {
224         if (! this.completer)
225             return;
226         this.completions_valid = false;
227         if (! this.auto_complete)
228             return;
229         var s = this;
230         var window = this.minibuffer.window;
231         if (this.auto_complete_delay > 0) {
232             if (this.completions_timer_ID != null)
233                 window.clearTimeout(this.completions_timer_ID);
234             this.completions_timer_ID = window.setTimeout(
235                 function () {
236                     s.completions_timer_ID = null;
237                     s.update_completions(true /* auto */, true /* update completions display */);
238                 }, this.auto_complete_delay);
239         } else {
240             s.update_completions(true /* auto */, true /* update completions display */);
241         }
242     },
244     update_completions_display: function () {
245         var m = this.minibuffer;
246         if (m.current_state == this) {
247             if (this.completions && this.completions.count > 0) {
248                 this.completions_display_element.view = this.completions_display_element.view;
249                 this.completions_display_element.setAttribute("collapsed", "false");
250                 this.completions_display_element.currentIndex = this.selected_completion_index;
251                 var max_display = this.completions_display_element.treeBoxObject.getPageLength();
252                 var mid_point = Math.floor(max_display / 2);
253                 if (this.completions.count - this.selected_completion_index <= mid_point)
254                     var pos = this.completions.count - max_display;
255                 else
256                     pos = Math.max(0, this.selected_completion_index - mid_point);
257                 this.completions_display_element.treeBoxObject.scrollToRow(pos);
258             } else {
259                 this.completions_display_element.setAttribute("collapsed", "true");
260             }
261         }
262     },
264     /* If auto is true, this update is due to auto completion, rather
265      * than specifically requested. */
266     update_completions: function (auto, update_display) {
267         var window = this.minibuffer.window;
268         if (this.completions_timer_ID != null) {
269             window.clearTimeout(this.completions_timer_ID);
270             this.completions_timer_ID = null;
271         }
272         var m = this.minibuffer;
273         if (this.completions_cont) {
274             this.completions_cont.cancel();
275             this.completions_cont = null;
276         }
277         if (m._selection_start > 0 || ! auto || ! this.auto_complete_conservative)
278             var c = this.completer.complete(m._input_text, m._selection_start);
279         if (is_coroutine(c)) {
280             var s = this;
281             var already_done = false;
282             this.completions_cont = spawn(function () {
283                 try {
284                     var x = yield c;
285                 } catch (e) {
286                     handle_interactive_error(window, e);
287                 } finally {
288                     s.completions_cont = null;
289                     already_done = true;
290                 }
291                 s.update_completions_done(x, update_display);
292             }());
293             // In case the completer actually already finished
294             if (already_done)
295                 this.completions_cont = null;
296         } else
297             this.update_completions_done(c, update_display);
298     },
300     update_completions_done: function (c, update_display) {
301         /* The completer should return undefined if completion was not
302          * attempted due to auto being true.  Otherwise, it can return
303          * null to indicate no completions. */
304         if (this.completions)
305             this.completions.destroy();
307         this.completions = c;
308         this.completions_valid = true;
309         this.applied_common_prefix = false;
311         this.require_match = this.completer.require_match;
312         if (this.require_match == null)
313             this.require_match = this.require_match_default;
315         if (c && c.count > 0) {
316             var i = -1;
317             if (this.require_match) {
318                 if (c.count == 1)
319                     i = 0;
320                 else if (c.default_completion != null)
321                     i = c.default_completion;
322                 else if (this.default_completion)
323                     i = this.completions.index_of(this.default_completion);
324             }
325             this.selected_completion_index = i;
326         }
328         if (update_display)
329             this.update_completions_display();
330     },
332     select_completion: function (i) {
333         this.selected_completion_index = i;
334         this.completions_display_element.currentIndex = i;
335         if (i >= 0)
336             this.completions_display_element.treeBoxObject.ensureRowIsVisible(i);
337         this.handle_completion_selected();
338     },
340     handle_completion_selected: function () {
341         /**
342          * When a completion is selected, apply it to the input text
343          * if a match is not "required"; otherwise, the completion is
344          * only displayed.
345          */
346         var i = this.selected_completion_index;
347         var m = this.minibuffer;
348         var c = this.completions;
350         if (this.completions_valid && c && !this.require_match && i >= 0 && i < c.count)
351             m.set_input_state(c.get_input_state(i));
352     }
355 function minibuffer_complete (window, count) {
356     var m = window.minibuffer;
357     var s = m.current_state;
358     if (! (s instanceof text_entry_minibuffer_state))
359         throw new Error("Invalid minibuffer state");
360     if (! s.completer)
361         return;
362     var just_completed_manually = false;
363     if (! s.completions_valid || s.completions === undefined) {
364         if (s.completions_timer_ID == null)
365             just_completed_manually = true;
366         //XXX: may need to use ignore_input_events here
367         s.update_completions(false /* not auto */, true /* update completions display */);
369         // If the completer is a coroutine, nothing we can do here
370         if (! s.completions_valid)
371             return;
372     }
374     var c = s.completions;
376     if (! c || c.count == 0)
377         return;
379     var e = s.completions_display_element;
380     var new_index = -1;
381     var common_prefix;
383     if (count == 1 && ! s.applied_common_prefix &&
384         (common_prefix = c.common_prefix_input_state))
385     {
386         //XXX: may need to use ignore_input_events here
387         m.set_input_state(common_prefix);
388         s.applied_common_prefix = true;
389     } else if (!just_completed_manually) {
390         if (e.currentIndex != -1) {
391             new_index = (e.currentIndex + count) % c.count;
392             if (new_index < 0)
393                 new_index += c.count;
394         } else {
395             new_index = (count - 1) % c.count;
396             if (new_index < 0)
397                 new_index += c.count;
398         }
399     }
401     if (new_index != -1) {
402        try {
403             m.ignore_input_events = true;
404             s.select_completion(new_index);
405         } finally {
406             m.ignore_input_events = false;
407         }
408     }
410 interactive("minibuffer-complete", null,
411     function (I) { minibuffer_complete(I.window, I.p); });
412 interactive("minibuffer-complete-previous", null,
413     function (I) { minibuffer_complete(I.window, -I.p); });
415 function exit_minibuffer (window) {
416     var m = window.minibuffer;
417     var s = m.current_state;
418     if (! (s instanceof text_entry_minibuffer_state))
419         throw new Error("Invalid minibuffer state");
421     var val = m._input_text;
423     if (s.validator != null && ! s.validator(val, m))
424         return;
426     var match = null;
428     if (s.completer && s.require_match) {
429         if (! s.completions_valid || s.completions === undefined)
430             s.update_completions(false /* not conservative */, false /* don't update */);
432         let c = s.completions;
433         let i = s.selected_completion_index;
434         if (c != null && i >= 0 && i < c.count) {
435             match = c.get_value(i);
436         } else {
437             m.message("No match");
438             return;
439         }
440     }
442     if (s.history) {
443         s.history.push(val);
444         if (s.history.length > minibuffer_history_max_items)
445             s.history.splice(0, s.history.length - minibuffer_history_max_items);
446     }
447     var deferred = s.deferred;
448     if (deferred) {
449         if (s.require_match) {
450             deferred.resolve(match);
451         }
452         else {
453             deferred.resolve(val);
454         }
455     }
456     m.pop_state();
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, forward_keywords(arguments));
503     this.push_state(s);
504     yield co_return(yield s.promise);
507 minibuffer.prototype.read_command = function () {
508     keywords(
509         arguments,
510         $prompt = "Command", $history = "command",
511         $completer = new prefix_completer(
512             $completions = function (visitor) {
513                 for (let [k,v] in Iterator(interactive_commands)) {
514                     visitor(v);
515                 }
516             },
517             $get_string = function (x) x.name,
518             $get_description = function (x) x.shortdoc || "",
519             $get_value = function (x) x.name),
520         $require_match,
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 = new 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         $require_match,
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 = new 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              $require_match,
573              $space_completes);
574     var result = yield this.read(forward_keywords(arguments));
575     yield co_return(result);
579 define_keywords("$object");
580 minibuffer.prototype.read_object_property = function () {
581     keywords(arguments,
582              $prompt = "Property:");
583     var o = arguments.$object || {};
584     var result = yield this.read(
585         $prompt = arguments.$prompt,
586         $completer = new prefix_completer(
587             $completions = function (push) {
588                 for (var i in o)
589                     push(i);
590             }),
591         $require_match,
592         $space_completes);
593     yield co_return(result);
597 provide("minibuffer-read");