2 * (C) Copyright 2007-2010,2012 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 "+
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' "+
20 var minibuffer_history_data = {};
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.");
27 define_variable("minibuffer_completion_rows", 8,
28 "Number of minibuffer completions to display at one time.");
30 var atom_service = Cc["@mozilla.org/atom-service;1"].getService(Ci.nsIAtomService);
32 function completions_tree_view (minibuffer_state) {
33 this.minibuffer_state = minibuffer_state;
35 completions_tree_view.prototype = {
36 constructor: completions_tree_view,
37 QueryInterface: XPCOMUtils.generateQI([Ci.nsITreeView]),
39 var c = this.minibuffer_state.completions;
44 getCellText: function (row,column) {
45 var c = this.minibuffer_state.completions;
48 if (column.index == 0)
49 return c.get_string(row);
50 return c.get_description(row);
52 setTree: function (treebox) { this.treeBox = treebox; },
53 isContainer: function (row) { return false; },
54 isSeparator: function (row) { return false; },
55 isSorted: function () { return false; },
56 getLevel: function (row) { return 0; },
57 getImageSrc: function (row, col) {
58 var c = this.minibuffer_state.completions;
59 if (this.minibuffer_state.enable_icons &&
62 return c.get_icon(row);
66 getRowProperties: function (row, props) {},
67 getCellProperties: function (row, col, props) {
69 var a = atom_service.getAtom("completion-string");
71 a = atom_service.getAtom("completion-description");
73 props.AppendElement(a);
76 getColumnProperties: function (colid, col, props) {}
80 /* The parameter `args' specifies the arguments. In addition, the
81 * arguments for basic_minibuffer_state are also allowed.
83 * history: [optional] specifies a string to identify the history list to use
89 * default_completion only used if require_match is set to true
91 * $valiator [optional]
92 * specifies a function
94 define_keywords("$keymap", "$history", "$validator",
95 "$completer", "$require_match", "$default_completion",
96 "$auto_complete", "$auto_complete_initial", "$auto_complete_conservative",
97 "$auto_complete_delay", "$enable_icons",
99 /* FIXME: support completing in another thread */
100 function text_entry_minibuffer_state (minibuffer) {
101 keywords(arguments, $keymap = minibuffer_keymap,
102 $enable_icons = false);
104 basic_minibuffer_state.call(this, minibuffer, forward_keywords(arguments));
106 let deferred = Promise.defer();
107 this.deferred = deferred;
108 this.promise = make_simple_cancelable(deferred);
110 if (arguments.$history) {
111 this.history = minibuffer_history_data[arguments.$history] =
112 minibuffer_history_data[arguments.$history] || [];
113 this.history_index = -1;
114 this.saved_last_history_entry = null;
117 this.validator = arguments.$validator;
119 if (arguments.$completer != null) {
120 this.completer = arguments.$completer;
121 let auto = arguments.$auto_complete;
122 while (typeof auto == "string")
123 auto = minibuffer_auto_complete_preferences[auto];
125 auto = minibuffer_auto_complete_default;
126 this.auto_complete = auto;
127 this.auto_complete_initial = !!arguments.$auto_complete_initial;
128 this.auto_complete_conservative = !!arguments.$auto_complete_conservative;
129 let delay = arguments.$auto_complete_delay;
131 delay = default_minibuffer_auto_complete_delay;
132 this.auto_complete_delay = delay;
133 this.completions = null;
134 this.completions_valid = false;
135 this.space_completes = !!arguments.$space_completes;
136 if (this.space_completes)
137 this.keymaps.push(minibuffer_space_completion_keymap);
138 this.completions_timer_ID = null;
139 this.completions_display_element = null;
140 this.selected_completion_index = -1;
141 this.require_match = !!arguments.$require_match;
142 this.require_match_default = this.require_match;
143 if (this.require_match)
144 this.default_completion = arguments.$default_completion;
145 this.enable_icons = arguments.$enable_icons;
148 text_entry_minibuffer_state.prototype = {
149 constructor: text_entry_minibuffer_state,
150 __proto__: basic_minibuffer_state.prototype,
152 basic_minibuffer_state.prototype.load.call(this);
153 var window = this.minibuffer.window;
154 if (this.completer) {
155 // Create completion display element if needed
156 if (! this.completion_element) {
157 /* FIXME: maybe use the dom_generator */
158 var tree = create_XUL(window, "tree");
160 tree.addEventListener("select", function () {
161 s.selected_completion_index = s.completions_display_element.currentIndex;
162 s.handle_completion_selected();
164 tree.setAttribute("class", "completions");
166 tree.setAttribute("rows", minibuffer_completion_rows);
168 tree.setAttribute("collapsed", "true");
170 tree.setAttribute("hidecolumnpicker", "true");
171 tree.setAttribute("hideheader", "true");
172 if (this.enable_icons)
173 tree.setAttribute("hasicons", "true");
175 var treecols = create_XUL(window, "treecols");
176 tree.appendChild(treecols);
177 var treecol = create_XUL(window, "treecol");
178 treecol.setAttribute("flex", "1");
179 treecols.appendChild(treecol);
180 treecol = create_XUL(window, "treecol");
181 treecol.setAttribute("flex", "1");
182 treecols.appendChild(treecol);
183 tree.appendChild(create_XUL(window, "treechildren"));
185 this.minibuffer.insert_before(tree);
186 tree.view = new completions_tree_view(this);
187 this.completions_display_element = tree;
189 // This is the initial loading of this minibuffer state.
190 // If this.auto_complete_initial is true, generate
192 if (this.auto_complete_initial)
195 this.update_completions_display();
199 unload: function () {
200 if (this.completions_display_element)
201 this.completions_display_element.setAttribute("collapsed", "true");
202 basic_minibuffer_state.prototype.unload.call(this);
205 destroy: function () {
206 if (this.completions)
207 this.completions.destroy();
208 delete this.completions;
209 if (this.completions_cont)
210 this.completions_cont.cancel();
211 delete this.completions_cont;
213 var el = this.completions_display_element;
215 el.parentNode.removeChild(el);
216 this.completions_display_element = null;
219 this.promise.cancel();
220 basic_minibuffer_state.prototype.destroy.call(this);
223 handle_input: function () {
224 if (! this.completer)
226 this.completions_valid = false;
227 if (! this.auto_complete)
230 var window = this.minibuffer.window;
231 if (this.auto_complete_delay > 0) {
232 if (this.completions_timer_ID != null)
233 window.clearTimeout(this.completions_timer_ID);
234 this.completions_timer_ID = window.setTimeout(
236 s.completions_timer_ID = null;
237 s.update_completions(true /* auto */, true /* update completions display */);
238 }, this.auto_complete_delay);
240 s.update_completions(true /* auto */, true /* update completions display */);
244 update_completions_display: function () {
245 var m = this.minibuffer;
246 if (m.current_state == this) {
247 if (this.completions && this.completions.count > 0) {
248 this.completions_display_element.view = this.completions_display_element.view;
249 this.completions_display_element.setAttribute("collapsed", "false");
250 this.completions_display_element.currentIndex = this.selected_completion_index;
251 var max_display = this.completions_display_element.treeBoxObject.getPageLength();
252 var mid_point = Math.floor(max_display / 2);
253 if (this.completions.count - this.selected_completion_index <= mid_point)
254 var pos = this.completions.count - max_display;
256 pos = Math.max(0, this.selected_completion_index - mid_point);
257 this.completions_display_element.treeBoxObject.scrollToRow(pos);
259 this.completions_display_element.setAttribute("collapsed", "true");
264 /* If auto is true, this update is due to auto completion, rather
265 * than specifically requested. */
266 update_completions: function (auto, update_display) {
267 var window = this.minibuffer.window;
268 if (this.completions_timer_ID != null) {
269 window.clearTimeout(this.completions_timer_ID);
270 this.completions_timer_ID = null;
272 var m = this.minibuffer;
273 if (this.completions_cont) {
274 this.completions_cont.cancel();
275 this.completions_cont = null;
277 if (m._selection_start > 0 || ! auto || ! this.auto_complete_conservative)
278 var c = this.completer.complete(m._input_text, m._selection_start);
279 if (is_coroutine(c)) {
281 var already_done = false;
282 this.completions_cont = spawn(function () {
286 handle_interactive_error(window, e);
288 s.completions_cont = null;
291 s.update_completions_done(x, update_display);
293 // In case the completer actually already finished
295 this.completions_cont = null;
297 this.update_completions_done(c, update_display);
300 update_completions_done: function (c, update_display) {
301 /* The completer should return undefined if completion was not
302 * attempted due to auto being true. Otherwise, it can return
303 * null to indicate no completions. */
304 if (this.completions)
305 this.completions.destroy();
307 this.completions = c;
308 this.completions_valid = true;
309 this.applied_common_prefix = false;
311 this.require_match = this.completer.require_match;
312 if (this.require_match == null)
313 this.require_match = this.require_match_default;
315 if (c && c.count > 0) {
317 if (this.require_match) {
320 else if (c.default_completion != null)
321 i = c.default_completion;
322 else if (this.default_completion)
323 i = this.completions.index_of(this.default_completion);
325 this.selected_completion_index = i;
329 this.update_completions_display();
332 select_completion: function (i) {
333 this.selected_completion_index = i;
334 this.completions_display_element.currentIndex = i;
336 this.completions_display_element.treeBoxObject.ensureRowIsVisible(i);
337 this.handle_completion_selected();
340 handle_completion_selected: function () {
342 * When a completion is selected, apply it to the input text
343 * if a match is not "required"; otherwise, the completion is
346 var i = this.selected_completion_index;
347 var m = this.minibuffer;
348 var c = this.completions;
350 if (this.completions_valid && c && !this.require_match && i >= 0 && i < c.count)
351 m.set_input_state(c.get_input_state(i));
355 function minibuffer_complete (window, count) {
356 var m = window.minibuffer;
357 var s = m.current_state;
358 if (! (s instanceof text_entry_minibuffer_state))
359 throw new Error("Invalid minibuffer state");
362 var just_completed_manually = false;
363 if (! s.completions_valid || s.completions === undefined) {
364 if (s.completions_timer_ID == null)
365 just_completed_manually = true;
366 //XXX: may need to use ignore_input_events here
367 s.update_completions(false /* not auto */, true /* update completions display */);
369 // If the completer is a coroutine, nothing we can do here
370 if (! s.completions_valid)
374 var c = s.completions;
376 if (! c || c.count == 0)
379 var e = s.completions_display_element;
383 if (count == 1 && ! s.applied_common_prefix &&
384 (common_prefix = c.common_prefix_input_state))
386 //XXX: may need to use ignore_input_events here
387 m.set_input_state(common_prefix);
388 s.applied_common_prefix = true;
389 } else if (!just_completed_manually) {
390 if (e.currentIndex != -1) {
391 new_index = (e.currentIndex + count) % c.count;
393 new_index += c.count;
395 new_index = (count - 1) % c.count;
397 new_index += c.count;
401 if (new_index != -1) {
403 m.ignore_input_events = true;
404 s.select_completion(new_index);
406 m.ignore_input_events = false;
410 interactive("minibuffer-complete", null,
411 function (I) { minibuffer_complete(I.window, I.p); });
412 interactive("minibuffer-complete-previous", null,
413 function (I) { minibuffer_complete(I.window, -I.p); });
415 function exit_minibuffer (window) {
416 var m = window.minibuffer;
417 var s = m.current_state;
418 if (! (s instanceof text_entry_minibuffer_state))
419 throw new Error("Invalid minibuffer state");
421 var val = m._input_text;
423 if (s.validator != null && ! s.validator(val, m))
428 if (s.completer && s.require_match) {
429 if (! s.completions_valid || s.completions === undefined)
430 s.update_completions(false /* not conservative */, false /* don't update */);
432 let c = s.completions;
433 let i = s.selected_completion_index;
434 if (c != null && i >= 0 && i < c.count) {
435 match = c.get_value(i);
437 m.message("No match");
444 if (s.history.length > minibuffer_history_max_items)
445 s.history.splice(0, s.history.length - minibuffer_history_max_items);
447 var deferred = s.deferred;
449 if (s.require_match) {
450 deferred.resolve(match);
453 deferred.resolve(val);
458 interactive("exit-minibuffer", null,
459 function (I) { exit_minibuffer(I.window); });
461 function minibuffer_history_next (window, count) {
462 var m = window.minibuffer;
463 var s = m.current_state;
464 if (! (s instanceof text_entry_minibuffer_state))
465 throw new Error("Invalid minibuffer state");
466 if (! s.history || s.history.length == 0)
467 throw interactive_error("No history available.");
470 var index = s.history_index;
471 if (count > 0 && index == -1)
472 throw interactive_error("End of history; no next item");
473 else if (count < 0 && index == 0)
474 throw interactive_error("Beginning of history; no preceding item");
476 s.saved_last_history_entry = m._input_text;
477 index = s.history.length + count;
479 index = index + count;
484 m._restore_normal_state();
485 if (index >= s.history.length) {
487 m._input_text = s.saved_last_history_entry;
489 m._input_text = s.history[index];
491 s.history_index = index;
495 interactive("minibuffer-history-next", null,
496 function (I) { minibuffer_history_next(I.window, I.p); });
497 interactive("minibuffer-history-previous", null,
498 function (I) { minibuffer_history_next(I.window, -I.p); });
500 // Define the asynchronous minibuffer.read function
501 minibuffer.prototype.read = function () {
502 var s = new text_entry_minibuffer_state(this, forward_keywords(arguments));
504 yield co_return(yield s.promise);
507 minibuffer.prototype.read_command = function () {
510 $prompt = "Command", $history = "command",
511 $completer = new prefix_completer(
512 $completions = function (visitor) {
513 for (let [k,v] in Iterator(interactive_commands)) {
517 $get_string = function (x) x.name,
518 $get_description = function (x) x.shortdoc || "",
519 $get_value = function (x) x.name),
522 var result = yield this.read(forward_keywords(arguments));
523 yield co_return(result);
526 minibuffer.prototype.read_user_variable = function () {
529 $prompt = "User variable", $history = "user_variable",
530 $completer = new prefix_completer(
531 $completions = function (visitor) {
532 for (var i in user_variables) visitor(i);
534 $get_string = function (x) x,
535 $get_description = function (x) user_variables[x].shortdoc || "",
536 $get_value = function (x) x),
539 var result = yield this.read(forward_keywords(arguments));
540 yield co_return(result);
543 minibuffer.prototype.read_preference = function () {
545 $prompt = "Preference:", $history = "preference",
546 $completer = new prefix_completer(
547 $completions = preferences.getBranch(null).getChildList("", {}),
548 $get_description = function (pref) {
549 let default_value = get_default_pref(pref);
550 let value = get_pref(pref);
551 if (value == default_value)
554 switch (preferences.getBranch(null).getPrefType(pref)) {
555 case Ci.nsIPrefBranch.PREF_STRING:
558 case Ci.nsIPrefBranch.PREF_INT:
561 case Ci.nsIPrefBranch.PREF_BOOL:
565 let out = type + ":";
567 out += " " + pretty_print_value(value);
568 if (default_value != null)
569 out += " (" + pretty_print_value(default_value) + ")";
574 var result = yield this.read(forward_keywords(arguments));
575 yield co_return(result);
579 define_keywords("$object");
580 minibuffer.prototype.read_object_property = function () {
582 $prompt = "Property:");
583 var o = arguments.$object || {};
584 var result = yield this.read(
585 $prompt = arguments.$prompt,
586 $completer = new prefix_completer(
587 $completions = function (push) {
593 yield co_return(result);
597 provide("minibuffer-read");