]> git.donarmstrong.com Git - dactyl.git/blob - common/content/commandline.js
Import 1.0b7.1 supporting Firefox up to 8.*
[dactyl.git] / common / content / commandline.js
1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
4 //
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
7 "use strict";
8
9 /** @scope modules */
10
11 var CommandWidgets = Class("CommandWidgets", {
12     depends: ["statusline"],
13
14     init: function init() {
15         let s = "dactyl-statusline-field-";
16
17         XML.ignoreWhitespace = true;
18         util.overlayWindow(window, {
19             objects: {
20                 eventTarget: commandline
21             },
22             append: <e4x xmlns={XUL} xmlns:dactyl={NS}>
23                 <vbox id={config.commandContainer}>
24                     <vbox class="dactyl-container" hidden="false" collapsed="true">
25                         <iframe class="dactyl-completions" id="dactyl-completions-dactyl-commandline" src="dactyl://content/buffer.xhtml"
26                                 contextmenu="dactyl-contextmenu"
27                                 flex="1" hidden="false" collapsed="false"
28                                 highlight="Events" events="mowEvents" />
29                     </vbox>
30
31                     <stack orient="horizontal" align="stretch" class="dactyl-container" id="dactyl-container" highlight="CmdLine CmdCmdLine">
32                         <textbox class="plain" id="dactyl-strut"   flex="1" crop="end" collapsed="true"/>
33                         <textbox class="plain" id="dactyl-mode"    flex="1" crop="end"/>
34                         <textbox class="plain" id="dactyl-message" flex="1" readonly="true"/>
35
36                         <hbox id="dactyl-commandline" hidden="false" class="dactyl-container" highlight="Normal CmdNormal" collapsed="true">
37                             <label   id="dactyl-commandline-prompt"  class="dactyl-commandline-prompt  plain" flex="0" crop="end" value="" collapsed="true"/>
38                             <textbox id="dactyl-commandline-command" class="dactyl-commandline-command plain" flex="1" type="input" timeout="100"
39                                      highlight="Events" />
40                         </hbox>
41                     </stack>
42
43                     <vbox class="dactyl-container" hidden="false" collapsed="false" highlight="CmdLine">
44                         <textbox id="dactyl-multiline-input" class="plain" flex="1" rows="1" hidden="false" collapsed="true" multiline="true"
45                                  highlight="Normal Events" events="multilineInputEvents" />
46                     </vbox>
47                 </vbox>
48
49                 <stack id="dactyl-statusline-stack">
50                     <hbox id={s + "commandline"} hidden="false" class="dactyl-container" highlight="Normal StatusNormal" collapsed="true">
51                         <label id={s + "commandline-prompt"}    class="dactyl-commandline-prompt  plain" flex="0" crop="end" value="" collapsed="true"/>
52                         <textbox id={s + "commandline-command"} class="dactyl-commandline-command plain" flex="1" type="text" timeout="100"
53                                  highlight="Events" />
54                     </hbox>
55                 </stack>
56             </e4x>.elements(),
57
58             before: <e4x xmlns={XUL} xmlns:dactyl={NS}>
59                 <toolbar id={statusline.statusBar.id}>
60                     <vbox id={"dactyl-completions-" + s + "commandline-container"} class="dactyl-container" hidden="false" collapsed="true">
61                         <iframe class="dactyl-completions" id={"dactyl-completions-" + s + "commandline"} src="dactyl://content/buffer.xhtml"
62                                 contextmenu="dactyl-contextmenu" flex="1" hidden="false" collapsed="false"
63                                 highlight="Events" events="mowEvents" />
64                     </vbox>
65                 </toolbar>
66             </e4x>.elements()
67         });
68
69         this.elements = {};
70
71         this.addElement({
72             name: "container",
73             noValue: true
74         });
75
76         this.addElement({
77             name: "commandline",
78             getGroup: function () options.get("guioptions").has("C") ? this.commandbar : this.statusbar,
79             getValue: function () this.command
80         });
81
82         this.addElement({
83             name: "strut",
84             defaultGroup: "Normal",
85             getGroup: function () this.commandbar,
86             getValue: function () options.get("guioptions").has("c")
87         });
88
89         this.addElement({
90             name: "command",
91             test: function test(stack, prev) stack.pop && !isinstance(prev.main, modes.COMMAND_LINE),
92             id: "commandline-command",
93             get: function command_get(elem) {
94                 // The long path is because of complications with the
95                 // completion preview.
96                 try {
97                     return elem.inputField.editor.rootElement.firstChild.textContent;
98                 }
99                 catch (e) {
100                     return elem.value;
101                 }
102             },
103             getElement: CommandWidgets.getEditor,
104             getGroup: function (value) this.activeGroup.commandline,
105             onChange: function command_onChange(elem, value) {
106                 if (elem.inputField != dactyl.focusedElement)
107                     try {
108                         elem.selectionStart = elem.value.length;
109                         elem.selectionEnd = elem.value.length;
110                     }
111                     catch (e) {}
112
113                 if (!elem.collapsed)
114                     dactyl.focus(elem);
115             },
116             onVisibility: function command_onVisibility(elem, visible) {
117                 if (visible)
118                     dactyl.focus(elem);
119             }
120         });
121
122         this.addElement({
123             name: "prompt",
124             id: "commandline-prompt",
125             defaultGroup: "CmdPrompt",
126             getGroup: function () this.activeGroup.commandline
127         });
128
129         this.addElement({
130             name: "message",
131             defaultGroup: "Normal",
132             getElement: CommandWidgets.getEditor,
133             getGroup: function (value) {
134                 if (this.command && !options.get("guioptions").has("M"))
135                     return this.statusbar;
136
137                 let statusElem = this.statusbar.message;
138                 if (value && !value[2] && statusElem.editor && statusElem.editor.rootElement.scrollWidth > statusElem.scrollWidth)
139                     return this.commandbar;
140                 return this.activeGroup.mode;
141             }
142         });
143
144         this.addElement({
145             name: "mode",
146             defaultGroup: "ModeMsg",
147             getGroup: function (value) {
148                 if (!options.get("guioptions").has("M"))
149                     if (this.commandbar.container.clientHeight == 0 ||
150                             value && !this.commandbar.commandline.collapsed)
151                         return this.statusbar;
152                 return this.commandbar;
153             }
154         });
155         this.updateVisibility();
156     },
157     addElement: function addElement(obj) {
158         const self = this;
159         this.elements[obj.name] = obj;
160
161         function get(prefix, map, id) (obj.getElement || util.identity)(map[id] || document.getElementById(prefix + id));
162
163         this.active.__defineGetter__(obj.name, function () self.activeGroup[obj.name][obj.name]);
164         this.activeGroup.__defineGetter__(obj.name, function () self.getGroup(obj.name));
165
166         memoize(this.statusbar, obj.name, function () get("dactyl-statusline-field-", statusline.widgets, (obj.id || obj.name)));
167         memoize(this.commandbar, obj.name, function () get("dactyl-", {}, (obj.id || obj.name)));
168
169         if (!(obj.noValue || obj.getValue)) {
170             Object.defineProperty(this, obj.name, Modes.boundProperty({
171                 test: obj.test,
172
173                 get: function get_widgetValue() {
174                     let elem = self.getGroup(obj.name, obj.value)[obj.name];
175                     if (obj.value != null)
176                         return [obj.value[0],
177                                 obj.get ? obj.get.call(this, elem) : elem.value]
178                                 .concat(obj.value.slice(2));
179                     return null;
180                 },
181
182                 set: function set_widgetValue(val) {
183                     if (val != null && !isArray(val))
184                         val = [obj.defaultGroup || "", val];
185                     obj.value = val;
186
187                     [this.commandbar, this.statusbar].forEach(function (nodeSet) {
188                         let elem = nodeSet[obj.name];
189                         if (val == null)
190                             elem.value = "";
191                         else {
192                             highlight.highlightNode(elem,
193                                 (val[0] != null ? val[0] : obj.defaultGroup)
194                                     .split(/\s/).filter(util.identity)
195                                     .map(function (g) g + " " + nodeSet.group + g)
196                                     .join(" "));
197                             elem.value = val[1];
198                             if (obj.onChange)
199                                 obj.onChange.call(this, elem, val);
200                         }
201                     }, this);
202
203                     this.updateVisibility();
204                     return val;
205                 }
206             }).init(obj.name));
207         }
208         else if (obj.defaultGroup) {
209             [this.commandbar, this.statusbar].forEach(function (nodeSet) {
210                 let elem = nodeSet[obj.name];
211                 if (elem)
212                     highlight.highlightNode(elem, obj.defaultGroup.split(/\s/)
213                                                      .map(function (g) g + " " + nodeSet.group + g).join(" "));
214             });
215         }
216     },
217
218     getGroup: function getgroup(name, value) {
219         if (!statusline.visible)
220             return this.commandbar;
221         return this.elements[name].getGroup.call(this, arguments.length > 1 ? value : this[name]);
222     },
223
224     updateVisibility: function updateVisibility() {
225         for (let elem in values(this.elements))
226             if (elem.getGroup) {
227                 let value = elem.getValue ? elem.getValue.call(this)
228                           : elem.noValue || this[elem.name];
229
230                 let activeGroup = this.getGroup(elem.name, value);
231                 for (let group in values([this.commandbar, this.statusbar])) {
232                     let meth, node = group[elem.name];
233                     let visible = (value && group === activeGroup);
234                     if (node && !node.collapsed == !visible) {
235                         node.collapsed = !visible;
236                         if (elem.onVisibility)
237                             elem.onVisibility.call(this, node, visible);
238                     }
239                 }
240             }
241
242         // Hack. Collapse hidden elements in the stack.
243         // Might possibly be better to use a deck and programmatically
244         // choose which element to select.
245         function check(node) {
246             if (util.computedStyle(node).display === "-moz-stack") {
247                 let nodes = Array.filter(node.children, function (n) !n.collapsed && n.boxObject.height);
248                 nodes.forEach(function (node, i) node.style.opacity = (i == nodes.length - 1) ? "" : "0");
249             }
250             Array.forEach(node.children, check);
251         }
252         [this.commandbar.container, this.statusbar.container].forEach(check);
253     },
254
255     active: Class.memoize(Object),
256     activeGroup: Class.memoize(Object),
257     commandbar: Class.memoize(function () ({ group: "Cmd" })),
258     statusbar: Class.memoize(function ()  ({ group: "Status" })),
259
260     _ready: function _ready(elem) {
261         return elem.contentDocument.documentURI === elem.getAttribute("src") &&
262                ["viewable", "complete"].indexOf(elem.contentDocument.readyState) >= 0;
263     },
264
265     _whenReady: function _whenReady(id, init) {
266         let elem = document.getElementById(id);
267         while (!this._ready(elem))
268             yield 10;
269
270         if (init)
271             init.call(this, elem);
272         yield elem;
273     },
274
275     completionContainer: Class.memoize(function () this.completionList.parentNode),
276
277     contextMenu: Class.memoize(function () {
278         ["copy", "copylink", "selectall"].forEach(function (tail) {
279             // some host apps use "hostPrefixContext-copy" ids
280             let xpath = "//xul:menuitem[contains(@id, '" + "ontext-" + tail + "') and not(starts-with(@id, 'dactyl-'))]";
281             document.getElementById("dactyl-context-" + tail).style.listStyleImage =
282                 util.computedStyle(util.evaluateXPath(xpath, document).snapshotItem(0)).listStyleImage;
283         });
284         return document.getElementById("dactyl-contextmenu");
285     }),
286
287     multilineOutput: Class.memoize(function () this._whenReady("dactyl-multiline-output", function (elem) {
288         elem.contentWindow.addEventListener("unload", function (event) { event.preventDefault(); }, true);
289         elem.contentDocument.documentElement.id = "dactyl-multiline-output-top";
290         elem.contentDocument.body.id = "dactyl-multiline-output-content";
291     }), true),
292
293     multilineInput: Class.memoize(function () document.getElementById("dactyl-multiline-input")),
294
295     mowContainer: Class.memoize(function () document.getElementById("dactyl-multiline-output-container"))
296 }, {
297     getEditor: function getEditor(elem) {
298         elem.inputField.QueryInterface(Ci.nsIDOMNSEditableElement);
299         return elem;
300     }
301 });
302
303 var CommandMode = Class("CommandMode", {
304     init: function CM_init() {
305         this.keepCommand = userContext.hidden_option_command_afterimage;
306     },
307
308     get autocomplete() options["autocomplete"].length,
309
310     get command() this.widgets.command[1],
311     set command(val) this.widgets.command = val,
312
313     get prompt() this.widgets.prompt,
314     set prompt(val) this.widgets.prompt = val,
315
316     open: function CM_open(command) {
317         dactyl.assert(isinstance(this.mode, modes.COMMAND_LINE),
318                       /*L*/"Not opening command line in non-command-line mode.");
319
320         this.messageCount = commandline.messageCount;
321         modes.push(this.mode, this.extendedMode, this.closure);
322
323         this.widgets.active.commandline.collapsed = false;
324         this.widgets.prompt = this.prompt;
325         this.widgets.command = command || "";
326
327         this.input = this.widgets.active.command.inputField;
328         if (this.historyKey)
329             this.history = CommandLine.History(this.input, this.historyKey, this);
330
331         if (this.complete)
332             this.completions = CommandLine.Completions(this.input, this);
333
334         if (this.completions && command && commandline.commandSession === this)
335             this.completions.autocompleteTimer.flush(true);
336     },
337
338     get active() this === commandline.commandSession,
339
340     get holdFocus() this.widgets.active.command.inputField,
341
342     get mappingSelf() this,
343
344     get widgets() commandline.widgets,
345
346     enter: function CM_enter(stack) {
347         commandline.commandSession = this;
348         if (stack.pop && commandline.command) {
349             this.onChange(commandline.command);
350             if (this.completions && stack.pop)
351                 this.completions.complete(true, false);
352         }
353     },
354
355     leave: function CM_leave(stack) {
356         if (!stack.push) {
357             commandline.commandSession = null;
358             this.input.dactylKeyPress = undefined;
359
360             if (this.completions)
361                 this.completions.cleanup();
362
363             if (this.history)
364                 this.history.save();
365
366             this.resetCompletions();
367             commandline.hideCompletions();
368
369             modes.delay(function () {
370                 if (!this.keepCommand || commandline.silent || commandline.quiet)
371                     commandline.hide();
372                 this[this.accepted ? "onSubmit" : "onCancel"](commandline.command);
373                 if (commandline.messageCount === this.messageCount)
374                     commandline.clearMessage();
375             }, this);
376         }
377     },
378
379     events: {
380         input: function CM_onInput(event) {
381             if (this.completions) {
382                 this.resetCompletions();
383
384                 this.completions.autocompleteTimer.tell(false);
385             }
386             this.onChange(commandline.command);
387         },
388         keyup: function CM_onKeyUp(event) {
389             let key = events.toString(event);
390             if (/-?Tab>$/.test(key) && this.completions)
391                 this.completions.tabTimer.flush();
392         }
393     },
394
395     keepCommand: false,
396
397     onKeyPress: function CM_onKeyPress(events) {
398         if (this.completions)
399             this.completions.previewClear();
400
401         return true; /* Pass event */
402     },
403
404     onCancel: function (value) {},
405
406     onChange: function (value) {},
407
408     onHistory: function (value) {},
409
410     onSubmit: function (value) {},
411
412     resetCompletions: function CM_resetCompletions() {
413         if (this.completions) {
414             this.completions.context.cancelAll();
415             this.completions.wildIndex = -1;
416             this.completions.previewClear();
417         }
418         if (this.history)
419             this.history.reset();
420     },
421 });
422
423 var CommandExMode = Class("CommandExMode", CommandMode, {
424
425     get mode() modes.EX,
426
427     historyKey: "command",
428
429     prompt: ["Normal", ":"],
430
431     complete: function CEM_complete(context) {
432         context.fork("ex", 0, completion, "ex");
433     },
434
435     onSubmit: function CEM_onSubmit(command) {
436         contexts.withContext({ file: /*L*/"[Command Line]", line: 1 },
437                              function _onSubmit() {
438             io.withSavedValues(["readHeredoc"], function _onSubmit() {
439                 this.readHeredoc = commandline.readHeredoc;
440                 commands.repeat = command;
441                 dactyl.execute(command);
442             });
443         });
444     }
445 });
446
447 var CommandPromptMode = Class("CommandPromptMode", CommandMode, {
448     init: function init(prompt, params) {
449         this.prompt = isArray(prompt) ? prompt : ["Question", prompt];
450         update(this, params);
451         init.supercall(this);
452     },
453
454     complete: function CPM_complete(context) {
455         if (this.completer)
456             context.forkapply("prompt", 0, this, "completer", Array.slice(arguments, 1));
457     },
458
459     get mode() modes.PROMPT
460 });
461
462 /**
463  * This class is used for prompting of user input and echoing of messages.
464  *
465  * It consists of a prompt and command field be sure to only create objects of
466  * this class when the chrome is ready.
467  */
468 var CommandLine = Module("commandline", {
469     init: function init() {
470         const self = this;
471
472         this._callbacks = {};
473
474         memoize(this, "_store", function () storage.newMap("command-history", { store: true, privateData: true }));
475
476         for (let name in values(["command", "search"]))
477             if (storage.exists("history-" + name)) {
478                 let ary = storage.newArray("history-" + name, { store: true, privateData: true });
479
480                 this._store.set(name, [v for ([k, v] in ary)]);
481                 ary.delete();
482                 this._store.changed();
483             }
484
485         this._messageHistory = { //{{{
486             _messages: [],
487             get messages() {
488                 let max = options["messages"];
489
490                 // resize if 'messages' has changed
491                 if (this._messages.length > max)
492                     this._messages = this._messages.splice(this._messages.length - max);
493
494                 return this._messages;
495             },
496
497             get length() this._messages.length,
498
499             clear: function clear() {
500                 this._messages = [];
501             },
502
503             filter: function filter(fn, self) {
504                 this._messages = this._messages.filter(fn, self);
505             },
506
507             add: function add(message) {
508                 if (!message)
509                     return;
510
511                 if (this._messages.length >= options["messages"])
512                     this._messages.shift();
513
514                 this._messages.push(update({
515                     timestamp: Date.now()
516                 }, message));
517             }
518         }; //}}}
519     },
520
521     signals: {
522         "browser.locationChange": function (webProgress, request, uri) {
523             this.clear();
524         }
525     },
526
527     /**
528      * Determines whether the command line should be visible.
529      *
530      * @returns {boolean}
531      */
532     get commandVisible() !!this.commandSession,
533
534     /**
535      * Ensure that the multiline input widget is the correct size.
536      */
537     _autosizeMultilineInputWidget: function _autosizeMultilineInputWidget() {
538         let lines = this.widgets.multilineInput.value.split("\n").length - 1;
539
540         this.widgets.multilineInput.setAttribute("rows", Math.max(lines, 1));
541     },
542
543     HL_NORMAL:     "Normal",
544     HL_ERRORMSG:   "ErrorMsg",
545     HL_MODEMSG:    "ModeMsg",
546     HL_MOREMSG:    "MoreMsg",
547     HL_QUESTION:   "Question",
548     HL_INFOMSG:    "InfoMsg",
549     HL_WARNINGMSG: "WarningMsg",
550     HL_LINENR:     "LineNr",
551
552     FORCE_MULTILINE    : 1 << 0,
553     FORCE_SINGLELINE   : 1 << 1,
554     DISALLOW_MULTILINE : 1 << 2, // If an echo() should try to use the single line
555                                  // but output nothing when the MOW is open; when also
556                                  // FORCE_MULTILINE is given, FORCE_MULTILINE takes precedence
557     APPEND_TO_MESSAGES : 1 << 3, // Add the string to the message history.
558     ACTIVE_WINDOW      : 1 << 4, // Only echo in active window.
559
560     get completionContext() this._completions.context,
561
562     _silent: false,
563     get silent() this._silent,
564     set silent(val) {
565         this._silent = val;
566         this.quiet = this.quiet;
567     },
568
569     _quite: false,
570     get quiet() this._quiet,
571     set quiet(val) {
572         this._quiet = val;
573         ["commandbar", "statusbar"].forEach(function (nodeSet) {
574             Array.forEach(this.widgets[nodeSet].commandline.children, function (node) {
575                 node.style.opacity = this._quiet || this._silent ? "0" : "";
576             }, this);
577         }, this);
578     },
579
580     widgets: Class.memoize(function () CommandWidgets()),
581
582     runSilently: function runSilently(func, self) {
583         this.withSavedValues(["silent"], function () {
584             this.silent = true;
585             func.call(self);
586         });
587     },
588
589     get completionList() {
590         let node = this.widgets.active.commandline;
591         if (this.commandSession && this.commandSession.completionList)
592             node = document.getElementById(this.commandSession.completionList);
593
594         if (!node.completionList) {
595             let elem = document.getElementById("dactyl-completions-" + node.id);
596             util.waitFor(bind(this.widgets._ready, null, elem));
597
598             node.completionList = ItemList(elem.id);
599         }
600         return node.completionList;
601     },
602
603     hideCompletions: function hideCompletions() {
604         for (let nodeSet in values([this.widgets.statusbar, this.widgets.commandbar]))
605             if (nodeSet.commandline.completionList)
606                 nodeSet.commandline.completionList.visible = false;
607     },
608
609     _lastClearable: Modes.boundProperty(),
610     messages: Modes.boundProperty(),
611
612     multilineInputVisible: Modes.boundProperty({
613         set: function set_miwVisible(value) { this.widgets.multilineInput.collapsed = !value; }
614     }),
615
616     get command() {
617         if (this.commandVisible && this.widgets.command)
618             return commands.lastCommand = this.widgets.command[1];
619         return commands.lastCommand;
620     },
621     set command(val) {
622         if (this.commandVisible && (modes.extended & modes.EX))
623             return this.widgets.command = val;
624         return commands.lastCommand = val;
625     },
626
627     clear: function clear(scroll) {
628         if (!scroll || Date.now() - this._lastEchoTime > 5000)
629             this.clearMessage();
630         this._lastEchoTime = 0;
631
632         if (!this.commandSession) {
633             this.widgets.command = null;
634             this.hideCompletions();
635         }
636
637         if (modes.main == modes.OUTPUT_MULTILINE && !mow.isScrollable(1))
638             modes.pop();
639
640         if (!modes.have(modes.OUTPUT_MULTILINE))
641             mow.visible = false;
642     },
643
644     clearMessage: function clearMessage() {
645         if (this.widgets.message && this.widgets.message[1] === this._lastClearable)
646             this.widgets.message = null;
647     },
648
649     /**
650      * Displays the multi-line output of a command, preceded by the last
651      * executed ex command string.
652      *
653      * @param {XML} xml The output as an E4X XML object.
654      */
655     commandOutput: function commandOutput(xml) {
656         XML.ignoreWhitespace = XML.prettyPrinting = false;
657         if (this.command)
658             this.echo(<><div xmlns={XHTML}>:{this.command}</div>&#x0d;{xml}</>, this.HIGHLIGHT_NORMAL, this.FORCE_MULTILINE);
659         else
660             this.echo(xml, this.HIGHLIGHT_NORMAL, this.FORCE_MULTILINE);
661         this.command = null;
662     },
663
664     /**
665      * Hides the command line, and shows any status messages that
666      * are under it.
667      */
668     hide: function hide() {
669         this.widgets.command = null;
670     },
671
672     /**
673      * Display a message in the command-line area.
674      *
675      * @param {string} str
676      * @param {string} highlightGroup
677      * @param {boolean} forceSingle If provided, don't let over-long
678      *     messages move to the MOW.
679      */
680     _echoLine: function echoLine(str, highlightGroup, forceSingle, silent) {
681         this.widgets.message = str ? [highlightGroup, str, forceSingle] : null;
682
683         dactyl.triggerObserver("echoLine", str, highlightGroup, null, forceSingle);
684
685         if (!this.commandVisible)
686             this.hide();
687
688         let field = this.widgets.active.message.inputField;
689         if (field.value && !forceSingle && field.editor.rootElement.scrollWidth > field.scrollWidth) {
690             this.widgets.message = null;
691             mow.echo(<span highlight="Message">{str}</span>, highlightGroup, true);
692         }
693     },
694
695     _lastEcho: null,
696
697     /**
698      * Output the given string onto the command line. With no flags, the
699      * message will be shown in the status line if it's short enough to
700      * fit, and contains no new lines, and isn't XML. Otherwise, it will be
701      * shown in the MOW.
702      *
703      * @param {string} str
704      * @param {string} highlightGroup The Highlight group for the
705      *     message.
706      * @default "Normal"
707      * @param {number} flags Changes the behavior as follows:
708      *   commandline.APPEND_TO_MESSAGES - Causes message to be added to the
709      *          messages history, and shown by :messages.
710      *   commandline.FORCE_SINGLELINE   - Forbids the command from being
711      *          pushed to the MOW if it's too long or of there are already
712      *          status messages being shown.
713      *   commandline.DISALLOW_MULTILINE - Cancels the operation if the MOW
714      *          is already visible.
715      *   commandline.FORCE_MULTILINE    - Forces the message to appear in
716      *          the MOW.
717      */
718     messageCount: 0,
719     echo: function echo(data, highlightGroup, flags) {
720         // dactyl.echo uses different order of flags as it omits the highlight group, change commandline.echo argument order? --mst
721         if (this._silent || !this.widgets)
722             return;
723
724         this.messageCount++;
725
726         highlightGroup = highlightGroup || this.HL_NORMAL;
727
728         if (flags & this.APPEND_TO_MESSAGES) {
729             let message = isObject(data) ? data : { message: data };
730
731             // Make sure the memoized message property is an instance property.
732             message.message;
733             this._messageHistory.add(update({ highlight: highlightGroup }, message));
734             data = message.message;
735         }
736
737         if ((flags & this.ACTIVE_WINDOW) &&
738             window != services.windowWatcher.activeWindow &&
739             services.windowWatcher.activeWindow.dactyl)
740             return;
741
742         if ((flags & this.DISALLOW_MULTILINE) && !this.widgets.mowContainer.collapsed)
743             return;
744
745         let single = flags & (this.FORCE_SINGLELINE | this.DISALLOW_MULTILINE);
746         let action = this._echoLine;
747
748         if ((flags & this.FORCE_MULTILINE) || (/\n/.test(data) || !isinstance(data, [_, "String"])) && !(flags & this.FORCE_SINGLELINE))
749             action = mow.closure.echo;
750
751         if (single)
752             this._lastEcho = null;
753         else {
754             if (this.widgets.message && this.widgets.message[1] == this._lastEcho)
755                 mow.echo(<span highlight="Message">{this._lastEcho}</span>,
756                          this.widgets.message[0], true);
757
758             if (action === this._echoLine && !(flags & this.FORCE_MULTILINE)
759                 && !(dactyl.fullyInitialized && this.widgets.mowContainer.collapsed)) {
760                 highlightGroup += " Message";
761                 action = mow.closure.echo;
762             }
763             this._lastEcho = (action == this._echoLine) && data;
764         }
765
766         this._lastClearable = action === this._echoLine && String(data);
767         this._lastEchoTime = (flags & this.FORCE_SINGLELINE) && Date.now();
768
769         if (action)
770             action.call(this, data, highlightGroup, single);
771     },
772     _lastEchoTime: 0,
773
774     /**
775      * Prompt the user. Sets modes.main to COMMAND_LINE, which the user may
776      * pop at any time to close the prompt.
777      *
778      * @param {string} prompt The input prompt to use.
779      * @param {function(string)} callback
780      * @param {Object} extra
781      * @... {function} onChange - A function to be called with the current
782      *     input every time it changes.
783      * @... {function(CompletionContext)} completer - A completion function
784      *     for the user's input.
785      * @... {string} promptHighlight - The HighlightGroup used for the
786      *     prompt. @default "Question"
787      * @... {string} default - The initial value that will be returned
788      *     if the user presses <CR> straightaway. @default ""
789      */
790     input: function _input(prompt, callback, extra) {
791         extra = extra || {};
792
793         CommandPromptMode(prompt, update({ onSubmit: callback }, extra)).open();
794     },
795
796     readHeredoc: function readHeredoc(end) {
797         let args;
798         commandline.inputMultiline(end, function (res) { args = res; });
799         util.waitFor(function () args !== undefined);
800         return args;
801     },
802
803     /**
804      * Get a multi-line input from a user, up to but not including the line
805      * which matches the given regular expression. Then execute the
806      * callback with that string as a parameter.
807      *
808      * @param {string} end
809      * @param {function(string)} callback
810      */
811     // FIXME: Buggy, especially when pasting.
812     inputMultiline: function inputMultiline(end, callback) {
813         let cmd = this.command;
814         let self = {
815             end: "\n" + end + "\n",
816             callback: callback
817         };
818
819         modes.push(modes.INPUT_MULTILINE, null, {
820             holdFocus: true,
821             leave: function leave() {
822                 if (!self.done)
823                     self.callback(null);
824             },
825             mappingSelf: self
826         });
827
828         if (cmd != false)
829             this._echoLine(cmd, this.HL_NORMAL);
830
831         // save the arguments, they are needed in the event handler onKeyPress
832
833         this.multilineInputVisible = true;
834         this.widgets.multilineInput.value = "";
835         this._autosizeMultilineInputWidget();
836
837         this.timeout(function () { dactyl.focus(this.widgets.multilineInput); }, 10);
838     },
839
840     get commandMode() this.commandSession && isinstance(modes.main, modes.COMMAND_LINE),
841
842     events: update(
843         iter(CommandMode.prototype.events).map(
844             function ([event, handler]) [
845                 event, function (event) {
846                     if (this.commandMode)
847                         handler.call(this.commandSession, event);
848                 }
849             ]).toObject(),
850         {
851             focus: function onFocus(event) {
852                 if (!this.commandSession
853                         && event.originalTarget === this.widgets.active.command.inputField) {
854                     event.target.blur();
855                     dactyl.beep();
856                 }
857             }
858         }
859     ),
860
861     get mowEvents() mow.events,
862
863     /**
864      * Multiline input events, they will come straight from
865      * #dactyl-multiline-input in the XUL.
866      *
867      * @param {Event} event
868      */
869     multilineInputEvents: {
870         blur: function onBlur(event) {
871             if (modes.main == modes.INPUT_MULTILINE)
872                 this.timeout(function () {
873                     dactyl.focus(this.widgets.multilineInput.inputField);
874                 });
875         },
876         input: function onInput(event) {
877             this._autosizeMultilineInputWidget();
878         }
879     },
880
881     updateOutputHeight: deprecated("mow.resize", function updateOutputHeight(open, extra) mow.resize(open, extra)),
882
883     withOutputToString: function withOutputToString(fn, self) {
884         dactyl.registerObserver("echoLine", observe, true);
885         dactyl.registerObserver("echoMultiline", observe, true);
886
887         let output = [];
888         function observe(str, highlight, dom) {
889             output.push(dom && !isString(str) ? dom : str);
890         }
891
892         this.savingOutput = true;
893         dactyl.trapErrors.apply(dactyl, [fn, self].concat(Array.slice(arguments, 2)));
894         this.savingOutput = false;
895         return output.map(function (elem) elem instanceof Node ? util.domToString(elem) : elem)
896                      .join("\n");
897     }
898 }, {
899     /**
900      * A class for managing the history of an input field.
901      *
902      * @param {HTMLInputElement} inputField
903      * @param {string} mode The mode for which we need history.
904      */
905     History: Class("History", {
906         init: function init(inputField, mode, session) {
907             this.mode = mode;
908             this.input = inputField;
909             this.reset();
910             this.session = session;
911         },
912         get store() commandline._store.get(this.mode, []),
913         set store(ary) { commandline._store.set(this.mode, ary); },
914         /**
915          * Reset the history index to the first entry.
916          */
917         reset: function reset() {
918             this.index = null;
919         },
920         /**
921          * Save the last entry to the permanent store. All duplicate entries
922          * are removed and the list is truncated, if necessary.
923          */
924         save: function save() {
925             if (events.feedingKeys)
926                 return;
927             let str = this.input.value;
928             if (/^\s*$/.test(str))
929                 return;
930             this.store = this.store.filter(function (line) (line.value || line) != str);
931             try {
932                 this.store.push({ value: str, timestamp: Date.now()*1000, privateData: this.checkPrivate(str) });
933             }
934             catch (e) {
935                 dactyl.reportError(e);
936             }
937             this.store = this.store.slice(-options["history"]);
938         },
939         /**
940          * @property {function} Returns whether a data item should be
941          * considered private.
942          */
943         checkPrivate: function checkPrivate(str) {
944             // Not really the ideal place for this check.
945             if (this.mode == "command")
946                 return commands.hasPrivateData(str);
947             return false;
948         },
949         /**
950          * Replace the current input field value.
951          *
952          * @param {string} val The new value.
953          */
954         replace: function replace(val) {
955             this.input.dactylKeyPress = undefined;
956             if (this.completions)
957                 this.completions.previewClear();
958             this.input.value = val;
959             this.session.onHistory(val);
960         },
961
962         /**
963          * Move forward or backward in history.
964          *
965          * @param {boolean} backward Direction to move.
966          * @param {boolean} matchCurrent Search for matches starting
967          *      with the current input value.
968          */
969         select: function select(backward, matchCurrent) {
970             // always reset the tab completion if we use up/down keys
971             if (this.session.completions)
972                 this.session.completions.reset();
973
974             let diff = backward ? -1 : 1;
975
976             if (this.index == null) {
977                 this.original = this.input.value;
978                 this.index = this.store.length;
979             }
980
981             // search the history for the first item matching the current
982             // command-line string
983             while (true) {
984                 this.index += diff;
985                 if (this.index < 0 || this.index > this.store.length) {
986                     this.index = Math.constrain(this.index, 0, this.store.length);
987                     dactyl.beep();
988                     // I don't know why this kludge is needed. It
989                     // prevents the caret from moving to the end of
990                     // the input field.
991                     if (this.input.value == "") {
992                         this.input.value = " ";
993                         this.input.value = "";
994                     }
995                     break;
996                 }
997
998                 let hist = this.store[this.index];
999                 // user pressed DOWN when there is no newer history item
1000                 if (!hist)
1001                     hist = this.original;
1002                 else
1003                     hist = (hist.value || hist);
1004
1005                 if (!matchCurrent || hist.substr(0, this.original.length) == this.original) {
1006                     this.replace(hist);
1007                     break;
1008                 }
1009             }
1010         }
1011     }),
1012
1013     /**
1014      * A class for tab completions on an input field.
1015      *
1016      * @param {Object} input
1017      */
1018     Completions: Class("Completions", {
1019         init: function init(input, session) {
1020             this.context = CompletionContext(input.QueryInterface(Ci.nsIDOMNSEditableElement).editor);
1021             this.context.onUpdate = this.closure._reset;
1022             this.editor = input.editor;
1023             this.input = input;
1024             this.session = session;
1025             this.selected = null;
1026             this.wildmode = options.get("wildmode");
1027             this.wildtypes = this.wildmode.value;
1028             this.itemList = commandline.completionList;
1029             this.itemList.setItems(this.context);
1030
1031             dactyl.registerObserver("events.doneFeeding", this.closure.onDoneFeeding, true);
1032
1033             this.autocompleteTimer = Timer(200, 500, function autocompleteTell(tabPressed) {
1034                 if (events.feedingKeys && !tabPressed)
1035                     this.ignoredCount++;
1036                 else if (this.session.autocomplete) {
1037                     this.itemList.visible = true;
1038                     this.complete(true, false);
1039                 }
1040             }, this);
1041             this.tabTimer = Timer(0, 0, function tabTell(event) {
1042                 this.tab(event.shiftKey, event.altKey && options["altwildmode"]);
1043             }, this);
1044         },
1045
1046         cleanup: function () {
1047             dactyl.unregisterObserver("events.doneFeeding", this.closure.onDoneFeeding);
1048             this.previewClear();
1049             this.tabTimer.reset();
1050             this.autocompleteTimer.reset();
1051             this.itemList.visible = false;
1052             this.input.dactylKeyPress = undefined;
1053         },
1054
1055         ignoredCount: 0,
1056         onDoneFeeding: function onDoneFeeding() {
1057             if (this.ignoredCount)
1058                 this.autocompleteTimer.flush(true);
1059             this.ignoredCount = 0;
1060         },
1061
1062         UP: {},
1063         DOWN: {},
1064         PAGE_UP: {},
1065         PAGE_DOWN: {},
1066         RESET: null,
1067
1068         lastSubstring: "",
1069
1070         get completion() {
1071             let str = commandline.command;
1072             return str.substring(this.prefix.length, str.length - this.suffix.length);
1073         },
1074         set completion(completion) {
1075             this.previewClear();
1076
1077             // Change the completion text.
1078             // The second line is a hack to deal with some substring
1079             // preview corner cases.
1080             let value = this.prefix + completion + this.suffix;
1081             commandline.widgets.active.command.value = value;
1082             this.editor.selection.focusNode.textContent = value;
1083
1084             // Reset the caret to one position after the completion.
1085             this.caret = this.prefix.length + completion.length;
1086             this._caret = this.caret;
1087
1088             this.input.dactylKeyPress = undefined;
1089         },
1090
1091         get caret() this.editor.selection.getRangeAt(0).startOffset,
1092         set caret(offset) {
1093             this.editor.selection.getRangeAt(0).setStart(this.editor.rootElement.firstChild, offset);
1094             this.editor.selection.getRangeAt(0).setEnd(this.editor.rootElement.firstChild, offset);
1095         },
1096
1097         get start() this.context.allItems.start,
1098
1099         get items() this.context.allItems.items,
1100
1101         get substring() this.context.longestAllSubstring,
1102
1103         get wildtype() this.wildtypes[this.wildIndex] || "",
1104
1105         complete: function complete(show, tabPressed) {
1106             this.session.ignoredCount = 0;
1107             this.context.reset();
1108             this.context.tabPressed = tabPressed;
1109             this.session.complete(this.context);
1110             if (!this.session.active)
1111                 return;
1112             this.context.updateAsync = true;
1113             this.reset(show, tabPressed);
1114             this.wildIndex = 0;
1115             this._caret = this.caret;
1116         },
1117
1118         haveType: function haveType(type)
1119             this.wildmode.checkHas(this.wildtype, type == "first" ? "" : type),
1120
1121         preview: function preview() {
1122             this.previewClear();
1123             if (this.wildIndex < 0 || this.suffix || !this.items.length)
1124                 return;
1125
1126             let substring = "";
1127             switch (this.wildtype.replace(/.*:/, "")) {
1128             case "":
1129                 substring = this.items[0].result;
1130                 break;
1131             case "longest":
1132                 if (this.items.length > 1) {
1133                     substring = this.substring;
1134                     break;
1135                 }
1136                 // Fallthrough
1137             case "full":
1138                 let item = this.items[this.selected != null ? this.selected + 1 : 0];
1139                 if (item)
1140                     substring = item.result;
1141                 break;
1142             }
1143
1144             // Don't show 1-character substrings unless we've just hit backspace
1145             if (substring.length < 2 && this.lastSubstring.indexOf(substring) !== 0)
1146                 return;
1147
1148             this.lastSubstring = substring;
1149
1150             let value = this.completion;
1151             if (util.compareIgnoreCase(value, substring.substr(0, value.length)))
1152                 return;
1153             substring = substring.substr(value.length);
1154             this.removeSubstring = substring;
1155
1156             let node = util.xmlToDom(<span highlight="Preview">{substring}</span>,
1157                 document);
1158             let start = this.caret;
1159             this.editor.insertNode(node, this.editor.rootElement, 1);
1160             this.caret = start;
1161         },
1162
1163         previewClear: function previewClear() {
1164             let node = this.editor.rootElement.firstChild;
1165             if (node && node.nextSibling) {
1166                 try {
1167                     this.editor.deleteNode(node.nextSibling);
1168                 }
1169                 catch (e) {
1170                     node.nextSibling.textContent = "";
1171                 }
1172             }
1173             else if (this.removeSubstring) {
1174                 let str = this.removeSubstring;
1175                 let cmd = commandline.widgets.active.command.value;
1176                 if (cmd.substr(cmd.length - str.length) == str)
1177                     commandline.widgets.active.command.value = cmd.substr(0, cmd.length - str.length);
1178             }
1179             delete this.removeSubstring;
1180         },
1181
1182         reset: function reset(show) {
1183             this.wildIndex = -1;
1184
1185             this.prefix = this.context.value.substring(0, this.start);
1186             this.value  = this.context.value.substring(this.start, this.caret);
1187             this.suffix = this.context.value.substring(this.caret);
1188
1189             if (show) {
1190                 this.itemList.reset();
1191                 if (this.haveType("list"))
1192                     this.itemList.visible = true;
1193                 this.selected = null;
1194                 this.wildIndex = 0;
1195             }
1196
1197             this.preview();
1198         },
1199
1200         _reset: function _reset() {
1201             let value = this.editor.selection.focusNode.textContent;
1202             this.prefix = value.substring(0, this.start);
1203             this.value  = value.substring(this.start, this.caret);
1204             this.suffix = value.substring(this.caret);
1205
1206             this.itemList.reset();
1207             this.itemList.selectItem(this.selected);
1208
1209             this.preview();
1210         },
1211
1212         select: function select(idx) {
1213             switch (idx) {
1214             case this.UP:
1215                 if (this.selected == null)
1216                     idx = -2;
1217                 else
1218                     idx = this.selected - 1;
1219                 break;
1220             case this.DOWN:
1221                 if (this.selected == null)
1222                     idx = 0;
1223                 else
1224                     idx = this.selected + 1;
1225                 break;
1226             case this.RESET:
1227                 idx = null;
1228                 break;
1229             default:
1230                 idx = Math.constrain(idx, 0, this.items.length - 1);
1231                 break;
1232             }
1233
1234             if (idx == -1 || this.items.length && idx >= this.items.length || idx == null) {
1235                 // Wrapped. Start again.
1236                 this.selected = null;
1237                 this.completion = this.value;
1238             }
1239             else {
1240                 // Wait for contexts to complete if necessary.
1241                 // FIXME: Need to make idx relative to individual contexts.
1242                 let list = this.context.contextList;
1243                 if (idx == -2)
1244                     list = list.slice().reverse();
1245                 let n = 0;
1246                 try {
1247                     this.waiting = true;
1248                     for (let [, context] in Iterator(list)) {
1249                         let done = function done() !(idx >= n + context.items.length || idx == -2 && !context.items.length);
1250
1251                         util.waitFor(function () !context.incomplete || done());
1252                         if (done())
1253                             break;
1254
1255                         n += context.items.length;
1256                     }
1257                 }
1258                 finally {
1259                     this.waiting = false;
1260                 }
1261
1262                 // See previous FIXME. This will break if new items in
1263                 // a previous context come in.
1264                 if (idx < 0)
1265                     idx = this.items.length - 1;
1266                 if (this.items.length == 0)
1267                     return;
1268
1269                 this.selected = idx;
1270                 this.completion = this.items[idx].result;
1271             }
1272
1273             this.itemList.selectItem(idx);
1274         },
1275
1276         tabs: [],
1277
1278         tab: function tab(reverse, wildmode) {
1279             this.autocompleteTimer.flush();
1280             this.ignoredCount = 0;
1281
1282             if (this._caret != this.caret)
1283                 this.reset();
1284             this._caret = this.caret;
1285
1286             // Check if we need to run the completer.
1287             if (this.context.waitingForTab || this.wildIndex == -1)
1288                 this.complete(true, true);
1289
1290             this.tabs.push([reverse, wildmode || options["wildmode"]]);
1291             if (this.waiting)
1292                 return;
1293
1294             while (this.tabs.length) {
1295                 [reverse, this.wildtypes] = this.tabs.shift();
1296
1297                 this.wildIndex = Math.min(this.wildIndex, this.wildtypes.length - 1);
1298                 switch (this.wildtype.replace(/.*:/, "")) {
1299                 case "":
1300                     this.select(0);
1301                     break;
1302                 case "longest":
1303                     if (this.items.length > 1) {
1304                         if (this.substring && this.substring.length > this.completion.length)
1305                             this.completion = this.substring;
1306                         break;
1307                     }
1308                     // Fallthrough
1309                 case "full":
1310                     this.select(reverse ? this.UP : this.DOWN);
1311                     break;
1312                 }
1313
1314                 if (this.haveType("list"))
1315                     this.itemList.visible = true;
1316
1317                 this.wildIndex++;
1318                 this.preview();
1319
1320                 if (this.selected == null)
1321                     statusline.progress = "";
1322                 else
1323                     statusline.progress = _("completion.matchIndex", this.selected + 1, this.items.length);
1324             }
1325
1326             if (this.items.length == 0)
1327                 dactyl.beep();
1328         }
1329     }),
1330
1331     /**
1332      * Evaluate a JavaScript expression and return a string suitable
1333      * to be echoed.
1334      *
1335      * @param {string} arg
1336      * @param {boolean} useColor When true, the result is a
1337      *     highlighted XML object.
1338      */
1339     echoArgumentToString: function (arg, useColor) {
1340         if (!arg)
1341             return "";
1342
1343         arg = dactyl.userEval(arg);
1344         if (isObject(arg))
1345             arg = util.objectToString(arg, useColor);
1346         else
1347             arg = String(arg);
1348         return arg;
1349     }
1350 }, {
1351     commands: function init_commands() {
1352         [
1353             {
1354                 name: "ec[ho]",
1355                 description: "Echo the expression",
1356                 action: dactyl.echo
1357             },
1358             {
1359                 name: "echoe[rr]",
1360                 description: "Echo the expression as an error message",
1361                 action: dactyl.echoerr
1362             },
1363             {
1364                 name: "echom[sg]",
1365                 description: "Echo the expression as an informational message",
1366                 action: dactyl.echomsg
1367             }
1368         ].forEach(function (command) {
1369             commands.add([command.name],
1370                 command.description,
1371                 function (args) {
1372                     command.action(CommandLine.echoArgumentToString(args[0] || "", true));
1373                 }, {
1374                     completer: function (context) completion.javascript(context),
1375                     literal: 0
1376                 });
1377         });
1378
1379         commands.add(["mes[sages]"],
1380             "Display previously shown messages",
1381             function () {
1382                 // TODO: are all messages single line? Some display an aggregation
1383                 //       of single line messages at least. E.g. :source
1384                 if (commandline._messageHistory.length == 1) {
1385                     let message = commandline._messageHistory.messages[0];
1386                     commandline.echo(message.message, message.highlight, commandline.FORCE_SINGLELINE);
1387                 }
1388                 else if (commandline._messageHistory.length > 1) {
1389                     XML.ignoreWhitespace = false;
1390                     commandline.commandOutput(
1391                         template.map(commandline._messageHistory.messages, function (message)
1392                             <div highlight={message.highlight + " Message"}>{message.message}</div>));
1393                 }
1394             },
1395             { argCount: "0" });
1396
1397         commands.add(["messc[lear]"],
1398             "Clear the message history",
1399             function () { commandline._messageHistory.clear(); },
1400             { argCount: "0" });
1401
1402         commands.add(["sil[ent]"],
1403             "Run a command silently",
1404             function (args) {
1405                 commandline.runSilently(function () commands.execute(args[0] || "", null, true));
1406             }, {
1407                 completer: function (context) completion.ex(context),
1408                 literal: 0,
1409                 subCommand: 0
1410             });
1411     },
1412     modes: function initModes() {
1413         modes.addMode("COMMAND_LINE", {
1414             char: "c",
1415             description: "Active when the command line is focused",
1416             insert: true,
1417             ownsFocus: true,
1418             get mappingSelf() commandline.commandSession
1419         });
1420         // this._extended modes, can include multiple modes, and even main modes
1421         modes.addMode("EX", {
1422             description: "Ex command mode, active when the command line is open for Ex commands",
1423             bases: [modes.COMMAND_LINE]
1424         });
1425         modes.addMode("PROMPT", {
1426             description: "Active when a prompt is open in the command line",
1427             bases: [modes.COMMAND_LINE]
1428         });
1429
1430         modes.addMode("INPUT_MULTILINE", {
1431             bases: [modes.INSERT]
1432         });
1433     },
1434     mappings: function init_mappings() {
1435
1436         mappings.add([modes.COMMAND],
1437             [":"], "Enter Command Line mode",
1438             function () { CommandExMode().open(""); });
1439
1440         mappings.add([modes.INPUT_MULTILINE],
1441             ["<Return>", "<C-j>", "<C-m>"], "Begin a new line",
1442             function ({ self }) {
1443                 let text = "\n" + commandline.widgets.multilineInput
1444                                              .value.substr(0, commandline.widgets.multilineInput.selectionStart)
1445                          + "\n";
1446
1447                 let index = text.indexOf(self.end);
1448                 if (index >= 0) {
1449                     self.done = true;
1450                     text = text.substring(1, index);
1451                     modes.pop();
1452
1453                     return function () self.callback.call(commandline, text);
1454                 }
1455                 return Events.PASS;
1456             });
1457
1458         let bind = function bind()
1459             mappings.add.apply(mappings, [[modes.COMMAND_LINE]].concat(Array.slice(arguments)))
1460
1461         // Any "non-keyword" character triggers abbreviation expansion
1462         // TODO: Add "<CR>" and "<Tab>" to this list
1463         //       At the moment, adding "<Tab>" breaks tab completion. Adding
1464         //       "<CR>" has no effect.
1465         // TODO: Make non-keyword recognition smarter so that there need not
1466         //       be two lists of the same characters (one here and a regexp in
1467         //       mappings.js)
1468         bind(["<Space>", '"', "'"], "Expand command line abbreviation",
1469              function ({ self }) {
1470                  self.resetCompletions();
1471                  editor.expandAbbreviation(modes.COMMAND_LINE);
1472                  return Events.PASS;
1473              });
1474
1475         bind(["<Return>", "<C-j>", "<C-m>"], "Accept the current input",
1476              function ({ self }) {
1477                  let command = commandline.command;
1478
1479                  self.accepted = true;
1480                  return function () { modes.pop(); };
1481              });
1482
1483         [
1484             [["<Up>", "<A-p>"],                   "previous matching", true,  true],
1485             [["<S-Up>", "<C-p>", "<PageUp>"],     "previous",          true,  false],
1486             [["<Down>", "<A-n>"],                 "next matching",     false, true],
1487             [["<S-Down>", "<C-n>", "<PageDown>"], "next",              false, false]
1488         ].forEach(function ([keys, desc, up, search]) {
1489             bind(keys, "Recall the " + desc + " command line from the history list",
1490                  function ({ self }) {
1491                      dactyl.assert(self.history);
1492                      self.history.select(up, search);
1493                  });
1494         });
1495
1496         bind(["<A-Tab>", "<Tab>"], "Select the next matching completion item",
1497              function ({ keypressEvents, self }) {
1498                  dactyl.assert(self.completions);
1499                  self.completions.tabTimer.tell(keypressEvents[0]);
1500              });
1501
1502         bind(["<A-S-Tab>", "<S-Tab>"], "Select the previous matching completion item",
1503              function ({ keypressEvents, self }) {
1504                  dactyl.assert(self.completions);
1505                  self.completions.tabTimer.tell(keypressEvents[0]);
1506              });
1507
1508         bind(["<BS>", "<C-h>"], "Delete the previous character",
1509              function () {
1510                  if (!commandline.command)
1511                      modes.pop();
1512                  else
1513                      return Events.PASS;
1514              });
1515
1516         bind(["<C-]>", "<C-5>"], "Expand command line abbreviation",
1517              function () { editor.expandAbbreviation(modes.COMMAND_LINE); });
1518     },
1519     options: function init_options() {
1520         options.add(["history", "hi"],
1521             "Number of Ex commands and search patterns to store in the command-line history",
1522             "number", 500,
1523             { validator: function (value) value >= 0 });
1524
1525         options.add(["maxitems"],
1526             "Maximum number of completion items to display at once",
1527             "number", 20,
1528             { validator: function (value) value >= 1 });
1529
1530         options.add(["messages", "msgs"],
1531             "Number of messages to store in the :messages history",
1532             "number", 100,
1533             { validator: function (value) value >= 0 });
1534     },
1535     sanitizer: function init_sanitizer() {
1536         sanitizer.addItem("commandline", {
1537             description: "Command-line and search history",
1538             persistent: true,
1539             action: function (timespan, host) {
1540                 let store = commandline._store;
1541                 for (let [k, v] in store) {
1542                     if (k == "command")
1543                         store.set(k, v.filter(function (item)
1544                             !(timespan.contains(item.timestamp) && (!host || commands.hasDomain(item.value, host)))));
1545                     else if (!host)
1546                         store.set(k, v.filter(function (item) !timespan.contains(item.timestamp)));
1547                 }
1548             }
1549         });
1550         // Delete history-like items from the commandline and messages on history purge
1551         sanitizer.addItem("history", {
1552             action: function (timespan, host) {
1553                 commandline._store.set("command",
1554                     commandline._store.get("command", []).filter(function (item)
1555                         !(timespan.contains(item.timestamp) && (host ? commands.hasDomain(item.value, host)
1556                                                                      : item.privateData))));
1557
1558                 commandline._messageHistory.filter(function (item) !timespan.contains(item.timestamp * 1000) ||
1559                     !item.domains && !item.privateData ||
1560                     host && (!item.domains || !item.domains.some(function (d) util.isSubdomain(d, host))));
1561             }
1562         });
1563         sanitizer.addItem("messages", {
1564             description: "Saved :messages",
1565             action: function (timespan, host) {
1566                 commandline._messageHistory.filter(function (item) !timespan.contains(item.timestamp * 1000) ||
1567                     host && (!item.domains || !item.domains.some(function (d) util.isSubdomain(d, host))));
1568             }
1569         });
1570     }
1571 });
1572
1573 /**
1574  * The list which is used for the completion box (and QuickFix window in
1575  * future).
1576  *
1577  * @param {string} id The id of the iframe which will display the list. It
1578  *     must be in its own container element, whose height it will update as
1579  *     necessary.
1580  */
1581 var ItemList = Class("ItemList", {
1582     init: function init(id) {
1583         this._completionElements = [];
1584
1585         var iframe = document.getElementById(id);
1586
1587         this._doc = iframe.contentDocument;
1588         this._win = iframe.contentWindow;
1589         this._container = iframe.parentNode;
1590
1591         this._doc.documentElement.id = id + "-top";
1592         this._doc.body.id = id + "-content";
1593         this._doc.body.className = iframe.className + "-content";
1594         this._doc.body.appendChild(this._doc.createTextNode(""));
1595         this._doc.body.style.borderTop = "1px solid black"; // FIXME: For cases where completions/MOW are shown at once, or ls=0. Should use :highlight.
1596
1597         this._items = null;
1598         this._startIndex = -1; // The index of the first displayed item
1599         this._endIndex = -1;   // The index one *after* the last displayed item
1600         this._selIndex = -1;   // The index of the currently selected element
1601         this._div = null;
1602         this._divNodes = {};
1603         this._minHeight = 0;
1604     },
1605
1606     _dom: function _dom(xml, map) util.xmlToDom(xml instanceof XML ? xml : <>{xml}</>, this._doc, map),
1607
1608     _autoSize: function _autoSize() {
1609         if (!this._div)
1610             return;
1611
1612         if (this._container.collapsed)
1613             this._div.style.minWidth = document.getElementById("dactyl-commandline").scrollWidth + "px";
1614
1615         this._minHeight = Math.max(this._minHeight,
1616             this._win.scrollY + this._divNodes.completions.getBoundingClientRect().bottom);
1617
1618         if (this._container.collapsed)
1619             this._div.style.minWidth = "";
1620
1621         // FIXME: Belongs elsewhere.
1622         mow.resize(false, Math.max(0, this._minHeight - this._container.height));
1623
1624         this._container.height = this._minHeight;
1625         this._container.height -= mow.spaceNeeded;
1626         mow.resize(false);
1627         this.timeout(function () {
1628             this._container.height -= mow.spaceNeeded;
1629         });
1630     },
1631
1632     _getCompletion: function _getCompletion(index) this._completionElements.snapshotItem(index - this._startIndex),
1633
1634     _init: function _init() {
1635         this._div = this._dom(
1636             <div class="ex-command-output" highlight="Normal" style="white-space: nowrap">
1637                 <div highlight="Completions" key="noCompletions"><span highlight="Title">{_("completion.noCompletions")}</span></div>
1638                 <div key="completions"/>
1639                 <div highlight="Completions">
1640                 {
1641                     template.map(util.range(0, options["maxitems"] * 2), function (i)
1642                     <div highlight="CompItem NonText">
1643                         <li>~</li>
1644                     </div>)
1645                 }
1646                 </div>
1647             </div>, this._divNodes);
1648         this._doc.body.replaceChild(this._div, this._doc.body.firstChild);
1649         util.scrollIntoView(this._div, true);
1650
1651         this._items.contextList.forEach(function init_eachContext(context) {
1652             delete context.cache.nodes;
1653             if (!context.items.length && !context.message && !context.incomplete)
1654                 return;
1655             context.cache.nodes = [];
1656             this._dom(<div key="root" highlight="CompGroup">
1657                     <div highlight="Completions">
1658                         { context.createRow(context.title || [], "CompTitle") }
1659                     </div>
1660                     <div highlight="CompTitleSep"/>
1661                     <div key="message" highlight="CompMsg"/>
1662                     <div key="up" highlight="CompLess"/>
1663                     <div key="items" highlight="Completions"/>
1664                     <div key="waiting" highlight="CompMsg">{ItemList.WAITING_MESSAGE}</div>
1665                     <div key="down" highlight="CompMore"/>
1666                 </div>, context.cache.nodes);
1667             this._divNodes.completions.appendChild(context.cache.nodes.root);
1668         }, this);
1669
1670         this.timeout(this._autoSize);
1671     },
1672
1673     /**
1674      * Uses the entries in "items" to fill the listbox and does incremental
1675      * filling to speed up things.
1676      *
1677      * @param {number} offset Start at this index and show options["maxitems"].
1678      */
1679     _fill: function _fill(offset) {
1680         XML.ignoreWhiteSpace = false;
1681         let diff = offset - this._startIndex;
1682         if (this._items == null || offset == null || diff == 0 || offset < 0)
1683             return false;
1684
1685         this._startIndex = offset;
1686         this._endIndex = Math.min(this._startIndex + options["maxitems"], this._items.allItems.items.length);
1687
1688         let haveCompletions = false;
1689         let off = 0;
1690         let end = this._startIndex + options["maxitems"];
1691         function getRows(context) {
1692             function fix(n) Math.constrain(n, 0, len);
1693             let len = context.items.length;
1694             let start = off;
1695             end -= !!context.message + context.incomplete;
1696             off += len;
1697
1698             let s = fix(offset - start), e = fix(end - start);
1699             return [s, e, context.incomplete && e >= offset && off - 1 < end];
1700         }
1701
1702         this._items.contextList.forEach(function fill_eachContext(context) {
1703             let nodes = context.cache.nodes;
1704             if (!nodes)
1705                 return;
1706             haveCompletions = true;
1707
1708             let root = nodes.root;
1709             let items = nodes.items;
1710             let [start, end, waiting] = getRows(context);
1711
1712             if (context.message) {
1713                 nodes.message.textContent = "";
1714                 nodes.message.appendChild(this._dom(context.message));
1715             }
1716             nodes.message.style.display = context.message ? "block" : "none";
1717             nodes.waiting.style.display = waiting ? "block" : "none";
1718             nodes.up.style.opacity = "0";
1719             nodes.down.style.display = "none";
1720
1721             for (let [i, row] in Iterator(context.getRows(start, end, this._doc)))
1722                 nodes[i] = row;
1723             for (let [i, row] in array.iterItems(nodes)) {
1724                 if (!row)
1725                     continue;
1726                 let display = (i >= start && i < end);
1727                 if (display && row.parentNode != items) {
1728                     do {
1729                         var next = nodes[++i];
1730                         if (next && next.parentNode != items)
1731                             next = null;
1732                     }
1733                     while (!next && i < end)
1734                     items.insertBefore(row, next);
1735                 }
1736                 else if (!display && row.parentNode == items)
1737                     items.removeChild(row);
1738             }
1739             if (context.items.length == 0)
1740                 return;
1741             nodes.up.style.opacity = (start == 0) ? "0" : "1";
1742             if (end != context.items.length)
1743                 nodes.down.style.display = "block";
1744             else
1745                 nodes.up.style.display = "block";
1746             if (start == end) {
1747                 nodes.up.style.display = "none";
1748                 nodes.down.style.display = "none";
1749             }
1750         }, this);
1751
1752         this._divNodes.noCompletions.style.display = haveCompletions ? "none" : "block";
1753
1754         this._completionElements = util.evaluateXPath("//xhtml:div[@dactyl:highlight='CompItem']", this._doc);
1755
1756         return true;
1757     },
1758
1759     clear: function clear() { this.setItems(); this._doc.body.innerHTML = ""; },
1760     get visible() !this._container.collapsed,
1761     set visible(val) this._container.collapsed = !val,
1762
1763     reset: function reset(brief) {
1764         this._startIndex = this._endIndex = this._selIndex = -1;
1765         this._div = null;
1766         if (!brief)
1767             this.selectItem(-1);
1768     },
1769
1770     // if @param selectedItem is given, show the list and select that item
1771     setItems: function setItems(newItems, selectedItem) {
1772         if (this._selItem > -1)
1773             this._getCompletion(this._selItem).removeAttribute("selected");
1774         if (this._container.collapsed) {
1775             this._minHeight = 0;
1776             this._container.height = 0;
1777         }
1778         this._startIndex = this._endIndex = this._selIndex = -1;
1779         this._items = newItems;
1780         this.reset(true);
1781         if (typeof selectedItem == "number") {
1782             this.selectItem(selectedItem);
1783             this.visible = true;
1784         }
1785     },
1786
1787     // select index, refill list if necessary
1788     selectItem: function selectItem(index) {
1789         //let now = Date.now();
1790
1791         if (this._div == null)
1792             this._init();
1793
1794         let sel = this._selIndex;
1795         let len = this._items.allItems.items.length;
1796         let newOffset = this._startIndex;
1797         let maxItems = options["maxitems"];
1798         let contextLines = Math.min(3, parseInt((maxItems - 1) / 2));
1799
1800         if (index == -1 || index == null || index == len) { // wrapped around
1801             if (this._selIndex < 0)
1802                 newOffset = 0;
1803             this._selIndex = -1;
1804             index = -1;
1805         }
1806         else {
1807             if (index <= this._startIndex + contextLines)
1808                 newOffset = index - contextLines;
1809             if (index >= this._endIndex - contextLines)
1810                 newOffset = index + contextLines - maxItems + 1;
1811
1812             newOffset = Math.min(newOffset, len - maxItems);
1813             newOffset = Math.max(newOffset, 0);
1814
1815             this._selIndex = index;
1816         }
1817
1818         if (sel > -1)
1819             this._getCompletion(sel).removeAttribute("selected");
1820         this._fill(newOffset);
1821         if (index >= 0) {
1822             this._getCompletion(index).setAttribute("selected", "true");
1823             if (this._container.height != 0)
1824                 util.scrollIntoView(this._getCompletion(index));
1825         }
1826
1827         //if (index == 0)
1828         //    this.start = now;
1829         //if (index == Math.min(len - 1, 100))
1830         //    util.dump({ time: Date.now() - this.start });
1831     },
1832
1833     onKeyPress: function onKeyPress(event) false
1834 }, {
1835     WAITING_MESSAGE: _("completion.generating")
1836 });
1837
1838 // vim: set fdm=marker sw=4 ts=4 et: