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.");
29 define_variable("minibuffer_completion_rows", 8,
30 "Number of minibuffer completions to display at one time.");
32 var atom_service = Cc["@mozilla.org/atom-service;1"].getService(Ci.nsIAtomService);
34 function completions_tree_view (minibuffer_state) {
35 this.minibuffer_state = minibuffer_state;
37 completions_tree_view.prototype = {
38 constructor: completions_tree_view,
39 QueryInterface: XPCOMUtils.generateQI([Ci.nsITreeView]),
41 var c = this.minibuffer_state.completions;
46 getCellText: function (row,column) {
47 var c = this.minibuffer_state.completions;
50 if (column.index == 0)
51 return c.get_string(row);
52 if (c.get_description)
53 return c.get_description(row);
56 setTree: function (treebox) { this.treeBox = treebox; },
57 isContainer: function (row) { return false; },
58 isSeparator: function (row) { return false; },
59 isSorted: function () { return false; },
60 getLevel: function (row) { return 0; },
61 getImageSrc: function (row, col) {
62 var c = this.minibuffer_state.completions;
63 if (this.minibuffer_state.enable_icons &&
64 c.get_icon && col.index == 0)
66 return c.get_icon(row);
70 getRowProperties: function (row, props) {},
71 getCellProperties: function (row, col, props) {
73 props.AppendElement(atom_service.getAtom("completion-string"));
75 props.AppendElement(atom_service.getAtom("completion-description"));
77 getColumnProperties: function (colid, col, props) {}
81 /* The parameter `args' specifies the arguments. In addition, the
82 * arguments for basic_minibuffer_state are also allowed.
84 * history: [optional] specifies a string to identify the history list to use
90 * default_completion only used if match_required is set to true
92 * $valiator [optional]
93 * specifies a function
95 define_keywords("$keymap", "$history", "$validator",
96 "$completer", "$match_required", "$default_completion",
97 "$auto_complete", "$auto_complete_initial", "$auto_complete_conservative",
98 "$auto_complete_delay", "$enable_icons",
100 /* FIXME: support completing in another thread */
101 function text_entry_minibuffer_state (minibuffer, continuation) {
102 keywords(arguments, $keymap = minibuffer_keymap,
103 $enable_icons = false);
105 basic_minibuffer_state.call(this, minibuffer, forward_keywords(arguments));
107 this.continuation = continuation;
108 if (arguments.$history) {
109 this.history = minibuffer_history_data[arguments.$history] =
110 minibuffer_history_data[arguments.$history] || [];
111 this.history_index = -1;
112 this.saved_last_history_entry = null;
115 this.validator = arguments.$validator;
117 if (arguments.$completer != null) {
118 this.completer = arguments.$completer;
119 let auto = arguments.$auto_complete;
120 while (typeof(auto) == "string")
121 auto = minibuffer_auto_complete_preferences[auto];
123 auto = minibuffer_auto_complete_default;
124 this.auto_complete = auto;
125 this.auto_complete_initial = !!arguments.$auto_complete_initial;
126 this.auto_complete_conservative = !!arguments.$auto_complete_conservative;
127 let delay = arguments.$auto_complete_delay;
129 delay = default_minibuffer_auto_complete_delay;
130 this.auto_complete_delay = delay;
131 this.completions = null;
132 this.completions_valid = false;
133 this.space_completes = !!arguments.$space_completes;
134 if (this.space_completes)
135 this.keymaps.push(minibuffer_space_completion_keymap);
136 this.completions_timer_ID = null;
137 this.completions_display_element = null;
138 this.selected_completion_index = -1;
139 this.match_required = !!arguments.$match_required;
140 this.match_required_default = this.match_required;
141 if (this.match_required)
142 this.default_completion = arguments.$default_completion;
143 this.enable_icons = arguments.$enable_icons;
146 text_entry_minibuffer_state.prototype = {
147 constructor: text_entry_minibuffer_state,
148 __proto__: basic_minibuffer_state.prototype,
150 basic_minibuffer_state.prototype.load.call(this);
151 var window = this.minibuffer.window;
152 if (this.completer) {
153 // Create completion display element if needed
154 if (!this.completion_element) {
155 /* FIXME: maybe use the dom_generator */
156 var tree = create_XUL(window, "tree");
158 tree.addEventListener("select", function () {
159 s.selected_completion_index = s.completions_display_element.currentIndex;
160 s.handle_completion_selected();
162 tree.setAttribute("class", "completions");
164 tree.setAttribute("rows", minibuffer_completion_rows);
166 tree.setAttribute("collapsed", "true");
168 tree.setAttribute("hidecolumnpicker", "true");
169 tree.setAttribute("hideheader", "true");
170 if (this.enable_icons)
171 tree.setAttribute("hasicons", "true");
173 var treecols = create_XUL(window, "treecols");
174 tree.appendChild(treecols);
175 var treecol = create_XUL(window, "treecol");
176 treecol.setAttribute("flex", "1");
177 treecols.appendChild(treecol);
178 treecol = create_XUL(window, "treecol");
179 treecol.setAttribute("flex", "1");
180 treecols.appendChild(treecol);
181 tree.appendChild(create_XUL(window, "treechildren"));
183 this.minibuffer.insert_before(tree);
184 tree.view = new completions_tree_view(this);
185 this.completions_display_element = tree;
187 /* This is the initial loading of this minibuffer
188 * state. If this.complete_initial is true, generate
190 if (this.auto_complete_initial)
193 this.update_completions_display();
197 unload: function () {
198 if (this.completions_display_element)
199 this.completions_display_element.setAttribute("collapsed", "true");
200 basic_minibuffer_state.prototype.unload.call(this);
203 destroy: function () {
204 if (this.completions != null && this.completions.destroy)
205 this.completions.destroy();
206 delete this.completions;
207 if (this.completions_cont)
208 this.completions_cont.throw(abort());
209 delete this.completions_cont;
211 var el = this.completions_display_element;
213 el.parentNode.removeChild(el);
214 this.completions_display_element = null;
216 if (this.continuation)
217 this.continuation.throw(abort());
218 basic_minibuffer_state.prototype.destroy.call(this);
221 handle_input: function () {
222 if (!this.completer) return;
224 this.completions_valid = false;
226 if (!this.auto_complete) return;
229 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);
241 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;
273 let m = this.minibuffer;
275 if (this.completions_cont) {
276 this.completions_cont.throw(abort());
277 this.completions_cont = null;
280 let c = this.completer(m._input_text, m._selection_start,
281 auto && this.auto_complete_conservative);
283 if (is_coroutine(c)) {
285 let already_done = false;
286 this.completions_cont = co_call(function () {
291 handle_interactive_error(window, e);
293 s.completions_cont = null;
296 s.update_completions_done(x, update_display);
299 // In case the completer actually already finished
301 this.completions_cont = null;
304 this.update_completions_done(c, update_display);
307 update_completions_done: function (c, update_display) {
308 /* The completer should return undefined if completion was not
309 * attempted due to auto being true. Otherwise, it can return
310 * null to indicate no completions. */
311 if (this.completions != null && this.completions.destroy)
312 this.completions.destroy();
314 this.completions = c;
315 this.completions_valid = true;
316 this.applied_common_prefix = false;
318 if (c && ("get_match_required" in c))
319 this.match_required = c.get_match_required();
320 if (this.match_required == null)
321 this.match_required = this.match_required_default;
324 if (c && c.count > 0) {
325 if (this.match_required) {
328 else if (c.default_completion != null)
329 i = c.default_completion;
330 else if (this.default_completion && this.completions.index_of)
331 i = this.completions.index_of(this.default_completion);
333 this.selected_completion_index = i;
337 this.update_completions_display();
340 select_completion: function (i) {
341 this.selected_completion_index = i;
342 this.completions_display_element.currentIndex = i;
344 this.completions_display_element.treeBoxObject.ensureRowIsVisible(i);
345 this.handle_completion_selected();
348 handle_completion_selected: function () {
350 * When a completion is selected, apply it to the input text
351 * if a match is not "required"; otherwise, the completion is
354 var i = this.selected_completion_index;
355 var m = this.minibuffer;
356 var c = this.completions;
358 if (this.completions_valid && c && !this.match_required && i >= 0 && i < c.count)
359 m.set_input_state(c.get_input_state(i));
363 function minibuffer_complete (window, count) {
364 var m = window.minibuffer;
365 var s = m.current_state;
366 if (!(s instanceof text_entry_minibuffer_state))
367 throw new Error("Invalid minibuffer state");
370 var just_completed_manually = false;
371 if (!s.completions_valid || s.completions === undefined) {
372 if (s.completions_timer_ID == null)
373 just_completed_manually = true;
374 //XXX: may need to use ignore_input_events here
375 s.update_completions(false /* not auto */, true /* update completions display */);
377 // If the completer is a coroutine, nothing we can do here
378 if (!s.completions_valid)
382 var c = s.completions;
384 if (!c || c.count == 0)
387 var e = s.completions_display_element;
392 if (count == 1 && !s.applied_common_prefix && (common_prefix = c.common_prefix_input_state)) {
393 //XXX: may need to use ignore_input_events here
394 m.set_input_state(common_prefix);
395 s.applied_common_prefix = true;
396 } else if (!just_completed_manually) {
397 if (e.currentIndex != -1) {
398 new_index = (e.currentIndex + count) % c.count;
400 new_index += c.count;
402 new_index = (count - 1) % c.count;
404 new_index += c.count;
408 if (new_index != -1) {
410 m.ignore_input_events = true;
411 s.select_completion(new_index);
413 m.ignore_input_events = false;
417 interactive("minibuffer-complete", null,
418 function (I) { minibuffer_complete(I.window, I.p); });
419 interactive("minibuffer-complete-previous", null,
420 function (I) { minibuffer_complete(I.window, -I.p); });
422 function exit_minibuffer (window) {
423 var m = window.minibuffer;
424 var s = m.current_state;
425 if (!(s instanceof text_entry_minibuffer_state))
426 throw new Error("Invalid minibuffer state");
428 var val = m._input_text;
430 if (s.validator != null && !s.validator(val, m))
435 if (s.completer && s.match_required) {
436 if (!s.completions_valid || s.completions === undefined)
437 s.update_completions(false /* not conservative */, false /* don't update */);
439 let c = s.completions;
440 let i = s.selected_completion_index;
441 if (c != null && i >= 0 && i < c.count) {
442 if (c.get_value != null)
443 match = c.get_value(i);
445 match = c.get_string(i);
447 m.message("No match");
454 if (s.history.length > minibuffer_history_max_items)
455 s.history.splice(0, s.history.length - minibuffer_history_max_items);
457 var cont = s.continuation;
458 delete s.continuation;
461 if (s.match_required)
467 interactive("exit-minibuffer", null,
468 function (I) { exit_minibuffer(I.window); });
470 function minibuffer_history_next (window, count) {
471 var m = window.minibuffer;
472 var s = m.current_state;
473 if (!(s instanceof text_entry_minibuffer_state))
474 throw new Error("Invalid minibuffer state");
475 if (!s.history || s.history.length == 0)
476 throw interactive_error("No history available.");
479 var index = s.history_index;
480 if (count > 0 && index == -1)
481 throw interactive_error("End of history; no next item");
482 else if (count < 0 && index == 0)
483 throw interactive_error("Beginning of history; no preceding item");
485 s.saved_last_history_entry = m._input_text;
486 index = s.history.length + count;
488 index = index + count;
493 m._restore_normal_state();
494 if (index >= s.history.length) {
496 m._input_text = s.saved_last_history_entry;
498 m._input_text = s.history[index];
500 s.history_index = index;
504 interactive("minibuffer-history-next", null,
505 function (I) { minibuffer_history_next(I.window, I.p); });
506 interactive("minibuffer-history-previous", null,
507 function (I) { minibuffer_history_next(I.window, -I.p); });
509 // Define the asynchronous minibuffer.read function
510 minibuffer.prototype.read = function () {
511 var s = new text_entry_minibuffer_state(this, (yield CONTINUATION), forward_keywords(arguments));
513 var result = yield SUSPEND;
514 yield co_return(result);
517 minibuffer.prototype.read_command = function () {
520 $prompt = "Command", $history = "command",
521 $completer = prefix_completer(
522 $completions = function (visitor) interactive_commands.for_each_value(visitor),
523 $get_string = function (x) x.name,
524 $get_description = function (x) x.shortdoc || "",
525 $get_value = function (x) x.name),
528 var result = yield this.read(forward_keywords(arguments));
529 yield co_return(result);
532 minibuffer.prototype.read_user_variable = function () {
535 $prompt = "User variable", $history = "user_variable",
536 $completer = prefix_completer(
537 $completions = function (visitor) {
538 for (var i in user_variables) visitor(i);
540 $get_string = function (x) x,
541 $get_description = function (x) user_variables[x].shortdoc || "",
542 $get_value = function (x) x),
545 var result = yield this.read(forward_keywords(arguments));
546 yield co_return(result);
549 minibuffer.prototype.read_preference = function () {
551 $prompt = "Preference:", $history = "preference",
552 $completer = prefix_completer(
553 $completions = preferences.getBranch(null).getChildList("", {}),
554 $get_description = function (pref) {
555 let default_value = get_default_pref(pref);
556 let value = get_pref(pref);
557 if (value == default_value)
560 switch (preferences.getBranch(null).getPrefType(pref)) {
561 case Ci.nsIPrefBranch.PREF_STRING:
564 case Ci.nsIPrefBranch.PREF_INT:
567 case Ci.nsIPrefBranch.PREF_BOOL:
571 let out = type + ":";
573 out += " " + pretty_print_value(value);
574 if (default_value != null)
575 out += " (" + pretty_print_value(default_value) + ")";
580 var result = yield this.read(forward_keywords(arguments));
581 yield co_return(result);
584 provide("minibuffer-read");