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 "+
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 = new string_hashmap();
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.");
28 /* The parameter `args' specifies the arguments. In addition, the
29 * arguments for basic_minibuffer_state are also allowed.
31 * history: [optional] specifies a string to identify the history list to use
37 * default_completion only used if match_required is set to true
39 * $valiator [optional]
40 * specifies a function
42 define_keywords("$history", "$validator",
43 "$completer", "$match_required", "$default_completion",
44 "$auto_complete", "$auto_complete_initial", "$auto_complete_conservative",
45 "$auto_complete_delay",
47 /* FIXME: support completing in another thread */
48 function text_entry_minibuffer_state (continuation) {
51 basic_minibuffer_state.call(this, forward_keywords(arguments));
52 this.keymap = minibuffer_keymap;
54 this.continuation = continuation;
55 if (arguments.$history) {
56 this.history = minibuffer_history_data.get_put_default(arguments.$history, []);
57 this.history_index = -1;
58 this.saved_last_history_entry = null;
61 this.validator = arguments.$validator;
63 if (arguments.$completer != null) {
64 this.completer = arguments.$completer;
65 let auto = arguments.$auto_complete;
66 while (typeof(auto) == "string")
67 auto = minibuffer_auto_complete_preferences[auto];
69 auto = minibuffer_auto_complete_default;
70 this.auto_complete = auto;
71 this.auto_complete_initial = !!arguments.$auto_complete_initial;
72 this.auto_complete_conservative = !!arguments.$auto_complete_conservative;
73 let delay = arguments.$auto_complete_delay;
75 delay = default_minibuffer_auto_complete_delay;
76 this.auto_complete_delay = delay;
77 this.completions = null;
78 this.completions_valid = false;
79 this.space_completes = !!arguments.$space_completes;
80 this.completions_timer_ID = null;
81 this.completions_display_element = null;
82 this.selected_completion_index = -1;
83 this.match_required = !!arguments.$match_required;
84 if (this.match_required)
85 this.default_completion = arguments.$default_completion;
89 function completions_tree_view (minibuffer_state) {
90 this.minibuffer_state = minibuffer_state;
93 var atom_service = Cc["@mozilla.org/atom-service;1"].getService(Ci.nsIAtomService);
95 completions_tree_view.prototype = {
97 var c = this.minibuffer_state.completions;
102 getCellText : function (row,column) {
103 var c = this.minibuffer_state.completions;
106 if (column.index == 0)
107 return c.get_string(row);
108 if (c.get_description)
109 return c.get_description(row);
112 setTree : function (treebox) { this.treebox = treebox; },
113 isContainer: function (row) { return false; },
114 isSeparator: function (row) { return false; },
115 isSorted: function () { return false; },
116 getLevel: function (row) { return 0; },
117 getImageSrc: function (row, col) { return null; },
118 getRowProperties: function (row, props) {},
119 getCellProperties: function (row, col, props) {
121 props.AppendElement(atom_service.getAtom("completion-string"));
123 props.AppendElement(atom_service.getAtom("completion-description"));
125 getColumnProperties: function (colid, col, props) {}
128 // inherit from basic_minibuffer_state
129 text_entry_minibuffer_state.prototype = {
130 __proto__: basic_minibuffer_state.prototype,
131 load : function (window) {
132 this.window = window;
133 if (this.completer) {
134 // Create completion display element if needed
135 if (!this.completion_element) {
136 /* FIXME: maybe use the dom_generator */
137 var tree = create_XUL(window, "tree");
139 tree.addEventListener("select", function () {
140 s.selected_completion_index = s.completions_display_element.currentIndex;
141 s.handle_completion_selected();
143 tree.setAttribute("class", "completions");
145 tree.setAttribute("rows", "8");
147 tree.setAttribute("collapsed", "true");
149 tree.setAttribute("hidecolumnpicker", "true");
150 tree.setAttribute("hideheader", "true");
152 var treecols = create_XUL(window, "treecols");
153 tree.appendChild(treecols);
154 var treecol = create_XUL(window, "treecol");
155 treecol.setAttribute("flex", "1");
156 treecols.appendChild(treecol);
157 treecol = create_XUL(window, "treecol");
158 treecol.setAttribute("flex", "1");
159 treecols.appendChild(treecol);
160 tree.appendChild(create_XUL(window, "treechildren"));
162 window.minibuffer.insert_before(tree);
163 tree.view = new completions_tree_view(this);
164 this.completions_display_element = tree;
166 /* This is the initial loading of this minibuffer
167 * state. If this.complete_initial is true, generate
169 if (this.auto_complete_initial)
173 this.update_completions_display();
177 unload : function (window) {
178 if (this.completions_display_element)
179 this.completions_display_element.setAttribute("collapsed", "true");
182 destroy : function (window) {
183 if (this.completions != null && this.completions.destroy)
184 this.completions.destroy();
185 delete this.completions;
186 if (this.completions_cont)
187 this.completions_cont.throw(abort());
188 delete this.completions_cont;
190 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) {
231 if (this.completions && this.completions.count > 0) {
232 this.completions_display_element.view = this.completions_display_element.view;
233 this.completions_display_element.setAttribute("collapsed", "false");
235 this.completions_display_element.currentIndex = this.selected_completion_index;
236 this.completions_display_element.treeBoxObject.scrollToRow(this.selected_completion_index);
238 this.completions_display_element.setAttribute("collapsed", "true");
243 /* If auto is true, this update is due to auto completion, rather
244 * than specifically requested. */
245 update_completions : function (auto, update_display) {
247 if (this.completions_timer_ID != null) {
248 this.window.clearTimeout(this.completions_timer_ID);
249 this.completions_timer_ID = null;
252 let m = this.window.minibuffer;
254 if (this.completions_cont) {
255 this.completions_cont.throw(abort());
256 this.completions_cont = null;
259 let c = this.completer(m._input_text, m._selection_start,
260 auto && this.auto_complete_conservative);
262 if (is_coroutine(c)) {
264 let already_done = false;
265 this.completions_cont = co_call(function () {
270 s.completions_cont = null;
273 s.update_completions_done(x, update_display);
276 // In case the completer actually already finished
278 this.completions_cont = null;
281 this.update_completions_done(c, update_display);
284 update_completions_done : function update_completions_done (c, update_display) {
286 /* The completer should return undefined if completion was not
287 * attempted due to auto being true. Otherwise, it can return
288 * null to indicate no completions. */
289 if (this.completions != null && this.completions.destroy)
290 this.completions.destroy();
292 this.completions = c;
293 this.completions_valid = true;
294 this.applied_common_prefix = false;
297 if (c && c.count > 0) {
298 if (this.match_required) {
301 else if (c.default_completion != null)
302 i = c.default_completion;
303 else if (this.default_completion && this.completions.index_of)
304 i = this.completions.index_of(this.default_completion);
306 this.selected_completion_index = i;
310 this.update_completions_display();
313 select_completion : function (i) {
314 this.selected_completion_index = i;
315 this.completions_display_element.currentIndex = i;
317 this.completions_display_element.treeBoxObject.ensureRowIsVisible(i);
318 this.handle_completion_selected();
321 handle_completion_selected : function () {
323 * When a completion is selected, apply it to the input text
324 * if a match is not "required"; otherwise, the completion is
327 var i = this.selected_completion_index;
328 var m = this.window.minibuffer;
329 var c = this.completions;
331 if (this.completions_valid && c && !this.match_required && i >= 0 && i < c.count)
333 m.set_input_state(c.get_input_state(i));
338 function minibuffer_complete (window, count) {
339 var m = window.minibuffer;
340 var s = m.current_state;
341 if (!(s instanceof text_entry_minibuffer_state))
342 throw new Error("Invalid minibuffer state");
345 var just_completed_manually = false;
346 if (!s.completions_valid || s.completions === undefined) {
347 if (s.completions_timer_ID == null)
348 just_completed_manually = true;
349 s.update_completions(false /* not auto */, true /* update completions display */);
351 // If the completer is a coroutine, nothing we can do here
352 if (!s.completions_valid)
356 var c = s.completions;
358 if (!c || c.count == 0)
361 var e = s.completions_display_element;
366 if (count == 1 && !s.applied_common_prefix && (common_prefix = c.common_prefix_input_state)) {
367 m.set_input_state(common_prefix);
368 s.applied_common_prefix = true;
369 } else if (!just_completed_manually) {
370 if (e.currentIndex != -1) {
371 new_index = (e.currentIndex + count) % c.count;
373 new_index += c.count;
375 new_index = (count - 1) % c.count;
377 new_index += c.count;
382 s.select_completion(new_index);
384 interactive("minibuffer-complete", null,
385 function (I) { minibuffer_complete(I.window, I.p); });
386 interactive("minibuffer-complete-previous", null,
387 function (I) { minibuffer_complete(I.window, -I.p); });
389 function exit_minibuffer (window) {
390 var m = window.minibuffer;
391 var s = m.current_state;
392 if (!(s instanceof text_entry_minibuffer_state))
393 throw new Error("Invalid minibuffer state");
395 var val = m._input_text;
397 if (s.validator != null && !s.validator(val, m))
402 if (s.completer && s.match_required) {
403 if (!s.completions_valid || s.completions === undefined)
404 s.update_completions(false /* not conservative */, false /* don't update */);
406 let c = s.completions;
407 let i = s.selected_completion_index;
408 if (c != null && i >= 0 && i < c.count) {
409 if (c.get_value != null)
410 match = c.get_value(i);
412 match = c.get_string(i);
414 m.message("No match");
421 if (s.history.length > minibuffer_history_max_items)
422 s.history.splice(0, s.history.length - minibuffer_history_max_items);
424 var cont = s.continuation;
425 delete s.continuation;
428 if (s.match_required)
434 interactive("exit-minibuffer", null,
435 function (I) { exit_minibuffer(I.window); });
437 function minibuffer_history_next (window, count) {
438 var m = window.minibuffer;
439 var s = m.current_state;
440 if (!(s instanceof text_entry_minibuffer_state))
441 throw new Error("Invalid minibuffer state");
442 if (!s.history || s.history.length == 0)
443 throw interactive_error("No history available.");
446 var index = s.history_index;
447 if (count > 0 && index == -1)
448 throw interactive_error("End of history; no next item");
449 else if (count < 0 && index == 0) {
450 throw interactive_error("Beginning of history; no preceding item");
453 s.saved_last_history_entry = m._input_text;
454 index = s.history.length + count;
456 index = index + count;
461 m._restore_normal_state();
462 if (index >= s.history.length) {
464 m._input_text = s.saved_last_history_entry;
466 m._input_text = s.history[index];
468 s.history_index = index;
472 interactive("minibuffer-history-next", null,
473 function (I) { minibuffer_history_next(I.window, I.p); });
474 interactive("minibuffer-history-previous", null,
475 function (I) { minibuffer_history_next(I.window, -I.p); });
477 // Define the asynchronous minibuffer.read function
478 minibuffer.prototype.read = function () {
479 var s = new text_entry_minibuffer_state((yield CONTINUATION), forward_keywords(arguments));
481 var result = yield SUSPEND;
482 yield co_return(result);
485 minibuffer.prototype.read_command = function () {
488 $prompt = "Command", $history = "command",
489 $completer = prefix_completer(
490 $completions = function (visitor) interactive_commands.for_each_value(visitor),
491 $get_string = function (x) x.name,
492 $get_description = function (x) x.shortdoc || "",
493 $get_value = function (x) x.name),
494 $match_required = true);
495 var result = yield this.read(forward_keywords(arguments));
496 yield co_return(result);
499 minibuffer.prototype.read_user_variable = function () {
502 $prompt = "User variable", $history = "user_variable",
503 $completer = prefix_completer(
504 $completions = function (visitor) {
505 [visitor(i, user_variables[i]) for (i in user_variables)];
507 $get_string = function (x) x,
508 $get_description = function (x) user_variables[x].shortdoc || "",
509 $get_value = function (x) x),
510 $match_required = true);
511 var result = yield this.read(forward_keywords(arguments));
512 yield co_return(result);
515 minibuffer.prototype.read_preference = function minibuffer__read_preference () {
517 $prompt = "Preference:", $history = "preference",
518 $completer = prefix_completer(
519 $completions = preferences.getBranch(null).getChildList("", {}),
520 $get_description = function (pref) {
521 let default_value = get_default_pref(pref);
522 let value = get_pref(pref);
523 if (value == default_value)
526 switch (preferences.getBranch(null).getPrefType(pref)) {
527 case Ci.nsIPrefBranch.PREF_STRING:
530 case Ci.nsIPrefBranch.PREF_INT:
533 case Ci.nsIPrefBranch.PREF_BOOL:
537 let out = type + ":";
539 out += " " + pretty_print_value(value);
540 if (default_value != null)
541 out += " (" + pretty_print_value(default_value) + ")";
544 $match_required = true);
545 var result = yield this.read(forward_keywords(arguments));
546 yield co_return(result);