Fix minibuffer handling for prefix completion
[conkeror.git] / modules / minibuffer-read.js
blobd24845c9d014dfcd98e45aff7169ddc968c80fcb
2 /* USER PREFERENCE */
3 var default_minibuffer_auto_complete_delay = 150;
5 /* USER PREFERENCE */
6 var minibuffer_auto_complete_preferences = {};
8 var minibuffer_auto_complete_default = false;
11 var minibuffer_history_data = new string_hashmap();
13 /* FIXME: These should possibly be saved to disk somewhere */
14 /* USER PREFERENCE */
15 var minibuffer_history_max_items = 100;
18 /* The parameter `args' specifies the arguments.  In addition, the
19  * arguments for basic_minibuffer_state are also allowed.
20  *
21  * history:           [optional] specifies a string to identify the history list to use
22  *
23  * completer
24  *
25  * match_required
26  *
27  * default_completion  only used if match_required is set to true
28  *
29  * $valiator          [optional]
30  *          specifies a function
31  */
32 define_keywords("$history", "$validator",
34                 "$completer", "$match_required", "$default_completion",
35                 "$auto_complete", "$auto_complete_initial", "$auto_complete_conservative",
36                 "$auto_complete_delay",
37                 "$space_completes");
38 /* FIXME: support completing in another thread */
39 function text_entry_minibuffer_state(continuation) {
40     keywords(arguments);
42     basic_minibuffer_state.call(this, forward_keywords(arguments));
43     this.keymap = minibuffer_keymap;
45     this.continuation = continuation;
46     if (arguments.$history)
47     {
48         this.history = minibuffer_history_data.get_put_default(arguments.$history, []);
49         this.history_index = this.history.length;
50     }
52     this.validator = arguments.$validator;
54     if (arguments.$completer != null)
55     {
56         this.completer = arguments.$completer;
57         let auto = arguments.$auto_complete;
58         while (typeof(auto) == "string")
59             auto = minibuffer_auto_complete_preferences[auto];
60         if (auto == null)
61             auto = minibuffer_auto_complete_default;
62         this.auto_complete = auto;
63         this.auto_complete_initial = !!arguments.$auto_complete_initial;
64         this.auto_complete_conservative = !!arguments.$auto_complete_conservative;
65         let delay = arguments.$auto_complete_delay;
66         if (delay == null)
67             delay = default_minibuffer_auto_complete_delay;
68         this.auto_complete_delay = delay;
69         this.completions = null;
70         this.completions_valid = false;
71         this.space_completes = !!arguments.$space_completes;
72         this.completions_timer_ID = null;
73         this.completions_display_element = null;
74         this.selected_completion_index = -1;
75         this.match_required  = !!arguments.$match_required;
76         if (this.match_required)
77             this.default_completion = arguments.$default_completion;
78     }
81 function completions_tree_view(minibuffer_state)
83     this.minibuffer_state = minibuffer_state;
86 var atom_service = Cc["@mozilla.org/atom-service;1"].getService(Ci.nsIAtomService);
88 completions_tree_view.prototype = {
89     get rowCount () {
90         var c = this.minibuffer_state.completions;
91         if (!c)
92             return 0;
93         return c.count;
94     },
95     getCellText : function(row,column){
96         var c = this.minibuffer_state.completions;
97         if (row >= c.count)
98             return null;
99         if (column.index == 0)
100             return c.get_string(row);
101         if (c.get_description)
102             return c.get_description(row);
103         return "";
104     },
105     setTree : function(treebox){ this.treebox = treebox; },
106     isContainer: function(row){ return false; },
107     isSeparator: function(row){ return false; },
108     isSorted: function(){ return false; },
109     getLevel: function(row){ return 0; },
110     getImageSrc: function(row,col){ return null; },
111     getRowProperties: function(row,props){},
112     getCellProperties: function(row,col,props){
113         if (col.index == 0)
114             props.AppendElement(atom_service.getAtom("completion-string"));
115         else
116             props.AppendElement(atom_service.getAtom("completion-description"));
117     },
118     getColumnProperties: function(colid,col,props){}
121 // inherit from basic_minibuffer_state
122 text_entry_minibuffer_state.prototype = {
123     __proto__: basic_minibuffer_state.prototype,
124     load : function (window) {
125         this.window = window;
126         if (this.completer) {
127             // Create completion display element if needed
128             if (!this.completion_element)
129             {
130                 /* FIXME: maybe use the dom_generator */
131                 var tree = create_XUL(window, "tree");
132                 var s = this;
133                 tree.addEventListener("select", function () {
134                         s.selected_completion_index = s.completions_display_element.currentIndex;
135                         s.handle_completion_selected();
136                     }, true, false);
137                 tree.setAttribute("class", "completions");
139                 tree.setAttribute("rows", "8");
141                 tree.setAttribute("collapsed", "true");
143                 tree.setAttribute("hidecolumnpicker", "true");
144                 tree.setAttribute("hideheader", "true");
146                 var treecols = create_XUL(window, "treecols");
147                 tree.appendChild(treecols);
148                 var treecol = create_XUL(window, "treecol");
149                 treecol.setAttribute("flex", "1");
150                 treecols.appendChild(treecol);
151                 treecol = create_XUL(window, "treecol");
152                 treecol.setAttribute("flex", "1");
153                 treecols.appendChild(treecol);
154                 tree.appendChild(create_XUL(window, "treechildren"));
156                 window.minibuffer.insert_before(tree);
157                 tree.view = new completions_tree_view(this);
158                 this.completions_display_element = tree;
160                 /* This is the initial loading of this minibuffer
161                  * state.  If this.complete_initial is true, generate
162                  * completions. */
163                 if (this.auto_complete_initial)
164                     this.handle_input_changed();
165             }
167             this.update_completions_display();
168         }
169     },
171     unload : function (window) {
172         if (this.completions_display_element)
173             this.completions_display_element.setAttribute("collapsed", "true");
174     },
176     destroy : function (window) {
177         if (this.completions != null && this.completions.destroy)
178             this.completions.destroy();
179         var el = this.completions_display_element;
180         if (el)
181         {
182             el.parentNode.removeChild(el);
183             this.completions_display_element = null;
184         }
185         if (this.continuation)
186             this.continuation.throw(abort());
187     },
189     handle_input_changed : function () {
190         if (!this.completer) return;
192         this.completions_valid = false;
194         if (!this.auto_complete) return;
196         var s = this;
198         if (this.auto_complete_delay > 0) {
199             if (this.completions_timer_ID != null)
200                 this.window.clearTimeout(this.completions_timer_ID);
201             this.completions_timer_ID = this.window.setTimeout(
202                 function () {
203                     s.completions_timer_ID = null;
204                     s.update_completions(true /* auto */);
205                     s.update_completions_display();
206                 }, this.auto_complete_delay);
207             return;
208         }
210         s.update_completions(true /* auto */);
211         s.update_completions_display();
212     },
214     update_completions_display : function () {
216         var m = this.window.minibuffer;
218         if (m.current_state == this)
219         {
220             if (this.completions && this.completions.count > 0)
221             {
222                 this.completions_display_element.view = this.completions_display_element.view;
223                 this.completions_display_element.setAttribute("collapsed", "false");
225                 this.completions_display_element.currentIndex = this.selected_completion_index;
226                 this.completions_display_element.treeBoxObject.scrollToRow(this.selected_completion_index);
227             } else {
228                 this.completions_display_element.setAttribute("collapsed", "true");
229             }
230         }
231     },
233     /* If auto is true, this update is due to auto completion, rather
234      * than specifically requested. */
235     update_completions : function (auto) {
238         if (this.completions_timer_ID != null) {
239             this.window.clearTimeout(this.completions_timer_ID);
240             this.completions_timer_ID = null;
241         }
243         let m = this.window.minibuffer;
245         /* The completer should return undefined if completion was not
246          * attempted due to auto being true.  Otherwise, it can return
247          * null to indicate no completions. */
248         if (this.completions != null && this.completions.destroy)
249             this.completions.destroy();
250         let c = this.completions = this.completer(m._input_text, m._selection_start,
251                                                   auto && this.auto_complete_conservative);
252         this.completions_valid = true;
254         let i = -1;
255         if (c && c.count > 0) {
256             if (this.match_required) {
257                 if (c.count == 1)
258                     i = 0;
259                 else if (c.default_completion != null)
260                     i = c.default_completion;
261                 else if (this.default_completion && this.completions.index_of)
262                     i = this.completions.index_of(this.default_completion);
263             }
264             this.selected_completion_index = i;
265         }
266     },
268     select_completion : function (i) {
269         this.selected_completion_index = i;
270         this.completions_display_element.currentIndex = i;
271         if (i >= 0)
272             this.completions_display_element.treeBoxObject.ensureRowIsVisible(i);
273         this.handle_completion_selected();
274     },
276     handle_completion_selected : function () {
277         /**
278          * When a completion is selected, apply it to the input text
279          * if a match is not "required"; otherwise, the completion is
280          * only displayed.
281          */
282         var i = this.selected_completion_index;
283         var m = this.window.minibuffer;
284         var c = this.completions;
286         if (this.completions_valid && c && !this.match_required && i >= 0 && i < c.count)
287         {
288             c.apply(i, m);
289         }
290     }
293 function minibuffer_complete(window, count)
295     var m = window.minibuffer;
296     var s = m.current_state;
297     if (!(s instanceof text_entry_minibuffer_state))
298         throw new Error("Invalid minibuffer state");
299     if (!s.completer)
300         return;
301     var just_completed_manually = false;
302     if (!s.completions_valid || s.completions === undefined) {
303         if (s.completions_timer_ID == null)
304             just_completed_manually = true;
305         s.update_completions(false /* not auto */);
306         s.update_completions_display();
307     }
309     var c = s.completions;
311     if (!c || c.count == 0)
312         return;
314     var e = s.completions_display_element;
315     var new_index = -1;
317     if (count == 1 && c.apply_common_prefix)
318     {
319         c.apply_common_prefix(m);
320         c.apply_common_prefix = null;
321     } else if (!just_completed_manually) {
322         if (e.currentIndex != -1)
323         {
324             new_index = (e.currentIndex + count) % c.count;
325             if (new_index < 0)
326                 new_index += c.count;
327         } else {
328             new_index = (count - 1) % c.count;
329             if (new_index < 0)
330                 new_index += c.count;
331         }
332     }
334     if (new_index != -1)
335         s.select_completion(new_index);
337 interactive("minibuffer-complete", function (I) {minibuffer_complete(I.window, I.p);});
338 interactive("minibuffer-complete-previous", function (I) {minibuffer_complete(I.window, -I.p);});
340 function exit_minibuffer(window)
342     var m = window.minibuffer;
343     var s = m.current_state;
344     if (!(s instanceof text_entry_minibuffer_state))
345         throw new Error("Invalid minibuffer state");
347     var val = m._input_text;
349     if (s.validator != null && !s.validator(val, m))
350         return;
352     var match = null;
354     if (s.completer && s.match_required) {
355         if (!s.completions_valid || s.completions === undefined)
356             s.update_completions(false);
358         let c = s.completions;
359         let i = s.selected_completion_index;
360         if (c != null && i >= 0 && i < c.count) {
361             if (c.get_value != null)
362                 match = c.get_value(i);
363             else
364                 match = c.get_string(i);
365         } else {
366             m.message("No match");
367             return;
368         }
369     }
371     if (s.history)
372     {
373         s.history.push(val);
374         if (s.history.length > minibuffer_history_max_items)
375             s.history.splice(0, s.history.length - minibuffer_history_max_items);
376     }
377     var cont = s.continuation;
378     delete s.continuation;
379     m.pop_state();
380     if (cont) {
381         if (s.match_required)
382             cont(match);
383         else
384             cont(val);
385     }
387 interactive("exit-minibuffer", function (I) {exit_minibuffer(I.window);});
389 function minibuffer_history_next (window, count)
391     var m = window.minibuffer;
392     var s = m.current_state;
393     if (!(s instanceof text_entry_minibuffer_state))
394         throw new Error("Invalid minibuffer state");
395     if (!s.history || s.history.length == 0)
396         return;
397     m._ensure_input_area_showing();
398     var index = s.history_index + count;
399     if (index < 0)
400         index = 0;
401     if (index >= s.history.length)
402         index = s.history.length - 1;
403     s.history_index = index;
404     m._input_text = s.history[index];
405     m._set_selection();
407 interactive("minibuffer-history-next", function (I) {minibuffer_history_next(I.window, I.p);});
408 interactive("minibuffer-history-previous", function (I) {minibuffer_history_next(I.window, -I.p);});
410 // Define the asynchronous minibuffer.read function
411 minibuffer.prototype.read = function () {
412     var s = new text_entry_minibuffer_state((yield CONTINUATION), forward_keywords(arguments));
413     this.push_state(s);
414     var result = yield SUSPEND;
415     yield co_return(result);
418 minibuffer.prototype.read_command = function () {
419     keywords(arguments);
420     var completer = prefix_completer(
421         $completions = function (visitor) interactive_commands.for_each_value(visitor),
422         $get_string = function (x) x.name,
423         $get_description = function (x) x.shortdoc || "",
424         $get_value = function (x) x.name
425     );
427     var result = yield this.read($prompt = "Command", $history = "command",
428         forward_keywords(arguments),
429         $completer = completer,
430         $match_required = true);
431     yield co_return(result);