2 * (C) Copyright 2007 John J. Foerch
3 * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
5 * Use, modification, and distribution are subject to the terms specified in the
9 define_variable("default_minibuffer_auto_complete_delay", 150,
10 "Delay (in milliseconds) after the most recent key stroke before auto-completing.");
12 define_variable("minibuffer_auto_complete_preferences", {});
14 define_variable("minibuffer_auto_complete_default", false, "Boolean specifying whether to auto-complete by default.\nThe user variable `minibuffer_auto_complete_preferences' overrides this.");
16 var minibuffer_history_data = new string_hashmap();
18 /* FIXME: These should possibly be saved to disk somewhere */
19 define_variable("minibuffer_history_max_items", 100, "Maximum number of minibuffer history entries stored.\nOlder history entries are truncated after this limit is reached.");
22 /* The parameter `args' specifies the arguments. In addition, the
23 * arguments for basic_minibuffer_state are also allowed.
25 * history: [optional] specifies a string to identify the history list to use
31 * default_completion only used if match_required is set to true
33 * $valiator [optional]
34 * specifies a function
36 define_keywords("$history", "$validator",
38 "$completer", "$match_required", "$default_completion",
39 "$auto_complete", "$auto_complete_initial", "$auto_complete_conservative",
40 "$auto_complete_delay",
42 /* FIXME: support completing in another thread */
43 function text_entry_minibuffer_state(continuation) {
46 basic_minibuffer_state.call(this, forward_keywords(arguments));
47 this.keymap = minibuffer_keymap;
49 this.continuation = continuation;
50 if (arguments.$history)
52 this.history = minibuffer_history_data.get_put_default(arguments.$history, []);
53 this.history_index = -1;
54 this.saved_last_history_entry = null;
57 this.validator = arguments.$validator;
59 if (arguments.$completer != null)
61 this.completer = arguments.$completer;
62 let auto = arguments.$auto_complete;
63 while (typeof(auto) == "string")
64 auto = minibuffer_auto_complete_preferences[auto];
66 auto = minibuffer_auto_complete_default;
67 this.auto_complete = auto;
68 this.auto_complete_initial = !!arguments.$auto_complete_initial;
69 this.auto_complete_conservative = !!arguments.$auto_complete_conservative;
70 let delay = arguments.$auto_complete_delay;
72 delay = default_minibuffer_auto_complete_delay;
73 this.auto_complete_delay = delay;
74 this.completions = null;
75 this.completions_valid = false;
76 this.space_completes = !!arguments.$space_completes;
77 this.completions_timer_ID = null;
78 this.completions_display_element = null;
79 this.selected_completion_index = -1;
80 this.match_required = !!arguments.$match_required;
81 if (this.match_required)
82 this.default_completion = arguments.$default_completion;
86 function completions_tree_view(minibuffer_state)
88 this.minibuffer_state = minibuffer_state;
91 var atom_service = Cc["@mozilla.org/atom-service;1"].getService(Ci.nsIAtomService);
93 completions_tree_view.prototype = {
95 var c = this.minibuffer_state.completions;
100 getCellText : function(row,column){
101 var c = this.minibuffer_state.completions;
104 if (column.index == 0)
105 return c.get_string(row);
106 if (c.get_description)
107 return c.get_description(row);
110 setTree : function(treebox){ this.treebox = treebox; },
111 isContainer: function(row){ return false; },
112 isSeparator: function(row){ return false; },
113 isSorted: function(){ return false; },
114 getLevel: function(row){ return 0; },
115 getImageSrc: function(row,col){ return null; },
116 getRowProperties: function(row,props){},
117 getCellProperties: function(row,col,props){
119 props.AppendElement(atom_service.getAtom("completion-string"));
121 props.AppendElement(atom_service.getAtom("completion-description"));
123 getColumnProperties: function(colid,col,props){}
126 // inherit from basic_minibuffer_state
127 text_entry_minibuffer_state.prototype = {
128 __proto__: basic_minibuffer_state.prototype,
129 load : function (window) {
130 this.window = window;
131 if (this.completer) {
132 // Create completion display element if needed
133 if (!this.completion_element)
135 /* FIXME: maybe use the dom_generator */
136 var tree = create_XUL(window, "tree");
138 tree.addEventListener("select", function () {
139 s.selected_completion_index = s.completions_display_element.currentIndex;
140 s.handle_completion_selected();
142 tree.setAttribute("class", "completions");
144 tree.setAttribute("rows", "8");
146 tree.setAttribute("collapsed", "true");
148 tree.setAttribute("hidecolumnpicker", "true");
149 tree.setAttribute("hideheader", "true");
151 var treecols = create_XUL(window, "treecols");
152 tree.appendChild(treecols);
153 var treecol = create_XUL(window, "treecol");
154 treecol.setAttribute("flex", "1");
155 treecols.appendChild(treecol);
156 treecol = create_XUL(window, "treecol");
157 treecol.setAttribute("flex", "1");
158 treecols.appendChild(treecol);
159 tree.appendChild(create_XUL(window, "treechildren"));
161 window.minibuffer.insert_before(tree);
162 tree.view = new completions_tree_view(this);
163 this.completions_display_element = tree;
165 /* This is the initial loading of this minibuffer
166 * state. If this.complete_initial is true, generate
168 if (this.auto_complete_initial)
172 this.update_completions_display();
176 unload : function (window) {
177 if (this.completions_display_element)
178 this.completions_display_element.setAttribute("collapsed", "true");
181 destroy : function (window) {
182 if (this.completions != null && this.completions.destroy)
183 this.completions.destroy();
184 delete this.completions;
185 if (this.completions_cont)
186 this.completions_cont.throw(abort());
187 delete this.completions_cont;
189 var el = this.completions_display_element;
192 el.parentNode.removeChild(el);
193 this.completions_display_element = null;
195 if (this.continuation)
196 this.continuation.throw(abort());
199 handle_input : function () {
200 if (!this.completer) return;
202 this.completions_valid = false;
204 if (!this.auto_complete) return;
208 if (this.auto_complete_delay > 0) {
209 if (this.completions_timer_ID != null)
210 this.window.clearTimeout(this.completions_timer_ID);
211 this.completions_timer_ID = this.window.setTimeout(
213 s.completions_timer_ID = null;
214 s.update_completions(true /* auto */, true /* update completions display */);
215 }, this.auto_complete_delay);
219 s.update_completions(true /* auto */, true /* update completions display */);
222 ran_minibuffer_command : function () {
226 update_completions_display : function () {
228 var m = this.window.minibuffer;
230 if (m.current_state == this)
232 if (this.completions && this.completions.count > 0)
234 this.completions_display_element.view = this.completions_display_element.view;
235 this.completions_display_element.setAttribute("collapsed", "false");
237 this.completions_display_element.currentIndex = this.selected_completion_index;
238 this.completions_display_element.treeBoxObject.scrollToRow(this.selected_completion_index);
240 this.completions_display_element.setAttribute("collapsed", "true");
245 /* If auto is true, this update is due to auto completion, rather
246 * than specifically requested. */
247 update_completions : function (auto, update_display) {
249 if (this.completions_timer_ID != null) {
250 this.window.clearTimeout(this.completions_timer_ID);
251 this.completions_timer_ID = null;
254 let m = this.window.minibuffer;
256 if (this.completions_cont) {
257 this.completions_cont.throw(abort());
258 this.completions_cont = null;
261 let c = this.completer(m._input_text, m._selection_start,
262 auto && this.auto_complete_conservative);
264 if (is_coroutine(c)) {
266 let already_done = false;
267 this.completions_cont = co_call(function () {
272 s.completions_cont = null;
275 s.update_completions_done(x, update_display);
278 // In case the completer actually already finished
280 this.completions_cont = null;
283 this.update_completions_done(c, update_display);
286 update_completions_done : function update_completions_done(c, update_display) {
288 /* The completer should return undefined if completion was not
289 * attempted due to auto being true. Otherwise, it can return
290 * null to indicate no completions. */
291 if (this.completions != null && this.completions.destroy)
292 this.completions.destroy();
294 this.completions = c;
295 this.completions_valid = true;
296 this.applied_common_prefix = false;
299 if (c && c.count > 0) {
300 if (this.match_required) {
303 else if (c.default_completion != null)
304 i = c.default_completion;
305 else if (this.default_completion && this.completions.index_of)
306 i = this.completions.index_of(this.default_completion);
308 this.selected_completion_index = i;
312 this.update_completions_display();
315 select_completion : function (i) {
316 this.selected_completion_index = i;
317 this.completions_display_element.currentIndex = i;
319 this.completions_display_element.treeBoxObject.ensureRowIsVisible(i);
320 this.handle_completion_selected();
323 handle_completion_selected : function () {
325 * When a completion is selected, apply it to the input text
326 * if a match is not "required"; otherwise, the completion is
329 var i = this.selected_completion_index;
330 var m = this.window.minibuffer;
331 var c = this.completions;
333 if (this.completions_valid && c && !this.match_required && i >= 0 && i < c.count)
335 m.set_input_state(c.get_input_state(i));
340 function minibuffer_complete(window, count)
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");
348 var just_completed_manually = false;
349 if (!s.completions_valid || s.completions === undefined) {
350 if (s.completions_timer_ID == null)
351 just_completed_manually = true;
352 s.update_completions(false /* not auto */, true /* update completions display */);
354 // If the completer is a coroutine, nothing we can do here
355 if (!s.completions_valid)
359 var c = s.completions;
361 if (!c || c.count == 0)
364 var e = s.completions_display_element;
369 if (count == 1 && !s.applied_common_prefix && (common_prefix = c.common_prefix_input_state))
371 m.set_input_state(common_prefix);
372 s.applied_common_prefix = true;
373 } else if (!just_completed_manually) {
374 if (e.currentIndex != -1)
376 new_index = (e.currentIndex + count) % c.count;
378 new_index += c.count;
380 new_index = (count - 1) % c.count;
382 new_index += c.count;
387 s.select_completion(new_index);
389 interactive("minibuffer-complete", null, function (I) {minibuffer_complete(I.window, I.p);});
390 interactive("minibuffer-complete-previous", null, function (I) {minibuffer_complete(I.window, -I.p);});
392 function exit_minibuffer(window)
394 var m = window.minibuffer;
395 var s = m.current_state;
396 if (!(s instanceof text_entry_minibuffer_state))
397 throw new Error("Invalid minibuffer state");
399 var val = m._input_text;
401 if (s.validator != null && !s.validator(val, m))
406 if (s.completer && s.match_required) {
407 if (!s.completions_valid || s.completions === undefined)
408 s.update_completions(false /* not conservative */, false /* don't update */);
410 let c = s.completions;
411 let i = s.selected_completion_index;
412 if (c != null && i >= 0 && i < c.count) {
413 if (c.get_value != null)
414 match = c.get_value(i);
416 match = c.get_string(i);
418 m.message("No match");
426 if (s.history.length > minibuffer_history_max_items)
427 s.history.splice(0, s.history.length - minibuffer_history_max_items);
429 var cont = s.continuation;
430 delete s.continuation;
433 if (s.match_required)
439 interactive("exit-minibuffer", null, function (I) {exit_minibuffer(I.window);});
441 function minibuffer_history_next (window, count)
443 var m = window.minibuffer;
444 var s = m.current_state;
445 if (!(s instanceof text_entry_minibuffer_state))
446 throw new Error("Invalid minibuffer state");
447 if (!s.history || s.history.length == 0)
448 throw interactive_error("No history available.");
451 var index = s.history_index;
452 if (count > 0 && index == -1)
453 throw interactive_error("End of history; no next item");
454 else if (count < 0 && index == 0) {
455 throw interactive_error("Beginning of history; no preceding item");
458 s.saved_last_history_entry = m._input_text;
459 index = s.history.length + count;
461 index = index + count;
466 m._restore_normal_state();
467 if (index >= s.history.length) {
469 m._input_text = s.saved_last_history_entry;
471 m._input_text = s.history[index];
473 s.history_index = index;
477 interactive("minibuffer-history-next", null, function (I) {minibuffer_history_next(I.window, I.p);});
478 interactive("minibuffer-history-previous", null, function (I) {minibuffer_history_next(I.window, -I.p);});
480 // Define the asynchronous minibuffer.read function
481 minibuffer.prototype.read = function () {
482 var s = new text_entry_minibuffer_state((yield CONTINUATION), forward_keywords(arguments));
484 var result = yield SUSPEND;
485 yield co_return(result);
488 minibuffer.prototype.read_command = function () {
491 $prompt = "Command", $history = "command",
492 $completer = prefix_completer(
493 $completions = function (visitor) interactive_commands.for_each_value(visitor),
494 $get_string = function (x) x.name,
495 $get_description = function (x) x.shortdoc || "",
496 $get_value = function (x) x.name),
497 $match_required = true);
498 var result = yield this.read(forward_keywords(arguments));
499 yield co_return(result);
502 minibuffer.prototype.read_user_variable = function () {
505 $prompt = "User variable", $history = "user_variable",
506 $completer = prefix_completer(
507 $completions = function (visitor) user_variables.for_each(visitor),
508 $get_string = function (x) x,
509 $get_description = function (x) user_variables.get(x).shortdoc || "",
510 $get_value = function (x) x),
511 $match_required = true);
512 var result = yield this.read(forward_keywords(arguments));
513 yield co_return(result);
516 minibuffer.prototype.read_preference = function minibuffer__read_preference () {
518 $prompt = "Preference:", $history = "preference",
519 $completer = prefix_completer(
520 $completions = preferences.getBranch(null).getChildList("", {}),
521 $get_description = function (pref) {
522 let default_value = get_default_pref(pref);
523 let value = get_pref(pref);
524 if (value == default_value)
527 switch (preferences.getBranch(null).getPrefType(pref)) {
528 case Ci.nsIPrefBranch.PREF_STRING:
531 case Ci.nsIPrefBranch.PREF_INT:
534 case Ci.nsIPrefBranch.PREF_BOOL:
538 let out = type + ":";
540 out += " " + pretty_print_value(value);
541 if (default_value != null)
542 out += " (" + pretty_print_value(default_value) + ")";
545 $match_required = true);
546 var result = yield this.read(forward_keywords(arguments));
547 yield co_return(result);