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 before auto-completing.");
12 define_variable("minibuffer_auto_complete_preferences", {});
14 define_variable("minibuffer_auto_complete_default", false, "Boolean specifying whether to auto-complete by default.\nThe user variable `minibuffer_auto_complete_preferences' overrides this.");
16 var minibuffer_history_data
= new string_hashmap();
18 /* FIXME: These should possibly be saved to disk somewhere */
19 define_variable("minibuffer_history_max_items", 100, "Maximum number of minibuffer history entries stored.\nOlder history entries are truncated after this limit is reached.");
22 /* The parameter `args' specifies the arguments. In addition, the
23 * arguments for basic_minibuffer_state are also allowed.
25 * history: [optional] specifies a string to identify the history list to use
31 * default_completion only used if match_required is set to true
33 * $valiator [optional]
34 * specifies a function
36 define_keywords("$history", "$validator",
38 "$completer", "$match_required", "$default_completion",
39 "$auto_complete", "$auto_complete_initial", "$auto_complete_conservative",
40 "$auto_complete_delay",
42 /* FIXME: support completing in another thread */
43 function text_entry_minibuffer_state(continuation
) {
46 basic_minibuffer_state
.call(this, forward_keywords(arguments
));
47 this.keymap
= minibuffer_keymap
;
49 this.continuation
= continuation
;
50 if (arguments
.$history
)
52 this.history
= minibuffer_history_data
.get_put_default(arguments
.$history
, []);
53 this.history_index
= -1;
54 this.saved_last_history_entry
= null;
57 this.validator
= arguments
.$validator
;
59 if (arguments
.$completer
!= null)
61 this.completer
= arguments
.$completer
;
62 let auto
= arguments
.$auto_complete
;
63 while (typeof(auto
) == "string")
64 auto
= minibuffer_auto_complete_preferences
[auto
];
66 auto
= minibuffer_auto_complete_default
;
67 this.auto_complete
= auto
;
68 this.auto_complete_initial
= !!arguments
.$auto_complete_initial
;
69 this.auto_complete_conservative
= !!arguments
.$auto_complete_conservative
;
70 let delay
= arguments
.$auto_complete_delay
;
72 delay
= default_minibuffer_auto_complete_delay
;
73 this.auto_complete_delay
= delay
;
74 this.completions
= null;
75 this.completions_valid
= false;
76 this.space_completes
= !!arguments
.$space_completes
;
77 this.completions_timer_ID
= null;
78 this.completions_display_element
= null;
79 this.selected_completion_index
= -1;
80 this.match_required
= !!arguments
.$match_required
;
81 if (this.match_required
)
82 this.default_completion
= arguments
.$default_completion
;
86 function completions_tree_view(minibuffer_state
)
88 this.minibuffer_state
= minibuffer_state
;
91 var atom_service
= Cc
["@mozilla.org/atom-service;1"].getService(Ci
.nsIAtomService
);
93 completions_tree_view
.prototype = {
95 var c
= this.minibuffer_state
.completions
;
100 getCellText : function(row
,column
){
101 var c
= this.minibuffer_state
.completions
;
104 if (column
.index
== 0)
105 return c
.get_string(row
);
106 if (c
.get_description
)
107 return c
.get_description(row
);
110 setTree : function(treebox
){ this.treebox
= treebox
; },
111 isContainer: function(row
){ return false; },
112 isSeparator: function(row
){ return false; },
113 isSorted: function(){ return false; },
114 getLevel: function(row
){ return 0; },
115 getImageSrc: function(row
,col
){ return null; },
116 getRowProperties: function(row
,props
){},
117 getCellProperties: function(row
,col
,props
){
119 props
.AppendElement(atom_service
.getAtom("completion-string"));
121 props
.AppendElement(atom_service
.getAtom("completion-description"));
123 getColumnProperties: function(colid
,col
,props
){}
126 // inherit from basic_minibuffer_state
127 text_entry_minibuffer_state
.prototype = {
128 __proto__
: basic_minibuffer_state
.prototype,
129 load : function (window
) {
130 this.window
= window
;
131 if (this.completer
) {
132 // Create completion display element if needed
133 if (!this.completion_element
)
135 /* FIXME: maybe use the dom_generator */
136 var tree
= create_XUL(window
, "tree");
138 tree
.addEventListener("select", function () {
139 s
.selected_completion_index
= s
.completions_display_element
.currentIndex
;
140 s
.handle_completion_selected();
142 tree
.setAttribute("class", "completions");
144 tree
.setAttribute("rows", "8");
146 tree
.setAttribute("collapsed", "true");
148 tree
.setAttribute("hidecolumnpicker", "true");
149 tree
.setAttribute("hideheader", "true");
151 var treecols
= create_XUL(window
, "treecols");
152 tree
.appendChild(treecols
);
153 var treecol
= create_XUL(window
, "treecol");
154 treecol
.setAttribute("flex", "1");
155 treecols
.appendChild(treecol
);
156 treecol
= create_XUL(window
, "treecol");
157 treecol
.setAttribute("flex", "1");
158 treecols
.appendChild(treecol
);
159 tree
.appendChild(create_XUL(window
, "treechildren"));
161 window
.minibuffer
.insert_before(tree
);
162 tree
.view
= new completions_tree_view(this);
163 this.completions_display_element
= tree
;
165 /* This is the initial loading of this minibuffer
166 * state. If this.complete_initial is true, generate
168 if (this.auto_complete_initial
)
172 this.update_completions_display();
176 unload : function (window
) {
177 if (this.completions_display_element
)
178 this.completions_display_element
.setAttribute("collapsed", "true");
181 destroy : function (window
) {
182 if (this.completions
!= null && this.completions
.destroy
)
183 this.completions
.destroy();
184 delete this.completions
;
185 if (this.completions_cont
)
186 this.completions_cont
.throw(abort());
187 delete this.completions_cont
;
189 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)
232 if (this.completions
&& this.completions
.count
> 0)
234 this.completions_display_element
.view
= this.completions_display_element
.view
;
235 this.completions_display_element
.setAttribute("collapsed", "false");
237 this.completions_display_element
.currentIndex
= this.selected_completion_index
;
238 this.completions_display_element
.treeBoxObject
.scrollToRow(this.selected_completion_index
);
240 this.completions_display_element
.setAttribute("collapsed", "true");
245 /* If auto is true, this update is due to auto completion, rather
246 * than specifically requested. */
247 update_completions : function (auto
, update_display
) {
249 if (this.completions_timer_ID
!= null) {
250 this.window
.clearTimeout(this.completions_timer_ID
);
251 this.completions_timer_ID
= null;
254 let m
= this.window
.minibuffer
;
256 if (this.completions_cont
) {
257 this.completions_cont
.throw(abort());
258 this.completions_cont
= null;
261 let c
= this.completer(m
._input_text
, m
._selection_start
,
262 auto
&& this.auto_complete_conservative
);
264 if (is_coroutine(c
)) {
266 let already_done
= false;
267 this.completions_cont
= co_call(function () {
272 s
.completions_cont
= null;
275 s
.update_completions_done(x
, update_display
);
278 // In case the completer actually already finished
280 this.completions_cont
= null;
283 this.update_completions_done(c
, update_display
);
286 update_completions_done
: function update_completions_done(c
, update_display
) {
288 /* The completer should return undefined if completion was not
289 * attempted due to auto being true. Otherwise, it can return
290 * null to indicate no completions. */
291 if (this.completions
!= null && this.completions
.destroy
)
292 this.completions
.destroy();
294 this.completions
= c
;
295 this.completions_valid
= true;
296 this.applied_common_prefix
= false;
299 if (c
&& c
.count
> 0) {
300 if (this.match_required
) {
303 else if (c
.default_completion
!= null)
304 i
= c
.default_completion
;
305 else if (this.default_completion
&& this.completions
.index_of
)
306 i
= this.completions
.index_of(this.default_completion
);
308 this.selected_completion_index
= i
;
312 this.update_completions_display();
315 select_completion : function (i
) {
316 this.selected_completion_index
= i
;
317 this.completions_display_element
.currentIndex
= i
;
319 this.completions_display_element
.treeBoxObject
.ensureRowIsVisible(i
);
320 this.handle_completion_selected();
323 handle_completion_selected : function () {
325 * When a completion is selected, apply it to the input text
326 * if a match is not "required"; otherwise, the completion is
329 var i
= this.selected_completion_index
;
330 var m
= this.window
.minibuffer
;
331 var c
= this.completions
;
333 if (this.completions_valid
&& c
&& !this.match_required
&& i
>= 0 && i
< c
.count
)
335 m
.set_input_state(c
.get_input_state(i
));
340 function minibuffer_complete(window
, count
)
342 var m
= window
.minibuffer
;
343 var s
= m
.current_state
;
344 if (!(s
instanceof text_entry_minibuffer_state
))
345 throw new Error("Invalid minibuffer state");
348 var just_completed_manually
= false;
349 if (!s
.completions_valid
|| s
.completions
=== undefined) {
350 if (s
.completions_timer_ID
== null)
351 just_completed_manually
= true;
352 s
.update_completions(false /* not auto */, true /* update completions display */);
354 // If the completer is a coroutine, nothing we can do here
355 if (!s
.completions_valid
)
359 var c
= s
.completions
;
361 if (!c
|| c
.count
== 0)
364 var e
= s
.completions_display_element
;
369 if (count
== 1 && !s
.applied_common_prefix
&& (common_prefix
= c
.common_prefix_input_state
))
371 m
.set_input_state(common_prefix
);
372 s
.applied_common_prefix
= true;
373 } else if (!just_completed_manually
) {
374 if (e
.currentIndex
!= -1)
376 new_index
= (e
.currentIndex
+ count
) % c
.count
;
378 new_index
+= c
.count
;
380 new_index
= (count
- 1) % c
.count
;
382 new_index
+= c
.count
;
387 s
.select_completion(new_index
);
389 interactive("minibuffer-complete", null, function (I
) {minibuffer_complete(I
.window
, I
.p
);});
390 interactive("minibuffer-complete-previous", null, function (I
) {minibuffer_complete(I
.window
, -I
.p
);});
392 function exit_minibuffer(window
)
394 var m
= window
.minibuffer
;
395 var s
= m
.current_state
;
396 if (!(s
instanceof text_entry_minibuffer_state
))
397 throw new Error("Invalid minibuffer state");
399 var val
= m
._input_text
;
401 if (s
.validator
!= null && !s
.validator(val
, m
))
406 if (s
.completer
&& s
.match_required
) {
407 if (!s
.completions_valid
|| s
.completions
=== undefined)
408 s
.update_completions(false /* not conservative */, false /* don't update */);
410 let c
= s
.completions
;
411 let i
= s
.selected_completion_index
;
412 if (c
!= null && i
>= 0 && i
< c
.count
) {
413 if (c
.get_value
!= null)
414 match
= c
.get_value(i
);
416 match
= c
.get_string(i
);
418 m
.message("No match");
426 if (s
.history
.length
> minibuffer_history_max_items
)
427 s
.history
.splice(0, s
.history
.length
- minibuffer_history_max_items
);
429 var cont
= s
.continuation
;
430 delete s
.continuation
;
433 if (s
.match_required
)
439 interactive("exit-minibuffer", null, function (I
) {exit_minibuffer(I
.window
);});
441 function minibuffer_history_next (window
, count
)
443 var m
= window
.minibuffer
;
444 var s
= m
.current_state
;
445 if (!(s
instanceof text_entry_minibuffer_state
))
446 throw new Error("Invalid minibuffer state");
447 if (!s
.history
|| s
.history
.length
== 0)
448 throw interactive_error("No history available.");
451 var index
= s
.history_index
;
452 if (count
> 0 && index
== -1)
453 throw interactive_error("End of history; no next item");
454 else if (count
< 0 && index
== 0) {
455 throw interactive_error("Beginning of history; no preceding item");
458 s
.saved_last_history_entry
= m
._input_text
;
459 index
= s
.history
.length
+ count
;
461 index
= index
+ count
;
466 m
._restore_normal_state();
467 if (index
>= s
.history
.length
) {
469 m
._input_text
= s
.saved_last_history_entry
;
471 m
._input_text
= s
.history
[index
];
473 s
.history_index
= index
;
477 interactive("minibuffer-history-next", null, function (I
) {minibuffer_history_next(I
.window
, I
.p
);});
478 interactive("minibuffer-history-previous", null, function (I
) {minibuffer_history_next(I
.window
, -I
.p
);});
480 // Define the asynchronous minibuffer.read function
481 minibuffer
.prototype.read = function () {
482 var s
= new text_entry_minibuffer_state((yield CONTINUATION
), forward_keywords(arguments
));
484 var result
= yield SUSPEND
;
485 yield co_return(result
);
488 minibuffer
.prototype.read_command = function () {
491 $prompt
= "Command", $history
= "command",
492 $completer
= prefix_completer(
493 $completions = function (visitor
) interactive_commands
.for_each_value(visitor
),
494 $get_string = function (x
) x
.name
,
495 $get_description = function (x
) x
.shortdoc
|| "",
496 $get_value = function (x
) x
.name
),
497 $match_required
= true);
498 var result
= yield this.read(forward_keywords(arguments
));
499 yield co_return(result
);
502 minibuffer
.prototype.read_user_variable = function () {
505 $prompt
= "User variable", $history
= "user_variable",
506 $completer
= prefix_completer(
507 $completions = function (visitor
) user_variables
.for_each(visitor
),
508 $get_string = function (x
) x
,
509 $get_description = function (x
) user_variables
.get(x
).shortdoc
|| "",
510 $get_value = function (x
) x
),
511 $match_required
= true);
512 var result
= yield this.read(forward_keywords(arguments
));
513 yield co_return(result
);
516 minibuffer
.prototype.read_preference
= function minibuffer__read_preference () {
518 $prompt
= "Preference:", $history
= "preference",
519 $completer
= prefix_completer(
520 $completions
= preferences
.getBranch(null).getChildList("", {}),
521 $get_description = function (pref
) {
522 let default_value
= get_default_pref(pref
);
523 let value
= get_pref(pref
);
524 if (value
== default_value
)
527 switch (preferences
.getBranch(null).getPrefType(pref
)) {
528 case Ci
.nsIPrefBranch
.PREF_STRING
:
531 case Ci
.nsIPrefBranch
.PREF_INT
:
534 case Ci
.nsIPrefBranch
.PREF_BOOL
:
538 let out
= type
+ ":";
540 out
+= " " + pretty_print_value(value
);
541 if (default_value
!= null)
542 out
+= " (" + pretty_print_value(default_value
) + ")";
545 $match_required
= true);
546 var result
= yield this.read(forward_keywords(arguments
));
547 yield co_return(result
);