2 * (C) Copyright 2007-2010 John J. Foerch
3 * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
5 * Use, modification, and distribution are subject to the terms specified in the
11 define_variable("default_minibuffer_auto_complete_delay", 150,
12 "Delay (in milliseconds) after the most recent key-stroke "+
13 "before auto-completing.");
15 define_variable("minibuffer_auto_complete_preferences", {});
17 define_variable("minibuffer_auto_complete_default", false,
18 "Boolean specifying whether to auto-complete by default. "+
19 "The user variable `minibuffer_auto_complete_preferences' "+
22 var minibuffer_history_data = {};
24 /* FIXME: These should possibly be saved to disk somewhere */
25 define_variable("minibuffer_history_max_items", 100,
26 "Maximum number of minibuffer history entries stored. Older "+
27 "history entries are truncated after this limit is reached.");
30 /* The parameter `args' specifies the arguments. In addition, the
31 * arguments for basic_minibuffer_state are also allowed.
33 * history: [optional] specifies a string to identify the history list to use
39 * default_completion only used if match_required is set to true
41 * $valiator [optional]
42 * specifies a function
44 define_keywords("$keymap", "$history", "$validator",
45 "$completer", "$match_required", "$default_completion",
46 "$auto_complete", "$auto_complete_initial", "$auto_complete_conservative",
47 "$auto_complete_delay",
49 /* FIXME: support completing in another thread */
50 function text_entry_minibuffer_state (minibuffer, continuation) {
51 keywords(arguments, $keymap = minibuffer_keymap);
53 basic_minibuffer_state.call(this, minibuffer, forward_keywords(arguments));
55 this.continuation = continuation;
56 if (arguments.$history) {
57 this.history = minibuffer_history_data[arguments.$history] =
58 minibuffer_history_data[arguments.$history] || [];
59 this.history_index = -1;
60 this.saved_last_history_entry = null;
63 this.validator = arguments.$validator;
65 if (arguments.$completer != null) {
66 this.completer = arguments.$completer;
67 let auto = arguments.$auto_complete;
68 while (typeof(auto) == "string")
69 auto = minibuffer_auto_complete_preferences[auto];
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;
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 this.match_required_default = this.match_required;
87 if (this.match_required)
88 this.default_completion = arguments.$default_completion;
92 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 constructor: completions_tree_view,
101 var c = this.minibuffer_state.completions;
106 getCellText: function (row,column) {
107 var c = this.minibuffer_state.completions;
110 if (column.index == 0)
111 return c.get_string(row);
112 if (c.get_description)
113 return c.get_description(row);
116 setTree: function (treebox) { this.treebox = treebox; },
117 isContainer: function (row) { return false; },
118 isSeparator: function (row) { return false; },
119 isSorted: function () { return false; },
120 getLevel: function (row) { return 0; },
121 getImageSrc: function (row, col) { return null; },
122 getRowProperties: function (row, props) {},
123 getCellProperties: function (row, col, props) {
125 props.AppendElement(atom_service.getAtom("completion-string"));
127 props.AppendElement(atom_service.getAtom("completion-description"));
129 getColumnProperties: function (colid, col, props) {}
132 // inherit from basic_minibuffer_state
133 text_entry_minibuffer_state.prototype = {
134 constructor: text_entry_minibuffer_state,
135 __proto__: basic_minibuffer_state.prototype,
137 basic_minibuffer_state.prototype.load.call(this);
138 var window = this.minibuffer.window;
139 if (this.completer) {
140 // Create completion display element if needed
141 if (!this.completion_element) {
142 /* FIXME: maybe use the dom_generator */
143 var tree = create_XUL(window, "tree");
145 tree.addEventListener("select", function () {
146 s.selected_completion_index = s.completions_display_element.currentIndex;
147 s.handle_completion_selected();
149 tree.setAttribute("class", "completions");
151 tree.setAttribute("rows", "8");
153 tree.setAttribute("collapsed", "true");
155 tree.setAttribute("hidecolumnpicker", "true");
156 tree.setAttribute("hideheader", "true");
158 var treecols = create_XUL(window, "treecols");
159 tree.appendChild(treecols);
160 var treecol = create_XUL(window, "treecol");
161 treecol.setAttribute("flex", "1");
162 treecols.appendChild(treecol);
163 treecol = create_XUL(window, "treecol");
164 treecol.setAttribute("flex", "1");
165 treecols.appendChild(treecol);
166 tree.appendChild(create_XUL(window, "treechildren"));
168 this.minibuffer.insert_before(tree);
169 tree.view = new completions_tree_view(this);
170 this.completions_display_element = tree;
172 /* This is the initial loading of this minibuffer
173 * state. If this.complete_initial is true, generate
175 if (this.auto_complete_initial)
178 this.update_completions_display();
182 unload: function () {
183 if (this.completions_display_element)
184 this.completions_display_element.setAttribute("collapsed", "true");
185 basic_minibuffer_state.prototype.unload.call(this);
188 destroy: function () {
189 if (this.completions != null && this.completions.destroy)
190 this.completions.destroy();
191 delete this.completions;
192 if (this.completions_cont)
193 this.completions_cont.throw(abort());
194 delete this.completions_cont;
196 var el = this.completions_display_element;
198 el.parentNode.removeChild(el);
199 this.completions_display_element = null;
201 if (this.continuation)
202 this.continuation.throw(abort());
203 basic_minibuffer_state.prototype.destroy.call(this);
206 handle_input: function () {
207 if (!this.completer) return;
209 this.completions_valid = false;
211 if (!this.auto_complete) return;
214 var window = this.minibuffer.window;
216 if (this.auto_complete_delay > 0) {
217 if (this.completions_timer_ID != null)
218 window.clearTimeout(this.completions_timer_ID);
219 this.completions_timer_ID = window.setTimeout(
221 s.completions_timer_ID = null;
222 s.update_completions(true /* auto */, true /* update completions display */);
223 }, this.auto_complete_delay);
226 s.update_completions(true /* auto */, true /* update completions display */);
229 ran_minibuffer_command: function () {
233 update_completions_display: function () {
234 var m = this.minibuffer;
235 if (m.current_state == this) {
236 if (this.completions && this.completions.count > 0) {
237 this.completions_display_element.view = this.completions_display_element.view;
238 this.completions_display_element.setAttribute("collapsed", "false");
240 this.completions_display_element.currentIndex = this.selected_completion_index;
241 this.completions_display_element.treeBoxObject.scrollToRow(this.selected_completion_index);
243 this.completions_display_element.setAttribute("collapsed", "true");
248 /* If auto is true, this update is due to auto completion, rather
249 * than specifically requested. */
250 update_completions: function (auto, update_display) {
251 var window = this.minibuffer.window;
252 if (this.completions_timer_ID != null) {
253 window.clearTimeout(this.completions_timer_ID);
254 this.completions_timer_ID = null;
257 let m = this.minibuffer;
259 if (this.completions_cont) {
260 this.completions_cont.throw(abort());
261 this.completions_cont = null;
264 let c = this.completer(m._input_text, m._selection_start,
265 auto && this.auto_complete_conservative);
267 if (is_coroutine(c)) {
269 let already_done = false;
270 this.completions_cont = co_call(function () {
275 handle_interactive_error(window, e);
277 s.completions_cont = null;
280 s.update_completions_done(x, update_display);
283 // In case the completer actually already finished
285 this.completions_cont = null;
288 this.update_completions_done(c, update_display);
291 update_completions_done: function (c, update_display) {
292 /* The completer should return undefined if completion was not
293 * attempted due to auto being true. Otherwise, it can return
294 * null to indicate no completions. */
295 if (this.completions != null && this.completions.destroy)
296 this.completions.destroy();
298 this.completions = c;
299 this.completions_valid = true;
300 this.applied_common_prefix = false;
302 if (c && ("get_match_required" in c))
303 this.match_required = c.get_match_required();
304 if (this.match_required == null)
305 this.match_required = this.match_required_default;
308 if (c && c.count > 0) {
309 if (this.match_required) {
312 else if (c.default_completion != null)
313 i = c.default_completion;
314 else if (this.default_completion && this.completions.index_of)
315 i = this.completions.index_of(this.default_completion);
317 this.selected_completion_index = i;
321 this.update_completions_display();
324 select_completion: function (i) {
325 this.selected_completion_index = i;
326 this.completions_display_element.currentIndex = i;
328 this.completions_display_element.treeBoxObject.ensureRowIsVisible(i);
329 this.handle_completion_selected();
332 handle_completion_selected: function () {
334 * When a completion is selected, apply it to the input text
335 * if a match is not "required"; otherwise, the completion is
338 var i = this.selected_completion_index;
339 var m = this.minibuffer;
340 var c = this.completions;
342 if (this.completions_valid && c && !this.match_required && i >= 0 && i < c.count)
343 m.set_input_state(c.get_input_state(i));
347 function minibuffer_complete (window, count) {
348 var m = window.minibuffer;
349 var s = m.current_state;
350 if (!(s instanceof text_entry_minibuffer_state))
351 throw new Error("Invalid minibuffer state");
354 var just_completed_manually = false;
355 if (!s.completions_valid || s.completions === undefined) {
356 if (s.completions_timer_ID == null)
357 just_completed_manually = true;
358 s.update_completions(false /* not auto */, true /* update completions display */);
360 // If the completer is a coroutine, nothing we can do here
361 if (!s.completions_valid)
365 var c = s.completions;
367 if (!c || c.count == 0)
370 var e = s.completions_display_element;
375 if (count == 1 && !s.applied_common_prefix && (common_prefix = c.common_prefix_input_state)) {
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 new_index = (e.currentIndex + count) % c.count;
382 new_index += c.count;
384 new_index = (count - 1) % c.count;
386 new_index += c.count;
391 s.select_completion(new_index);
393 interactive("minibuffer-complete", null,
394 function (I) { minibuffer_complete(I.window, I.p); });
395 interactive("minibuffer-complete-previous", null,
396 function (I) { minibuffer_complete(I.window, -I.p); });
398 function exit_minibuffer (window) {
399 var m = window.minibuffer;
400 var s = m.current_state;
401 if (!(s instanceof text_entry_minibuffer_state))
402 throw new Error("Invalid minibuffer state");
404 var val = m._input_text;
406 if (s.validator != null && !s.validator(val, m))
411 if (s.completer && s.match_required) {
412 if (!s.completions_valid || s.completions === undefined)
413 s.update_completions(false /* not conservative */, false /* don't update */);
415 let c = s.completions;
416 let i = s.selected_completion_index;
417 if (c != null && i >= 0 && i < c.count) {
418 if (c.get_value != null)
419 match = c.get_value(i);
421 match = c.get_string(i);
423 m.message("No match");
430 if (s.history.length > minibuffer_history_max_items)
431 s.history.splice(0, s.history.length - minibuffer_history_max_items);
433 var cont = s.continuation;
434 delete s.continuation;
437 if (s.match_required)
443 interactive("exit-minibuffer", null,
444 function (I) { exit_minibuffer(I.window); });
446 function minibuffer_history_next (window, count) {
447 var m = window.minibuffer;
448 var s = m.current_state;
449 if (!(s instanceof text_entry_minibuffer_state))
450 throw new Error("Invalid minibuffer state");
451 if (!s.history || s.history.length == 0)
452 throw interactive_error("No history available.");
455 var index = s.history_index;
456 if (count > 0 && index == -1)
457 throw interactive_error("End of history; no next item");
458 else if (count < 0 && index == 0)
459 throw interactive_error("Beginning of history; no preceding item");
461 s.saved_last_history_entry = m._input_text;
462 index = s.history.length + count;
464 index = index + count;
469 m._restore_normal_state();
470 if (index >= s.history.length) {
472 m._input_text = s.saved_last_history_entry;
474 m._input_text = s.history[index];
476 s.history_index = index;
480 interactive("minibuffer-history-next", null,
481 function (I) { minibuffer_history_next(I.window, I.p); });
482 interactive("minibuffer-history-previous", null,
483 function (I) { minibuffer_history_next(I.window, -I.p); });
485 // Define the asynchronous minibuffer.read function
486 minibuffer.prototype.read = function () {
487 var s = new text_entry_minibuffer_state(this, (yield CONTINUATION), forward_keywords(arguments));
489 var result = yield SUSPEND;
490 yield co_return(result);
493 minibuffer.prototype.read_command = function () {
496 $prompt = "Command", $history = "command",
497 $completer = prefix_completer(
498 $completions = function (visitor) interactive_commands.for_each_value(visitor),
499 $get_string = function (x) x.name,
500 $get_description = function (x) x.shortdoc || "",
501 $get_value = function (x) x.name),
502 $match_required = true);
503 var result = yield this.read(forward_keywords(arguments));
504 yield co_return(result);
507 minibuffer.prototype.read_user_variable = function () {
510 $prompt = "User variable", $history = "user_variable",
511 $completer = prefix_completer(
512 $completions = function (visitor) {
513 for (var i in user_variables) visitor(i);
515 $get_string = function (x) x,
516 $get_description = function (x) user_variables[x].shortdoc || "",
517 $get_value = function (x) x),
518 $match_required = true);
519 var result = yield this.read(forward_keywords(arguments));
520 yield co_return(result);
523 minibuffer.prototype.read_preference = function () {
525 $prompt = "Preference:", $history = "preference",
526 $completer = prefix_completer(
527 $completions = preferences.getBranch(null).getChildList("", {}),
528 $get_description = function (pref) {
529 let default_value = get_default_pref(pref);
530 let value = get_pref(pref);
531 if (value == default_value)
534 switch (preferences.getBranch(null).getPrefType(pref)) {
535 case Ci.nsIPrefBranch.PREF_STRING:
538 case Ci.nsIPrefBranch.PREF_INT:
541 case Ci.nsIPrefBranch.PREF_BOOL:
545 let out = type + ":";
547 out += " " + pretty_print_value(value);
548 if (default_value != null)
549 out += " (" + pretty_print_value(default_value) + ")";
552 $match_required = true);
553 var result = yield this.read(forward_keywords(arguments));
554 yield co_return(result);
557 provide("minibuffer-read");