minibuffer: better support for input methods
[conkeror.git] / modules / minibuffer-read.js
blobc66f3a5c78af25464f0905766ba7631575338242
2 define_variable("default_minibuffer_auto_complete_delay", 150,
3                      "Delay (in milliseconds) after the most recent key stroke before auto-completing.");
5 define_variable("minibuffer_auto_complete_preferences", {});
7 define_variable("minibuffer_auto_complete_default", false, "Boolean specifying whether to auto-complete by default.\nThe user variable `minibuffer_auto_complete_preferences' overrides this.");
9 var minibuffer_history_data = new string_hashmap();
11 /* FIXME: These should possibly be saved to disk somewhere */
12 define_variable("minibuffer_history_max_items", 100, "Maximum number of minibuffer history entries stored.\nOlder history entries are truncated after this limit is reached.");
15 /* The parameter `args' specifies the arguments.  In addition, the
16  * arguments for basic_minibuffer_state are also allowed.
17  *
18  * history:           [optional] specifies a string to identify the history list to use
19  *
20  * completer
21  *
22  * match_required
23  *
24  * default_completion  only used if match_required is set to true
25  *
26  * $valiator          [optional]
27  *          specifies a function
28  */
29 define_keywords("$history", "$validator",
31                 "$completer", "$match_required", "$default_completion",
32                 "$auto_complete", "$auto_complete_initial", "$auto_complete_conservative",
33                 "$auto_complete_delay",
34                 "$space_completes");
35 /* FIXME: support completing in another thread */
36 function text_entry_minibuffer_state(continuation) {
37     keywords(arguments);
39     basic_minibuffer_state.call(this, forward_keywords(arguments));
40     this.keymap = minibuffer_keymap;
42     this.continuation = continuation;
43     if (arguments.$history)
44     {
45         this.history = minibuffer_history_data.get_put_default(arguments.$history, []);
46         this.history_index = this.history.length;
47     }
49     this.validator = arguments.$validator;
51     if (arguments.$completer != null)
52     {
53         this.completer = arguments.$completer;
54         let auto = arguments.$auto_complete;
55         while (typeof(auto) == "string")
56             auto = minibuffer_auto_complete_preferences[auto];
57         if (auto == null)
58             auto = minibuffer_auto_complete_default;
59         this.auto_complete = auto;
60         this.auto_complete_initial = !!arguments.$auto_complete_initial;
61         this.auto_complete_conservative = !!arguments.$auto_complete_conservative;
62         let delay = arguments.$auto_complete_delay;
63         if (delay == null)
64             delay = default_minibuffer_auto_complete_delay;
65         this.auto_complete_delay = delay;
66         this.completions = null;
67         this.completions_valid = false;
68         this.space_completes = !!arguments.$space_completes;
69         this.completions_timer_ID = null;
70         this.completions_display_element = null;
71         this.selected_completion_index = -1;
72         this.match_required  = !!arguments.$match_required;
73         if (this.match_required)
74             this.default_completion = arguments.$default_completion;
75     }
78 function completions_tree_view(minibuffer_state)
80     this.minibuffer_state = minibuffer_state;
83 var atom_service = Cc["@mozilla.org/atom-service;1"].getService(Ci.nsIAtomService);
85 completions_tree_view.prototype = {
86     get rowCount () {
87         var c = this.minibuffer_state.completions;
88         if (!c)
89             return 0;
90         return c.count;
91     },
92     getCellText : function(row,column){
93         var c = this.minibuffer_state.completions;
94         if (row >= c.count)
95             return null;
96         if (column.index == 0)
97             return c.get_string(row);
98         if (c.get_description)
99             return c.get_description(row);
100         return "";
101     },
102     setTree : function(treebox){ this.treebox = treebox; },
103     isContainer: function(row){ return false; },
104     isSeparator: function(row){ return false; },
105     isSorted: function(){ return false; },
106     getLevel: function(row){ return 0; },
107     getImageSrc: function(row,col){ return null; },
108     getRowProperties: function(row,props){},
109     getCellProperties: function(row,col,props){
110         if (col.index == 0)
111             props.AppendElement(atom_service.getAtom("completion-string"));
112         else
113             props.AppendElement(atom_service.getAtom("completion-description"));
114     },
115     getColumnProperties: function(colid,col,props){}
118 // inherit from basic_minibuffer_state
119 text_entry_minibuffer_state.prototype = {
120     __proto__: basic_minibuffer_state.prototype,
121     load : function (window) {
122         this.window = window;
123         if (this.completer) {
124             // Create completion display element if needed
125             if (!this.completion_element)
126             {
127                 /* FIXME: maybe use the dom_generator */
128                 var tree = create_XUL(window, "tree");
129                 var s = this;
130                 tree.addEventListener("select", function () {
131                         s.selected_completion_index = s.completions_display_element.currentIndex;
132                         s.handle_completion_selected();
133                     }, true, false);
134                 tree.setAttribute("class", "completions");
136                 tree.setAttribute("rows", "8");
138                 tree.setAttribute("collapsed", "true");
140                 tree.setAttribute("hidecolumnpicker", "true");
141                 tree.setAttribute("hideheader", "true");
143                 var treecols = create_XUL(window, "treecols");
144                 tree.appendChild(treecols);
145                 var treecol = create_XUL(window, "treecol");
146                 treecol.setAttribute("flex", "1");
147                 treecols.appendChild(treecol);
148                 treecol = create_XUL(window, "treecol");
149                 treecol.setAttribute("flex", "1");
150                 treecols.appendChild(treecol);
151                 tree.appendChild(create_XUL(window, "treechildren"));
153                 window.minibuffer.insert_before(tree);
154                 tree.view = new completions_tree_view(this);
155                 this.completions_display_element = tree;
157                 /* This is the initial loading of this minibuffer
158                  * state.  If this.complete_initial is true, generate
159                  * completions. */
160                 if (this.auto_complete_initial)
161                     this.handle_input_changed();
162             }
164             this.update_completions_display();
165         }
166     },
168     unload : function (window) {
169         if (this.completions_display_element)
170             this.completions_display_element.setAttribute("collapsed", "true");
171     },
173     destroy : function (window) {
174         if (this.completions != null && this.completions.destroy)
175             this.completions.destroy();
176         var el = this.completions_display_element;
177         if (el)
178         {
179             el.parentNode.removeChild(el);
180             this.completions_display_element = null;
181         }
182         if (this.continuation)
183             this.continuation.throw(abort());
184     },
186     handle_input : function () {
187         if (!this.completer) return;
189         this.completions_valid = false;
191         if (!this.auto_complete) return;
193         var s = this;
195         if (this.auto_complete_delay > 0) {
196             if (this.completions_timer_ID != null)
197                 this.window.clearTimeout(this.completions_timer_ID);
198             this.completions_timer_ID = this.window.setTimeout(
199                 function () {
200                     s.completions_timer_ID = null;
201                     s.update_completions(true /* auto */);
202                     s.update_completions_display();
203                 }, this.auto_complete_delay);
204             return;
205         }
207         s.update_completions(true /* auto */);
208         s.update_completions_display();
209     },
211     ran_minibuffer_command : function () {
212         this.handle_input();
213     },
215     update_completions_display : function () {
217         var m = this.window.minibuffer;
219         if (m.current_state == this)
220         {
221             if (this.completions && this.completions.count > 0)
222             {
223                 this.completions_display_element.view = this.completions_display_element.view;
224                 this.completions_display_element.setAttribute("collapsed", "false");
226                 this.completions_display_element.currentIndex = this.selected_completion_index;
227                 this.completions_display_element.treeBoxObject.scrollToRow(this.selected_completion_index);
228             } else {
229                 this.completions_display_element.setAttribute("collapsed", "true");
230             }
231         }
232     },
234     /* If auto is true, this update is due to auto completion, rather
235      * than specifically requested. */
236     update_completions : function (auto) {
239         if (this.completions_timer_ID != null) {
240             this.window.clearTimeout(this.completions_timer_ID);
241             this.completions_timer_ID = null;
242         }
244         let m = this.window.minibuffer;
246         /* The completer should return undefined if completion was not
247          * attempted due to auto being true.  Otherwise, it can return
248          * null to indicate no completions. */
249         if (this.completions != null && this.completions.destroy)
250             this.completions.destroy();
251         let c = this.completions = this.completer(m._input_text, m._selection_start,
252                                                   auto && this.auto_complete_conservative);
253         this.completions_valid = true;
255         let i = -1;
256         if (c && c.count > 0) {
257             if (this.match_required) {
258                 if (c.count == 1)
259                     i = 0;
260                 else if (c.default_completion != null)
261                     i = c.default_completion;
262                 else if (this.default_completion && this.completions.index_of)
263                     i = this.completions.index_of(this.default_completion);
264             }
265             this.selected_completion_index = i;
266         }
267     },
269     select_completion : function (i) {
270         this.selected_completion_index = i;
271         this.completions_display_element.currentIndex = i;
272         if (i >= 0)
273             this.completions_display_element.treeBoxObject.ensureRowIsVisible(i);
274         this.handle_completion_selected();
275     },
277     handle_completion_selected : function () {
278         /**
279          * When a completion is selected, apply it to the input text
280          * if a match is not "required"; otherwise, the completion is
281          * only displayed.
282          */
283         var i = this.selected_completion_index;
284         var m = this.window.minibuffer;
285         var c = this.completions;
287         if (this.completions_valid && c && !this.match_required && i >= 0 && i < c.count)
288         {
289             c.apply(i, m);
290         }
291     }
294 function minibuffer_complete(window, count)
296     var m = window.minibuffer;
297     var s = m.current_state;
298     if (!(s instanceof text_entry_minibuffer_state))
299         throw new Error("Invalid minibuffer state");
300     if (!s.completer)
301         return;
302     var just_completed_manually = false;
303     if (!s.completions_valid || s.completions === undefined) {
304         if (s.completions_timer_ID == null)
305             just_completed_manually = true;
306         s.update_completions(false /* not auto */);
307         s.update_completions_display();
308     }
310     var c = s.completions;
312     if (!c || c.count == 0)
313         return;
315     var e = s.completions_display_element;
316     var new_index = -1;
318     if (count == 1 && c.apply_common_prefix)
319     {
320         c.apply_common_prefix(m);
321         c.apply_common_prefix = null;
322     } else if (!just_completed_manually) {
323         if (e.currentIndex != -1)
324         {
325             new_index = (e.currentIndex + count) % c.count;
326             if (new_index < 0)
327                 new_index += c.count;
328         } else {
329             new_index = (count - 1) % c.count;
330             if (new_index < 0)
331                 new_index += c.count;
332         }
333     }
335     if (new_index != -1)
336         s.select_completion(new_index);
338 interactive("minibuffer-complete", function (I) {minibuffer_complete(I.window, I.p);});
339 interactive("minibuffer-complete-previous", function (I) {minibuffer_complete(I.window, -I.p);});
341 function exit_minibuffer(window)
343     var m = window.minibuffer;
344     var s = m.current_state;
345     if (!(s instanceof text_entry_minibuffer_state))
346         throw new Error("Invalid minibuffer state");
348     var val = m._input_text;
350     if (s.validator != null && !s.validator(val, m))
351         return;
353     var match = null;
355     if (s.completer && s.match_required) {
356         if (!s.completions_valid || s.completions === undefined)
357             s.update_completions(false);
359         let c = s.completions;
360         let i = s.selected_completion_index;
361         if (c != null && i >= 0 && i < c.count) {
362             if (c.get_value != null)
363                 match = c.get_value(i);
364             else
365                 match = c.get_string(i);
366         } else {
367             m.message("No match");
368             return;
369         }
370     }
372     if (s.history)
373     {
374         s.history.push(val);
375         if (s.history.length > minibuffer_history_max_items)
376             s.history.splice(0, s.history.length - minibuffer_history_max_items);
377     }
378     var cont = s.continuation;
379     delete s.continuation;
380     m.pop_state();
381     if (cont) {
382         if (s.match_required)
383             cont(match);
384         else
385             cont(val);
386     }
388 interactive("exit-minibuffer", function (I) {exit_minibuffer(I.window);});
390 function minibuffer_history_next (window, count)
392     var m = window.minibuffer;
393     var s = m.current_state;
394     if (!(s instanceof text_entry_minibuffer_state))
395         throw new Error("Invalid minibuffer state");
396     if (!s.history || s.history.length == 0)
397         return;
398     m._restore_normal_state();
399     var index = s.history_index + count;
400     if (index < 0)
401         index = 0;
402     if (index >= s.history.length)
403         index = s.history.length - 1;
404     s.history_index = index;
405     m._input_text = s.history[index];
406     m._set_selection();
408 interactive("minibuffer-history-next", function (I) {minibuffer_history_next(I.window, I.p);});
409 interactive("minibuffer-history-previous", function (I) {minibuffer_history_next(I.window, -I.p);});
411 // Define the asynchronous minibuffer.read function
412 minibuffer.prototype.read = function () {
413     var s = new text_entry_minibuffer_state((yield CONTINUATION), forward_keywords(arguments));
414     this.push_state(s);
415     var result = yield SUSPEND;
416     yield co_return(result);
419 minibuffer.prototype.read_command = function () {
420     keywords(arguments);
421     var completer = prefix_completer(
422         $completions = function (visitor) interactive_commands.for_each_value(visitor),
423         $get_string = function (x) x.name,
424         $get_description = function (x) x.shortdoc || "",
425         $get_value = function (x) x.name
426     );
428     var result = yield this.read($prompt = "Command", $history = "command",
429         forward_keywords(arguments),
430         $completer = completer,
431         $match_required = true);
432     yield co_return(result);
435 minibuffer.prototype.read_user_variable = function () {
436     keywords(arguments);
437     var completer = prefix_completer(
438         $completions = function (visitor) user_variables.for_each(visitor),
439         $get_string = function (x) x,
440         $get_description = function (x) user_variables.get(x).shortdoc || "",
441         $get_value = function (x) x
442     );
444     var result = yield this.read($prompt = "User variable", $history = "user_variable",
445         forward_keywords(arguments),
446         $completer = completer,
447         $match_required = true);
448     yield co_return(result);