user_variables: replace string_hashmap with ordinary javascript object implementation
[conkeror.git] / modules / minibuffer-read.js
blobbd3e7e1674fc54909bd707797d07ee66a836691c
1 /**
2  * (C) Copyright 2007 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("$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(continuation) {
49     keywords(arguments);
51     basic_minibuffer_state.call(this, forward_keywords(arguments));
52     this.keymap = minibuffer_keymap;
54     this.continuation = continuation;
55     if (arguments.$history)
56     {
57         this.history = minibuffer_history_data.get_put_default(arguments.$history, []);
58         this.history_index = -1;
59         this.saved_last_history_entry = null;
60     }
62     this.validator = arguments.$validator;
64     if (arguments.$completer != null)
65     {
66         this.completer = arguments.$completer;
67         let auto = arguments.$auto_complete;
68         while (typeof(auto) == "string")
69             auto = minibuffer_auto_complete_preferences[auto];
70         if (auto == null)
71             auto = minibuffer_auto_complete_default;
72         this.auto_complete = auto;
73         this.auto_complete_initial = !!arguments.$auto_complete_initial;
74         this.auto_complete_conservative = !!arguments.$auto_complete_conservative;
75         let delay = arguments.$auto_complete_delay;
76         if (delay == null)
77             delay = default_minibuffer_auto_complete_delay;
78         this.auto_complete_delay = delay;
79         this.completions = null;
80         this.completions_valid = false;
81         this.space_completes = !!arguments.$space_completes;
82         this.completions_timer_ID = null;
83         this.completions_display_element = null;
84         this.selected_completion_index = -1;
85         this.match_required  = !!arguments.$match_required;
86         if (this.match_required)
87             this.default_completion = arguments.$default_completion;
88     }
91 function completions_tree_view(minibuffer_state)
93     this.minibuffer_state = minibuffer_state;
96 var atom_service = Cc["@mozilla.org/atom-service;1"].getService(Ci.nsIAtomService);
98 completions_tree_view.prototype = {
99     get rowCount () {
100         var c = this.minibuffer_state.completions;
101         if (!c)
102             return 0;
103         return c.count;
104     },
105     getCellText : function(row,column){
106         var c = this.minibuffer_state.completions;
107         if (row >= c.count)
108             return null;
109         if (column.index == 0)
110             return c.get_string(row);
111         if (c.get_description)
112             return c.get_description(row);
113         return "";
114     },
115     setTree : function(treebox){ this.treebox = treebox; },
116     isContainer: function(row){ return false; },
117     isSeparator: function(row){ return false; },
118     isSorted: function(){ return false; },
119     getLevel: function(row){ return 0; },
120     getImageSrc: function(row,col){ return null; },
121     getRowProperties: function(row,props){},
122     getCellProperties: function(row,col,props){
123         if (col.index == 0)
124             props.AppendElement(atom_service.getAtom("completion-string"));
125         else
126             props.AppendElement(atom_service.getAtom("completion-description"));
127     },
128     getColumnProperties: function(colid,col,props){}
131 // inherit from basic_minibuffer_state
132 text_entry_minibuffer_state.prototype = {
133     __proto__: basic_minibuffer_state.prototype,
134     load : function (window) {
135         this.window = window;
136         if (this.completer) {
137             // Create completion display element if needed
138             if (!this.completion_element)
139             {
140                 /* FIXME: maybe use the dom_generator */
141                 var tree = create_XUL(window, "tree");
142                 var s = this;
143                 tree.addEventListener("select", function () {
144                         s.selected_completion_index = s.completions_display_element.currentIndex;
145                         s.handle_completion_selected();
146                     }, true, false);
147                 tree.setAttribute("class", "completions");
149                 tree.setAttribute("rows", "8");
151                 tree.setAttribute("collapsed", "true");
153                 tree.setAttribute("hidecolumnpicker", "true");
154                 tree.setAttribute("hideheader", "true");
156                 var treecols = create_XUL(window, "treecols");
157                 tree.appendChild(treecols);
158                 var treecol = create_XUL(window, "treecol");
159                 treecol.setAttribute("flex", "1");
160                 treecols.appendChild(treecol);
161                 treecol = create_XUL(window, "treecol");
162                 treecol.setAttribute("flex", "1");
163                 treecols.appendChild(treecol);
164                 tree.appendChild(create_XUL(window, "treechildren"));
166                 window.minibuffer.insert_before(tree);
167                 tree.view = new completions_tree_view(this);
168                 this.completions_display_element = tree;
170                 /* This is the initial loading of this minibuffer
171                  * state.  If this.complete_initial is true, generate
172                  * completions. */
173                 if (this.auto_complete_initial)
174                     this.handle_input();
175             }
177             this.update_completions_display();
178         }
179     },
181     unload : function (window) {
182         if (this.completions_display_element)
183             this.completions_display_element.setAttribute("collapsed", "true");
184     },
186     destroy : function (window) {
187         if (this.completions != null && this.completions.destroy)
188             this.completions.destroy();
189         delete this.completions;
190         if (this.completions_cont)
191             this.completions_cont.throw(abort());
192         delete this.completions_cont;
194         var el = this.completions_display_element;
195         if (el)
196         {
197             el.parentNode.removeChild(el);
198             this.completions_display_element = null;
199         }
200         if (this.continuation)
201             this.continuation.throw(abort());
202     },
204     handle_input : function () {
205         if (!this.completer) return;
207         this.completions_valid = false;
209         if (!this.auto_complete) return;
211         var s = this;
213         if (this.auto_complete_delay > 0) {
214             if (this.completions_timer_ID != null)
215                 this.window.clearTimeout(this.completions_timer_ID);
216             this.completions_timer_ID = this.window.setTimeout(
217                 function () {
218                     s.completions_timer_ID = null;
219                     s.update_completions(true /* auto */, true /* update completions display */);
220                 }, this.auto_complete_delay);
221             return;
222         }
224         s.update_completions(true /* auto */, true /* update completions display */);
225     },
227     ran_minibuffer_command : function () {
228         this.handle_input();
229     },
231     update_completions_display : function () {
233         var m = this.window.minibuffer;
235         if (m.current_state == this)
236         {
237             if (this.completions && this.completions.count > 0)
238             {
239                 this.completions_display_element.view = this.completions_display_element.view;
240                 this.completions_display_element.setAttribute("collapsed", "false");
242                 this.completions_display_element.currentIndex = this.selected_completion_index;
243                 this.completions_display_element.treeBoxObject.scrollToRow(this.selected_completion_index);
244             } else {
245                 this.completions_display_element.setAttribute("collapsed", "true");
246             }
247         }
248     },
250     /* If auto is true, this update is due to auto completion, rather
251      * than specifically requested. */
252     update_completions : function (auto, update_display) {
254         if (this.completions_timer_ID != null) {
255             this.window.clearTimeout(this.completions_timer_ID);
256             this.completions_timer_ID = null;
257         }
259         let m = this.window.minibuffer;
261         if (this.completions_cont) {
262             this.completions_cont.throw(abort());
263             this.completions_cont = null;
264         }
266         let c = this.completer(m._input_text, m._selection_start,
267                                auto && this.auto_complete_conservative);
269         if (is_coroutine(c)) {
270             let s = this;
271             let already_done = false;
272             this.completions_cont = co_call(function () {
273                 var x;
274                 try {
275                     x = yield c;
276                 } finally {
277                     s.completions_cont = null;
278                     already_done = true;
279                 }
280                 s.update_completions_done(x, update_display);
281             }());
283             // In case the completer actually already finished
284             if (already_done)
285                 this.completions_cont = null;
286             return;
287         } else
288             this.update_completions_done(c, update_display);
289     },
291     update_completions_done : function update_completions_done(c, update_display) {
293         /* The completer should return undefined if completion was not
294          * attempted due to auto being true.  Otherwise, it can return
295          * null to indicate no completions. */
296         if (this.completions != null && this.completions.destroy)
297             this.completions.destroy();
299         this.completions = c;
300         this.completions_valid = true;
301         this.applied_common_prefix = false;
303         let i = -1;
304         if (c && c.count > 0) {
305             if (this.match_required) {
306                 if (c.count == 1)
307                     i = 0;
308                 else if (c.default_completion != null)
309                     i = c.default_completion;
310                 else if (this.default_completion && this.completions.index_of)
311                     i = this.completions.index_of(this.default_completion);
312             }
313             this.selected_completion_index = i;
314         }
316         if (update_display)
317             this.update_completions_display();
318     },
320     select_completion : function (i) {
321         this.selected_completion_index = i;
322         this.completions_display_element.currentIndex = i;
323         if (i >= 0)
324             this.completions_display_element.treeBoxObject.ensureRowIsVisible(i);
325         this.handle_completion_selected();
326     },
328     handle_completion_selected : function () {
329         /**
330          * When a completion is selected, apply it to the input text
331          * if a match is not "required"; otherwise, the completion is
332          * only displayed.
333          */
334         var i = this.selected_completion_index;
335         var m = this.window.minibuffer;
336         var c = this.completions;
338         if (this.completions_valid && c && !this.match_required && i >= 0 && i < c.count)
339         {
340             m.set_input_state(c.get_input_state(i));
341         }
342     }
345 function minibuffer_complete(window, count)
347     var m = window.minibuffer;
348     var s = m.current_state;
349     if (!(s instanceof text_entry_minibuffer_state))
350         throw new Error("Invalid minibuffer state");
351     if (!s.completer)
352         return;
353     var just_completed_manually = false;
354     if (!s.completions_valid || s.completions === undefined) {
355         if (s.completions_timer_ID == null)
356             just_completed_manually = true;
357         s.update_completions(false /* not auto */, true /* update completions display */);
359         // If the completer is a coroutine, nothing we can do here
360         if (!s.completions_valid)
361             return;
362     }
364     var c = s.completions;
366     if (!c || c.count == 0)
367         return;
369     var e = s.completions_display_element;
370     var new_index = -1;
372     let common_prefix;
374     if (count == 1 && !s.applied_common_prefix && (common_prefix = c.common_prefix_input_state))
375     {
376         m.set_input_state(common_prefix);
377         s.applied_common_prefix = true;
378     } else if (!just_completed_manually) {
379         if (e.currentIndex != -1)
380         {
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)
401     var m = window.minibuffer;
402     var s = m.current_state;
403     if (!(s instanceof text_entry_minibuffer_state))
404         throw new Error("Invalid minibuffer state");
406     var val = m._input_text;
408     if (s.validator != null && !s.validator(val, m))
409         return;
411     var match = null;
413     if (s.completer && s.match_required) {
414         if (!s.completions_valid || s.completions === undefined)
415             s.update_completions(false /* not conservative */, false /* don't update */);
417         let c = s.completions;
418         let i = s.selected_completion_index;
419         if (c != null && i >= 0 && i < c.count) {
420             if (c.get_value != null)
421                 match = c.get_value(i);
422             else
423                 match = c.get_string(i);
424         } else {
425             m.message("No match");
426             return;
427         }
428     }
430     if (s.history)
431     {
432         s.history.push(val);
433         if (s.history.length > minibuffer_history_max_items)
434             s.history.splice(0, s.history.length - minibuffer_history_max_items);
435     }
436     var cont = s.continuation;
437     delete s.continuation;
438     m.pop_state();
439     if (cont) {
440         if (s.match_required)
441             cont(match);
442         else
443             cont(val);
444     }
446 interactive("exit-minibuffer", null,
447     function (I) { exit_minibuffer(I.window); });
449 function minibuffer_history_next (window, count)
451     var m = window.minibuffer;
452     var s = m.current_state;
453     if (!(s instanceof text_entry_minibuffer_state))
454         throw new Error("Invalid minibuffer state");
455     if (!s.history || s.history.length == 0)
456         throw interactive_error("No history available.");
457     if (count == 0)
458         return;
459     var index = s.history_index;
460     if (count > 0 && index == -1)
461         throw interactive_error("End of history; no next item");
462     else if (count < 0 && index == 0) {
463         throw interactive_error("Beginning of history; no preceding item");
464     }
465     if (index == -1) {
466         s.saved_last_history_entry = m._input_text;
467         index = s.history.length + count;
468     } else
469         index = index + count;
471     if (index < 0)
472         index = 0;
474     m._restore_normal_state();
475     if (index >= s.history.length) {
476         index = -1;
477         m._input_text = s.saved_last_history_entry;
478     } else {
479         m._input_text = s.history[index];
480     }
481     s.history_index = index;
482     m._set_selection();
483     s.handle_input();
485 interactive("minibuffer-history-next", null,
486     function (I) { minibuffer_history_next(I.window, I.p); });
487 interactive("minibuffer-history-previous", null,
488     function (I) { minibuffer_history_next(I.window, -I.p); });
490 // Define the asynchronous minibuffer.read function
491 minibuffer.prototype.read = function () {
492     var s = new text_entry_minibuffer_state((yield CONTINUATION), forward_keywords(arguments));
493     this.push_state(s);
494     var result = yield SUSPEND;
495     yield co_return(result);
498 minibuffer.prototype.read_command = function () {
499     keywords(
500         arguments,
501         $prompt = "Command", $history = "command",
502         $completer = prefix_completer(
503             $completions = function (visitor) interactive_commands.for_each_value(visitor),
504             $get_string = function (x) x.name,
505             $get_description = function (x) x.shortdoc || "",
506             $get_value = function (x) x.name),
507         $match_required = true);
508     var result = yield this.read(forward_keywords(arguments));
509     yield co_return(result);
512 minibuffer.prototype.read_user_variable = function () {
513     keywords(
514         arguments,
515         $prompt = "User variable", $history = "user_variable",
516         $completer = prefix_completer(
517             $completions = function (visitor) {
518                 [visitor(i, user_variables[i]) for (i in user_variables)];
519             },
520             $get_string = function (x) x,
521             $get_description = function (x) user_variables[x].shortdoc || "",
522             $get_value = function (x) x),
523         $match_required = true);
524     var result = yield this.read(forward_keywords(arguments));
525     yield co_return(result);
528 minibuffer.prototype.read_preference = function minibuffer__read_preference () {
529     keywords(arguments,
530              $prompt = "Preference:", $history = "preference",
531              $completer = prefix_completer(
532                  $completions = preferences.getBranch(null).getChildList("", {}),
533                  $get_description = function (pref) {
534                      let default_value = get_default_pref(pref);
535                      let value = get_pref(pref);
536                      if (value == default_value)
537                          value = null;
538                      let type;
539                      switch (preferences.getBranch(null).getPrefType(pref)) {
540                      case Ci.nsIPrefBranch.PREF_STRING:
541                          type = "string";
542                          break;
543                      case Ci.nsIPrefBranch.PREF_INT:
544                          type = "int";
545                          break;
546                      case Ci.nsIPrefBranch.PREF_BOOL:
547                          type = "boolean";
548                          break;
549                      }
550                      let out = type + ":";
551                      if (value != null)
552                          out += " " + pretty_print_value(value);
553                      if (default_value != null)
554                          out += " (" + pretty_print_value(default_value) + ")";
555                      return out;
556                  }),
557              $match_required = true);
558     var result = yield this.read(forward_keywords(arguments));
559     yield co_return(result);