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>
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
11 var CommandWidgets = Class("CommandWidgets", {
12 depends: ["statusline"],
14 init: function init() {
15 let s = "dactyl-statusline-field-";
17 XML.ignoreWhitespace = true;
18 overlay.overlayWindow(window, {
20 eventTarget: commandline
22 append: <e4x xmlns={XUL} xmlns:dactyl={NS}>
23 <vbox id={config.ids.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" />
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"/>
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"
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" />
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"
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" />
78 getGroup: function () options.get("guioptions").has("C") ? this.commandbar : this.statusbar,
79 getValue: function () this.command
84 defaultGroup: "Normal",
85 getGroup: function () this.commandbar,
86 getValue: function () options.get("guioptions").has("c")
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.
97 return elem.inputField.editor.rootElement.firstChild.textContent;
103 getElement: CommandWidgets.getEditor,
104 getGroup: function (value) this.activeGroup.commandline,
105 onChange: function command_onChange(elem, value) {
106 if (elem.inputField != dactyl.focusedElement)
108 elem.selectionStart = elem.value.length;
109 elem.selectionEnd = elem.value.length;
116 onVisibility: function command_onVisibility(elem, visible) {
124 id: "commandline-prompt",
125 defaultGroup: "CmdPrompt",
126 getGroup: function () this.activeGroup.commandline
131 defaultGroup: "Normal",
132 getElement: CommandWidgets.getEditor,
133 getGroup: function (value) {
134 if (this.command && !options.get("guioptions").has("M"))
135 return this.statusbar;
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;
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;
155 this.updateVisibility();
157 this.initialized = true;
159 addElement: function addElement(obj) {
161 this.elements[obj.name] = obj;
163 function get(prefix, map, id) (obj.getElement || util.identity)(map[id] || document.getElementById(prefix + id));
165 this.active.__defineGetter__(obj.name, function () self.activeGroup[obj.name][obj.name]);
166 this.activeGroup.__defineGetter__(obj.name, function () self.getGroup(obj.name));
168 memoize(this.statusbar, obj.name, function () get("dactyl-statusline-field-", statusline.widgets, (obj.id || obj.name)));
169 memoize(this.commandbar, obj.name, function () get("dactyl-", {}, (obj.id || obj.name)));
171 if (!(obj.noValue || obj.getValue)) {
172 Object.defineProperty(this, obj.name, Modes.boundProperty({
175 get: function get_widgetValue() {
176 let elem = self.getGroup(obj.name, obj.value)[obj.name];
177 if (obj.value != null)
178 return [obj.value[0],
179 obj.get ? obj.get.call(this, elem) : elem.value]
180 .concat(obj.value.slice(2));
184 set: function set_widgetValue(val) {
185 if (val != null && !isArray(val))
186 val = [obj.defaultGroup || "", val];
189 [this.commandbar, this.statusbar].forEach(function (nodeSet) {
190 let elem = nodeSet[obj.name];
194 highlight.highlightNode(elem,
195 (val[0] != null ? val[0] : obj.defaultGroup)
196 .split(/\s/).filter(util.identity)
197 .map(function (g) g + " " + nodeSet.group + g)
201 obj.onChange.call(this, elem, val);
205 this.updateVisibility();
210 else if (obj.defaultGroup) {
211 [this.commandbar, this.statusbar].forEach(function (nodeSet) {
212 let elem = nodeSet[obj.name];
214 highlight.highlightNode(elem, obj.defaultGroup.split(/\s/)
215 .map(function (g) g + " " + nodeSet.group + g).join(" "));
220 getGroup: function getgroup(name, value) {
221 if (!statusline.visible)
222 return this.commandbar;
223 return this.elements[name].getGroup.call(this, arguments.length > 1 ? value : this[name]);
226 updateVisibility: function updateVisibility() {
227 for (let elem in values(this.elements))
229 let value = elem.getValue ? elem.getValue.call(this)
230 : elem.noValue || this[elem.name];
232 let activeGroup = this.getGroup(elem.name, value);
233 for (let group in values([this.commandbar, this.statusbar])) {
234 let meth, node = group[elem.name];
235 let visible = (value && group === activeGroup);
236 if (node && !node.collapsed == !visible) {
237 node.collapsed = !visible;
238 if (elem.onVisibility)
239 elem.onVisibility.call(this, node, visible);
244 // Hack. Collapse hidden elements in the stack.
245 // Might possibly be better to use a deck and programmatically
246 // choose which element to select.
247 function check(node) {
248 if (DOM(node).style.display === "-moz-stack") {
249 let nodes = Array.filter(node.children, function (n) !n.collapsed && n.boxObject.height);
250 nodes.forEach(function (node, i) node.style.opacity = (i == nodes.length - 1) ? "" : "0");
252 Array.forEach(node.children, check);
254 [this.commandbar.container, this.statusbar.container].forEach(check);
256 if (this.initialized && loaded.mow && mow.visible)
260 active: Class.Memoize(Object),
261 activeGroup: Class.Memoize(Object),
262 commandbar: Class.Memoize(function () ({ group: "Cmd" })),
263 statusbar: Class.Memoize(function () ({ group: "Status" })),
265 _ready: function _ready(elem) {
266 return elem.contentDocument.documentURI === elem.getAttribute("src") &&
267 ["viewable", "complete"].indexOf(elem.contentDocument.readyState) >= 0;
270 _whenReady: function _whenReady(id, init) {
271 let elem = document.getElementById(id);
272 while (!this._ready(elem))
276 init.call(this, elem);
280 completionContainer: Class.Memoize(function () this.completionList.parentNode),
282 contextMenu: Class.Memoize(function () {
283 ["copy", "copylink", "selectall"].forEach(function (tail) {
284 // some host apps use "hostPrefixContext-copy" ids
285 let css = "menuitem[id$='ontext-" + tail + "']:not([id^=dactyl-])";
286 let style = DOM(css, document).style;
287 DOM("#dactyl-context-" + tail, document).css({
288 listStyleImage: style.listStyleImage,
289 MozImageRegion: style.MozImageRegion
292 return document.getElementById("dactyl-contextmenu");
295 multilineOutput: Class.Memoize(function () this._whenReady("dactyl-multiline-output", function (elem) {
296 highlight.highlightNode(elem.contentDocument.body, "MOW");
299 multilineInput: Class.Memoize(function () document.getElementById("dactyl-multiline-input")),
301 mowContainer: Class.Memoize(function () document.getElementById("dactyl-multiline-output-container"))
303 getEditor: function getEditor(elem) {
304 elem.inputField.QueryInterface(Ci.nsIDOMNSEditableElement);
309 var CommandMode = Class("CommandMode", {
310 init: function CM_init() {
311 this.keepCommand = userContext.hidden_option_command_afterimage;
314 get autocomplete() options["autocomplete"].length,
316 get command() this.widgets.command[1],
317 set command(val) this.widgets.command = val,
319 get prompt() this._open ? this.widgets.prompt : this._prompt,
322 this.widgets.prompt = val;
327 open: function CM_open(command) {
328 dactyl.assert(isinstance(this.mode, modes.COMMAND_LINE),
329 /*L*/"Not opening command line in non-command-line mode.",
332 this.messageCount = commandline.messageCount;
333 modes.push(this.mode, this.extendedMode, this.closure);
335 this.widgets.active.commandline.collapsed = false;
336 this.widgets.prompt = this.prompt;
337 this.widgets.command = command || "";
341 this.input = this.widgets.active.command.inputField;
343 this.history = CommandLine.History(this.input, this.historyKey, this);
346 this.completions = CommandLine.Completions(this.input, this);
348 if (this.completions && command && commandline.commandSession === this)
349 this.completions.autocompleteTimer.flush(true);
352 get active() this === commandline.commandSession,
354 get holdFocus() this.widgets.active.command.inputField,
356 get mappingSelf() this,
358 get widgets() commandline.widgets,
360 enter: function CM_enter(stack) {
361 commandline.commandSession = this;
362 if (stack.pop && commandline.command) {
363 this.onChange(commandline.command);
364 if (this.completions && stack.pop)
365 this.completions.complete(true, false);
369 leave: function CM_leave(stack) {
371 commandline.commandSession = null;
372 this.input.dactylKeyPress = undefined;
374 let waiting = this.accepted && this.completions && this.completions.waiting;
376 this.completions.onComplete = bind("onSubmit", this);
378 if (this.completions)
379 this.completions.cleanup();
384 commandline.hideCompletions();
386 modes.delay(function () {
387 if (!this.keepCommand || commandline.silent || commandline.quiet)
390 this[this.accepted ? "onSubmit" : "onCancel"](commandline.command);
391 if (commandline.messageCount === this.messageCount)
392 commandline.clearMessage();
398 input: function CM_onInput(event) {
399 if (this.completions) {
400 this.resetCompletions();
402 this.completions.autocompleteTimer.tell(false);
404 this.onChange(commandline.command);
406 keyup: function CM_onKeyUp(event) {
407 let key = DOM.Event.stringify(event);
408 if (/-?Tab>$/.test(key) && this.completions)
409 this.completions.tabTimer.flush();
415 onKeyPress: function CM_onKeyPress(events) {
416 if (this.completions)
417 this.completions.previewClear();
419 return true; /* Pass event */
422 onCancel: function (value) {},
424 onChange: function (value) {},
426 onHistory: function (value) {},
428 onSubmit: function (value) {},
430 resetCompletions: function CM_resetCompletions() {
431 if (this.completions)
432 this.completions.clear();
434 this.history.reset();
438 var CommandExMode = Class("CommandExMode", CommandMode, {
442 historyKey: "command",
444 prompt: ["Normal", ":"],
446 complete: function CEM_complete(context) {
448 context.fork("ex", 0, completion, "ex");
451 context.message = _("error.error", e);
455 onSubmit: function CEM_onSubmit(command) {
456 contexts.withContext({ file: /*L*/"[Command Line]", line: 1 },
457 function _onSubmit() {
458 io.withSavedValues(["readHeredoc"], function _onSubmit() {
459 this.readHeredoc = commandline.readHeredoc;
460 commands.repeat = command;
461 dactyl.execute(command);
467 var CommandPromptMode = Class("CommandPromptMode", CommandMode, {
468 init: function init(prompt, params) {
469 this.prompt = isArray(prompt) ? prompt : ["Question", prompt];
470 update(this, params);
471 init.supercall(this);
474 complete: function CPM_complete(context) {
476 context.forkapply("prompt", 0, this, "completer", Array.slice(arguments, 1));
479 get mode() modes.PROMPT
483 * This class is used for prompting of user input and echoing of messages.
485 * It consists of a prompt and command field be sure to only create objects of
486 * this class when the chrome is ready.
488 var CommandLine = Module("commandline", {
489 init: function init() {
492 this._callbacks = {};
494 memoize(this, "_store", function () storage.newMap("command-history", { store: true, privateData: true }));
496 for (let name in values(["command", "search"]))
497 if (storage.exists("history-" + name)) {
498 let ary = storage.newArray("history-" + name, { store: true, privateData: true });
500 this._store.set(name, [v for ([k, v] in ary)]);
502 this._store.changed();
505 this._messageHistory = { //{{{
508 let max = options["messages"];
510 // resize if 'messages' has changed
511 if (this._messages.length > max)
512 this._messages = this._messages.splice(this._messages.length - max);
514 return this._messages;
517 get length() this._messages.length,
519 clear: function clear() {
523 filter: function filter(fn, self) {
524 this._messages = this._messages.filter(fn, self);
527 add: function add(message) {
531 if (this._messages.length >= options["messages"])
532 this._messages.shift();
534 this._messages.push(update({
535 timestamp: Date.now()
542 "browser.locationChange": function (webProgress, request, uri) {
548 * Determines whether the command line should be visible.
552 get commandVisible() !!this.commandSession,
555 * Ensure that the multiline input widget is the correct size.
557 _autosizeMultilineInputWidget: function _autosizeMultilineInputWidget() {
558 let lines = this.widgets.multilineInput.value.split("\n").length - 1;
560 this.widgets.multilineInput.setAttribute("rows", Math.max(lines, 1));
564 HL_ERRORMSG: "ErrorMsg",
565 HL_MODEMSG: "ModeMsg",
566 HL_MOREMSG: "MoreMsg",
567 HL_QUESTION: "Question",
568 HL_INFOMSG: "InfoMsg",
569 HL_WARNINGMSG: "WarningMsg",
572 FORCE_MULTILINE : 1 << 0,
573 FORCE_SINGLELINE : 1 << 1,
574 DISALLOW_MULTILINE : 1 << 2, // If an echo() should try to use the single line
575 // but output nothing when the MOW is open; when also
576 // FORCE_MULTILINE is given, FORCE_MULTILINE takes precedence
577 APPEND_TO_MESSAGES : 1 << 3, // Add the string to the message history.
578 ACTIVE_WINDOW : 1 << 4, // Only echo in active window.
580 get completionContext() this._completions.context,
583 get silent() this._silent,
586 this.quiet = this.quiet;
590 get quiet() this._quiet,
593 ["commandbar", "statusbar"].forEach(function (nodeSet) {
594 Array.forEach(this.widgets[nodeSet].commandline.children, function (node) {
595 node.style.opacity = this._quiet || this._silent ? "0" : "";
600 widgets: Class.Memoize(function () CommandWidgets()),
602 runSilently: function runSilently(func, self) {
603 this.withSavedValues(["silent"], function () {
609 get completionList() {
610 let node = this.widgets.active.commandline;
611 if (this.commandSession && this.commandSession.completionList)
612 node = document.getElementById(this.commandSession.completionList);
614 if (!node.completionList) {
615 let elem = document.getElementById("dactyl-completions-" + node.id);
616 util.waitFor(bind(this.widgets._ready, null, elem));
618 node.completionList = ItemList(elem);
619 node.completionList.isAboveMow = node.id ==
620 this.widgets.statusbar.commandline.id
622 return node.completionList;
625 hideCompletions: function hideCompletions() {
626 for (let nodeSet in values([this.widgets.statusbar, this.widgets.commandbar]))
627 if (nodeSet.commandline.completionList)
628 nodeSet.commandline.completionList.visible = false;
631 _lastClearable: Modes.boundProperty(),
632 messages: Modes.boundProperty(),
634 multilineInputVisible: Modes.boundProperty({
635 set: function set_miwVisible(value) { this.widgets.multilineInput.collapsed = !value; }
639 if (this.commandVisible && this.widgets.command)
640 return commands.lastCommand = this.widgets.command[1];
641 return commands.lastCommand;
644 if (this.commandVisible && (modes.extended & modes.EX))
645 return this.widgets.command = val;
646 return commands.lastCommand = val;
649 clear: function clear(scroll) {
650 if (!scroll || Date.now() - this._lastEchoTime > 5000)
652 this._lastEchoTime = 0;
654 if (!this.commandSession) {
655 this.widgets.command = null;
656 this.hideCompletions();
659 if (modes.main == modes.OUTPUT_MULTILINE && !mow.isScrollable(1))
662 if (!modes.have(modes.OUTPUT_MULTILINE))
666 clearMessage: function clearMessage() {
667 if (this.widgets.message && this.widgets.message[1] === this._lastClearable)
668 this.widgets.message = null;
672 * Displays the multi-line output of a command, preceded by the last
673 * executed ex command string.
675 * @param {XML} xml The output as an E4X XML object.
677 commandOutput: function commandOutput(xml) {
678 XML.ignoreWhitespace = XML.prettyPrinting = false;
680 this.echo(<><div xmlns={XHTML}>:{this.command}</div>
{xml}</>, this.HIGHLIGHT_NORMAL, this.FORCE_MULTILINE);
682 this.echo(xml, this.HIGHLIGHT_NORMAL, this.FORCE_MULTILINE);
687 * Hides the command line, and shows any status messages that
690 hide: function hide() {
691 this.widgets.command = null;
695 * Display a message in the command-line area.
697 * @param {string} str
698 * @param {string} highlightGroup
699 * @param {boolean} forceSingle If provided, don't let over-long
700 * messages move to the MOW.
702 _echoLine: function echoLine(str, highlightGroup, forceSingle, silent) {
703 this.widgets.message = str ? [highlightGroup, str, forceSingle] : null;
705 dactyl.triggerObserver("echoLine", str, highlightGroup, null, forceSingle);
707 if (!this.commandVisible)
710 let field = this.widgets.active.message.inputField;
711 if (field.value && !forceSingle && field.editor.rootElement.scrollWidth > field.scrollWidth) {
712 this.widgets.message = null;
713 mow.echo(<span highlight="Message">{str}</span>, highlightGroup, true);
720 * Output the given string onto the command line. With no flags, the
721 * message will be shown in the status line if it's short enough to
722 * fit, and contains no new lines, and isn't XML. Otherwise, it will be
725 * @param {string} str
726 * @param {string} highlightGroup The Highlight group for the
729 * @param {number} flags Changes the behavior as follows:
730 * commandline.APPEND_TO_MESSAGES - Causes message to be added to the
731 * messages history, and shown by :messages.
732 * commandline.FORCE_SINGLELINE - Forbids the command from being
733 * pushed to the MOW if it's too long or of there are already
734 * status messages being shown.
735 * commandline.DISALLOW_MULTILINE - Cancels the operation if the MOW
736 * is already visible.
737 * commandline.FORCE_MULTILINE - Forces the message to appear in
741 echo: function echo(data, highlightGroup, flags) {
742 // dactyl.echo uses different order of flags as it omits the highlight group, change commandline.echo argument order? --mst
743 if (this._silent || !this.widgets)
748 highlightGroup = highlightGroup || this.HL_NORMAL;
750 if (flags & this.APPEND_TO_MESSAGES) {
751 let message = isObject(data) ? data : { message: data };
753 // Make sure the memoized message property is an instance property.
755 this._messageHistory.add(update({ highlight: highlightGroup }, message));
756 data = message.message;
759 if ((flags & this.ACTIVE_WINDOW) && window != overlay.activeWindow)
762 if ((flags & this.DISALLOW_MULTILINE) && !this.widgets.mowContainer.collapsed)
765 let single = flags & (this.FORCE_SINGLELINE | this.DISALLOW_MULTILINE);
766 let action = this._echoLine;
768 if ((flags & this.FORCE_MULTILINE) || (/\n/.test(data) || !isinstance(data, [_, "String"])) && !(flags & this.FORCE_SINGLELINE))
769 action = mow.closure.echo;
772 this._lastEcho = null;
774 if (this.widgets.message && this.widgets.message[1] == this._lastEcho)
775 mow.echo(<span highlight="Message">{this._lastEcho}</span>,
776 this.widgets.message[0], true);
778 if (action === this._echoLine && !(flags & this.FORCE_MULTILINE)
779 && !(dactyl.fullyInitialized && this.widgets.mowContainer.collapsed)) {
780 highlightGroup += " Message";
781 action = mow.closure.echo;
783 this._lastEcho = (action == this._echoLine) && data;
786 this._lastClearable = action === this._echoLine && String(data);
787 this._lastEchoTime = (flags & this.FORCE_SINGLELINE) && Date.now();
790 action.call(this, data, highlightGroup, single);
795 * Prompt the user. Sets modes.main to COMMAND_LINE, which the user may
796 * pop at any time to close the prompt.
798 * @param {string} prompt The input prompt to use.
799 * @param {function(string)} callback
800 * @param {Object} extra
801 * @... {function} onChange - A function to be called with the current
802 * input every time it changes.
803 * @... {function(CompletionContext)} completer - A completion function
804 * for the user's input.
805 * @... {string} promptHighlight - The HighlightGroup used for the
806 * prompt. @default "Question"
807 * @... {string} default - The initial value that will be returned
808 * if the user presses <CR> straightaway. @default ""
810 input: function _input(prompt, callback, extra) {
813 CommandPromptMode(prompt, update({ onSubmit: callback }, extra)).open();
816 readHeredoc: function readHeredoc(end) {
818 commandline.inputMultiline(end, function (res) { args = res; });
819 util.waitFor(function () args !== undefined);
824 * Get a multi-line input from a user, up to but not including the line
825 * which matches the given regular expression. Then execute the
826 * callback with that string as a parameter.
828 * @param {string} end
829 * @param {function(string)} callback
831 // FIXME: Buggy, especially when pasting.
832 inputMultiline: function inputMultiline(end, callback) {
833 let cmd = this.command;
835 end: "\n" + end + "\n",
839 modes.push(modes.INPUT_MULTILINE, null, {
841 leave: function leave() {
849 this._echoLine(cmd, this.HL_NORMAL);
851 // save the arguments, they are needed in the event handler onKeyPress
853 this.multilineInputVisible = true;
854 this.widgets.multilineInput.value = "";
855 this._autosizeMultilineInputWidget();
857 this.timeout(function () { dactyl.focus(this.widgets.multilineInput); }, 10);
860 get commandMode() this.commandSession && isinstance(modes.main, modes.COMMAND_LINE),
863 iter(CommandMode.prototype.events).map(
864 function ([event, handler]) [
865 event, function (event) {
866 if (this.commandMode)
867 handler.call(this.commandSession, event);
871 focus: function onFocus(event) {
872 if (!this.commandSession
873 && event.originalTarget === this.widgets.active.command.inputField) {
881 get mowEvents() mow.events,
884 * Multiline input events, they will come straight from
885 * #dactyl-multiline-input in the XUL.
887 * @param {Event} event
889 multilineInputEvents: {
890 blur: function onBlur(event) {
891 if (modes.main == modes.INPUT_MULTILINE)
892 this.timeout(function () {
893 dactyl.focus(this.widgets.multilineInput.inputField);
896 input: function onInput(event) {
897 this._autosizeMultilineInputWidget();
901 updateOutputHeight: deprecated("mow.resize", function updateOutputHeight(open, extra) mow.resize(open, extra)),
903 withOutputToString: function withOutputToString(fn, self) {
904 dactyl.registerObserver("echoLine", observe, true);
905 dactyl.registerObserver("echoMultiline", observe, true);
908 function observe(str, highlight, dom) {
909 output.push(dom && !isString(str) ? dom : str);
912 this.savingOutput = true;
913 dactyl.trapErrors.apply(dactyl, [fn, self].concat(Array.slice(arguments, 2)));
914 this.savingOutput = false;
915 return output.map(function (elem) elem instanceof Node ? DOM.stringify(elem) : elem)
920 * A class for managing the history of an input field.
922 * @param {HTMLInputElement} inputField
923 * @param {string} mode The mode for which we need history.
925 History: Class("History", {
926 init: function init(inputField, mode, session) {
928 this.input = inputField;
930 this.session = session;
932 get store() commandline._store.get(this.mode, []),
933 set store(ary) { commandline._store.set(this.mode, ary); },
935 * Reset the history index to the first entry.
937 reset: function reset() {
941 * Save the last entry to the permanent store. All duplicate entries
942 * are removed and the list is truncated, if necessary.
944 save: function save() {
945 if (events.feedingKeys)
947 let str = this.input.value;
948 if (/^\s*$/.test(str))
950 this.store = this.store.filter(function (line) (line.value || line) != str);
951 dactyl.trapErrors(function () {
952 this.store.push({ value: str, timestamp: Date.now()*1000, privateData: this.checkPrivate(str) });
954 this.store = this.store.slice(Math.max(0, this.store.length - options["history"]));
957 * @property {function} Returns whether a data item should be
958 * considered private.
960 checkPrivate: function checkPrivate(str) {
961 // Not really the ideal place for this check.
962 if (this.mode == "command")
963 return commands.hasPrivateData(str);
967 * Replace the current input field value.
969 * @param {string} val The new value.
971 replace: function replace(val) {
972 editor.withSavedValues(["skipSave"], function () {
973 editor.skipSave = true;
975 this.input.dactylKeyPress = undefined;
976 if (this.completions)
977 this.completions.previewClear();
978 this.input.value = val;
979 this.session.onHistory(val);
984 * Move forward or backward in history.
986 * @param {boolean} backward Direction to move.
987 * @param {boolean} matchCurrent Search for matches starting
988 * with the current input value.
990 select: function select(backward, matchCurrent) {
991 // always reset the tab completion if we use up/down keys
992 if (this.session.completions)
993 this.session.completions.reset();
995 let diff = backward ? -1 : 1;
997 if (this.index == null) {
998 this.original = this.input.value;
999 this.index = this.store.length;
1002 // search the history for the first item matching the current
1003 // command-line string
1006 if (this.index < 0 || this.index > this.store.length) {
1007 this.index = Math.constrain(this.index, 0, this.store.length);
1009 // I don't know why this kludge is needed. It
1010 // prevents the caret from moving to the end of
1012 if (this.input.value == "") {
1013 this.input.value = " ";
1014 this.input.value = "";
1019 let hist = this.store[this.index];
1020 // user pressed DOWN when there is no newer history item
1022 hist = this.original;
1024 hist = (hist.value || hist);
1026 if (!matchCurrent || hist.substr(0, this.original.length) == this.original) {
1035 * A class for tab completions on an input field.
1037 * @param {Object} input
1039 Completions: Class("Completions", {
1048 init: function init(input, session) {
1051 this.context = CompletionContext(input.QueryInterface(Ci.nsIDOMNSEditableElement).editor);
1052 this.context.onUpdate = function onUpdate() { self.asyncUpdate(this); };
1054 this.editor = input.editor;
1056 this.session = session;
1058 this.wildmode = options.get("wildmode");
1059 this.wildtypes = this.wildmode.value;
1061 this.itemList = commandline.completionList;
1062 this.itemList.open(this.context);
1064 dactyl.registerObserver("events.doneFeeding", this.closure.onDoneFeeding, true);
1066 this.autocompleteTimer = Timer(200, 500, function autocompleteTell(tabPressed) {
1067 if (events.feedingKeys && !tabPressed)
1068 this.ignoredCount++;
1069 else if (this.session.autocomplete) {
1070 this.itemList.visible = true;
1071 this.complete(true, false);
1075 this.tabTimer = Timer(0, 0, function tabTell(event) {
1076 let tabCount = this.tabCount;
1078 this.tab(tabCount, event.altKey && options["altwildmode"]);
1089 onDoneFeeding: function onDoneFeeding() {
1090 if (this.ignoredCount)
1091 this.autocompleteTimer.flush(true);
1092 this.ignoredCount = 0;
1098 onTab: function onTab(event) {
1099 this.tabCount += event.shiftKey ? -1 : 1;
1100 this.tabTimer.tell(event);
1103 get activeContexts() this.context.contextList
1104 .filter(function (c) c.items.length || c.incomplete),
1107 * Returns the current completion string relative to the
1108 * offset of the currently selected context.
1111 let offset = this.selected ? this.selected[0].offset : this.start;
1112 return commandline.command.slice(offset, this.caret);
1116 * Updates the input field from *offset* to {@link #caret}
1117 * with the value *value*. Afterward, the caret is moved
1118 * just after the end of the updated text.
1120 * @param {number} offset The offset in the original input
1121 * string at which to insert *value*.
1122 * @param {string} value The value to insert.
1124 setCompletion: function setCompletion(offset, value) {
1125 editor.withSavedValues(["skipSave"], function () {
1126 editor.skipSave = true;
1127 this.previewClear();
1130 var [input, caret] = [this.originalValue, this.originalCaret];
1132 input = this.getCompletion(offset, value);
1133 caret = offset + value.length;
1136 // Change the completion text.
1137 // The second line is a hack to deal with some substring
1138 // preview corner cases.
1139 commandline.widgets.active.command.value = input;
1140 this.editor.selection.focusNode.textContent = input;
1143 this._caret = this.caret;
1145 this.input.dactylKeyPress = undefined;
1150 * For a given offset and completion string, returns the
1151 * full input value after selecting that item.
1153 * @param {number} offset The offset at which to insert the
1155 * @param {string} value The value to insert.
1156 * @returns {string};
1158 getCompletion: function getCompletion(offset, value) {
1159 return this.originalValue.substr(0, offset)
1161 + this.originalValue.substr(this.originalCaret);
1164 get selected() this.itemList.selected,
1165 set selected(tuple) {
1166 if (!array.equals(tuple || [],
1167 this.itemList.selected || []))
1168 this.itemList.select(tuple);
1171 this.setCompletion(null);
1173 let [ctxt, idx] = tuple;
1174 this.setCompletion(ctxt.offset, ctxt.items[idx].result);
1178 get caret() this.editor.selection.getRangeAt(0).startOffset,
1180 this.editor.selection.collapse(this.editor.rootElement.firstChild, offset);
1183 get start() this.context.allItems.start,
1185 get items() this.context.allItems.items,
1187 get substring() this.context.longestAllSubstring,
1189 get wildtype() this.wildtypes[this.wildIndex] || "",
1192 * Cleanup resources used by this completion session. This
1193 * instance should not be used again once this method is
1196 cleanup: function cleanup() {
1197 dactyl.unregisterObserver("events.doneFeeding", this.closure.onDoneFeeding);
1198 this.previewClear();
1200 this.tabTimer.reset();
1201 this.autocompleteTimer.reset();
1202 if (!this.onComplete)
1203 this.context.cancelAll();
1205 this.itemList.visible = false;
1206 this.input.dactylKeyPress = undefined;
1207 this.hasQuit = true;
1211 * Run the completer.
1213 * @param {boolean} show Passed to {@link #reset}.
1214 * @param {boolean} tabPressed Should be set to true if, and
1215 * only if, this function is being called in response
1218 complete: function complete(show, tabPressed) {
1219 this.session.ignoredCount = 0;
1221 this.waiting = null;
1222 this.context.reset();
1223 this.context.tabPressed = tabPressed;
1225 this.session.complete(this.context);
1226 if (!this.session.active)
1229 this.reset(show, tabPressed);
1231 this._caret = this.caret;
1235 * Clear any preview string and cancel any pending
1236 * asynchronous context. Called when there is further input
1239 clear: function clear() {
1240 this.context.cancelAll();
1241 this.wildIndex = -1;
1242 this.previewClear();
1246 * Saves the current input state. To be called before an
1247 * item is selected in a new set of completion responses.
1250 saveInput: function saveInput() {
1251 this.originalValue = this.context.value;
1252 this.originalCaret = this.caret;
1256 * Resets the completion state.
1258 * @param {boolean} show If true and options allow the
1259 * completion list to be shown, show it.
1261 reset: function reset(show) {
1262 this.waiting = null;
1263 this.wildIndex = -1;
1268 this.itemList.update();
1269 this.context.updateAsync = true;
1270 if (this.haveType("list"))
1271 this.itemList.visible = true;
1279 * Calls when an asynchronous completion context has new
1280 * results to return.
1282 * @param {CompletionContext} context The changed context.
1285 asyncUpdate: function asyncUpdate(context) {
1287 let item = this.getItem(this.waiting);
1288 if (item && this.waiting && this.onComplete) {
1289 util.trapErrors("onComplete", this,
1290 this.getCompletion(this.waiting[0].offset,
1292 this.waiting = null;
1293 this.context.cancelAll();
1298 let value = this.editor.selection.focusNode.textContent;
1301 if (this.itemList.visible)
1302 this.itemList.updateContext(context);
1304 if (this.waiting && this.waiting[0] == context)
1305 this.select(this.waiting);
1306 else if (!this.waiting) {
1307 let cursor = this.selected;
1308 if (cursor && cursor[0] == context) {
1309 let item = this.getItem(cursor);
1310 if (!item || this.completion != item.result)
1311 this.itemList.select(null);
1319 * Returns true if the currently selected 'wildmode' index
1320 * has the given completion type.
1322 haveType: function haveType(type)
1323 this.wildmode.checkHas(this.wildtype, type == "first" ? "" : type),
1326 * Returns the completion item for the given selection
1329 * @param {[CompletionContext,number]} tuple The spec of the
1331 * @default {@link #selected}
1334 getItem: function getItem(tuple) {
1335 tuple = tuple || this.selected;
1336 return tuple && tuple[0] && tuple[0].items[tuple[1]];
1340 * Returns a tuple representing the next item, at the given
1341 * *offset*, from *tuple*.
1343 * @param {[CompletionContext,number]} tuple The offset from
1345 * @default {@link #selected}
1346 * @param {number} offset The positive or negative offset to
1349 * @param {boolean} noWrap If true, and the search would
1350 * wrap, return null.
1352 nextItem: function nextItem(tuple, offset, noWrap) {
1353 if (tuple === undefined)
1354 tuple = this.selected;
1356 return this.itemList.getRelativeItem(offset || 1, tuple, noWrap);
1360 * The last previewed substring.
1366 * Displays a preview of the text provided by the next <Tab>
1367 * press if the current input is an anchored substring of
1370 preview: function preview() {
1371 this.previewClear();
1372 if (this.wildIndex < 0 || this.caret < this.input.value.length
1373 || !this.activeContexts.length || this.waiting)
1377 switch (this.wildtype.replace(/.*:/, "")) {
1379 var cursor = this.nextItem(null);
1382 if (this.items.length > 1) {
1383 substring = this.substring;
1388 cursor = this.nextItem();
1392 substring = this.getItem(cursor).result;
1394 // Don't show 1-character substrings unless we've just hit backspace
1395 if (substring.length < 2 && this.lastSubstring.indexOf(substring))
1398 this.lastSubstring = substring;
1400 let value = this.completion;
1401 if (util.compareIgnoreCase(value, substring.substr(0, value.length)))
1404 substring = substring.substr(value.length);
1405 this.removeSubstring = substring;
1407 let node = DOM.fromXML(<span highlight="Preview">{substring}</span>,
1410 this.withSavedValues(["caret"], function () {
1411 this.editor.insertNode(node, this.editor.rootElement, 1);
1416 * Clears the currently displayed next-<Tab> preview string.
1418 previewClear: function previewClear() {
1419 let node = this.editor.rootElement.firstChild;
1420 if (node && node.nextSibling) {
1422 DOM(node.nextSibling).remove();
1425 node.nextSibling.textContent = "";
1428 else if (this.removeSubstring) {
1429 let str = this.removeSubstring;
1430 let cmd = commandline.widgets.active.command.value;
1431 if (cmd.substr(cmd.length - str.length) == str)
1432 commandline.widgets.active.command.value = cmd.substr(0, cmd.length - str.length);
1434 delete this.removeSubstring;
1438 * Selects a completion based on the value of *idx*.
1440 * @param {[CompletionContext,number]|const object} The
1441 * (context,index) tuple of the item to select, or an
1442 * offset constant from this object.
1443 * @param {number} count When given an offset constant,
1444 * select *count* units.
1446 * @param {boolean} fromTab If true, this function was
1447 * called by {@link #tab}.
1450 select: function select(idx, count, fromTab) {
1456 idx = this.nextItem(this.waiting || this.selected,
1457 idx == this.UP ? -count : count,
1462 case this.CTXT_DOWN:
1463 let groups = this.itemList.activeGroups;
1464 let i = Math.max(0, groups.indexOf(this.itemList.selectedGroup));
1466 i += idx == this.CTXT_DOWN ? 1 : -1;
1472 idx = [groups[i].context, 0];
1476 case this.PAGE_DOWN:
1477 idx = this.itemList.getRelativePage(idx == this.PAGE_DOWN ? 1 : -1);
1489 this.wildIndex = this.wildtypes.length - 1;
1491 if (idx && idx[1] >= idx[0].items.length) {
1492 if (!idx[0].incomplete)
1493 this.waiting = null;
1496 statusline.progress = _("completion.waitingForResults");
1501 this.waiting = null;
1503 this.itemList.select(idx, null, position);
1504 this.selected = idx;
1508 if (this.selected == null)
1509 statusline.progress = "";
1511 statusline.progress = _("completion.matchIndex",
1512 this.itemList.getOffset(idx),
1513 this.itemList.itemCount);
1517 * Selects a completion result based on the 'wildmode'
1518 * option, or the value of the *wildmode* parameter.
1520 * @param {number} offset The positive or negative number of
1521 * tab presses to process.
1522 * @param {[string]} wildmode A 'wildmode' value to
1523 * substitute for the value of the 'wildmode' option.
1526 tab: function tab(offset, wildmode) {
1527 this.autocompleteTimer.flush();
1528 this.ignoredCount = 0;
1530 if (this._caret != this.caret)
1532 this._caret = this.caret;
1534 // Check if we need to run the completer.
1535 if (this.context.waitingForTab || this.wildIndex == -1)
1536 this.complete(true, true);
1538 this.wildtypes = wildmode || options["wildmode"];
1539 let count = Math.abs(offset);
1540 let steps = Math.constrain(this.wildtypes.length - this.wildIndex,
1542 count = Math.max(1, count - steps);
1545 this.wildIndex = Math.min(this.wildIndex, this.wildtypes.length - 1);
1546 switch (this.wildtype.replace(/.*:/, "")) {
1548 this.select(this.nextItem(null));
1551 if (this.itemList.itemCount > 1) {
1552 if (this.substring && this.substring.length > this.completion.length)
1553 this.setCompletion(this.start, this.substring);
1558 let c = steps ? 1 : count;
1559 this.select(offset < 0 ? this.UP : this.DOWN, c, true);
1563 if (this.haveType("list"))
1564 this.itemList.visible = true;
1569 if (this.items.length == 0 && !this.waiting)
1575 * Evaluate a JavaScript expression and return a string suitable
1578 * @param {string} arg
1579 * @param {boolean} useColor When true, the result is a
1580 * highlighted XML object.
1582 echoArgumentToString: function (arg, useColor) {
1586 arg = dactyl.userEval(arg);
1588 arg = util.objectToString(arg, useColor);
1589 else if (callable(arg))
1590 arg = String.replace(arg, "/* use strict */ \n", "/* use strict */ ");
1591 else if (!isString(arg) && useColor)
1592 arg = template.highlight(arg);
1596 commands: function init_commands() {
1600 description: "Echo the expression",
1605 description: "Echo the expression as an error message",
1606 action: dactyl.echoerr
1610 description: "Echo the expression as an informational message",
1611 action: dactyl.echomsg
1613 ].forEach(function (command) {
1614 commands.add([command.name],
1615 command.description,
1617 command.action(CommandLine.echoArgumentToString(args[0] || "", true));
1619 completer: function (context) completion.javascript(context),
1624 commands.add(["mes[sages]"],
1625 "Display previously shown messages",
1627 // TODO: are all messages single line? Some display an aggregation
1628 // of single line messages at least. E.g. :source
1629 if (commandline._messageHistory.length == 1) {
1630 let message = commandline._messageHistory.messages[0];
1631 commandline.echo(message.message, message.highlight, commandline.FORCE_SINGLELINE);
1633 else if (commandline._messageHistory.length > 1) {
1634 XML.ignoreWhitespace = false;
1635 commandline.commandOutput(
1636 template.map(commandline._messageHistory.messages, function (message)
1637 <div highlight={message.highlight + " Message"}>{message.message}</div>));
1642 commands.add(["messc[lear]"],
1643 "Clear the message history",
1644 function () { commandline._messageHistory.clear(); },
1647 commands.add(["sil[ent]"],
1648 "Run a command silently",
1650 commandline.runSilently(function () commands.execute(args[0] || "", null, true));
1652 completer: function (context) completion.ex(context),
1657 modes: function initModes() {
1658 initModes.require("editor");
1660 modes.addMode("COMMAND_LINE", {
1662 description: "Active when the command line is focused",
1665 get mappingSelf() commandline.commandSession
1667 // this._extended modes, can include multiple modes, and even main modes
1668 modes.addMode("EX", {
1669 description: "Ex command mode, active when the command line is open for Ex commands",
1670 bases: [modes.COMMAND_LINE]
1672 modes.addMode("PROMPT", {
1673 description: "Active when a prompt is open in the command line",
1674 bases: [modes.COMMAND_LINE]
1677 modes.addMode("INPUT_MULTILINE", {
1678 description: "Active when the command line's multiline input buffer is open",
1679 bases: [modes.INSERT]
1682 mappings: function init_mappings() {
1684 mappings.add([modes.COMMAND],
1685 [":"], "Enter Command Line mode",
1686 function () { CommandExMode().open(""); });
1688 mappings.add([modes.INPUT_MULTILINE],
1689 ["<Return>", "<C-j>", "<C-m>"], "Begin a new line",
1690 function ({ self }) {
1691 let text = "\n" + commandline.widgets.multilineInput
1692 .value.substr(0, commandline.widgets.multilineInput.selectionStart)
1695 let index = text.indexOf(self.end);
1698 text = text.substring(1, index);
1701 return function () self.callback.call(commandline, text);
1706 let bind = function bind()
1707 mappings.add.apply(mappings, [[modes.COMMAND_LINE]].concat(Array.slice(arguments)))
1709 bind(["<Esc>", "<C-[>"], "Stop waiting for completions or exit Command Line mode",
1710 function ({ self }) {
1711 if (self.completions && self.completions.waiting)
1712 self.completions.waiting = null;
1717 // Any "non-keyword" character triggers abbreviation expansion
1718 // TODO: Add "<CR>" and "<Tab>" to this list
1719 // At the moment, adding "<Tab>" breaks tab completion. Adding
1720 // "<CR>" has no effect.
1721 // TODO: Make non-keyword recognition smarter so that there need not
1722 // be two lists of the same characters (one here and a regexp in
1724 bind(["<Space>", '"', "'"], "Expand command line abbreviation",
1725 function ({ self }) {
1726 self.resetCompletions();
1727 editor.expandAbbreviation(modes.COMMAND_LINE);
1731 bind(["<Return>", "<C-j>", "<C-m>"], "Accept the current input",
1732 function ({ self }) {
1733 if (self.completions)
1734 self.completions.tabTimer.flush();
1736 let command = commandline.command;
1738 self.accepted = true;
1739 return function () { modes.pop(); };
1743 [["<Up>", "<A-p>", "<cmd-prev-match>"], "previous matching", true, true],
1744 [["<S-Up>", "<C-p>", "<cmd-prev>"], "previous", true, false],
1745 [["<Down>", "<A-n>", "<cmd-next-match>"], "next matching", false, true],
1746 [["<S-Down>", "<C-n>", "<cmd-next>"], "next", false, false]
1747 ].forEach(function ([keys, desc, up, search]) {
1748 bind(keys, "Recall the " + desc + " command line from the history list",
1749 function ({ self }) {
1750 dactyl.assert(self.history);
1751 self.history.select(up, search);
1755 bind(["<A-Tab>", "<Tab>", "<A-compl-next>", "<compl-next>"],
1756 "Select the next matching completion item",
1757 function ({ keypressEvents, self }) {
1758 dactyl.assert(self.completions);
1759 self.completions.onTab(keypressEvents[0]);
1762 bind(["<A-S-Tab>", "<S-Tab>", "<A-compl-prev>", "<compl-prev>"],
1763 "Select the previous matching completion item",
1764 function ({ keypressEvents, self }) {
1765 dactyl.assert(self.completions);
1766 self.completions.onTab(keypressEvents[0]);
1769 bind(["<C-Tab>", "<A-f>", "<compl-next-group>"],
1770 "Select the next matching completion group",
1771 function ({ keypressEvents, self }) {
1772 dactyl.assert(self.completions);
1773 self.completions.tabTimer.flush();
1774 self.completions.select(self.completions.CTXT_DOWN);
1777 bind(["<C-S-Tab>", "<A-S-f>", "<compl-prev-group>"],
1778 "Select the previous matching completion group",
1779 function ({ keypressEvents, self }) {
1780 dactyl.assert(self.completions);
1781 self.completions.tabTimer.flush();
1782 self.completions.select(self.completions.CTXT_UP);
1785 bind(["<C-f>", "<PageDown>", "<compl-next-page>"],
1786 "Select the next page of completions",
1787 function ({ keypressEvents, self }) {
1788 dactyl.assert(self.completions);
1789 self.completions.tabTimer.flush();
1790 self.completions.select(self.completions.PAGE_DOWN);
1793 bind(["<C-b>", "<PageUp>", "<compl-prev-page>"],
1794 "Select the previous page of completions",
1795 function ({ keypressEvents, self }) {
1796 dactyl.assert(self.completions);
1797 self.completions.tabTimer.flush();
1798 self.completions.select(self.completions.PAGE_UP);
1801 bind(["<BS>", "<C-h>"], "Delete the previous character",
1803 if (!commandline.command)
1809 bind(["<C-]>", "<C-5>"], "Expand command line abbreviation",
1810 function () { editor.expandAbbreviation(modes.COMMAND_LINE); });
1812 options: function init_options() {
1813 options.add(["history", "hi"],
1814 "Number of Ex commands and search patterns to store in the command-line history",
1816 { validator: function (value) value >= 0 });
1818 options.add(["maxitems"],
1819 "Maximum number of completion items to display at once",
1821 { validator: function (value) value >= 1 });
1823 options.add(["messages", "msgs"],
1824 "Number of messages to store in the :messages history",
1826 { validator: function (value) value >= 0 });
1828 sanitizer: function init_sanitizer() {
1829 sanitizer.addItem("commandline", {
1830 description: "Command-line and search history",
1832 action: function (timespan, host) {
1833 let store = commandline._store;
1834 for (let [k, v] in store) {
1836 store.set(k, v.filter(function (item)
1837 !(timespan.contains(item.timestamp) && (!host || commands.hasDomain(item.value, host)))));
1839 store.set(k, v.filter(function (item) !timespan.contains(item.timestamp)));
1843 // Delete history-like items from the commandline and messages on history purge
1844 sanitizer.addItem("history", {
1845 action: function (timespan, host) {
1846 commandline._store.set("command",
1847 commandline._store.get("command", []).filter(function (item)
1848 !(timespan.contains(item.timestamp) && (host ? commands.hasDomain(item.value, host)
1849 : item.privateData))));
1851 commandline._messageHistory.filter(function (item) !timespan.contains(item.timestamp * 1000) ||
1852 !item.domains && !item.privateData ||
1853 host && (!item.domains || !item.domains.some(function (d) util.isSubdomain(d, host))));
1856 sanitizer.addItem("messages", {
1857 description: "Saved :messages",
1858 action: function (timespan, host) {
1859 commandline._messageHistory.filter(function (item) !timespan.contains(item.timestamp * 1000) ||
1860 host && (!item.domains || !item.domains.some(function (d) util.isSubdomain(d, host))));
1867 * The list which is used for the completion box.
1869 * @param {string} id The id of the iframe which will display the list. It
1870 * must be in its own container element, whose height it will update as
1874 var ItemList = Class("ItemList", {
1877 init: function init(frame) {
1880 this.doc = frame.contentDocument;
1881 this.win = frame.contentWindow;
1882 this.body = this.doc.body;
1883 this.container = frame.parentNode;
1885 highlight.highlightNode(this.doc.body, "Comp");
1887 this._onResize = Timer(20, 400, function _onResize(event) {
1889 this.onResize(event);
1891 this._resize = Timer(20, 400, function _resize(flags) {
1896 DOM(this.win).resize(this._onResize.closure.tell);
1900 <div highlight="Normal" style="white-space: nowrap" key="root">
1902 <div highlight="Completions" key="noCompletions"><span highlight="Title">{_("completion.noCompletions")}</span></div>
1903 <div key="completions"/>
1906 <div highlight="Completions">{
1907 template.map(util.range(0, options["maxitems"] * 2), function (i)
1908 <div highlight="CompItem NonText"><li>~</li></div>)
1913 get itemCount() this.context.contextList.reduce(function (acc, ctxt) acc + ctxt.items.length, 0),
1915 get visible() !this.container.collapsed,
1916 set visible(val) this.container.collapsed = !val,
1918 get activeGroups() this.context.contextList
1919 .filter(function (c) c.items.length || c.message || c.incomplete)
1920 .map(this.getGroup, this),
1922 get selected() let (g = this.selectedGroup) g && g.selectedIdx != null
1923 ? [g.context, g.selectedIdx] : null,
1925 getRelativeItem: function getRelativeItem(offset, tuple, noWrap) {
1926 let groups = this.activeGroups;
1930 let group = this.selectedGroup || groups[0];
1931 let start = group.selectedIdx || 0;
1932 if (tuple === null) { // Kludge.
1934 tuple = [this.activeGroups[0], -1];
1936 let group = this.activeGroups.slice(-1)[0];
1937 tuple = [group, group.itemCount];
1941 [group, start] = tuple;
1943 group = this.getGroup(group);
1945 start = (group.offsets.start + start + offset);
1947 start %= this.itemCount || 1;
1948 if (start < 0 && (!noWrap || arguments[1] === null))
1949 start += this.itemCount;
1951 if (noWrap && offset > 0) {
1952 // Check if we've passed any incomplete contexts
1954 let i = groups.indexOf(group);
1955 util.assert(i >= 0, undefined, false);
1956 for (; i < groups.length; i++) {
1957 let end = groups[i].offsets.start + groups[i].itemCount;
1958 if (start >= end && groups[i].context.incomplete)
1959 return [groups[i].context, start - groups[i].offsets.start];
1966 if (start < 0 || start >= this.itemCount)
1969 group = array.nth(groups, function (g) let (i = start - g.offsets.start) i >= 0 && i < g.itemCount, 0)
1970 return [group.context, start - group.offsets.start];
1973 getRelativePage: function getRelativePage(offset, tuple, noWrap) {
1974 offset *= this.maxItems;
1975 // Try once with wrapping disabled.
1976 let res = this.getRelativeItem(offset, tuple, true);
1980 let sign = offset / Math.abs(offset);
1982 let off = this.getOffset(tuple === null ? null : tuple || this.selected);
1984 // Unselected. Defer to getRelativeItem.
1985 res = this.getRelativeItem(offset, null, noWrap);
1986 else if (~[0, this.itemCount - 1].indexOf(off))
1987 // At start or end. Jump to other end.
1988 res = this.getRelativeItem(sign, null, noWrap);
1990 // Wrapped. Go to beginning or end.
1991 res = this.getRelativeItem(-sign, null);
1997 * Initializes the ItemList for use with a new root completion
2000 * @param {CompletionContext} context The new root context.
2002 open: function open(context) {
2003 this.context = context;
2005 this.container.height = 0;
2007 this.maxItems = options["maxitems"];
2009 DOM(this.rootXML, this.doc, this.nodes)
2010 .appendTo(DOM(this.body).empty());
2016 * Updates the absolute result indices of all groups after
2017 * results have changed.
2020 updateOffsets: function updateOffsets() {
2021 let total = this.itemCount;
2023 for (let group in values(this.activeGroups)) {
2024 group.offsets = { start: count, end: total - count - group.itemCount };
2025 count += group.itemCount;
2030 * Updates the set and state of active groups for a new set of
2031 * completion results.
2033 update: function update() {
2034 DOM(this.nodes.completions).empty();
2036 let container = DOM(this.nodes.completions);
2037 let groups = this.activeGroups;
2038 for (let group in values(groups)) {
2040 container.append(group.nodes.root);
2043 this.updateOffsets();
2045 DOM(this.nodes.noCompletions).toggle(!groups.length);
2047 this.startPos = null;
2048 this.select(groups[0] && groups[0].context, null);
2050 this._resize.tell();
2054 * Updates the group for *context* after an asynchronous update
2057 * @param {CompletionContext} context The context which has
2060 updateContext: function updateContext(context) {
2061 let group = this.getGroup(context);
2062 this.updateOffsets();
2064 if (~this.activeGroups.indexOf(group))
2067 DOM(group.nodes.root).remove();
2068 if (this.selectedGroup == group)
2069 this.selectedGroup = null;
2072 let g = this.selectedGroup;
2073 this.select(g, g && g.selectedIdx);
2077 * Updates the DOM to reflect the current state of all groups.
2080 draw: function draw() {
2081 for (let group in values(this.activeGroups))
2084 // We need to collect all of the rescrolling functions in
2085 // one go, as the height calculation that they need to do
2086 // would force a reflow after each DOM modification.
2087 this.activeGroups.filter(function (g) !g.collapsed)
2088 .map(function (g) g.rescrollFunc)
2092 this.win.scrollTo(0, 0);
2094 this._resize.tell(ItemList.RESIZE_BRIEF);
2097 onResize: function onResize() {
2098 if (this.selectedGroup)
2099 this.selectedGroup.rescrollFunc();
2105 * Resizes the list after an update.
2108 resize: function resize(flags) {
2109 let { completions, root } = this.nodes;
2112 root.style.minWidth = document.getElementById("dactyl-commandline").scrollWidth + "px";
2114 let { minHeight } = this;
2115 if (mow.visible && this.isAboveMow) // Kludge.
2116 minHeight -= mow.wantedHeight;
2118 let needed = this.win.scrollY + DOM(completions).rect.bottom;
2119 this.minHeight = Math.max(minHeight, needed);
2122 root.style.minWidth = "";
2124 let height = this.visible ? parseFloat(this.container.height) : 0;
2125 if (this.minHeight <= minHeight || !mow.visible)
2126 this.container.height = Math.min(this.minHeight,
2127 height + config.outputHeight - mow.spaceNeeded);
2129 // FIXME: Belongs elsewhere.
2130 mow.resize(false, Math.max(0, this.minHeight - this.container.height));
2132 this.container.height = this.minHeight - mow.spaceNeeded;
2134 this.timeout(function () {
2135 this.container.height -= mow.spaceNeeded;
2141 * Selects the item at the given *group* and *index*.
2143 * @param {CompletionContext|[CompletionContext,number]} *group* The
2144 * completion context to select, or a tuple specifying the
2145 * context and item index.
2146 * @param {number} index The item index in *group* to select.
2147 * @param {number} position If non-null, try to position the
2148 * selected item at the *position*th row from the top of
2149 * the screen. Note that at least {@link #CONTEXT_LINES}
2150 * lines will be visible above and below the selected item
2151 * unless there aren't enough results to make this possible.
2154 select: function select(group, index, position) {
2156 [group, index] = group;
2158 group = this.getGroup(group);
2160 if (this.selectedGroup && (!group || group != this.selectedGroup))
2161 this.selectedGroup.selectedIdx = null;
2163 this.selectedGroup = group;
2166 group.selectedIdx = index;
2168 let groups = this.activeGroups;
2170 if (position != null || !this.startPos && groups.length)
2171 this.startPos = [group || groups[0], position || 0];
2173 if (groups.length) {
2174 group = group || groups[0];
2175 let idx = groups.indexOf(group);
2177 let start = this.startPos[0].getOffset(this.startPos[1]);
2179 let idx = group.selectedIdx || 0;
2180 let off = group.getOffset(idx);
2182 start = Math.constrain(start,
2183 off + Math.min(this.CONTEXT_LINES, group.itemCount - idx + group.offsets.end)
2184 - this.maxItems + 1,
2185 off - Math.min(this.CONTEXT_LINES, idx + group.offsets.start));
2188 let count = this.maxItems;
2189 for (let group in values(groups)) {
2190 let off = Math.max(0, start - group.offsets.start);
2192 group.count = Math.constrain(group.itemCount - off, 0, count);
2193 count -= group.count;
2195 group.collapsed = group.offsets.start >= start + this.maxItems
2196 || group.offsets.start + group.itemCount < start;
2198 group.range = ItemList.Range(off, off + group.count);
2201 var startPos = [group, group.range.start];
2203 this.startPos = startPos;
2209 * Returns an ItemList group for the given completion context,
2210 * creating one if necessary.
2212 * @param {CompletionContext} context
2213 * @returns {ItemList.Group}
2215 getGroup: function getGroup(context)
2216 context instanceof ItemList.Group ? context
2217 : context && context.getCache("itemlist-group",
2218 bind("Group", ItemList, this, context)),
2220 getOffset: function getOffset(tuple) tuple && this.getGroup(tuple[0]).getOffset(tuple[1])
2222 RESIZE_BRIEF: 1 << 0,
2224 WAITING_MESSAGE: _("completion.generating"),
2226 Group: Class("ItemList.Group", {
2227 init: function init(parent, context) {
2228 this.parent = parent;
2229 this.context = context;
2231 this.range = ItemList.Range(0, 0);
2235 <div key="root" highlight="CompGroup">
2236 <div highlight="Completions">
2237 { this.context.createRow(this.context.title || [], "CompTitle") }
2239 <div highlight="CompTitleSep"/>
2240 <div key="contents">
2241 <div key="up" highlight="CompLess"/>
2242 <div key="message" highlight="CompMsg">{this.context.message}</div>
2243 <div key="itemsContainer" class="completion-items-container">
2244 <div key="items" highlight="Completions"/>
2246 <div key="waiting" highlight="CompMsg">{ItemList.WAITING_MESSAGE}</div>
2247 <div key="down" highlight="CompMore"/>
2251 get doc() this.parent.doc,
2252 get win() this.parent.win,
2253 get maxItems() this.parent.maxItems,
2255 get itemCount() this.context.items.length,
2258 * Returns a function which will update the scroll offsets
2259 * and heights of various DOM members.
2262 get rescrollFunc() {
2263 let container = this.nodes.itemsContainer;
2264 let pos = DOM(container).rect.top;
2265 let start = DOM(this.getRow(this.range.start)).rect.top;
2266 let height = DOM(this.getRow(this.range.end - 1)).rect.bottom - start || 0;
2267 let scroll = start + container.scrollTop - pos;
2270 let row = this.selectedRow;
2271 if (row && this.parent.minHeight) {
2272 let { rect } = DOM(this.selectedRow);
2273 var scrollY = this.win.scrollY + rect.bottom - this.win.innerHeight;
2276 return function () {
2277 container.style.height = height + "px";
2278 container.scrollTop = scroll;
2279 if (scrollY != null)
2280 win.scrollTo(0, Math.max(scrollY, 0));
2285 * Reset this group for use with a new set of results.
2287 reset: function reset() {
2289 this.generatedRange = ItemList.Range(0, 0);
2291 DOM.fromXML(this.rootXML, this.doc, this.nodes);
2295 * Update this group after an asynchronous results push.
2297 update: function update() {
2298 this.generatedRange = ItemList.Range(0, 0);
2299 DOM(this.nodes.items).empty();
2301 if (this.context.message)
2302 DOM(this.nodes.message).empty().append(<>{this.context.message}</>);
2304 if (!this.selectedIdx > this.itemCount)
2305 this.selectedIdx = null;
2309 * Updates the DOM to reflect the current state of this
2313 draw: function draw() {
2314 DOM(this.nodes.contents).toggle(!this.collapsed);
2318 DOM(this.nodes.message).toggle(this.context.message && this.range.start == 0);
2319 DOM(this.nodes.waiting).toggle(this.context.incomplete && this.range.end <= this.itemCount);
2320 DOM(this.nodes.up).toggle(this.range.start > 0);
2321 DOM(this.nodes.down).toggle(this.range.end < this.itemCount);
2323 if (!this.generatedRange.contains(this.range)) {
2324 if (this.generatedRange.end == 0)
2325 var [start, end] = this.range;
2327 start = this.range.start - (this.range.start <= this.generatedRange.start
2328 ? this.maxItems / 2 : 0);
2329 end = this.range.end + (this.range.end > this.generatedRange.end
2330 ? this.maxItems / 2 : 0);
2333 let range = ItemList.Range(Math.max(0, start - start % 2),
2334 Math.min(this.itemCount, end));
2337 for (let [i, row] in this.context.getRows(this.generatedRange.start,
2338 this.generatedRange.end,
2340 if (!range.contains(i))
2345 let container = DOM(this.nodes.items);
2346 let before = first ? DOM(first).closure.before
2347 : DOM(this.nodes.items).closure.append;
2349 for (let [i, row] in this.context.getRows(range.start, range.end,
2351 if (i < this.generatedRange.start)
2353 else if (i >= this.generatedRange.end)
2354 container.append(row);
2355 if (i == this.selectedIdx)
2356 this.selectedIdx = this.selectedIdx;
2359 this.generatedRange = range;
2363 getRow: function getRow(idx) this.context.getRow(idx, this.doc),
2365 getOffset: function getOffset(idx) this.offsets.start + (idx || 0),
2367 get selectedRow() this.getRow(this._selectedIdx),
2369 get selectedIdx() this._selectedIdx,
2370 set selectedIdx(idx) {
2371 if (this.selectedRow && this._selectedIdx != idx)
2372 DOM(this.selectedRow).attr("selected", null);
2374 this._selectedIdx = idx;
2376 if (this.selectedRow)
2377 DOM(this.selectedRow).attr("selected", true);
2381 Range: Class.Memoize(function () {
2382 let Range = Struct("ItemList.Range", "start", "end");
2383 update(Range.prototype, {
2384 contains: function contains(idx)
2385 typeof idx == "number" ? idx >= this.start && idx < this.end
2386 : this.contains(idx.start) &&
2387 idx.end >= this.start && idx.end <= this.end
2393 // vim: set fdm=marker sw=4 ts=4 et: