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");