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-2013 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 overlay.overlayWindow(window, {
19 eventTarget: commandline
22 ["vbox", { id: config.ids.commandContainer, xmlns: "xul" },
23 ["vbox", { class: "dactyl-container", hidden: "false", collapsed: "true" },
24 ["iframe", { class: "dactyl-completions", id: "dactyl-completions-dactyl-commandline",
25 src: "dactyl://content/buffer.xhtml", contextmenu: "dactyl-contextmenu",
26 flex: "1", hidden: "false", collapsed: "false",
27 highlight: "Events", events: "mowEvents" }]],
29 ["stack", { orient: "horizontal", align: "stretch", class: "dactyl-container",
30 id: "dactyl-container", highlight: "CmdLine CmdCmdLine" },
31 ["textbox", { class: "plain", id: "dactyl-strut", flex: "1", crop: "end", collapsed: "true" }],
32 ["textbox", { class: "plain", id: "dactyl-mode", flex: "1", crop: "end" }],
33 ["hbox", { id: "dactyl-message-box" },
34 ["label", { class: "plain", id: "dactyl-message-pre", flex: "0", readonly: "true", highlight: "WarningMsg" }],
35 ["textbox", { class: "plain", id: "dactyl-message", flex: "1", readonly: "true" }]],
37 ["hbox", { id: "dactyl-commandline", hidden: "false", class: "dactyl-container", highlight: "Normal CmdNormal", collapsed: "true" },
38 ["label", { id: "dactyl-commandline-prompt", class: "dactyl-commandline-prompt plain", flex: "0", crop: "end", value: "", collapsed: "true" }],
39 ["textbox", { id: "dactyl-commandline-command", class: "dactyl-commandline-command plain", flex: "1", type: "input", timeout: "100",
40 highlight: "Events" }]]],
42 ["vbox", { class: "dactyl-container", hidden: "false", collapsed: "false", highlight: "CmdLine" },
43 ["textbox", { id: "dactyl-multiline-input", class: "plain", flex: "1", rows: "1", hidden: "false", collapsed: "true",
44 multiline: "true", highlight: "Normal Events", events: "multilineInputEvents" }]]],
46 ["stack", { id: "dactyl-statusline-stack", xmlns: "xul" },
47 ["hbox", { id: s + "commandline", hidden: "false", class: "dactyl-container", highlight: "Normal StatusNormal", collapsed: "true" },
48 ["label", { id: s + "commandline-prompt", class: "dactyl-commandline-prompt plain", flex: "0", crop: "end", value: "", collapsed: "true" }],
49 ["textbox", { id: s + "commandline-command", class: "dactyl-commandline-command plain", flex: "1", type: "text", timeout: "100",
50 highlight: "Events", }]]]],
53 ["toolbar", { id: statusline.statusBar.id, xmlns: "xul" },
54 ["vbox", { id: "dactyl-completions-" + s + "commandline-container", class: "dactyl-container", hidden: "false", collapsed: "true" },
55 ["iframe", { class: "dactyl-completions", id: "dactyl-completions-" + s + "commandline", src: "dactyl://content/buffer.xhtml",
56 contextmenu: "dactyl-contextmenu", flex: "1", hidden: "false", collapsed: "false", highlight: "Events",
57 events: "mowEvents" }]]]]
69 getGroup: function () options.get("guioptions").has("C") ? this.commandbar : this.statusbar,
70 getValue: function () this.command
75 defaultGroup: "Normal",
76 getGroup: function () this.commandbar,
77 getValue: function () options.get("guioptions").has("c")
82 test: function test(stack, prev) stack.pop && !isinstance(prev.main, modes.COMMAND_LINE),
83 id: "commandline-command",
84 get: function command_get(elem) {
85 // The long path is because of complications with the
86 // completion preview.
88 return elem.inputField.editor.rootElement.firstChild.textContent;
94 getElement: CommandWidgets.getEditor,
95 getGroup: function (value) this.activeGroup.commandline,
96 onChange: function command_onChange(elem, value) {
97 if (elem.inputField != dactyl.focusedElement)
99 elem.selectionStart = elem.value.length;
100 elem.selectionEnd = elem.value.length;
107 onVisibility: function command_onVisibility(elem, visible) {
115 id: "commandline-prompt",
116 defaultGroup: "CmdPrompt",
117 getGroup: function () this.activeGroup.commandline
122 defaultGroup: "Normal",
123 getElement: CommandWidgets.getEditor,
124 getGroup: function (value) {
125 if (this.command && !options.get("guioptions").has("M"))
126 return this.statusbar;
128 let statusElem = this.statusbar.message;
129 // Currently doesn't work as expected with <hbox> parent.
130 if (false && value && !value[2] && statusElem.editor && statusElem.editor.rootElement.scrollWidth > statusElem.scrollWidth)
131 return this.commandbar;
132 return this.activeGroup.mode;
138 defaultGroup: "WarningMsg",
139 getGroup: function () this.activeGroup.message
144 defaultGroup: "Normal",
145 getGroup: function () this.activeGroup.message,
146 getValue: function () this.message
151 defaultGroup: "ModeMsg",
152 getGroup: function (value) {
153 if (!options.get("guioptions").has("M"))
154 if (this.commandbar.container.clientHeight == 0 ||
155 value && !this.commandbar.commandline.collapsed)
156 return this.statusbar;
157 return this.commandbar;
160 this.updateVisibility();
162 this.initialized = true;
164 addElement: function addElement(obj) {
166 this.elements[obj.name] = obj;
168 function get(prefix, map, id) (obj.getElement || util.identity)(map[id] || document.getElementById(prefix + id));
170 this.active.__defineGetter__(obj.name, () => this.activeGroup[obj.name][obj.name]);
171 this.activeGroup.__defineGetter__(obj.name, () => this.getGroup(obj.name));
173 memoize(this.statusbar, obj.name, () => get("dactyl-statusline-field-", statusline.widgets, (obj.id || obj.name)));
174 memoize(this.commandbar, obj.name, () => get("dactyl-", {}, (obj.id || obj.name)));
176 if (!(obj.noValue || obj.getValue)) {
177 Object.defineProperty(this, obj.name, Modes.boundProperty({
180 get: function get_widgetValue() {
181 let elem = self.getGroup(obj.name, obj.value)[obj.name];
182 if (obj.value != null)
183 return [obj.value[0],
184 obj.get ? obj.get.call(this, elem) : elem.value]
185 .concat(obj.value.slice(2));
189 set: function set_widgetValue(val) {
190 if (val != null && !isArray(val))
191 val = [obj.defaultGroup || "", val];
194 [this.commandbar, this.statusbar].forEach(function (nodeSet) {
195 let elem = nodeSet[obj.name];
199 highlight.highlightNode(elem,
200 (val[0] != null ? val[0] : obj.defaultGroup)
201 .split(/\s/).filter(util.identity)
202 .map(g => g + " " + nodeSet.group + g)
206 obj.onChange.call(this, elem, val);
210 this.updateVisibility();
215 else if (obj.defaultGroup) {
216 [this.commandbar, this.statusbar].forEach(function (nodeSet) {
217 let elem = nodeSet[obj.name];
219 highlight.highlightNode(elem, obj.defaultGroup.split(/\s/)
220 .map(g => g + " " + nodeSet.group + g)
226 getGroup: function getgroup(name, value) {
227 if (!statusline.visible)
228 return this.commandbar;
229 return this.elements[name].getGroup.call(this, arguments.length > 1 ? value : this[name]);
232 updateVisibility: function updateVisibility() {
234 for (let elem in values(this.elements))
236 let value = elem.getValue ? elem.getValue.call(this)
237 : elem.noValue || this[elem.name];
239 let activeGroup = this.getGroup(elem.name, value);
240 for (let group in values([this.commandbar, this.statusbar])) {
241 let meth, node = group[elem.name];
242 let visible = (value && group === activeGroup);
243 if (node && !node.collapsed == !visible) {
245 node.collapsed = !visible;
246 if (elem.onVisibility)
247 elem.onVisibility.call(this, node, visible);
252 // Hack. Collapse hidden elements in the stack.
253 // Might possibly be better to use a deck and programmatically
254 // choose which element to select.
255 function check(node) {
256 if (DOM(node).style.display === "-moz-stack") {
257 let nodes = Array.filter(node.children, n => !n.collapsed && n.boxObject.height);
258 nodes.forEach((node, i) => {
259 node.style.opacity = (i == nodes.length - 1) ? "" : "0";
262 Array.forEach(node.children, check);
264 [this.commandbar.container, this.statusbar.container].forEach(check);
266 // Work around a redrawing bug.
267 if (changed && config.haveGecko("16", "20")) {
268 util.delay(function () {
270 statusline.statusBar.style.paddingRight = "1px";
271 DOM(statusline.statusBar).rect; // Force reflow.
272 statusline.statusBar.style.paddingRight = "";
276 if (this.initialized && loaded.mow && mow.visible)
280 active: Class.Memoize(Object),
281 activeGroup: Class.Memoize(Object),
282 commandbar: Class.Memoize(function () ({ group: "Cmd" })),
283 statusbar: Class.Memoize(function () ({ group: "Status" })),
285 _ready: function _ready(elem) {
286 return elem.contentDocument.documentURI === elem.getAttribute("src") &&
287 ["viewable", "complete"].indexOf(elem.contentDocument.readyState) >= 0;
290 _whenReady: function _whenReady(id, init) {
291 let elem = document.getElementById(id);
292 while (!this._ready(elem))
296 init.call(this, elem);
300 completionContainer: Class.Memoize(function () this.completionList.parentNode),
302 contextMenu: Class.Memoize(function () {
303 ["copy", "copylink", "selectall"].forEach(function (tail) {
304 // some host apps use "hostPrefixContext-copy" ids
305 let css = "menuitem[id$='ontext-" + tail + "']:not([id^=dactyl-])";
306 let style = DOM(css, document).style;
307 DOM("#dactyl-context-" + tail, document).css({
308 listStyleImage: style.listStyleImage,
309 MozImageRegion: style.MozImageRegion
312 return document.getElementById("dactyl-contextmenu");
315 multilineOutput: Class.Memoize(function () this._whenReady("dactyl-multiline-output",
317 highlight.highlightNode(elem.contentDocument.body, "MOW");
320 multilineInput: Class.Memoize(() => document.getElementById("dactyl-multiline-input")),
322 mowContainer: Class.Memoize(() => document.getElementById("dactyl-multiline-output-container"))
324 getEditor: function getEditor(elem) {
325 elem.inputField.QueryInterface(Ci.nsIDOMNSEditableElement);
330 var CommandMode = Class("CommandMode", {
331 init: function CM_init() {
332 this.keepCommand = userContext.hidden_option_command_afterimage;
335 get autocomplete() options["autocomplete"].length,
337 get command() this.widgets.command[1],
338 set command(val) this.widgets.command = val,
340 get prompt() this._open ? this.widgets.prompt : this._prompt,
343 this.widgets.prompt = val;
348 open: function CM_open(command) {
349 dactyl.assert(isinstance(this.mode, modes.COMMAND_LINE),
350 /*L*/"Not opening command line in non-command-line mode.",
353 this.messageCount = commandline.messageCount;
354 modes.push(this.mode, this.extendedMode, this.closure);
356 this.widgets.active.commandline.collapsed = false;
357 this.widgets.prompt = this.prompt;
358 this.widgets.command = command || "";
362 this.input = this.widgets.active.command.inputField;
364 this.history = CommandLine.History(this.input, this.historyKey, this);
367 this.completions = CommandLine.Completions(this.input, this);
369 if (this.completions && command && commandline.commandSession === this)
370 this.completions.autocompleteTimer.flush(true);
373 get active() this === commandline.commandSession,
375 get holdFocus() this.widgets.active.command.inputField,
377 get mappingSelf() this,
379 get widgets() commandline.widgets,
381 enter: function CM_enter(stack) {
382 commandline.commandSession = this;
383 if (stack.pop && commandline.command) {
384 this.onChange(commandline.command);
385 if (this.completions && stack.pop)
386 this.completions.complete(true, false);
390 leave: function CM_leave(stack) {
392 commandline.commandSession = null;
393 this.input.dactylKeyPress = undefined;
395 let waiting = this.accepted && this.completions && this.completions.waiting;
397 this.completions.onComplete = bind("onSubmit", this);
399 if (this.completions)
400 this.completions.cleanup();
405 commandline.hideCompletions();
407 modes.delay(function () {
408 if (!this.keepCommand || commandline.silent || commandline.quiet)
411 this[this.accepted ? "onSubmit" : "onCancel"](commandline.command);
412 if (commandline.messageCount === this.messageCount)
413 commandline.clearMessage();
419 input: function CM_onInput(event) {
420 if (this.completions) {
421 this.resetCompletions();
423 this.completions.autocompleteTimer.tell(false);
425 this.onChange(commandline.command);
427 keyup: function CM_onKeyUp(event) {
428 let key = DOM.Event.stringify(event);
429 if (/-?Tab>$/.test(key) && this.completions)
430 this.completions.tabTimer.flush();
436 onKeyPress: function CM_onKeyPress(events) {
437 if (this.completions)
438 this.completions.previewClear();
440 return true; /* Pass event */
443 onCancel: function (value) {},
445 onChange: function (value) {},
447 onHistory: function (value) {},
449 onSubmit: function (value) {},
451 resetCompletions: function CM_resetCompletions() {
452 if (this.completions)
453 this.completions.clear();
455 this.history.reset();
459 var CommandExMode = Class("CommandExMode", CommandMode, {
463 historyKey: "command",
465 prompt: ["Normal", ":"],
467 complete: function CEM_complete(context) {
469 context.fork("ex", 0, completion, "ex");
472 context.message = _("error.error", e);
476 onSubmit: function CEM_onSubmit(command) {
477 contexts.withContext({ file: /*L*/"[Command Line]", line: 1 },
478 function _onSubmit() {
479 io.withSavedValues(["readHeredoc"], function _onSubmit() {
480 this.readHeredoc = commandline.readHeredoc;
481 commands.repeat = command;
482 dactyl.execute(command);
488 var CommandPromptMode = Class("CommandPromptMode", CommandMode, {
489 init: function init(prompt, params) {
490 this.prompt = isArray(prompt) ? prompt : ["Question", prompt];
491 update(this, params);
492 init.supercall(this);
495 complete: function CPM_complete(context, ...args) {
497 context.forkapply("prompt", 0, this, "completer", args);
500 get mode() modes.PROMPT
504 * This class is used for prompting of user input and echoing of messages.
506 * It consists of a prompt and command field be sure to only create objects of
507 * this class when the chrome is ready.
509 var CommandLine = Module("commandline", {
510 init: function init() {
511 this._callbacks = {};
513 memoize(this, "_store", function () storage.newMap("command-history", { store: true, privateData: true }));
515 for (let name in values(["command", "search"]))
516 if (storage.exists("history-" + name)) {
517 let ary = storage.newArray("history-" + name, { store: true, privateData: true });
519 this._store.set(name, [v for ([k, v] in ary)]);
521 this._store.changed();
524 this._messageHistory = { //{{{
527 let max = options["messages"];
529 // resize if 'messages' has changed
530 if (this._messages.length > max)
531 this._messages = this._messages.splice(this._messages.length - max);
533 return this._messages;
536 get length() this._messages.length,
538 clear: function clear() {
542 filter: function filter(fn, self) {
543 this._messages = this._messages.filter(fn, self);
546 add: function add(message) {
550 if (this._messages.length >= options["messages"])
551 this._messages.shift();
553 this._messages.push(update({
554 timestamp: Date.now()
561 "browser.locationChange": function (webProgress, request, uri) {
567 * Determines whether the command line should be visible.
571 get commandVisible() !!this.commandSession,
574 * Ensure that the multiline input widget is the correct size.
576 _autosizeMultilineInputWidget: function _autosizeMultilineInputWidget() {
577 let lines = this.widgets.multilineInput.value.split("\n").length - 1;
579 this.widgets.multilineInput.setAttribute("rows", Math.max(lines, 1));
583 HL_ERRORMSG: "ErrorMsg",
584 HL_MODEMSG: "ModeMsg",
585 HL_MOREMSG: "MoreMsg",
586 HL_QUESTION: "Question",
587 HL_INFOMSG: "InfoMsg",
588 HL_WARNINGMSG: "WarningMsg",
591 FORCE_MULTILINE : 1 << 0,
592 FORCE_SINGLELINE : 1 << 1,
593 DISALLOW_MULTILINE : 1 << 2, // If an echo() should try to use the single line
594 // but output nothing when the MOW is open; when also
595 // FORCE_MULTILINE is given, FORCE_MULTILINE takes precedence
596 APPEND_TO_MESSAGES : 1 << 3, // Add the string to the message history.
597 ACTIVE_WINDOW : 1 << 4, // Only echo in active window.
599 get completionContext() this._completions.context,
602 get silent() this._silent,
605 this.quiet = this.quiet;
609 get quiet() this._quiet,
612 ["commandbar", "statusbar"].forEach(function (nodeSet) {
613 Array.forEach(this.widgets[nodeSet].commandline.children, function (node) {
614 node.style.opacity = this._quiet || this._silent ? "0" : "";
619 widgets: Class.Memoize(() => CommandWidgets()),
621 runSilently: function runSilently(func, self) {
622 this.withSavedValues(["silent"], function () {
628 get completionList() {
629 let node = this.widgets.active.commandline;
630 if (this.commandSession && this.commandSession.completionList)
631 node = document.getElementById(this.commandSession.completionList);
633 if (!node.completionList) {
634 let elem = document.getElementById("dactyl-completions-" + node.id);
635 util.waitFor(bind(this.widgets._ready, null, elem));
637 node.completionList = ItemList(elem);
638 node.completionList.isAboveMow = node.id ==
639 this.widgets.statusbar.commandline.id;
641 return node.completionList;
644 hideCompletions: function hideCompletions() {
645 for (let nodeSet in values([this.widgets.statusbar, this.widgets.commandbar]))
646 if (nodeSet.commandline.completionList)
647 nodeSet.commandline.completionList.visible = false;
650 _lastClearable: Modes.boundProperty(),
651 messages: Modes.boundProperty(),
653 multilineInputVisible: Modes.boundProperty({
654 set: function set_miwVisible(value) { this.widgets.multilineInput.collapsed = !value; }
658 if (this.commandVisible && this.widgets.command)
659 return commands.lastCommand = this.widgets.command[1];
660 return commands.lastCommand;
663 if (this.commandVisible && (modes.extended & modes.EX))
664 return this.widgets.command = val;
665 return commands.lastCommand = val;
668 clear: function clear(scroll) {
669 if (!scroll || Date.now() - this._lastEchoTime > 5000)
671 this._lastEchoTime = 0;
672 this.hiddenMessages = 0;
674 if (!this.commandSession) {
675 this.widgets.command = null;
676 this.hideCompletions();
679 if (modes.main == modes.OUTPUT_MULTILINE && !mow.isScrollable(1))
682 if (!modes.have(modes.OUTPUT_MULTILINE))
686 clearMessage: function clearMessage() {
687 if (this.widgets.message && this.widgets.message[1] === this._lastClearable) {
688 this.widgets.message = null;
689 this.hiddenMessages = 0;
694 * Displays the multi-line output of a command, preceded by the last
695 * executed ex command string.
697 * @param {XML} xml The output as an E4X XML object.
699 commandOutput: function commandOutput(xml) {
701 this.echo(xml, this.HIGHLIGHT_NORMAL, this.FORCE_MULTILINE);
703 this.echo([["div", { xmlns: "html" }, ":" + this.command], "\n", xml],
704 this.HIGHLIGHT_NORMAL, this.FORCE_MULTILINE);
709 * Hides the command line, and shows any status messages that
712 hide: function hide() {
713 this.widgets.command = null;
717 * Display a message in the command-line area.
719 * @param {string} str
720 * @param {string} highlightGroup
721 * @param {boolean} forceSingle If provided, don't let over-long
722 * messages move to the MOW.
724 _echoLine: function echoLine(str, highlightGroup, forceSingle, silent) {
725 this.widgets.message = str ? [highlightGroup, str, forceSingle] : null;
727 dactyl.triggerObserver("echoLine", str, highlightGroup, null, forceSingle);
729 if (!this.commandVisible)
732 let field = this.widgets.active.message.inputField;
733 if (field.value && !forceSingle && field.editor.rootElement.scrollWidth > field.scrollWidth) {
734 this.widgets.message = null;
735 mow.echo(["span", { highlight: "Message" }, str], highlightGroup, true);
740 get hiddenMessages() this._hiddenMessages,
741 set hiddenMessages(val) {
742 this._hiddenMessages = val;
744 this.widgets["message-pre"] = _("commandline.moreMessages", val) + " ";
746 this.widgets["message-pre"] = null;
752 * Output the given string onto the command line. With no flags, the
753 * message will be shown in the status line if it's short enough to
754 * fit, and contains no new lines, and isn't XML. Otherwise, it will be
757 * @param {string} str
758 * @param {string} highlightGroup The Highlight group for the
761 * @param {number} flags Changes the behavior as follows:
762 * commandline.APPEND_TO_MESSAGES - Causes message to be added to the
763 * messages history, and shown by :messages.
764 * commandline.FORCE_SINGLELINE - Forbids the command from being
765 * pushed to the MOW if it's too long or of there are already
766 * status messages being shown.
767 * commandline.DISALLOW_MULTILINE - Cancels the operation if the MOW
768 * is already visible.
769 * commandline.FORCE_MULTILINE - Forces the message to appear in
773 echo: function echo(data, highlightGroup, flags) {
774 // dactyl.echo uses different order of flags as it omits the highlight group, change commandline.echo argument order? --mst
775 if (this._silent || !this.widgets)
780 highlightGroup = highlightGroup || this.HL_NORMAL;
782 let appendToMessages = (data) => {
783 let message = isObject(data) && !DOM.isJSONXML(data) ? data : { message: data };
785 // Make sure the memoized message property is an instance property.
787 this._messageHistory.add(update({ highlight: highlightGroup }, message));
788 return message.message;
791 if (flags & this.APPEND_TO_MESSAGES)
792 data = appendToMessages(data);
794 if ((flags & this.ACTIVE_WINDOW) && window != overlay.activeWindow)
797 if ((flags & this.DISALLOW_MULTILINE) && !this.widgets.mowContainer.collapsed)
800 let forceSingle = flags & (this.FORCE_SINGLELINE | this.DISALLOW_MULTILINE);
801 let action = this._echoLine;
803 if ((flags & this.FORCE_MULTILINE) || (/\n/.test(data) || !isinstance(data, [_, "String"])) && !(flags & this.FORCE_SINGLELINE))
804 action = mow.closure.echo;
806 let checkSingleLine = () => action == this._echoLine;
809 this._lastEcho = null;
810 this.hiddenMessages = 0;
814 if (checkSingleLine() && !this.widgets.mowContainer.collapsed) {
815 highlightGroup += " Message";
816 action = mow.closure.echo;
818 else if (!checkSingleLine() && this.widgets.mowContainer.collapsed) {
819 if (this._lastEcho && this.widgets.message && this.widgets.message[1] == this._lastEcho.msg) {
820 if (!(this._lastEcho.flags & this.APPEND_TO_MESSAGES))
821 appendToMessages(this._lastEcho.data);
824 ["span", { highlight: "Message" },
825 ["span", { highlight: "WarningMsg" },
826 _("commandline.moreMessages", this.hiddenMessages + 1) + " "],
828 this.widgets.message[0], true);
830 this.hiddenMessages = 0;
833 else if (this._lastEcho && this.widgets.message && this.widgets.message[1] == this._lastEcho.msg) {
834 if (!(this._lastEcho.flags & this.APPEND_TO_MESSAGES))
835 appendToMessages(this._lastEcho.data);
836 if (checkSingleLine() && !(flags & this.APPEND_TO_MESSAGES))
837 appendToMessages(data);
839 flags |= this.APPEND_TO_MESSAGES;
840 this.hiddenMessages++;
842 this._lastEcho = checkSingleLine() && { flags: flags, msg: data, data: arguments[0] };
845 this._lastClearable = action === this._echoLine && String(data);
846 this._lastEchoTime = (flags & this.FORCE_SINGLELINE) && Date.now();
849 action.call(this, data, highlightGroup, checkSingleLine());
854 * Prompt the user. Sets modes.main to COMMAND_LINE, which the user may
855 * pop at any time to close the prompt.
857 * @param {string} prompt The input prompt to use.
858 * @param {function(string)} callback
859 * @param {Object} extra
860 * @... {function} onChange - A function to be called with the current
861 * input every time it changes.
862 * @... {function(CompletionContext)} completer - A completion function
863 * for the user's input.
864 * @... {string} promptHighlight - The HighlightGroup used for the
865 * prompt. @default "Question"
866 * @... {string} default - The initial value that will be returned
867 * if the user presses <CR> straightaway. @default ""
869 input: function _input(prompt, callback, extra = {}) {
870 CommandPromptMode(prompt, update({ onSubmit: callback }, extra)).open();
873 readHeredoc: function readHeredoc(end) {
875 commandline.inputMultiline(end, function (res) { args = res; });
876 util.waitFor(() => args !== undefined);
881 * Get a multi-line input from a user, up to but not including the line
882 * which matches the given regular expression. Then execute the
883 * callback with that string as a parameter.
885 * @param {string} end
886 * @param {function(string)} callback
888 // FIXME: Buggy, especially when pasting.
889 inputMultiline: function inputMultiline(end, callback) {
890 let cmd = this.command;
892 end: "\n" + end + "\n",
896 modes.push(modes.INPUT_MULTILINE, null, {
898 leave: function leave() {
906 this._echoLine(cmd, this.HL_NORMAL);
908 // save the arguments, they are needed in the event handler onKeyPress
910 this.multilineInputVisible = true;
911 this.widgets.multilineInput.value = "";
912 this._autosizeMultilineInputWidget();
914 this.timeout(function () { dactyl.focus(this.widgets.multilineInput); }, 10);
917 get commandMode() this.commandSession && isinstance(modes.main, modes.COMMAND_LINE),
920 iter(CommandMode.prototype.events).map(
921 ([event, handler]) => [
922 event, function (event) {
923 if (this.commandMode)
924 handler.call(this.commandSession, event);
928 focus: function onFocus(event) {
929 if (!this.commandSession
930 && event.originalTarget === this.widgets.active.command.inputField) {
938 get mowEvents() mow.events,
941 * Multiline input events, they will come straight from
942 * #dactyl-multiline-input in the XUL.
944 * @param {Event} event
946 multilineInputEvents: {
947 blur: function onBlur(event) {
948 if (modes.main == modes.INPUT_MULTILINE)
949 this.timeout(function () {
950 dactyl.focus(this.widgets.multilineInput.inputField);
953 input: function onInput(event) {
954 this._autosizeMultilineInputWidget();
958 updateOutputHeight: deprecated("mow.resize", function updateOutputHeight(open, extra) mow.resize(open, extra)),
960 withOutputToString: function withOutputToString(fn, self, ...args) {
961 dactyl.registerObserver("echoLine", observe, true);
962 dactyl.registerObserver("echoMultiline", observe, true);
965 function observe(str, highlight, dom) {
966 output.push(dom && !isString(str) ? dom : str);
969 this.savingOutput = true;
970 dactyl.trapErrors.apply(dactyl, [fn, self].concat(args));
971 this.savingOutput = false;
972 return output.map(elem => elem instanceof Node ? DOM.stringify(elem) : elem)
977 * A class for managing the history of an input field.
979 * @param {HTMLInputElement} inputField
980 * @param {string} mode The mode for which we need history.
982 History: Class("History", {
983 init: function init(inputField, mode, session) {
985 this.input = inputField;
987 this.session = session;
989 get store() commandline._store.get(this.mode, []),
990 set store(ary) { commandline._store.set(this.mode, ary); },
992 * Reset the history index to the first entry.
994 reset: function reset() {
998 * Save the last entry to the permanent store. All duplicate entries
999 * are removed and the list is truncated, if necessary.
1001 save: function save() {
1002 if (events.feedingKeys)
1005 let str = this.input.value;
1006 if (/^\s*$/.test(str))
1009 let privateData = this.checkPrivate(str);
1010 if (privateData == "never-save")
1013 this.store = this.store.filter(line => (line.value || line) != str);
1014 dactyl.trapErrors(function () {
1015 this.store.push({ value: str, timestamp: Date.now() * 1000, privateData: privateData });
1017 this.store = this.store.slice(Math.max(0, this.store.length - options["history"]));
1020 * @property {function} Returns whether a data item should be
1021 * considered private.
1023 checkPrivate: function checkPrivate(str) {
1024 // Not really the ideal place for this check.
1025 if (this.mode == "command")
1026 return commands.hasPrivateData(str);
1030 * Replace the current input field value.
1032 * @param {string} val The new value.
1034 replace: function replace(val) {
1035 editor.withSavedValues(["skipSave"], function () {
1036 editor.skipSave = true;
1038 this.input.dactylKeyPress = undefined;
1039 if (this.completions)
1040 this.completions.previewClear();
1041 this.input.value = val;
1042 this.session.onHistory(val);
1047 * Move forward or backward in history.
1049 * @param {boolean} backward Direction to move.
1050 * @param {boolean} matchCurrent Search for matches starting
1051 * with the current input value.
1053 select: function select(backward, matchCurrent) {
1054 // always reset the tab completion if we use up/down keys
1055 if (this.session.completions)
1056 this.session.completions.reset();
1058 let diff = backward ? -1 : 1;
1060 if (this.index == null) {
1061 this.original = this.input.value;
1062 this.index = this.store.length;
1065 // search the history for the first item matching the current
1066 // command-line string
1069 if (this.index < 0 || this.index > this.store.length) {
1070 this.index = Math.constrain(this.index, 0, this.store.length);
1072 // I don't know why this kludge is needed. It
1073 // prevents the caret from moving to the end of
1075 if (this.input.value == "") {
1076 this.input.value = " ";
1077 this.input.value = "";
1082 let hist = this.store[this.index];
1083 // user pressed DOWN when there is no newer history item
1085 hist = this.original;
1087 hist = (hist.value || hist);
1089 if (!matchCurrent || hist.substr(0, this.original.length) == this.original) {
1098 * A class for tab completions on an input field.
1100 * @param {Object} input
1102 Completions: Class("Completions", {
1111 init: function init(input, session) {
1114 this.context = CompletionContext(input.QueryInterface(Ci.nsIDOMNSEditableElement).editor);
1115 this.context.onUpdate = function onUpdate() { self.asyncUpdate(this); };
1117 this.editor = input.editor;
1119 this.session = session;
1121 this.wildmode = options.get("wildmode");
1122 this.wildtypes = this.wildmode.value;
1124 this.itemList = commandline.completionList;
1125 this.itemList.open(this.context);
1127 dactyl.registerObserver("events.doneFeeding", this.closure.onDoneFeeding, true);
1129 this.autocompleteTimer = Timer(200, 500, function autocompleteTell(tabPressed) {
1130 if (events.feedingKeys && !tabPressed)
1131 this.ignoredCount++;
1132 else if (this.session.autocomplete) {
1133 this.itemList.visible = true;
1134 this.complete(true, false);
1138 this.tabTimer = Timer(0, 0, function tabTell(event) {
1139 let tabCount = this.tabCount;
1141 this.tab(tabCount, event.altKey && options["altwildmode"]);
1152 onDoneFeeding: function onDoneFeeding() {
1153 if (this.ignoredCount)
1154 this.autocompleteTimer.flush(true);
1155 this.ignoredCount = 0;
1161 onTab: function onTab(event) {
1162 this.tabCount += event.shiftKey ? -1 : 1;
1163 this.tabTimer.tell(event);
1166 get activeContexts() this.context.contextList
1167 .filter(c => c.items.length || c.incomplete),
1170 * Returns the current completion string relative to the
1171 * offset of the currently selected context.
1174 let offset = this.selected ? this.selected[0].offset : this.start;
1175 return commandline.command.slice(offset, this.caret);
1179 * Updates the input field from *offset* to {@link #caret}
1180 * with the value *value*. Afterward, the caret is moved
1181 * just after the end of the updated text.
1183 * @param {number} offset The offset in the original input
1184 * string at which to insert *value*.
1185 * @param {string} value The value to insert.
1187 setCompletion: function setCompletion(offset, value) {
1188 editor.withSavedValues(["skipSave"], function () {
1189 editor.skipSave = true;
1190 this.previewClear();
1193 var [input, caret] = [this.originalValue, this.originalCaret];
1195 input = this.getCompletion(offset, value);
1196 caret = offset + value.length;
1199 // Change the completion text.
1200 // The second line is a hack to deal with some substring
1201 // preview corner cases.
1202 commandline.widgets.active.command.value = input;
1203 this.editor.selection.focusNode.textContent = input;
1206 this._caret = this.caret;
1208 this.input.dactylKeyPress = undefined;
1213 * For a given offset and completion string, returns the
1214 * full input value after selecting that item.
1216 * @param {number} offset The offset at which to insert the
1218 * @param {string} value The value to insert.
1219 * @returns {string};
1221 getCompletion: function getCompletion(offset, value) {
1222 return this.originalValue.substr(0, offset)
1224 + this.originalValue.substr(this.originalCaret);
1227 get selected() this.itemList.selected,
1228 set selected(tuple) {
1229 if (!array.equals(tuple || [],
1230 this.itemList.selected || []))
1231 this.itemList.select(tuple);
1234 this.setCompletion(null);
1236 let [ctxt, idx] = tuple;
1237 this.setCompletion(ctxt.offset, ctxt.items[idx].result);
1241 get caret() this.editor.selection.getRangeAt(0).startOffset,
1243 this.editor.selection.collapse(this.editor.rootElement.firstChild, offset);
1246 get start() this.context.allItems.start,
1248 get items() this.context.allItems.items,
1250 get substring() this.context.longestAllSubstring,
1252 get wildtype() this.wildtypes[this.wildIndex] || "",
1255 * Cleanup resources used by this completion session. This
1256 * instance should not be used again once this method is
1259 cleanup: function cleanup() {
1260 dactyl.unregisterObserver("events.doneFeeding", this.closure.onDoneFeeding);
1261 this.previewClear();
1263 this.tabTimer.reset();
1264 this.autocompleteTimer.reset();
1265 if (!this.onComplete)
1266 this.context.cancelAll();
1268 this.itemList.visible = false;
1269 this.input.dactylKeyPress = undefined;
1270 this.hasQuit = true;
1274 * Run the completer.
1276 * @param {boolean} show Passed to {@link #reset}.
1277 * @param {boolean} tabPressed Should be set to true if, and
1278 * only if, this function is being called in response
1281 complete: function complete(show, tabPressed) {
1282 this.session.ignoredCount = 0;
1284 this.waiting = null;
1285 this.context.reset();
1286 this.context.tabPressed = tabPressed;
1288 this.session.complete(this.context);
1289 if (!this.session.active)
1292 this.reset(show, tabPressed);
1294 this._caret = this.caret;
1298 * Clear any preview string and cancel any pending
1299 * asynchronous context. Called when there is further input
1302 clear: function clear() {
1303 this.context.cancelAll();
1304 this.wildIndex = -1;
1305 this.previewClear();
1309 * Saves the current input state. To be called before an
1310 * item is selected in a new set of completion responses.
1313 saveInput: function saveInput() {
1314 this.originalValue = this.context.value;
1315 this.originalCaret = this.caret;
1319 * Resets the completion state.
1321 * @param {boolean} show If true and options allow the
1322 * completion list to be shown, show it.
1324 reset: function reset(show) {
1325 this.waiting = null;
1326 this.wildIndex = -1;
1331 this.itemList.update();
1332 this.context.updateAsync = true;
1333 if (this.haveType("list"))
1334 this.itemList.visible = true;
1342 * Calls when an asynchronous completion context has new
1343 * results to return.
1345 * @param {CompletionContext} context The changed context.
1348 asyncUpdate: function asyncUpdate(context) {
1350 let item = this.getItem(this.waiting);
1351 if (item && this.waiting && this.onComplete) {
1352 util.trapErrors("onComplete", this,
1353 this.getCompletion(this.waiting[0].offset,
1355 this.waiting = null;
1356 this.context.cancelAll();
1361 let value = this.editor.selection.focusNode.textContent;
1364 if (this.itemList.visible)
1365 this.itemList.updateContext(context);
1367 if (this.waiting && this.waiting[0] == context)
1368 this.select(this.waiting);
1369 else if (!this.waiting) {
1370 let cursor = this.selected;
1371 if (cursor && cursor[0] == context) {
1372 let item = this.getItem(cursor);
1373 if (!item || this.completion != item.result)
1374 this.itemList.select(null);
1382 * Returns true if the currently selected 'wildmode' index
1383 * has the given completion type.
1385 haveType: function haveType(type)
1386 this.wildmode.checkHas(this.wildtype, type == "first" ? "" : type),
1389 * Returns the completion item for the given selection
1392 * @param {[CompletionContext,number]} tuple The spec of the
1394 * @default {@link #selected}
1397 getItem: function getItem(tuple = this.selected)
1398 tuple && tuple[0] && tuple[0].items[tuple[1]],
1401 * Returns a tuple representing the next item, at the given
1402 * *offset*, from *tuple*.
1404 * @param {[CompletionContext,number]} tuple The offset from
1406 * @default {@link #selected}
1407 * @param {number} offset The positive or negative offset to
1410 * @param {boolean} noWrap If true, and the search would
1411 * wrap, return null.
1413 nextItem: function nextItem(tuple, offset, noWrap) {
1414 if (tuple === undefined)
1415 tuple = this.selected;
1417 return this.itemList.getRelativeItem(offset || 1, tuple, noWrap);
1421 * The last previewed substring.
1427 * Displays a preview of the text provided by the next <Tab>
1428 * press if the current input is an anchored substring of
1431 preview: function preview() {
1432 this.previewClear();
1433 if (this.wildIndex < 0 || this.caret < this.input.value.length
1434 || !this.activeContexts.length || this.waiting)
1438 switch (this.wildtype.replace(/.*:/, "")) {
1440 var cursor = this.nextItem(null);
1443 if (this.items.length > 1) {
1444 substring = this.substring;
1449 cursor = this.nextItem();
1453 substring = this.getItem(cursor).result;
1455 // Don't show 1-character substrings unless we've just hit backspace
1456 if (substring.length < 2 && this.lastSubstring.indexOf(substring))
1459 this.lastSubstring = substring;
1461 let value = this.completion;
1462 if (util.compareIgnoreCase(value, substring.substr(0, value.length)))
1465 substring = substring.substr(value.length);
1466 this.removeSubstring = substring;
1468 let node = DOM.fromJSON(["span", { highlight: "Preview" }, substring],
1471 this.withSavedValues(["caret"], function () {
1472 this.editor.insertNode(node, this.editor.rootElement, 1);
1477 * Clears the currently displayed next-<Tab> preview string.
1479 previewClear: function previewClear() {
1480 let node = this.editor.rootElement.firstChild;
1481 if (node && node.nextSibling) {
1483 DOM(node.nextSibling).remove();
1486 node.nextSibling.textContent = "";
1489 else if (this.removeSubstring) {
1490 let str = this.removeSubstring;
1491 let cmd = commandline.widgets.active.command.value;
1492 if (cmd.substr(cmd.length - str.length) == str)
1493 commandline.widgets.active.command.value = cmd.substr(0, cmd.length - str.length);
1495 delete this.removeSubstring;
1499 * Selects a completion based on the value of *idx*.
1501 * @param {[CompletionContext,number]|const object} The
1502 * (context,index) tuple of the item to select, or an
1503 * offset constant from this object.
1504 * @param {number} count When given an offset constant,
1505 * select *count* units.
1507 * @param {boolean} fromTab If true, this function was
1508 * called by {@link #tab}.
1512 select: function select(idx, count = 1, fromTab = false) {
1516 idx = this.nextItem(this.waiting || this.selected,
1517 idx == this.UP ? -count : count,
1522 case this.CTXT_DOWN:
1523 let groups = this.itemList.activeGroups;
1524 let i = Math.max(0, groups.indexOf(this.itemList.selectedGroup));
1526 i += idx == this.CTXT_DOWN ? 1 : -1;
1532 idx = [groups[i].context, 0];
1536 case this.PAGE_DOWN:
1537 idx = this.itemList.getRelativePage(idx == this.PAGE_DOWN ? 1 : -1);
1549 this.wildIndex = this.wildtypes.length - 1;
1551 if (idx && idx[1] >= idx[0].items.length) {
1552 if (!idx[0].incomplete)
1553 this.waiting = null;
1556 statusline.progress = _("completion.waitingForResults");
1561 this.waiting = null;
1563 this.itemList.select(idx, null, position);
1564 this.selected = idx;
1568 if (this.selected == null)
1569 statusline.progress = "";
1571 statusline.progress = _("completion.matchIndex",
1572 this.itemList.getOffset(idx),
1573 this.itemList.itemCount);
1577 * Selects a completion result based on the 'wildmode'
1578 * option, or the value of the *wildmode* parameter.
1580 * @param {number} offset The positive or negative number of
1581 * tab presses to process.
1582 * @param {[string]} wildmode A 'wildmode' value to
1583 * substitute for the value of the 'wildmode' option.
1586 tab: function tab(offset, wildmode) {
1587 this.autocompleteTimer.flush();
1588 this.ignoredCount = 0;
1590 if (this._caret != this.caret)
1592 this._caret = this.caret;
1594 // Check if we need to run the completer.
1595 if (this.context.waitingForTab || this.wildIndex == -1)
1596 this.complete(true, true);
1598 this.wildtypes = wildmode || options["wildmode"];
1599 let count = Math.abs(offset);
1600 let steps = Math.constrain(this.wildtypes.length - this.wildIndex,
1602 count = Math.max(1, count - steps);
1605 this.wildIndex = Math.min(this.wildIndex, this.wildtypes.length - 1);
1606 switch (this.wildtype.replace(/.*:/, "")) {
1608 this.select(this.nextItem(null));
1611 if (this.itemList.itemCount > 1) {
1612 if (this.substring && this.substring.length > this.completion.length)
1613 this.setCompletion(this.start, this.substring);
1618 let c = steps ? 1 : count;
1619 this.select(offset < 0 ? this.UP : this.DOWN, c, true);
1623 if (this.haveType("list"))
1624 this.itemList.visible = true;
1629 if (this.items.length == 0 && !this.waiting)
1635 * Evaluate a JavaScript expression and return a string suitable
1638 * @param {string} arg
1639 * @param {boolean} useColor When true, the result is a
1640 * highlighted XML object.
1642 echoArgumentToString: function (arg, useColor) {
1646 arg = dactyl.userEval(arg);
1648 arg = util.objectToString(arg, useColor);
1649 else if (callable(arg))
1650 arg = String.replace(arg, "/* use strict */ \n", "/* use strict */ ");
1651 else if (!isString(arg) && useColor)
1652 arg = template.highlight(arg);
1656 commands: function initCommands() {
1660 description: "Echo the expression",
1665 description: "Echo the expression as an error message",
1666 action: dactyl.echoerr
1670 description: "Echo the expression as an informational message",
1671 action: dactyl.echomsg
1673 ].forEach(function (command) {
1674 commands.add([command.name],
1675 command.description,
1677 command.action(CommandLine.echoArgumentToString(args[0] || "", true));
1679 completer: function (context) completion.javascript(context),
1684 commands.add(["mes[sages]"],
1685 "Display previously shown messages",
1687 // TODO: are all messages single line? Some display an aggregation
1688 // of single line messages at least. E.g. :source
1689 if (commandline._messageHistory.length == 1) {
1690 let message = commandline._messageHistory.messages[0];
1691 commandline.echo(message.message, message.highlight, commandline.FORCE_SINGLELINE);
1693 else if (commandline._messageHistory.length > 1) {
1694 commandline.commandOutput(
1695 template.map(commandline._messageHistory.messages, message =>
1696 ["div", { highlight: message.highlight + " Message" },
1702 commands.add(["messc[lear]"],
1703 "Clear the message history",
1704 function () { commandline._messageHistory.clear(); },
1707 commands.add(["sil[ent]"],
1708 "Run a command silently",
1710 commandline.runSilently(() => { commands.execute(args[0] || "", null, true); });
1712 completer: function (context) completion.ex(context),
1717 modes: function initModes() {
1718 initModes.require("editor");
1720 modes.addMode("COMMAND_LINE", {
1722 description: "Active when the command line is focused",
1725 get mappingSelf() commandline.commandSession
1727 // this._extended modes, can include multiple modes, and even main modes
1728 modes.addMode("EX", {
1729 description: "Ex command mode, active when the command line is open for Ex commands",
1730 bases: [modes.COMMAND_LINE]
1732 modes.addMode("PROMPT", {
1733 description: "Active when a prompt is open in the command line",
1734 bases: [modes.COMMAND_LINE]
1737 modes.addMode("INPUT_MULTILINE", {
1738 description: "Active when the command line's multiline input buffer is open",
1739 bases: [modes.INSERT]
1742 mappings: function initMappings() {
1744 mappings.add([modes.COMMAND],
1745 [":"], "Enter Command Line mode",
1746 function () { CommandExMode().open(""); });
1748 mappings.add([modes.INPUT_MULTILINE],
1749 ["<Return>", "<C-j>", "<C-m>"], "Begin a new line",
1750 function ({ self }) {
1751 let text = "\n" + commandline.widgets.multilineInput
1752 .value.substr(0, commandline.widgets.multilineInput.selectionStart)
1755 let index = text.indexOf(self.end);
1758 text = text.substring(1, index);
1761 return () => self.callback.call(commandline, text);
1766 let bind = function bind(...args) mappings.add.apply(mappings, [[modes.COMMAND_LINE]].concat(args));
1768 bind(["<Esc>", "<C-[>"], "Stop waiting for completions or exit Command Line mode",
1769 function ({ self }) {
1770 if (self.completions && self.completions.waiting)
1771 self.completions.waiting = null;
1776 // Any "non-keyword" character triggers abbreviation expansion
1777 // TODO: Add "<CR>" and "<Tab>" to this list
1778 // At the moment, adding "<Tab>" breaks tab completion. Adding
1779 // "<CR>" has no effect.
1780 // TODO: Make non-keyword recognition smarter so that there need not
1781 // be two lists of the same characters (one here and a regexp in
1783 bind(["<Space>", '"', "'"], "Expand command line abbreviation",
1784 function ({ self }) {
1785 self.resetCompletions();
1786 editor.expandAbbreviation(modes.COMMAND_LINE);
1790 bind(["<Return>", "<C-j>", "<C-m>"], "Accept the current input",
1791 function ({ self }) {
1792 if (self.completions)
1793 self.completions.tabTimer.flush();
1795 let command = commandline.command;
1797 self.accepted = true;
1798 return function () { modes.pop(); };
1802 [["<Up>", "<A-p>", "<cmd-prev-match>"], "previous matching", true, true],
1803 [["<S-Up>", "<C-p>", "<cmd-prev>"], "previous", true, false],
1804 [["<Down>", "<A-n>", "<cmd-next-match>"], "next matching", false, true],
1805 [["<S-Down>", "<C-n>", "<cmd-next>"], "next", false, false]
1806 ].forEach(function ([keys, desc, up, search]) {
1807 bind(keys, "Recall the " + desc + " command line from the history list",
1808 function ({ self }) {
1809 dactyl.assert(self.history);
1810 self.history.select(up, search);
1814 bind(["<A-Tab>", "<Tab>", "<A-compl-next>", "<compl-next>"],
1815 "Select the next matching completion item",
1816 function ({ keypressEvents, self }) {
1817 dactyl.assert(self.completions);
1818 self.completions.onTab(keypressEvents[0]);
1821 bind(["<A-S-Tab>", "<S-Tab>", "<A-compl-prev>", "<compl-prev>"],
1822 "Select the previous matching completion item",
1823 function ({ keypressEvents, self }) {
1824 dactyl.assert(self.completions);
1825 self.completions.onTab(keypressEvents[0]);
1828 bind(["<C-Tab>", "<A-f>", "<compl-next-group>"],
1829 "Select the next matching completion group",
1830 function ({ keypressEvents, self }) {
1831 dactyl.assert(self.completions);
1832 self.completions.tabTimer.flush();
1833 self.completions.select(self.completions.CTXT_DOWN);
1836 bind(["<C-S-Tab>", "<A-S-f>", "<compl-prev-group>"],
1837 "Select the previous matching completion group",
1838 function ({ keypressEvents, self }) {
1839 dactyl.assert(self.completions);
1840 self.completions.tabTimer.flush();
1841 self.completions.select(self.completions.CTXT_UP);
1844 bind(["<C-f>", "<PageDown>", "<compl-next-page>"],
1845 "Select the next page of completions",
1846 function ({ keypressEvents, self }) {
1847 dactyl.assert(self.completions);
1848 self.completions.tabTimer.flush();
1849 self.completions.select(self.completions.PAGE_DOWN);
1852 bind(["<C-b>", "<PageUp>", "<compl-prev-page>"],
1853 "Select the previous page of completions",
1854 function ({ keypressEvents, self }) {
1855 dactyl.assert(self.completions);
1856 self.completions.tabTimer.flush();
1857 self.completions.select(self.completions.PAGE_UP);
1860 bind(["<BS>", "<C-h>"], "Delete the previous character",
1862 if (!commandline.command)
1868 bind(["<C-]>", "<C-5>"], "Expand command line abbreviation",
1869 function () { editor.expandAbbreviation(modes.COMMAND_LINE); });
1871 options: function initOptions() {
1872 options.add(["history", "hi"],
1873 "Number of Ex commands and search patterns to store in the command-line history",
1875 { validator: function (value) value >= 0 });
1877 options.add(["maxitems"],
1878 "Maximum number of completion items to display at once",
1880 { validator: function (value) value >= 1 });
1882 options.add(["messages", "msgs"],
1883 "Number of messages to store in the :messages history",
1885 { validator: function (value) value >= 0 });
1887 sanitizer: function initSanitizer() {
1888 sanitizer.addItem("commandline", {
1889 description: "Command-line and search history",
1891 action: function (timespan, host) {
1892 let store = commandline._store;
1893 for (let [k, v] in store) {
1895 store.set(k, v.filter(item =>
1896 !(timespan.contains(item.timestamp) && (!host || commands.hasDomain(item.value, host)))));
1898 store.set(k, v.filter(item => !timespan.contains(item.timestamp)));
1902 // Delete history-like items from the commandline and messages on history purge
1903 sanitizer.addItem("history", {
1904 action: function (timespan, host) {
1905 commandline._store.set("command",
1906 commandline._store.get("command", []).filter(item =>
1907 !(timespan.contains(item.timestamp) && (host ? commands.hasDomain(item.value, host)
1908 : item.privateData))));
1910 commandline._messageHistory.filter(item =>
1911 ( !timespan.contains(item.timestamp * 1000)
1912 || !item.domains && !item.privateData
1913 || host && ( !item.domains
1914 || !item.domains.some(d => util.isSubdomain(d, host)))));
1917 sanitizer.addItem("messages", {
1918 description: "Saved :messages",
1919 action: function (timespan, host) {
1920 commandline._messageHistory.filter(item =>
1921 ( !timespan.contains(item.timestamp * 1000)
1922 || host && ( !item.domains
1923 || !item.domains.some(d => util.isSubdomain(d, host)))));
1930 * The list which is used for the completion box.
1932 * @param {string} id The id of the iframe which will display the list. It
1933 * must be in its own container element, whose height it will update as
1937 var ItemList = Class("ItemList", {
1940 init: function init(frame) {
1943 this.doc = frame.contentDocument;
1944 this.win = frame.contentWindow;
1945 this.body = this.doc.body;
1946 this.container = frame.parentNode;
1948 highlight.highlightNode(this.doc.body, "Comp");
1950 this._onResize = Timer(20, 400, function _onResize(event) {
1952 this.onResize(event);
1954 this._resize = Timer(20, 400, function _resize(flags) {
1959 DOM(this.win).resize(this._onResize.closure.tell);
1963 ["div", { highlight: "Normal", style: "white-space: nowrap", key: "root" },
1964 ["div", { key: "wrapper" },
1965 ["div", { highlight: "Completions", key: "noCompletions" },
1966 ["span", { highlight: "Title" },
1967 _("completion.noCompletions")]],
1968 ["div", { key: "completions" }]],
1970 ["div", { highlight: "Completions" },
1971 template.map(util.range(0, options["maxitems"] * 2), i =>
1972 ["div", { highlight: "CompItem NonText" },
1975 get itemCount() this.context.contextList
1976 .reduce((acc, ctxt) => acc + ctxt.items.length, 0),
1978 get visible() !this.container.collapsed,
1979 set visible(val) this.container.collapsed = !val,
1981 get activeGroups() this.context.contextList
1982 .filter(c => c.items.length || c.message || c.incomplete)
1983 .map(this.getGroup, this),
1985 get selected() let (g = this.selectedGroup) g && g.selectedIdx != null
1986 ? [g.context, g.selectedIdx] : null,
1988 getRelativeItem: function getRelativeItem(offset, tuple, noWrap) {
1989 let groups = this.activeGroups;
1993 let group = this.selectedGroup || groups[0];
1994 let start = group.selectedIdx || 0;
1995 if (tuple === null) { // Kludge.
1997 tuple = [this.activeGroups[0], -1];
1999 let group = this.activeGroups.slice(-1)[0];
2000 tuple = [group, group.itemCount];
2004 [group, start] = tuple;
2006 group = this.getGroup(group);
2008 start = (group.offsets.start + start + offset);
2010 start %= this.itemCount || 1;
2011 if (start < 0 && (!noWrap || arguments[1] === null))
2012 start += this.itemCount;
2014 if (noWrap && offset > 0) {
2015 // Check if we've passed any incomplete contexts
2017 let i = groups.indexOf(group);
2018 util.assert(i >= 0, undefined, false);
2019 for (; i < groups.length; i++) {
2020 let end = groups[i].offsets.start + groups[i].itemCount;
2021 if (start >= end && groups[i].context.incomplete)
2022 return [groups[i].context, start - groups[i].offsets.start];
2029 if (start < 0 || start >= this.itemCount)
2032 group = array.nth(groups, g => let (i = start - g.offsets.start) i >= 0 && i < g.itemCount, 0);
2033 return [group.context, start - group.offsets.start];
2036 getRelativePage: function getRelativePage(offset, tuple, noWrap) {
2037 offset *= this.maxItems;
2038 // Try once with wrapping disabled.
2039 let res = this.getRelativeItem(offset, tuple, true);
2043 let sign = offset / Math.abs(offset);
2045 let off = this.getOffset(tuple === null ? null : tuple || this.selected);
2047 // Unselected. Defer to getRelativeItem.
2048 res = this.getRelativeItem(offset, null, noWrap);
2049 else if (~[0, this.itemCount - 1].indexOf(off))
2050 // At start or end. Jump to other end.
2051 res = this.getRelativeItem(sign, null, noWrap);
2053 // Wrapped. Go to beginning or end.
2054 res = this.getRelativeItem(-sign, null);
2060 * Initializes the ItemList for use with a new root completion
2063 * @param {CompletionContext} context The new root context.
2065 open: function open(context) {
2066 this.context = context;
2068 this.container.height = 0;
2070 this.maxItems = options["maxitems"];
2072 DOM(this.rootXML, this.doc, this.nodes)
2073 .appendTo(DOM(this.body).empty());
2079 * Updates the absolute result indices of all groups after
2080 * results have changed.
2083 updateOffsets: function updateOffsets() {
2084 let total = this.itemCount;
2086 for (let group in values(this.activeGroups)) {
2087 group.offsets = { start: count, end: total - count - group.itemCount };
2088 count += group.itemCount;
2093 * Updates the set and state of active groups for a new set of
2094 * completion results.
2096 update: function update() {
2097 DOM(this.nodes.completions).empty();
2099 let container = DOM(this.nodes.completions);
2100 let groups = this.activeGroups;
2101 for (let group in values(groups)) {
2103 container.append(group.nodes.root);
2106 this.updateOffsets();
2108 DOM(this.nodes.noCompletions).toggle(!groups.length);
2110 this.startPos = null;
2111 this.select(groups[0] && groups[0].context, null);
2113 this._resize.tell();
2117 * Updates the group for *context* after an asynchronous update
2120 * @param {CompletionContext} context The context which has
2123 updateContext: function updateContext(context) {
2124 let group = this.getGroup(context);
2125 this.updateOffsets();
2127 if (~this.activeGroups.indexOf(group))
2130 DOM(group.nodes.root).remove();
2131 if (this.selectedGroup == group)
2132 this.selectedGroup = null;
2135 let g = this.selectedGroup;
2136 this.select(g, g && g.selectedIdx);
2140 * Updates the DOM to reflect the current state of all groups.
2143 draw: function draw() {
2144 for (let group in values(this.activeGroups))
2147 // We need to collect all of the rescrolling functions in
2148 // one go, as the height calculation that they need to do
2149 // would force a reflow after each DOM modification.
2150 this.activeGroups.filter(g => !g.collapsed)
2151 .map(g => g.rescrollFunc)
2155 this.win.scrollTo(0, 0);
2157 this._resize.tell(ItemList.RESIZE_BRIEF);
2160 onResize: function onResize() {
2161 if (this.selectedGroup)
2162 this.selectedGroup.rescrollFunc();
2168 * Resizes the list after an update.
2171 resize: function resize(flags) {
2172 let { completions, root } = this.nodes;
2175 root.style.minWidth = document.getElementById("dactyl-commandline").scrollWidth + "px";
2177 let { minHeight } = this;
2178 if (mow.visible && this.isAboveMow) // Kludge.
2179 minHeight -= mow.wantedHeight;
2181 let needed = this.win.scrollY + DOM(completions).rect.bottom;
2182 this.minHeight = Math.max(minHeight, needed);
2185 root.style.minWidth = "";
2187 let height = this.visible ? parseFloat(this.container.height) : 0;
2188 if (this.minHeight <= minHeight || !mow.visible)
2189 this.container.height = Math.min(this.minHeight,
2190 height + config.outputHeight - mow.spaceNeeded);
2192 // FIXME: Belongs elsewhere.
2193 mow.resize(false, Math.max(0, this.minHeight - this.container.height));
2195 this.container.height = this.minHeight - mow.spaceNeeded;
2197 this.timeout(function () {
2198 this.container.height -= mow.spaceNeeded;
2204 * Selects the item at the given *group* and *index*.
2206 * @param {CompletionContext|[CompletionContext,number]} *group* The
2207 * completion context to select, or a tuple specifying the
2208 * context and item index.
2209 * @param {number} index The item index in *group* to select.
2210 * @param {number} position If non-null, try to position the
2211 * selected item at the *position*th row from the top of
2212 * the screen. Note that at least {@link #CONTEXT_LINES}
2213 * lines will be visible above and below the selected item
2214 * unless there aren't enough results to make this possible.
2217 select: function select(group, index, position) {
2219 [group, index] = group;
2221 group = this.getGroup(group);
2223 if (this.selectedGroup && (!group || group != this.selectedGroup))
2224 this.selectedGroup.selectedIdx = null;
2226 this.selectedGroup = group;
2229 group.selectedIdx = index;
2231 let groups = this.activeGroups;
2233 if (position != null || !this.startPos && groups.length)
2234 this.startPos = [group || groups[0], position || 0];
2236 if (groups.length) {
2237 group = group || groups[0];
2238 let idx = groups.indexOf(group);
2240 let start = this.startPos[0].getOffset(this.startPos[1]);
2242 let idx = group.selectedIdx || 0;
2243 let off = group.getOffset(idx);
2245 start = Math.constrain(start,
2246 off + Math.min(this.CONTEXT_LINES,
2247 group.itemCount - idx + group.offsets.end)
2248 - this.maxItems + 1,
2249 off - Math.min(this.CONTEXT_LINES,
2250 idx + group.offsets.start));
2253 let count = this.maxItems;
2254 for (let group in values(groups)) {
2255 let off = Math.max(0, start - group.offsets.start);
2257 group.count = Math.constrain(group.itemCount - off, 0, count);
2258 count -= group.count;
2260 group.collapsed = group.offsets.start >= start + this.maxItems
2261 || group.offsets.start + group.itemCount < start;
2263 group.range = ItemList.Range(off, off + group.count);
2266 var startPos = [group, group.range.start];
2268 this.startPos = startPos;
2274 * Returns an ItemList group for the given completion context,
2275 * creating one if necessary.
2277 * @param {CompletionContext} context
2278 * @returns {ItemList.Group}
2280 getGroup: function getGroup(context)
2281 context instanceof ItemList.Group ? context
2282 : context && context.getCache("itemlist-group",
2283 bind("Group", ItemList, this, context)),
2285 getOffset: function getOffset(tuple) tuple && this.getGroup(tuple[0]).getOffset(tuple[1])
2287 RESIZE_BRIEF: 1 << 0,
2289 WAITING_MESSAGE: _("completion.generating"),
2291 Group: Class("ItemList.Group", {
2292 init: function init(parent, context) {
2293 this.parent = parent;
2294 this.context = context;
2296 this.range = ItemList.Range(0, 0);
2300 ["div", { key: "root", highlight: "CompGroup" },
2301 ["div", { highlight: "Completions" },
2302 this.context.createRow(this.context.title || [], "CompTitle")],
2303 ["div", { highlight: "CompTitleSep" }],
2304 ["div", { key: "contents" },
2305 ["div", { key: "up", highlight: "CompLess" }],
2306 ["div", { key: "message", highlight: "CompMsg" },
2307 this.context.message || []],
2308 ["div", { key: "itemsContainer", class: "completion-items-container" },
2309 ["div", { key: "items", highlight: "Completions" }]],
2310 ["div", { key: "waiting", highlight: "CompMsg" },
2311 ItemList.WAITING_MESSAGE],
2312 ["div", { key: "down", highlight: "CompMore" }]]],
2314 get doc() this.parent.doc,
2315 get win() this.parent.win,
2316 get maxItems() this.parent.maxItems,
2318 get itemCount() this.context.items.length,
2321 * Returns a function which will update the scroll offsets
2322 * and heights of various DOM members.
2325 get rescrollFunc() {
2326 let container = this.nodes.itemsContainer;
2327 let pos = DOM(container).rect.top;
2328 let start = DOM(this.getRow(this.range.start)).rect.top;
2329 let height = DOM(this.getRow(this.range.end - 1)).rect.bottom - start || 0;
2330 let scroll = start + container.scrollTop - pos;
2333 let row = this.selectedRow;
2334 if (row && this.parent.minHeight) {
2335 let { rect } = DOM(this.selectedRow);
2336 var scrollY = this.win.scrollY + rect.bottom - this.win.innerHeight;
2339 return function () {
2340 container.style.height = height + "px";
2341 container.scrollTop = scroll;
2342 if (scrollY != null)
2343 win.scrollTo(0, Math.max(scrollY, 0));
2348 * Reset this group for use with a new set of results.
2350 reset: function reset() {
2352 this.generatedRange = ItemList.Range(0, 0);
2354 DOM.fromJSON(this.rootXML, this.doc, this.nodes);
2358 * Update this group after an asynchronous results push.
2360 update: function update() {
2361 this.generatedRange = ItemList.Range(0, 0);
2362 DOM(this.nodes.items).empty();
2364 if (this.context.message)
2365 DOM(this.nodes.message).empty()
2366 .append(DOM.fromJSON(this.context.message, this.doc));
2368 if (this.selectedIdx > this.itemCount)
2369 this.selectedIdx = null;
2373 * Updates the DOM to reflect the current state of this
2377 draw: function draw() {
2378 DOM(this.nodes.contents).toggle(!this.collapsed);
2382 DOM(this.nodes.message).toggle(this.context.message && this.range.start == 0);
2383 DOM(this.nodes.waiting).toggle(this.context.incomplete && this.range.end <= this.itemCount);
2384 DOM(this.nodes.up).toggle(this.range.start > 0);
2385 DOM(this.nodes.down).toggle(this.range.end < this.itemCount);
2387 if (!this.generatedRange.contains(this.range)) {
2388 if (this.generatedRange.end == 0)
2389 var [start, end] = this.range;
2391 start = this.range.start - (this.range.start <= this.generatedRange.start
2392 ? this.maxItems / 2 : 0);
2393 end = this.range.end + (this.range.end > this.generatedRange.end
2394 ? this.maxItems / 2 : 0);
2397 let range = ItemList.Range(Math.max(0, start - start % 2),
2398 Math.min(this.itemCount, end));
2401 for (let [i, row] in this.context.getRows(this.generatedRange.start,
2402 this.generatedRange.end,
2404 if (!range.contains(i))
2409 let container = DOM(this.nodes.items);
2410 let before = first ? DOM(first).closure.before
2411 : DOM(this.nodes.items).closure.append;
2413 for (let [i, row] in this.context.getRows(range.start, range.end,
2415 if (i < this.generatedRange.start)
2417 else if (i >= this.generatedRange.end)
2418 container.append(row);
2419 if (i == this.selectedIdx)
2420 this.selectedIdx = this.selectedIdx;
2423 this.generatedRange = range;
2427 getRow: function getRow(idx) this.context.getRow(idx, this.doc),
2429 getOffset: function getOffset(idx) this.offsets.start + (idx || 0),
2431 get selectedRow() this.getRow(this._selectedIdx),
2433 get selectedIdx() this._selectedIdx,
2434 set selectedIdx(idx) {
2435 if (this.selectedRow && this._selectedIdx != idx)
2436 DOM(this.selectedRow).attr("selected", null);
2438 this._selectedIdx = idx;
2440 if (this.selectedRow)
2441 DOM(this.selectedRow).attr("selected", true);
2445 Range: Class.Memoize(function () {
2446 let Range = Struct("ItemList.Range", "start", "end");
2447 update(Range.prototype, {
2448 contains: function contains(idx)
2449 typeof idx == "number" ? idx >= this.start && idx < this.end
2450 : this.contains(idx.start) &&
2451 idx.end >= this.start && idx.end <= this.end
2457 // vim: set fdm=marker sw=4 sts=4 ts=8 et: