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