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 util.overlayWindow(window, {
20 eventTarget: commandline
22 append: <e4x xmlns={XUL} xmlns:dactyl={NS}>
23 <vbox id={config.commandContainer}>
24 <vbox class="dactyl-container" hidden="false" collapsed="true">
25 <iframe class="dactyl-completions" id="dactyl-completions-dactyl-commandline" src="dactyl://content/buffer.xhtml"
26 contextmenu="dactyl-contextmenu"
27 flex="1" hidden="false" collapsed="false"
28 highlight="Events" events="mowEvents" />
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 addElement: function addElement(obj) {
159 this.elements[obj.name] = obj;
161 function get(prefix, map, id) (obj.getElement || util.identity)(map[id] || document.getElementById(prefix + id));
163 this.active.__defineGetter__(obj.name, function () self.activeGroup[obj.name][obj.name]);
164 this.activeGroup.__defineGetter__(obj.name, function () self.getGroup(obj.name));
166 memoize(this.statusbar, obj.name, function () get("dactyl-statusline-field-", statusline.widgets, (obj.id || obj.name)));
167 memoize(this.commandbar, obj.name, function () get("dactyl-", {}, (obj.id || obj.name)));
169 if (!(obj.noValue || obj.getValue)) {
170 Object.defineProperty(this, obj.name, Modes.boundProperty({
173 get: function get_widgetValue() {
174 let elem = self.getGroup(obj.name, obj.value)[obj.name];
175 if (obj.value != null)
176 return [obj.value[0],
177 obj.get ? obj.get.call(this, elem) : elem.value]
178 .concat(obj.value.slice(2));
182 set: function set_widgetValue(val) {
183 if (val != null && !isArray(val))
184 val = [obj.defaultGroup || "", val];
187 [this.commandbar, this.statusbar].forEach(function (nodeSet) {
188 let elem = nodeSet[obj.name];
192 highlight.highlightNode(elem,
193 (val[0] != null ? val[0] : obj.defaultGroup)
194 .split(/\s/).filter(util.identity)
195 .map(function (g) g + " " + nodeSet.group + g)
199 obj.onChange.call(this, elem, val);
203 this.updateVisibility();
208 else if (obj.defaultGroup) {
209 [this.commandbar, this.statusbar].forEach(function (nodeSet) {
210 let elem = nodeSet[obj.name];
212 highlight.highlightNode(elem, obj.defaultGroup.split(/\s/)
213 .map(function (g) g + " " + nodeSet.group + g).join(" "));
218 getGroup: function getgroup(name, value) {
219 if (!statusline.visible)
220 return this.commandbar;
221 return this.elements[name].getGroup.call(this, arguments.length > 1 ? value : this[name]);
224 updateVisibility: function updateVisibility() {
225 for (let elem in values(this.elements))
227 let value = elem.getValue ? elem.getValue.call(this)
228 : elem.noValue || this[elem.name];
230 let activeGroup = this.getGroup(elem.name, value);
231 for (let group in values([this.commandbar, this.statusbar])) {
232 let meth, node = group[elem.name];
233 let visible = (value && group === activeGroup);
234 if (node && !node.collapsed == !visible) {
235 node.collapsed = !visible;
236 if (elem.onVisibility)
237 elem.onVisibility.call(this, node, visible);
242 // Hack. Collapse hidden elements in the stack.
243 // Might possibly be better to use a deck and programmatically
244 // choose which element to select.
245 function check(node) {
246 if (util.computedStyle(node).display === "-moz-stack") {
247 let nodes = Array.filter(node.children, function (n) !n.collapsed && n.boxObject.height);
248 nodes.forEach(function (node, i) node.style.opacity = (i == nodes.length - 1) ? "" : "0");
250 Array.forEach(node.children, check);
252 [this.commandbar.container, this.statusbar.container].forEach(check);
255 active: Class.memoize(Object),
256 activeGroup: Class.memoize(Object),
257 commandbar: Class.memoize(function () ({ group: "Cmd" })),
258 statusbar: Class.memoize(function () ({ group: "Status" })),
260 _ready: function _ready(elem) {
261 return elem.contentDocument.documentURI === elem.getAttribute("src") &&
262 ["viewable", "complete"].indexOf(elem.contentDocument.readyState) >= 0;
265 _whenReady: function _whenReady(id, init) {
266 let elem = document.getElementById(id);
267 while (!this._ready(elem))
271 init.call(this, elem);
275 completionContainer: Class.memoize(function () this.completionList.parentNode),
277 contextMenu: Class.memoize(function () {
278 ["copy", "copylink", "selectall"].forEach(function (tail) {
279 // some host apps use "hostPrefixContext-copy" ids
280 let xpath = "//xul:menuitem[contains(@id, '" + "ontext-" + tail + "') and not(starts-with(@id, 'dactyl-'))]";
281 document.getElementById("dactyl-context-" + tail).style.listStyleImage =
282 util.computedStyle(util.evaluateXPath(xpath, document).snapshotItem(0)).listStyleImage;
284 return document.getElementById("dactyl-contextmenu");
287 multilineOutput: Class.memoize(function () this._whenReady("dactyl-multiline-output", function (elem) {
288 elem.contentWindow.addEventListener("unload", function (event) { event.preventDefault(); }, true);
289 elem.contentDocument.documentElement.id = "dactyl-multiline-output-top";
290 elem.contentDocument.body.id = "dactyl-multiline-output-content";
293 multilineInput: Class.memoize(function () document.getElementById("dactyl-multiline-input")),
295 mowContainer: Class.memoize(function () document.getElementById("dactyl-multiline-output-container"))
297 getEditor: function getEditor(elem) {
298 elem.inputField.QueryInterface(Ci.nsIDOMNSEditableElement);
303 var CommandMode = Class("CommandMode", {
304 init: function CM_init() {
305 this.keepCommand = userContext.hidden_option_command_afterimage;
308 get autocomplete() options["autocomplete"].length,
310 get command() this.widgets.command[1],
311 set command(val) this.widgets.command = val,
313 get prompt() this.widgets.prompt,
314 set prompt(val) this.widgets.prompt = val,
316 open: function CM_open(command) {
317 dactyl.assert(isinstance(this.mode, modes.COMMAND_LINE),
318 /*L*/"Not opening command line in non-command-line mode.");
320 this.messageCount = commandline.messageCount;
321 modes.push(this.mode, this.extendedMode, this.closure);
323 this.widgets.active.commandline.collapsed = false;
324 this.widgets.prompt = this.prompt;
325 this.widgets.command = command || "";
327 this.input = this.widgets.active.command.inputField;
329 this.history = CommandLine.History(this.input, this.historyKey, this);
332 this.completions = CommandLine.Completions(this.input, this);
334 if (this.completions && command && commandline.commandSession === this)
335 this.completions.autocompleteTimer.flush(true);
338 get active() this === commandline.commandSession,
340 get holdFocus() this.widgets.active.command.inputField,
342 get mappingSelf() this,
344 get widgets() commandline.widgets,
346 enter: function CM_enter(stack) {
347 commandline.commandSession = this;
348 if (stack.pop && commandline.command) {
349 this.onChange(commandline.command);
350 if (this.completions && stack.pop)
351 this.completions.complete(true, false);
355 leave: function CM_leave(stack) {
357 commandline.commandSession = null;
358 this.input.dactylKeyPress = undefined;
360 if (this.completions)
361 this.completions.cleanup();
366 this.resetCompletions();
367 commandline.hideCompletions();
369 modes.delay(function () {
370 if (!this.keepCommand || commandline.silent || commandline.quiet)
372 this[this.accepted ? "onSubmit" : "onCancel"](commandline.command);
373 if (commandline.messageCount === this.messageCount)
374 commandline.clearMessage();
380 input: function CM_onInput(event) {
381 if (this.completions) {
382 this.resetCompletions();
384 this.completions.autocompleteTimer.tell(false);
386 this.onChange(commandline.command);
388 keyup: function CM_onKeyUp(event) {
389 let key = events.toString(event);
390 if (/-?Tab>$/.test(key) && this.completions)
391 this.completions.tabTimer.flush();
397 onKeyPress: function CM_onKeyPress(events) {
398 if (this.completions)
399 this.completions.previewClear();
401 return true; /* Pass event */
404 onCancel: function (value) {},
406 onChange: function (value) {},
408 onHistory: function (value) {},
410 onSubmit: function (value) {},
412 resetCompletions: function CM_resetCompletions() {
413 if (this.completions) {
414 this.completions.context.cancelAll();
415 this.completions.wildIndex = -1;
416 this.completions.previewClear();
419 this.history.reset();
423 var CommandExMode = Class("CommandExMode", CommandMode, {
427 historyKey: "command",
429 prompt: ["Normal", ":"],
431 complete: function CEM_complete(context) {
432 context.fork("ex", 0, completion, "ex");
435 onSubmit: function CEM_onSubmit(command) {
436 contexts.withContext({ file: /*L*/"[Command Line]", line: 1 },
437 function _onSubmit() {
438 io.withSavedValues(["readHeredoc"], function _onSubmit() {
439 this.readHeredoc = commandline.readHeredoc;
440 commands.repeat = command;
441 dactyl.execute(command);
447 var CommandPromptMode = Class("CommandPromptMode", CommandMode, {
448 init: function init(prompt, params) {
449 this.prompt = isArray(prompt) ? prompt : ["Question", prompt];
450 update(this, params);
451 init.supercall(this);
454 complete: function CPM_complete(context) {
456 context.forkapply("prompt", 0, this, "completer", Array.slice(arguments, 1));
459 get mode() modes.PROMPT
463 * This class is used for prompting of user input and echoing of messages.
465 * It consists of a prompt and command field be sure to only create objects of
466 * this class when the chrome is ready.
468 var CommandLine = Module("commandline", {
469 init: function init() {
472 this._callbacks = {};
474 memoize(this, "_store", function () storage.newMap("command-history", { store: true, privateData: true }));
476 for (let name in values(["command", "search"]))
477 if (storage.exists("history-" + name)) {
478 let ary = storage.newArray("history-" + name, { store: true, privateData: true });
480 this._store.set(name, [v for ([k, v] in ary)]);
482 this._store.changed();
485 this._messageHistory = { //{{{
488 let max = options["messages"];
490 // resize if 'messages' has changed
491 if (this._messages.length > max)
492 this._messages = this._messages.splice(this._messages.length - max);
494 return this._messages;
497 get length() this._messages.length,
499 clear: function clear() {
503 filter: function filter(fn, self) {
504 this._messages = this._messages.filter(fn, self);
507 add: function add(message) {
511 if (this._messages.length >= options["messages"])
512 this._messages.shift();
514 this._messages.push(update({
515 timestamp: Date.now()
522 "browser.locationChange": function (webProgress, request, uri) {
528 * Determines whether the command line should be visible.
532 get commandVisible() !!this.commandSession,
535 * Ensure that the multiline input widget is the correct size.
537 _autosizeMultilineInputWidget: function _autosizeMultilineInputWidget() {
538 let lines = this.widgets.multilineInput.value.split("\n").length - 1;
540 this.widgets.multilineInput.setAttribute("rows", Math.max(lines, 1));
544 HL_ERRORMSG: "ErrorMsg",
545 HL_MODEMSG: "ModeMsg",
546 HL_MOREMSG: "MoreMsg",
547 HL_QUESTION: "Question",
548 HL_INFOMSG: "InfoMsg",
549 HL_WARNINGMSG: "WarningMsg",
552 FORCE_MULTILINE : 1 << 0,
553 FORCE_SINGLELINE : 1 << 1,
554 DISALLOW_MULTILINE : 1 << 2, // If an echo() should try to use the single line
555 // but output nothing when the MOW is open; when also
556 // FORCE_MULTILINE is given, FORCE_MULTILINE takes precedence
557 APPEND_TO_MESSAGES : 1 << 3, // Add the string to the message history.
558 ACTIVE_WINDOW : 1 << 4, // Only echo in active window.
560 get completionContext() this._completions.context,
563 get silent() this._silent,
566 this.quiet = this.quiet;
570 get quiet() this._quiet,
573 ["commandbar", "statusbar"].forEach(function (nodeSet) {
574 Array.forEach(this.widgets[nodeSet].commandline.children, function (node) {
575 node.style.opacity = this._quiet || this._silent ? "0" : "";
580 widgets: Class.memoize(function () CommandWidgets()),
582 runSilently: function runSilently(func, self) {
583 this.withSavedValues(["silent"], function () {
589 get completionList() {
590 let node = this.widgets.active.commandline;
591 if (this.commandSession && this.commandSession.completionList)
592 node = document.getElementById(this.commandSession.completionList);
594 if (!node.completionList) {
595 let elem = document.getElementById("dactyl-completions-" + node.id);
596 util.waitFor(bind(this.widgets._ready, null, elem));
598 node.completionList = ItemList(elem.id);
600 return node.completionList;
603 hideCompletions: function hideCompletions() {
604 for (let nodeSet in values([this.widgets.statusbar, this.widgets.commandbar]))
605 if (nodeSet.commandline.completionList)
606 nodeSet.commandline.completionList.visible = false;
609 _lastClearable: Modes.boundProperty(),
610 messages: Modes.boundProperty(),
612 multilineInputVisible: Modes.boundProperty({
613 set: function set_miwVisible(value) { this.widgets.multilineInput.collapsed = !value; }
617 if (this.commandVisible && this.widgets.command)
618 return commands.lastCommand = this.widgets.command[1];
619 return commands.lastCommand;
622 if (this.commandVisible && (modes.extended & modes.EX))
623 return this.widgets.command = val;
624 return commands.lastCommand = val;
627 clear: function clear(scroll) {
628 if (!scroll || Date.now() - this._lastEchoTime > 5000)
630 this._lastEchoTime = 0;
632 if (!this.commandSession) {
633 this.widgets.command = null;
634 this.hideCompletions();
637 if (modes.main == modes.OUTPUT_MULTILINE && !mow.isScrollable(1))
640 if (!modes.have(modes.OUTPUT_MULTILINE))
644 clearMessage: function clearMessage() {
645 if (this.widgets.message && this.widgets.message[1] === this._lastClearable)
646 this.widgets.message = null;
650 * Displays the multi-line output of a command, preceded by the last
651 * executed ex command string.
653 * @param {XML} xml The output as an E4X XML object.
655 commandOutput: function commandOutput(xml) {
656 XML.ignoreWhitespace = XML.prettyPrinting = false;
658 this.echo(<><div xmlns={XHTML}>:{this.command}</div>
{xml}</>, this.HIGHLIGHT_NORMAL, this.FORCE_MULTILINE);
660 this.echo(xml, this.HIGHLIGHT_NORMAL, this.FORCE_MULTILINE);
665 * Hides the command line, and shows any status messages that
668 hide: function hide() {
669 this.widgets.command = null;
673 * Display a message in the command-line area.
675 * @param {string} str
676 * @param {string} highlightGroup
677 * @param {boolean} forceSingle If provided, don't let over-long
678 * messages move to the MOW.
680 _echoLine: function echoLine(str, highlightGroup, forceSingle, silent) {
681 this.widgets.message = str ? [highlightGroup, str, forceSingle] : null;
683 dactyl.triggerObserver("echoLine", str, highlightGroup, null, forceSingle);
685 if (!this.commandVisible)
688 let field = this.widgets.active.message.inputField;
689 if (field.value && !forceSingle && field.editor.rootElement.scrollWidth > field.scrollWidth) {
690 this.widgets.message = null;
691 mow.echo(<span highlight="Message">{str}</span>, highlightGroup, true);
698 * Output the given string onto the command line. With no flags, the
699 * message will be shown in the status line if it's short enough to
700 * fit, and contains no new lines, and isn't XML. Otherwise, it will be
703 * @param {string} str
704 * @param {string} highlightGroup The Highlight group for the
707 * @param {number} flags Changes the behavior as follows:
708 * commandline.APPEND_TO_MESSAGES - Causes message to be added to the
709 * messages history, and shown by :messages.
710 * commandline.FORCE_SINGLELINE - Forbids the command from being
711 * pushed to the MOW if it's too long or of there are already
712 * status messages being shown.
713 * commandline.DISALLOW_MULTILINE - Cancels the operation if the MOW
714 * is already visible.
715 * commandline.FORCE_MULTILINE - Forces the message to appear in
719 echo: function echo(data, highlightGroup, flags) {
720 // dactyl.echo uses different order of flags as it omits the highlight group, change commandline.echo argument order? --mst
721 if (this._silent || !this.widgets)
726 highlightGroup = highlightGroup || this.HL_NORMAL;
728 if (flags & this.APPEND_TO_MESSAGES) {
729 let message = isObject(data) ? data : { message: data };
731 // Make sure the memoized message property is an instance property.
733 this._messageHistory.add(update({ highlight: highlightGroup }, message));
734 data = message.message;
737 if ((flags & this.ACTIVE_WINDOW) &&
738 window != services.windowWatcher.activeWindow &&
739 services.windowWatcher.activeWindow.dactyl)
742 if ((flags & this.DISALLOW_MULTILINE) && !this.widgets.mowContainer.collapsed)
745 let single = flags & (this.FORCE_SINGLELINE | this.DISALLOW_MULTILINE);
746 let action = this._echoLine;
748 if ((flags & this.FORCE_MULTILINE) || (/\n/.test(data) || !isinstance(data, [_, "String"])) && !(flags & this.FORCE_SINGLELINE))
749 action = mow.closure.echo;
752 this._lastEcho = null;
754 if (this.widgets.message && this.widgets.message[1] == this._lastEcho)
755 mow.echo(<span highlight="Message">{this._lastEcho}</span>,
756 this.widgets.message[0], true);
758 if (action === this._echoLine && !(flags & this.FORCE_MULTILINE)
759 && !(dactyl.fullyInitialized && this.widgets.mowContainer.collapsed)) {
760 highlightGroup += " Message";
761 action = mow.closure.echo;
763 this._lastEcho = (action == this._echoLine) && data;
766 this._lastClearable = action === this._echoLine && String(data);
767 this._lastEchoTime = (flags & this.FORCE_SINGLELINE) && Date.now();
770 action.call(this, data, highlightGroup, single);
775 * Prompt the user. Sets modes.main to COMMAND_LINE, which the user may
776 * pop at any time to close the prompt.
778 * @param {string} prompt The input prompt to use.
779 * @param {function(string)} callback
780 * @param {Object} extra
781 * @... {function} onChange - A function to be called with the current
782 * input every time it changes.
783 * @... {function(CompletionContext)} completer - A completion function
784 * for the user's input.
785 * @... {string} promptHighlight - The HighlightGroup used for the
786 * prompt. @default "Question"
787 * @... {string} default - The initial value that will be returned
788 * if the user presses <CR> straightaway. @default ""
790 input: function _input(prompt, callback, extra) {
793 CommandPromptMode(prompt, update({ onSubmit: callback }, extra)).open();
796 readHeredoc: function readHeredoc(end) {
798 commandline.inputMultiline(end, function (res) { args = res; });
799 util.waitFor(function () args !== undefined);
804 * Get a multi-line input from a user, up to but not including the line
805 * which matches the given regular expression. Then execute the
806 * callback with that string as a parameter.
808 * @param {string} end
809 * @param {function(string)} callback
811 // FIXME: Buggy, especially when pasting.
812 inputMultiline: function inputMultiline(end, callback) {
813 let cmd = this.command;
815 end: "\n" + end + "\n",
819 modes.push(modes.INPUT_MULTILINE, null, {
821 leave: function leave() {
829 this._echoLine(cmd, this.HL_NORMAL);
831 // save the arguments, they are needed in the event handler onKeyPress
833 this.multilineInputVisible = true;
834 this.widgets.multilineInput.value = "";
835 this._autosizeMultilineInputWidget();
837 this.timeout(function () { dactyl.focus(this.widgets.multilineInput); }, 10);
840 get commandMode() this.commandSession && isinstance(modes.main, modes.COMMAND_LINE),
843 iter(CommandMode.prototype.events).map(
844 function ([event, handler]) [
845 event, function (event) {
846 if (this.commandMode)
847 handler.call(this.commandSession, event);
851 focus: function onFocus(event) {
852 if (!this.commandSession
853 && event.originalTarget === this.widgets.active.command.inputField) {
861 get mowEvents() mow.events,
864 * Multiline input events, they will come straight from
865 * #dactyl-multiline-input in the XUL.
867 * @param {Event} event
869 multilineInputEvents: {
870 blur: function onBlur(event) {
871 if (modes.main == modes.INPUT_MULTILINE)
872 this.timeout(function () {
873 dactyl.focus(this.widgets.multilineInput.inputField);
876 input: function onInput(event) {
877 this._autosizeMultilineInputWidget();
881 updateOutputHeight: deprecated("mow.resize", function updateOutputHeight(open, extra) mow.resize(open, extra)),
883 withOutputToString: function withOutputToString(fn, self) {
884 dactyl.registerObserver("echoLine", observe, true);
885 dactyl.registerObserver("echoMultiline", observe, true);
888 function observe(str, highlight, dom) {
889 output.push(dom && !isString(str) ? dom : str);
892 this.savingOutput = true;
893 dactyl.trapErrors.apply(dactyl, [fn, self].concat(Array.slice(arguments, 2)));
894 this.savingOutput = false;
895 return output.map(function (elem) elem instanceof Node ? util.domToString(elem) : elem)
900 * A class for managing the history of an input field.
902 * @param {HTMLInputElement} inputField
903 * @param {string} mode The mode for which we need history.
905 History: Class("History", {
906 init: function init(inputField, mode, session) {
908 this.input = inputField;
910 this.session = session;
912 get store() commandline._store.get(this.mode, []),
913 set store(ary) { commandline._store.set(this.mode, ary); },
915 * Reset the history index to the first entry.
917 reset: function reset() {
921 * Save the last entry to the permanent store. All duplicate entries
922 * are removed and the list is truncated, if necessary.
924 save: function save() {
925 if (events.feedingKeys)
927 let str = this.input.value;
928 if (/^\s*$/.test(str))
930 this.store = this.store.filter(function (line) (line.value || line) != str);
932 this.store.push({ value: str, timestamp: Date.now()*1000, privateData: this.checkPrivate(str) });
935 dactyl.reportError(e);
937 this.store = this.store.slice(-options["history"]);
940 * @property {function} Returns whether a data item should be
941 * considered private.
943 checkPrivate: function checkPrivate(str) {
944 // Not really the ideal place for this check.
945 if (this.mode == "command")
946 return commands.hasPrivateData(str);
950 * Replace the current input field value.
952 * @param {string} val The new value.
954 replace: function replace(val) {
955 this.input.dactylKeyPress = undefined;
956 if (this.completions)
957 this.completions.previewClear();
958 this.input.value = val;
959 this.session.onHistory(val);
963 * Move forward or backward in history.
965 * @param {boolean} backward Direction to move.
966 * @param {boolean} matchCurrent Search for matches starting
967 * with the current input value.
969 select: function select(backward, matchCurrent) {
970 // always reset the tab completion if we use up/down keys
971 if (this.session.completions)
972 this.session.completions.reset();
974 let diff = backward ? -1 : 1;
976 if (this.index == null) {
977 this.original = this.input.value;
978 this.index = this.store.length;
981 // search the history for the first item matching the current
982 // command-line string
985 if (this.index < 0 || this.index > this.store.length) {
986 this.index = Math.constrain(this.index, 0, this.store.length);
988 // I don't know why this kludge is needed. It
989 // prevents the caret from moving to the end of
991 if (this.input.value == "") {
992 this.input.value = " ";
993 this.input.value = "";
998 let hist = this.store[this.index];
999 // user pressed DOWN when there is no newer history item
1001 hist = this.original;
1003 hist = (hist.value || hist);
1005 if (!matchCurrent || hist.substr(0, this.original.length) == this.original) {
1014 * A class for tab completions on an input field.
1016 * @param {Object} input
1018 Completions: Class("Completions", {
1019 init: function init(input, session) {
1020 this.context = CompletionContext(input.QueryInterface(Ci.nsIDOMNSEditableElement).editor);
1021 this.context.onUpdate = this.closure._reset;
1022 this.editor = input.editor;
1024 this.session = session;
1025 this.selected = null;
1026 this.wildmode = options.get("wildmode");
1027 this.wildtypes = this.wildmode.value;
1028 this.itemList = commandline.completionList;
1029 this.itemList.setItems(this.context);
1031 dactyl.registerObserver("events.doneFeeding", this.closure.onDoneFeeding, true);
1033 this.autocompleteTimer = Timer(200, 500, function autocompleteTell(tabPressed) {
1034 if (events.feedingKeys && !tabPressed)
1035 this.ignoredCount++;
1036 else if (this.session.autocomplete) {
1037 this.itemList.visible = true;
1038 this.complete(true, false);
1041 this.tabTimer = Timer(0, 0, function tabTell(event) {
1042 this.tab(event.shiftKey, event.altKey && options["altwildmode"]);
1046 cleanup: function () {
1047 dactyl.unregisterObserver("events.doneFeeding", this.closure.onDoneFeeding);
1048 this.previewClear();
1049 this.tabTimer.reset();
1050 this.autocompleteTimer.reset();
1051 this.itemList.visible = false;
1052 this.input.dactylKeyPress = undefined;
1056 onDoneFeeding: function onDoneFeeding() {
1057 if (this.ignoredCount)
1058 this.autocompleteTimer.flush(true);
1059 this.ignoredCount = 0;
1071 let str = commandline.command;
1072 return str.substring(this.prefix.length, str.length - this.suffix.length);
1074 set completion(completion) {
1075 this.previewClear();
1077 // Change the completion text.
1078 // The second line is a hack to deal with some substring
1079 // preview corner cases.
1080 let value = this.prefix + completion + this.suffix;
1081 commandline.widgets.active.command.value = value;
1082 this.editor.selection.focusNode.textContent = value;
1084 // Reset the caret to one position after the completion.
1085 this.caret = this.prefix.length + completion.length;
1086 this._caret = this.caret;
1088 this.input.dactylKeyPress = undefined;
1091 get caret() this.editor.selection.getRangeAt(0).startOffset,
1093 this.editor.selection.getRangeAt(0).setStart(this.editor.rootElement.firstChild, offset);
1094 this.editor.selection.getRangeAt(0).setEnd(this.editor.rootElement.firstChild, offset);
1097 get start() this.context.allItems.start,
1099 get items() this.context.allItems.items,
1101 get substring() this.context.longestAllSubstring,
1103 get wildtype() this.wildtypes[this.wildIndex] || "",
1105 complete: function complete(show, tabPressed) {
1106 this.session.ignoredCount = 0;
1107 this.context.reset();
1108 this.context.tabPressed = tabPressed;
1109 this.session.complete(this.context);
1110 if (!this.session.active)
1112 this.context.updateAsync = true;
1113 this.reset(show, tabPressed);
1115 this._caret = this.caret;
1118 haveType: function haveType(type)
1119 this.wildmode.checkHas(this.wildtype, type == "first" ? "" : type),
1121 preview: function preview() {
1122 this.previewClear();
1123 if (this.wildIndex < 0 || this.suffix || !this.items.length)
1127 switch (this.wildtype.replace(/.*:/, "")) {
1129 substring = this.items[0].result;
1132 if (this.items.length > 1) {
1133 substring = this.substring;
1138 let item = this.items[this.selected != null ? this.selected + 1 : 0];
1140 substring = item.result;
1144 // Don't show 1-character substrings unless we've just hit backspace
1145 if (substring.length < 2 && this.lastSubstring.indexOf(substring) !== 0)
1148 this.lastSubstring = substring;
1150 let value = this.completion;
1151 if (util.compareIgnoreCase(value, substring.substr(0, value.length)))
1153 substring = substring.substr(value.length);
1154 this.removeSubstring = substring;
1156 let node = util.xmlToDom(<span highlight="Preview">{substring}</span>,
1158 let start = this.caret;
1159 this.editor.insertNode(node, this.editor.rootElement, 1);
1163 previewClear: function previewClear() {
1164 let node = this.editor.rootElement.firstChild;
1165 if (node && node.nextSibling) {
1167 this.editor.deleteNode(node.nextSibling);
1170 node.nextSibling.textContent = "";
1173 else if (this.removeSubstring) {
1174 let str = this.removeSubstring;
1175 let cmd = commandline.widgets.active.command.value;
1176 if (cmd.substr(cmd.length - str.length) == str)
1177 commandline.widgets.active.command.value = cmd.substr(0, cmd.length - str.length);
1179 delete this.removeSubstring;
1182 reset: function reset(show) {
1183 this.wildIndex = -1;
1185 this.prefix = this.context.value.substring(0, this.start);
1186 this.value = this.context.value.substring(this.start, this.caret);
1187 this.suffix = this.context.value.substring(this.caret);
1190 this.itemList.reset();
1191 if (this.haveType("list"))
1192 this.itemList.visible = true;
1193 this.selected = null;
1200 _reset: function _reset() {
1201 let value = this.editor.selection.focusNode.textContent;
1202 this.prefix = value.substring(0, this.start);
1203 this.value = value.substring(this.start, this.caret);
1204 this.suffix = value.substring(this.caret);
1206 this.itemList.reset();
1207 this.itemList.selectItem(this.selected);
1212 select: function select(idx) {
1215 if (this.selected == null)
1218 idx = this.selected - 1;
1221 if (this.selected == null)
1224 idx = this.selected + 1;
1230 idx = Math.constrain(idx, 0, this.items.length - 1);
1234 if (idx == -1 || this.items.length && idx >= this.items.length || idx == null) {
1235 // Wrapped. Start again.
1236 this.selected = null;
1237 this.completion = this.value;
1240 // Wait for contexts to complete if necessary.
1241 // FIXME: Need to make idx relative to individual contexts.
1242 let list = this.context.contextList;
1244 list = list.slice().reverse();
1247 this.waiting = true;
1248 for (let [, context] in Iterator(list)) {
1249 let done = function done() !(idx >= n + context.items.length || idx == -2 && !context.items.length);
1251 util.waitFor(function () !context.incomplete || done());
1255 n += context.items.length;
1259 this.waiting = false;
1262 // See previous FIXME. This will break if new items in
1263 // a previous context come in.
1265 idx = this.items.length - 1;
1266 if (this.items.length == 0)
1269 this.selected = idx;
1270 this.completion = this.items[idx].result;
1273 this.itemList.selectItem(idx);
1278 tab: function tab(reverse, wildmode) {
1279 this.autocompleteTimer.flush();
1280 this.ignoredCount = 0;
1282 if (this._caret != this.caret)
1284 this._caret = this.caret;
1286 // Check if we need to run the completer.
1287 if (this.context.waitingForTab || this.wildIndex == -1)
1288 this.complete(true, true);
1290 this.tabs.push([reverse, wildmode || options["wildmode"]]);
1294 while (this.tabs.length) {
1295 [reverse, this.wildtypes] = this.tabs.shift();
1297 this.wildIndex = Math.min(this.wildIndex, this.wildtypes.length - 1);
1298 switch (this.wildtype.replace(/.*:/, "")) {
1303 if (this.items.length > 1) {
1304 if (this.substring && this.substring.length > this.completion.length)
1305 this.completion = this.substring;
1310 this.select(reverse ? this.UP : this.DOWN);
1314 if (this.haveType("list"))
1315 this.itemList.visible = true;
1320 if (this.selected == null)
1321 statusline.progress = "";
1323 statusline.progress = _("completion.matchIndex", this.selected + 1, this.items.length);
1326 if (this.items.length == 0)
1332 * Evaluate a JavaScript expression and return a string suitable
1335 * @param {string} arg
1336 * @param {boolean} useColor When true, the result is a
1337 * highlighted XML object.
1339 echoArgumentToString: function (arg, useColor) {
1343 arg = dactyl.userEval(arg);
1345 arg = util.objectToString(arg, useColor);
1351 commands: function init_commands() {
1355 description: "Echo the expression",
1360 description: "Echo the expression as an error message",
1361 action: dactyl.echoerr
1365 description: "Echo the expression as an informational message",
1366 action: dactyl.echomsg
1368 ].forEach(function (command) {
1369 commands.add([command.name],
1370 command.description,
1372 command.action(CommandLine.echoArgumentToString(args[0] || "", true));
1374 completer: function (context) completion.javascript(context),
1379 commands.add(["mes[sages]"],
1380 "Display previously shown messages",
1382 // TODO: are all messages single line? Some display an aggregation
1383 // of single line messages at least. E.g. :source
1384 if (commandline._messageHistory.length == 1) {
1385 let message = commandline._messageHistory.messages[0];
1386 commandline.echo(message.message, message.highlight, commandline.FORCE_SINGLELINE);
1388 else if (commandline._messageHistory.length > 1) {
1389 XML.ignoreWhitespace = false;
1390 commandline.commandOutput(
1391 template.map(commandline._messageHistory.messages, function (message)
1392 <div highlight={message.highlight + " Message"}>{message.message}</div>));
1397 commands.add(["messc[lear]"],
1398 "Clear the message history",
1399 function () { commandline._messageHistory.clear(); },
1402 commands.add(["sil[ent]"],
1403 "Run a command silently",
1405 commandline.runSilently(function () commands.execute(args[0] || "", null, true));
1407 completer: function (context) completion.ex(context),
1412 modes: function initModes() {
1413 modes.addMode("COMMAND_LINE", {
1415 description: "Active when the command line is focused",
1418 get mappingSelf() commandline.commandSession
1420 // this._extended modes, can include multiple modes, and even main modes
1421 modes.addMode("EX", {
1422 description: "Ex command mode, active when the command line is open for Ex commands",
1423 bases: [modes.COMMAND_LINE]
1425 modes.addMode("PROMPT", {
1426 description: "Active when a prompt is open in the command line",
1427 bases: [modes.COMMAND_LINE]
1430 modes.addMode("INPUT_MULTILINE", {
1431 bases: [modes.INSERT]
1434 mappings: function init_mappings() {
1436 mappings.add([modes.COMMAND],
1437 [":"], "Enter Command Line mode",
1438 function () { CommandExMode().open(""); });
1440 mappings.add([modes.INPUT_MULTILINE],
1441 ["<Return>", "<C-j>", "<C-m>"], "Begin a new line",
1442 function ({ self }) {
1443 let text = "\n" + commandline.widgets.multilineInput
1444 .value.substr(0, commandline.widgets.multilineInput.selectionStart)
1447 let index = text.indexOf(self.end);
1450 text = text.substring(1, index);
1453 return function () self.callback.call(commandline, text);
1458 let bind = function bind()
1459 mappings.add.apply(mappings, [[modes.COMMAND_LINE]].concat(Array.slice(arguments)))
1461 // Any "non-keyword" character triggers abbreviation expansion
1462 // TODO: Add "<CR>" and "<Tab>" to this list
1463 // At the moment, adding "<Tab>" breaks tab completion. Adding
1464 // "<CR>" has no effect.
1465 // TODO: Make non-keyword recognition smarter so that there need not
1466 // be two lists of the same characters (one here and a regexp in
1468 bind(["<Space>", '"', "'"], "Expand command line abbreviation",
1469 function ({ self }) {
1470 self.resetCompletions();
1471 editor.expandAbbreviation(modes.COMMAND_LINE);
1475 bind(["<Return>", "<C-j>", "<C-m>"], "Accept the current input",
1476 function ({ self }) {
1477 let command = commandline.command;
1479 self.accepted = true;
1480 return function () { modes.pop(); };
1484 [["<Up>", "<A-p>"], "previous matching", true, true],
1485 [["<S-Up>", "<C-p>", "<PageUp>"], "previous", true, false],
1486 [["<Down>", "<A-n>"], "next matching", false, true],
1487 [["<S-Down>", "<C-n>", "<PageDown>"], "next", false, false]
1488 ].forEach(function ([keys, desc, up, search]) {
1489 bind(keys, "Recall the " + desc + " command line from the history list",
1490 function ({ self }) {
1491 dactyl.assert(self.history);
1492 self.history.select(up, search);
1496 bind(["<A-Tab>", "<Tab>"], "Select the next matching completion item",
1497 function ({ keypressEvents, self }) {
1498 dactyl.assert(self.completions);
1499 self.completions.tabTimer.tell(keypressEvents[0]);
1502 bind(["<A-S-Tab>", "<S-Tab>"], "Select the previous matching completion item",
1503 function ({ keypressEvents, self }) {
1504 dactyl.assert(self.completions);
1505 self.completions.tabTimer.tell(keypressEvents[0]);
1508 bind(["<BS>", "<C-h>"], "Delete the previous character",
1510 if (!commandline.command)
1516 bind(["<C-]>", "<C-5>"], "Expand command line abbreviation",
1517 function () { editor.expandAbbreviation(modes.COMMAND_LINE); });
1519 options: function init_options() {
1520 options.add(["history", "hi"],
1521 "Number of Ex commands and search patterns to store in the command-line history",
1523 { validator: function (value) value >= 0 });
1525 options.add(["maxitems"],
1526 "Maximum number of completion items to display at once",
1528 { validator: function (value) value >= 1 });
1530 options.add(["messages", "msgs"],
1531 "Number of messages to store in the :messages history",
1533 { validator: function (value) value >= 0 });
1535 sanitizer: function init_sanitizer() {
1536 sanitizer.addItem("commandline", {
1537 description: "Command-line and search history",
1539 action: function (timespan, host) {
1540 let store = commandline._store;
1541 for (let [k, v] in store) {
1543 store.set(k, v.filter(function (item)
1544 !(timespan.contains(item.timestamp) && (!host || commands.hasDomain(item.value, host)))));
1546 store.set(k, v.filter(function (item) !timespan.contains(item.timestamp)));
1550 // Delete history-like items from the commandline and messages on history purge
1551 sanitizer.addItem("history", {
1552 action: function (timespan, host) {
1553 commandline._store.set("command",
1554 commandline._store.get("command", []).filter(function (item)
1555 !(timespan.contains(item.timestamp) && (host ? commands.hasDomain(item.value, host)
1556 : item.privateData))));
1558 commandline._messageHistory.filter(function (item) !timespan.contains(item.timestamp * 1000) ||
1559 !item.domains && !item.privateData ||
1560 host && (!item.domains || !item.domains.some(function (d) util.isSubdomain(d, host))));
1563 sanitizer.addItem("messages", {
1564 description: "Saved :messages",
1565 action: function (timespan, host) {
1566 commandline._messageHistory.filter(function (item) !timespan.contains(item.timestamp * 1000) ||
1567 host && (!item.domains || !item.domains.some(function (d) util.isSubdomain(d, host))));
1574 * The list which is used for the completion box (and QuickFix window in
1577 * @param {string} id The id of the iframe which will display the list. It
1578 * must be in its own container element, whose height it will update as
1581 var ItemList = Class("ItemList", {
1582 init: function init(id) {
1583 this._completionElements = [];
1585 var iframe = document.getElementById(id);
1587 this._doc = iframe.contentDocument;
1588 this._win = iframe.contentWindow;
1589 this._container = iframe.parentNode;
1591 this._doc.documentElement.id = id + "-top";
1592 this._doc.body.id = id + "-content";
1593 this._doc.body.className = iframe.className + "-content";
1594 this._doc.body.appendChild(this._doc.createTextNode(""));
1595 this._doc.body.style.borderTop = "1px solid black"; // FIXME: For cases where completions/MOW are shown at once, or ls=0. Should use :highlight.
1598 this._startIndex = -1; // The index of the first displayed item
1599 this._endIndex = -1; // The index one *after* the last displayed item
1600 this._selIndex = -1; // The index of the currently selected element
1602 this._divNodes = {};
1603 this._minHeight = 0;
1606 _dom: function _dom(xml, map) util.xmlToDom(xml instanceof XML ? xml : <>{xml}</>, this._doc, map),
1608 _autoSize: function _autoSize() {
1612 if (this._container.collapsed)
1613 this._div.style.minWidth = document.getElementById("dactyl-commandline").scrollWidth + "px";
1615 this._minHeight = Math.max(this._minHeight,
1616 this._win.scrollY + this._divNodes.completions.getBoundingClientRect().bottom);
1618 if (this._container.collapsed)
1619 this._div.style.minWidth = "";
1621 // FIXME: Belongs elsewhere.
1622 mow.resize(false, Math.max(0, this._minHeight - this._container.height));
1624 this._container.height = this._minHeight;
1625 this._container.height -= mow.spaceNeeded;
1627 this.timeout(function () {
1628 this._container.height -= mow.spaceNeeded;
1632 _getCompletion: function _getCompletion(index) this._completionElements.snapshotItem(index - this._startIndex),
1634 _init: function _init() {
1635 this._div = this._dom(
1636 <div class="ex-command-output" highlight="Normal" style="white-space: nowrap">
1637 <div highlight="Completions" key="noCompletions"><span highlight="Title">{_("completion.noCompletions")}</span></div>
1638 <div key="completions"/>
1639 <div highlight="Completions">
1641 template.map(util.range(0, options["maxitems"] * 2), function (i)
1642 <div highlight="CompItem NonText">
1647 </div>, this._divNodes);
1648 this._doc.body.replaceChild(this._div, this._doc.body.firstChild);
1649 util.scrollIntoView(this._div, true);
1651 this._items.contextList.forEach(function init_eachContext(context) {
1652 delete context.cache.nodes;
1653 if (!context.items.length && !context.message && !context.incomplete)
1655 context.cache.nodes = [];
1656 this._dom(<div key="root" highlight="CompGroup">
1657 <div highlight="Completions">
1658 { context.createRow(context.title || [], "CompTitle") }
1660 <div highlight="CompTitleSep"/>
1661 <div key="message" highlight="CompMsg"/>
1662 <div key="up" highlight="CompLess"/>
1663 <div key="items" highlight="Completions"/>
1664 <div key="waiting" highlight="CompMsg">{ItemList.WAITING_MESSAGE}</div>
1665 <div key="down" highlight="CompMore"/>
1666 </div>, context.cache.nodes);
1667 this._divNodes.completions.appendChild(context.cache.nodes.root);
1670 this.timeout(this._autoSize);
1674 * Uses the entries in "items" to fill the listbox and does incremental
1675 * filling to speed up things.
1677 * @param {number} offset Start at this index and show options["maxitems"].
1679 _fill: function _fill(offset) {
1680 XML.ignoreWhiteSpace = false;
1681 let diff = offset - this._startIndex;
1682 if (this._items == null || offset == null || diff == 0 || offset < 0)
1685 this._startIndex = offset;
1686 this._endIndex = Math.min(this._startIndex + options["maxitems"], this._items.allItems.items.length);
1688 let haveCompletions = false;
1690 let end = this._startIndex + options["maxitems"];
1691 function getRows(context) {
1692 function fix(n) Math.constrain(n, 0, len);
1693 let len = context.items.length;
1695 end -= !!context.message + context.incomplete;
1698 let s = fix(offset - start), e = fix(end - start);
1699 return [s, e, context.incomplete && e >= offset && off - 1 < end];
1702 this._items.contextList.forEach(function fill_eachContext(context) {
1703 let nodes = context.cache.nodes;
1706 haveCompletions = true;
1708 let root = nodes.root;
1709 let items = nodes.items;
1710 let [start, end, waiting] = getRows(context);
1712 if (context.message) {
1713 nodes.message.textContent = "";
1714 nodes.message.appendChild(this._dom(context.message));
1716 nodes.message.style.display = context.message ? "block" : "none";
1717 nodes.waiting.style.display = waiting ? "block" : "none";
1718 nodes.up.style.opacity = "0";
1719 nodes.down.style.display = "none";
1721 for (let [i, row] in Iterator(context.getRows(start, end, this._doc)))
1723 for (let [i, row] in array.iterItems(nodes)) {
1726 let display = (i >= start && i < end);
1727 if (display && row.parentNode != items) {
1729 var next = nodes[++i];
1730 if (next && next.parentNode != items)
1733 while (!next && i < end)
1734 items.insertBefore(row, next);
1736 else if (!display && row.parentNode == items)
1737 items.removeChild(row);
1739 if (context.items.length == 0)
1741 nodes.up.style.opacity = (start == 0) ? "0" : "1";
1742 if (end != context.items.length)
1743 nodes.down.style.display = "block";
1745 nodes.up.style.display = "block";
1747 nodes.up.style.display = "none";
1748 nodes.down.style.display = "none";
1752 this._divNodes.noCompletions.style.display = haveCompletions ? "none" : "block";
1754 this._completionElements = util.evaluateXPath("//xhtml:div[@dactyl:highlight='CompItem']", this._doc);
1759 clear: function clear() { this.setItems(); this._doc.body.innerHTML = ""; },
1760 get visible() !this._container.collapsed,
1761 set visible(val) this._container.collapsed = !val,
1763 reset: function reset(brief) {
1764 this._startIndex = this._endIndex = this._selIndex = -1;
1767 this.selectItem(-1);
1770 // if @param selectedItem is given, show the list and select that item
1771 setItems: function setItems(newItems, selectedItem) {
1772 if (this._selItem > -1)
1773 this._getCompletion(this._selItem).removeAttribute("selected");
1774 if (this._container.collapsed) {
1775 this._minHeight = 0;
1776 this._container.height = 0;
1778 this._startIndex = this._endIndex = this._selIndex = -1;
1779 this._items = newItems;
1781 if (typeof selectedItem == "number") {
1782 this.selectItem(selectedItem);
1783 this.visible = true;
1787 // select index, refill list if necessary
1788 selectItem: function selectItem(index) {
1789 //let now = Date.now();
1791 if (this._div == null)
1794 let sel = this._selIndex;
1795 let len = this._items.allItems.items.length;
1796 let newOffset = this._startIndex;
1797 let maxItems = options["maxitems"];
1798 let contextLines = Math.min(3, parseInt((maxItems - 1) / 2));
1800 if (index == -1 || index == null || index == len) { // wrapped around
1801 if (this._selIndex < 0)
1803 this._selIndex = -1;
1807 if (index <= this._startIndex + contextLines)
1808 newOffset = index - contextLines;
1809 if (index >= this._endIndex - contextLines)
1810 newOffset = index + contextLines - maxItems + 1;
1812 newOffset = Math.min(newOffset, len - maxItems);
1813 newOffset = Math.max(newOffset, 0);
1815 this._selIndex = index;
1819 this._getCompletion(sel).removeAttribute("selected");
1820 this._fill(newOffset);
1822 this._getCompletion(index).setAttribute("selected", "true");
1823 if (this._container.height != 0)
1824 util.scrollIntoView(this._getCompletion(index));
1828 // this.start = now;
1829 //if (index == Math.min(len - 1, 100))
1830 // util.dump({ time: Date.now() - this.start });
1833 onKeyPress: function onKeyPress(event) false
1835 WAITING_MESSAGE: _("completion.generating")
1838 // vim: set fdm=marker sw=4 ts=4 et: