// Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
-// Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
+// Copyright (c) 2008-2014 Kris Maglione <maglione.k@gmail.com>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
init: function init() {
let s = "dactyl-statusline-field-";
- XML.ignoreWhitespace = true;
- util.overlayWindow(window, {
+ overlay.overlayWindow(window, {
objects: {
eventTarget: commandline
},
- append: <e4x xmlns={XUL} xmlns:dactyl={NS}>
- <vbox id={config.commandContainer}>
- <vbox class="dactyl-container" hidden="false" collapsed="true">
- <iframe class="dactyl-completions" id="dactyl-completions-dactyl-commandline" src="dactyl://content/buffer.xhtml"
- contextmenu="dactyl-contextmenu"
- flex="1" hidden="false" collapsed="false"
- highlight="Events" events="mowEvents" />
- </vbox>
-
- <stack orient="horizontal" align="stretch" class="dactyl-container" id="dactyl-container" highlight="CmdLine CmdCmdLine">
- <textbox class="plain" id="dactyl-strut" flex="1" crop="end" collapsed="true"/>
- <textbox class="plain" id="dactyl-mode" flex="1" crop="end"/>
- <textbox class="plain" id="dactyl-message" flex="1" readonly="true"/>
-
- <hbox id="dactyl-commandline" hidden="false" class="dactyl-container" highlight="Normal CmdNormal" collapsed="true">
- <label id="dactyl-commandline-prompt" class="dactyl-commandline-prompt plain" flex="0" crop="end" value="" collapsed="true"/>
- <textbox id="dactyl-commandline-command" class="dactyl-commandline-command plain" flex="1" type="input" timeout="100"
- highlight="Events" />
- </hbox>
- </stack>
-
- <vbox class="dactyl-container" hidden="false" collapsed="false" highlight="CmdLine">
- <textbox id="dactyl-multiline-input" class="plain" flex="1" rows="1" hidden="false" collapsed="true" multiline="true"
- highlight="Normal Events" events="multilineInputEvents" />
- </vbox>
- </vbox>
-
- <stack id="dactyl-statusline-stack">
- <hbox id={s + "commandline"} hidden="false" class="dactyl-container" highlight="Normal StatusNormal" collapsed="true">
- <label id={s + "commandline-prompt"} class="dactyl-commandline-prompt plain" flex="0" crop="end" value="" collapsed="true"/>
- <textbox id={s + "commandline-command"} class="dactyl-commandline-command plain" flex="1" type="text" timeout="100"
- highlight="Events" />
- </hbox>
- </stack>
- </e4x>.elements(),
-
- before: <e4x xmlns={XUL} xmlns:dactyl={NS}>
- <toolbar id={statusline.statusBar.id}>
- <vbox id={"dactyl-completions-" + s + "commandline-container"} class="dactyl-container" hidden="false" collapsed="true">
- <iframe class="dactyl-completions" id={"dactyl-completions-" + s + "commandline"} src="dactyl://content/buffer.xhtml"
- contextmenu="dactyl-contextmenu" flex="1" hidden="false" collapsed="false"
- highlight="Events" events="mowEvents" />
- </vbox>
- </toolbar>
- </e4x>.elements()
+ append: [
+ ["vbox", { id: config.ids.commandContainer, xmlns: "xul" },
+ ["vbox", { class: "dactyl-container", hidden: "false", collapsed: "true" },
+ ["iframe", { class: "dactyl-completions", id: "dactyl-completions-dactyl-commandline",
+ src: "dactyl://content/buffer.xhtml", contextmenu: "dactyl-contextmenu",
+ flex: "1", hidden: "false", collapsed: "false",
+ highlight: "Events", events: "mowEvents" }]],
+
+ ["stack", { orient: "horizontal", align: "stretch", class: "dactyl-container",
+ id: "dactyl-container", highlight: "CmdLine CmdCmdLine" },
+ ["textbox", { class: "plain", id: "dactyl-strut", flex: "1", crop: "end", collapsed: "true" }],
+ ["textbox", { class: "plain", id: "dactyl-mode", flex: "1", crop: "end" }],
+ ["hbox", { id: "dactyl-message-box" },
+ ["label", { class: "plain", id: "dactyl-message-pre", flex: "0", readonly: "true", highlight: "WarningMsg" }],
+ ["textbox", { class: "plain", id: "dactyl-message", flex: "1", readonly: "true" }]],
+
+ ["hbox", { id: "dactyl-commandline", hidden: "false", class: "dactyl-container", highlight: "Normal CmdNormal", collapsed: "true" },
+ ["label", { id: "dactyl-commandline-prompt", class: "dactyl-commandline-prompt plain", flex: "0", crop: "end", value: "", collapsed: "true" }],
+ ["textbox", { id: "dactyl-commandline-command", class: "dactyl-commandline-command plain", flex: "1", type: "input", timeout: "100",
+ highlight: "Events" }]]],
+
+ ["vbox", { class: "dactyl-container", hidden: "false", collapsed: "false", highlight: "CmdLine" },
+ ["textbox", { id: "dactyl-multiline-input", class: "plain", flex: "1", rows: "1", hidden: "false", collapsed: "true",
+ multiline: "true", highlight: "Normal Events", events: "multilineInputEvents" }]]],
+
+ ["stack", { id: "dactyl-statusline-stack", xmlns: "xul" },
+ ["hbox", { id: s + "commandline", hidden: "false", class: "dactyl-container", highlight: "Normal StatusNormal", collapsed: "true" },
+ ["label", { id: s + "commandline-prompt", class: "dactyl-commandline-prompt plain", flex: "0", crop: "end", value: "", collapsed: "true" }],
+ ["textbox", { id: s + "commandline-command", class: "dactyl-commandline-command plain", flex: "1", type: "text", timeout: "100",
+ highlight: "Events", }]]]],
+
+ before: [
+ ["toolbar", { id: statusline.statusBar.id, xmlns: "xul" },
+ ["vbox", { id: "dactyl-completions-" + s + "commandline-container", class: "dactyl-container", hidden: "false", collapsed: "true" },
+ ["iframe", { class: "dactyl-completions", id: "dactyl-completions-" + s + "commandline", src: "dactyl://content/buffer.xhtml",
+ contextmenu: "dactyl-contextmenu", flex: "1", hidden: "false", collapsed: "false", highlight: "Events",
+ events: "mowEvents" }]]]]
});
this.elements = {};
return this.statusbar;
let statusElem = this.statusbar.message;
- if (value && !value[2] && statusElem.editor && statusElem.editor.rootElement.scrollWidth > statusElem.scrollWidth)
+ // Currently doesn't work as expected with <hbox> parent.
+ if (false && value && !value[2] && statusElem.editor && statusElem.editor.rootElement.scrollWidth > statusElem.scrollWidth)
return this.commandbar;
return this.activeGroup.mode;
}
});
+ this.addElement({
+ name: "message-pre",
+ defaultGroup: "WarningMsg",
+ getGroup: function () this.activeGroup.message
+ });
+
+ this.addElement({
+ name: "message-box",
+ defaultGroup: "Normal",
+ getGroup: function () this.activeGroup.message,
+ getValue: function () this.message
+ });
+
this.addElement({
name: "mode",
defaultGroup: "ModeMsg",
}
});
this.updateVisibility();
+
+ this.initialized = true;
},
addElement: function addElement(obj) {
const self = this;
function get(prefix, map, id) (obj.getElement || util.identity)(map[id] || document.getElementById(prefix + id));
- this.active.__defineGetter__(obj.name, function () self.activeGroup[obj.name][obj.name]);
- this.activeGroup.__defineGetter__(obj.name, function () self.getGroup(obj.name));
+ this.active.__defineGetter__(obj.name, () => this.activeGroup[obj.name][obj.name]);
+ this.activeGroup.__defineGetter__(obj.name, () => this.getGroup(obj.name));
- memoize(this.statusbar, obj.name, function () get("dactyl-statusline-field-", statusline.widgets, (obj.id || obj.name)));
- memoize(this.commandbar, obj.name, function () get("dactyl-", {}, (obj.id || obj.name)));
+ memoize(this.statusbar, obj.name, () => get("dactyl-statusline-field-", statusline.widgets, (obj.id || obj.name)));
+ memoize(this.commandbar, obj.name, () => get("dactyl-", {}, (obj.id || obj.name)));
if (!(obj.noValue || obj.getValue)) {
Object.defineProperty(this, obj.name, Modes.boundProperty({
highlight.highlightNode(elem,
(val[0] != null ? val[0] : obj.defaultGroup)
.split(/\s/).filter(util.identity)
- .map(function (g) g + " " + nodeSet.group + g)
+ .map(g => g + " " + nodeSet.group + g)
.join(" "));
elem.value = val[1];
if (obj.onChange)
let elem = nodeSet[obj.name];
if (elem)
highlight.highlightNode(elem, obj.defaultGroup.split(/\s/)
- .map(function (g) g + " " + nodeSet.group + g).join(" "));
+ .map(g => g + " " + nodeSet.group + g)
+ .join(" "));
});
}
},
},
updateVisibility: function updateVisibility() {
+ let changed = 0;
for (let elem in values(this.elements))
if (elem.getGroup) {
let value = elem.getValue ? elem.getValue.call(this)
let meth, node = group[elem.name];
let visible = (value && group === activeGroup);
if (node && !node.collapsed == !visible) {
+ changed++;
node.collapsed = !visible;
if (elem.onVisibility)
elem.onVisibility.call(this, node, visible);
// Might possibly be better to use a deck and programmatically
// choose which element to select.
function check(node) {
- if (util.computedStyle(node).display === "-moz-stack") {
- let nodes = Array.filter(node.children, function (n) !n.collapsed && n.boxObject.height);
- nodes.forEach(function (node, i) node.style.opacity = (i == nodes.length - 1) ? "" : "0");
+ if (DOM(node).style.display === "-moz-stack") {
+ let nodes = Array.filter(node.children, n => !n.collapsed && n.boxObject.height);
+ nodes.forEach((node, i) => {
+ node.style.opacity = (i == nodes.length - 1) ? "" : "0";
+ });
}
Array.forEach(node.children, check);
}
[this.commandbar.container, this.statusbar.container].forEach(check);
+
+ if (this.initialized && loaded.has("mow") && mow.visible)
+ mow.resize(false);
},
- active: Class.memoize(Object),
- activeGroup: Class.memoize(Object),
- commandbar: Class.memoize(function () ({ group: "Cmd" })),
- statusbar: Class.memoize(function () ({ group: "Status" })),
+ active: Class.Memoize(Object),
+ activeGroup: Class.Memoize(Object),
+ commandbar: Class.Memoize(function () ({ group: "Cmd" })),
+ statusbar: Class.Memoize(function () ({ group: "Status" })),
_ready: function _ready(elem) {
return elem.contentDocument.documentURI === elem.getAttribute("src") &&
yield elem;
},
- completionContainer: Class.memoize(function () this.completionList.parentNode),
+ completionContainer: Class.Memoize(function () this.completionList.parentNode),
- contextMenu: Class.memoize(function () {
+ contextMenu: Class.Memoize(function () {
["copy", "copylink", "selectall"].forEach(function (tail) {
// some host apps use "hostPrefixContext-copy" ids
- let xpath = "//xul:menuitem[contains(@id, '" + "ontext-" + tail + "') and not(starts-with(@id, 'dactyl-'))]";
- document.getElementById("dactyl-context-" + tail).style.listStyleImage =
- util.computedStyle(util.evaluateXPath(xpath, document).snapshotItem(0)).listStyleImage;
+ let css = "menuitem[id$='ontext-" + tail + "']:not([id^=dactyl-])";
+ let style = DOM(css, document).style;
+ DOM("#dactyl-context-" + tail, document).css({
+ listStyleImage: style.listStyleImage,
+ MozImageRegion: style.MozImageRegion
+ });
});
return document.getElementById("dactyl-contextmenu");
}),
- multilineOutput: Class.memoize(function () this._whenReady("dactyl-multiline-output", function (elem) {
- elem.contentWindow.addEventListener("unload", function (event) { event.preventDefault(); }, true);
- elem.contentDocument.documentElement.id = "dactyl-multiline-output-top";
- elem.contentDocument.body.id = "dactyl-multiline-output-content";
+ multilineOutput: Class.Memoize(function () this._whenReady("dactyl-multiline-output",
+ elem => {
+ highlight.highlightNode(elem.contentDocument.body, "MOW");
}), true),
- multilineInput: Class.memoize(function () document.getElementById("dactyl-multiline-input")),
+ multilineInput: Class.Memoize(() => document.getElementById("dactyl-multiline-input")),
- mowContainer: Class.memoize(function () document.getElementById("dactyl-multiline-output-container"))
+ mowContainer: Class.Memoize(() => document.getElementById("dactyl-multiline-output-container"))
}, {
getEditor: function getEditor(elem) {
elem.inputField.QueryInterface(Ci.nsIDOMNSEditableElement);
get command() this.widgets.command[1],
set command(val) this.widgets.command = val,
- get prompt() this.widgets.prompt,
- set prompt(val) this.widgets.prompt = val,
+ get prompt() this._open ? this.widgets.prompt : this._prompt,
+ set prompt(val) {
+ if (this._open)
+ this.widgets.prompt = val;
+ else
+ this._prompt = val;
+ },
open: function CM_open(command) {
dactyl.assert(isinstance(this.mode, modes.COMMAND_LINE),
- /*L*/"Not opening command line in non-command-line mode.");
+ /*L*/"Not opening command line in non-command-line mode.",
+ false);
this.messageCount = commandline.messageCount;
- modes.push(this.mode, this.extendedMode, this.closure);
+ modes.push(this.mode, this.extendedMode, this.bound);
this.widgets.active.commandline.collapsed = false;
this.widgets.prompt = this.prompt;
this.widgets.command = command || "";
+ this._open = true;
+
this.input = this.widgets.active.command.inputField;
if (this.historyKey)
this.history = CommandLine.History(this.input, this.historyKey, this);
commandline.commandSession = null;
this.input.dactylKeyPress = undefined;
+ let waiting = this.accepted && this.completions && this.completions.waiting;
+ if (waiting)
+ this.completions.onComplete = bind("onSubmit", this);
+
if (this.completions)
this.completions.cleanup();
if (this.history)
this.history.save();
- this.resetCompletions();
commandline.hideCompletions();
modes.delay(function () {
if (!this.keepCommand || commandline.silent || commandline.quiet)
commandline.hide();
- this[this.accepted ? "onSubmit" : "onCancel"](commandline.command);
+ if (!waiting)
+ this[this.accepted ? "onSubmit" : "onCancel"](commandline.command);
if (commandline.messageCount === this.messageCount)
commandline.clearMessage();
}, this);
this.onChange(commandline.command);
},
keyup: function CM_onKeyUp(event) {
- let key = events.toString(event);
+ let key = DOM.Event.stringify(event);
if (/-?Tab>$/.test(key) && this.completions)
this.completions.tabTimer.flush();
}
onSubmit: function (value) {},
resetCompletions: function CM_resetCompletions() {
- if (this.completions) {
- this.completions.context.cancelAll();
- this.completions.wildIndex = -1;
- this.completions.previewClear();
- }
+ if (this.completions)
+ this.completions.clear();
if (this.history)
this.history.reset();
},
prompt: ["Normal", ":"],
complete: function CEM_complete(context) {
- context.fork("ex", 0, completion, "ex");
+ try {
+ context.fork("ex", 0, completion, "ex");
+ }
+ catch (e) {
+ context.message = _("error.error", e);
+ }
},
onSubmit: function CEM_onSubmit(command) {
init.supercall(this);
},
- complete: function CPM_complete(context) {
+ complete: function CPM_complete(context, ...args) {
if (this.completer)
- context.forkapply("prompt", 0, this, "completer", Array.slice(arguments, 1));
+ context.forkapply("prompt", 0, this, "completer", args);
},
get mode() modes.PROMPT
*/
var CommandLine = Module("commandline", {
init: function init() {
- const self = this;
-
this._callbacks = {};
memoize(this, "_store", function () storage.newMap("command-history", { store: true, privateData: true }));
}, this);
},
- widgets: Class.memoize(function () CommandWidgets()),
+ widgets: Class.Memoize(() => CommandWidgets()),
runSilently: function runSilently(func, self) {
this.withSavedValues(["silent"], function () {
let elem = document.getElementById("dactyl-completions-" + node.id);
util.waitFor(bind(this.widgets._ready, null, elem));
- node.completionList = ItemList(elem.id);
+ node.completionList = ItemList(elem);
+ node.completionList.isAboveMow = node.id ==
+ this.widgets.statusbar.commandline.id;
}
return node.completionList;
},
if (!scroll || Date.now() - this._lastEchoTime > 5000)
this.clearMessage();
this._lastEchoTime = 0;
+ this.hiddenMessages = 0;
if (!this.commandSession) {
this.widgets.command = null;
},
clearMessage: function clearMessage() {
- if (this.widgets.message && this.widgets.message[1] === this._lastClearable)
+ if (this.widgets.message && this.widgets.message[1] === this._lastClearable) {
this.widgets.message = null;
+ this.hiddenMessages = 0;
+ }
},
/**
* Displays the multi-line output of a command, preceded by the last
* executed ex command string.
*
- * @param {XML} xml The output as an E4X XML object.
+ * @param {object} xml The output as a JSON XML object.
*/
commandOutput: function commandOutput(xml) {
- XML.ignoreWhitespace = XML.prettyPrinting = false;
- if (this.command)
- this.echo(<><div xmlns={XHTML}>:{this.command}</div>
{xml}</>, this.HIGHLIGHT_NORMAL, this.FORCE_MULTILINE);
- else
+ if (!this.command)
this.echo(xml, this.HIGHLIGHT_NORMAL, this.FORCE_MULTILINE);
+ else
+ this.echo([["div", { xmlns: "html" }, ":" + this.command], "\n", xml],
+ this.HIGHLIGHT_NORMAL, this.FORCE_MULTILINE);
this.command = null;
},
let field = this.widgets.active.message.inputField;
if (field.value && !forceSingle && field.editor.rootElement.scrollWidth > field.scrollWidth) {
this.widgets.message = null;
- mow.echo(<span highlight="Message">{str}</span>, highlightGroup, true);
+ mow.echo(["span", { highlight: "Message" }, str], highlightGroup, true);
}
},
+ _hiddenMessages: 0,
+ get hiddenMessages() this._hiddenMessages,
+ set hiddenMessages(val) {
+ this._hiddenMessages = val;
+ if (val)
+ this.widgets["message-pre"] = _("commandline.moreMessages", val) + " ";
+ else
+ this.widgets["message-pre"] = null;
+ },
+
_lastEcho: null,
/**
highlightGroup = highlightGroup || this.HL_NORMAL;
- if (flags & this.APPEND_TO_MESSAGES) {
- let message = isObject(data) ? data : { message: data };
+ let appendToMessages = (data) => {
+ let message = isObject(data) && !DOM.isJSONXML(data) ? data : { message: data };
// Make sure the memoized message property is an instance property.
message.message;
this._messageHistory.add(update({ highlight: highlightGroup }, message));
- data = message.message;
+ return message.message;
}
- if ((flags & this.ACTIVE_WINDOW) &&
- window != services.windowWatcher.activeWindow &&
- services.windowWatcher.activeWindow.dactyl)
+ if (flags & this.APPEND_TO_MESSAGES)
+ data = appendToMessages(data);
+
+ if ((flags & this.ACTIVE_WINDOW) && window != overlay.activeWindow)
return;
if ((flags & this.DISALLOW_MULTILINE) && !this.widgets.mowContainer.collapsed)
return;
- let single = flags & (this.FORCE_SINGLELINE | this.DISALLOW_MULTILINE);
+ let forceSingle = flags & (this.FORCE_SINGLELINE | this.DISALLOW_MULTILINE);
let action = this._echoLine;
if ((flags & this.FORCE_MULTILINE) || (/\n/.test(data) || !isinstance(data, [_, "String"])) && !(flags & this.FORCE_SINGLELINE))
- action = mow.closure.echo;
+ action = mow.bound.echo;
+
+ let checkSingleLine = () => action == this._echoLine;
- if (single)
+ if (forceSingle) {
this._lastEcho = null;
+ this.hiddenMessages = 0;
+ }
else {
- if (this.widgets.message && this.widgets.message[1] == this._lastEcho)
- mow.echo(<span highlight="Message">{this._lastEcho}</span>,
- this.widgets.message[0], true);
-
- if (action === this._echoLine && !(flags & this.FORCE_MULTILINE)
- && !(dactyl.fullyInitialized && this.widgets.mowContainer.collapsed)) {
+ // So complicated...
+ if (checkSingleLine() && !this.widgets.mowContainer.collapsed) {
highlightGroup += " Message";
- action = mow.closure.echo;
+ action = mow.bound.echo;
+ }
+ else if (!checkSingleLine() && this.widgets.mowContainer.collapsed) {
+ if (this._lastEcho && this.widgets.message && this.widgets.message[1] == this._lastEcho.msg) {
+ if (!(this._lastEcho.flags & this.APPEND_TO_MESSAGES))
+ appendToMessages(this._lastEcho.data);
+
+ mow.echo(
+ ["span", { highlight: "Message" },
+ ["span", { highlight: "WarningMsg" },
+ _("commandline.moreMessages", this.hiddenMessages + 1) + " "],
+ this._lastEcho.msg],
+ this.widgets.message[0], true);
+
+ this.hiddenMessages = 0;
+ }
+ }
+ else if (this._lastEcho && this.widgets.message && this.widgets.message[1] == this._lastEcho.msg) {
+ if (!(this._lastEcho.flags & this.APPEND_TO_MESSAGES))
+ appendToMessages(this._lastEcho.data);
+ if (checkSingleLine() && !(flags & this.APPEND_TO_MESSAGES))
+ appendToMessages(data);
+
+ flags |= this.APPEND_TO_MESSAGES;
+ this.hiddenMessages++;
}
- this._lastEcho = (action == this._echoLine) && data;
+ this._lastEcho = checkSingleLine() && { flags: flags, msg: data, data: arguments[0] };
}
this._lastClearable = action === this._echoLine && String(data);
this._lastEchoTime = (flags & this.FORCE_SINGLELINE) && Date.now();
if (action)
- action.call(this, data, highlightGroup, single);
+ action.call(this, data, highlightGroup, checkSingleLine());
},
_lastEchoTime: 0,
* pop at any time to close the prompt.
*
* @param {string} prompt The input prompt to use.
- * @param {function(string)} callback
* @param {Object} extra
* @... {function} onChange - A function to be called with the current
* input every time it changes.
* @... {string} default - The initial value that will be returned
* if the user presses <CR> straightaway. @default ""
*/
- input: function _input(prompt, callback, extra) {
- extra = extra || {};
+ input: promises.withCallbacks(function _input([callback, reject], prompt, extra={}, thing={}) {
+ if (callable(extra))
+ // Deprecated.
+ [callback, extra] = [extra, thing];
- CommandPromptMode(prompt, update({ onSubmit: callback }, extra)).open();
- },
+ CommandPromptMode(prompt, update({ onSubmit: callback, onCancel: reject }, extra)).open();
+ }),
readHeredoc: function readHeredoc(end) {
- let args;
- commandline.inputMultiline(end, function (res) { args = res; });
- util.waitFor(function () args !== undefined);
- return args;
+ return util.waitFor(commandline.inputMultiline(end));
},
/**
* callback with that string as a parameter.
*
* @param {string} end
- * @param {function(string)} callback
+ * @returns {Promise<string>}
*/
// FIXME: Buggy, especially when pasting.
- inputMultiline: function inputMultiline(end, callback) {
+ inputMultiline: promises.withCallbacks(function inputMultiline([callback], end) {
let cmd = this.command;
let self = {
end: "\n" + end + "\n",
this._autosizeMultilineInputWidget();
this.timeout(function () { dactyl.focus(this.widgets.multilineInput); }, 10);
- },
+ }),
get commandMode() this.commandSession && isinstance(modes.main, modes.COMMAND_LINE),
events: update(
iter(CommandMode.prototype.events).map(
- function ([event, handler]) [
+ ([event, handler]) => [
event, function (event) {
if (this.commandMode)
handler.call(this.commandSession, event);
updateOutputHeight: deprecated("mow.resize", function updateOutputHeight(open, extra) mow.resize(open, extra)),
- withOutputToString: function withOutputToString(fn, self) {
+ withOutputToString: function withOutputToString(fn, self, ...args) {
dactyl.registerObserver("echoLine", observe, true);
dactyl.registerObserver("echoMultiline", observe, true);
}
this.savingOutput = true;
- dactyl.trapErrors.apply(dactyl, [fn, self].concat(Array.slice(arguments, 2)));
+ dactyl.trapErrors.apply(dactyl, [fn, self].concat(args));
this.savingOutput = false;
- return output.map(function (elem) elem instanceof Node ? util.domToString(elem) : elem)
+ return output.map(elem => elem instanceof Node ? DOM.stringify(elem) : elem)
.join("\n");
}
}, {
save: function save() {
if (events.feedingKeys)
return;
+
let str = this.input.value;
if (/^\s*$/.test(str))
return;
- this.store = this.store.filter(function (line) (line.value || line) != str);
- try {
- this.store.push({ value: str, timestamp: Date.now()*1000, privateData: this.checkPrivate(str) });
- }
- catch (e) {
- dactyl.reportError(e);
- }
- this.store = this.store.slice(-options["history"]);
+
+ let privateData = this.checkPrivate(str);
+ if (privateData == "never-save")
+ return;
+
+ let store = Array.filter(this.store, line => (line.value || line) != str);
+ dactyl.trapErrors(
+ () => store.push({ value: str, timestamp: Date.now() * 1000, privateData: privateData }));
+ this.store = store.slice(Math.max(0, store.length - options["history"]));
},
/**
* @property {function} Returns whether a data item should be
* @param {string} val The new value.
*/
replace: function replace(val) {
- this.input.dactylKeyPress = undefined;
- if (this.completions)
- this.completions.previewClear();
- this.input.value = val;
- this.session.onHistory(val);
+ editor.withSavedValues(["skipSave"], function () {
+ editor.skipSave = true;
+
+ this.input.dactylKeyPress = undefined;
+ if (this.completions)
+ this.completions.previewClear();
+ this.input.value = val;
+ this.session.onHistory(val);
+ }, this);
},
/**
* @param {Object} input
*/
Completions: Class("Completions", {
+ UP: {},
+ DOWN: {},
+ CTXT_UP: {},
+ CTXT_DOWN: {},
+ PAGE_UP: {},
+ PAGE_DOWN: {},
+ RESET: null,
+
init: function init(input, session) {
+ let self = this;
+
this.context = CompletionContext(input.QueryInterface(Ci.nsIDOMNSEditableElement).editor);
- this.context.onUpdate = this.closure._reset;
+ this.context.onUpdate = function onUpdate() { self.asyncUpdate(this); };
+
this.editor = input.editor;
this.input = input;
this.session = session;
- this.selected = null;
+
this.wildmode = options.get("wildmode");
this.wildtypes = this.wildmode.value;
+
this.itemList = commandline.completionList;
- this.itemList.setItems(this.context);
+ this.itemList.open(this.context);
- dactyl.registerObserver("events.doneFeeding", this.closure.onDoneFeeding, true);
+ dactyl.registerObserver("events.doneFeeding", this.bound.onDoneFeeding, true);
this.autocompleteTimer = Timer(200, 500, function autocompleteTell(tabPressed) {
if (events.feedingKeys && !tabPressed)
this.complete(true, false);
}
}, this);
+
this.tabTimer = Timer(0, 0, function tabTell(event) {
- this.tab(event.shiftKey, event.altKey && options["altwildmode"]);
+ let tabCount = this.tabCount;
+ this.tabCount = 0;
+ this.tab(tabCount, event.altKey && options["altwildmode"]);
}, this);
},
- cleanup: function () {
- dactyl.unregisterObserver("events.doneFeeding", this.closure.onDoneFeeding);
- this.previewClear();
- this.tabTimer.reset();
- this.autocompleteTimer.reset();
- this.itemList.visible = false;
- this.input.dactylKeyPress = undefined;
- },
+ tabCount: 0,
ignoredCount: 0,
+
+ /**
+ * @private
+ */
onDoneFeeding: function onDoneFeeding() {
if (this.ignoredCount)
this.autocompleteTimer.flush(true);
this.ignoredCount = 0;
},
- UP: {},
- DOWN: {},
- PAGE_UP: {},
- PAGE_DOWN: {},
- RESET: null,
+ /**
+ * @private
+ */
+ onTab: function onTab(event) {
+ this.tabCount += event.shiftKey ? -1 : 1;
+ this.tabTimer.tell(event);
+ },
- lastSubstring: "",
+ get activeContexts() this.context.contextList
+ .filter(c => c.items.length || c.incomplete),
+ /**
+ * Returns the current completion string relative to the
+ * offset of the currently selected context.
+ */
get completion() {
- let str = commandline.command;
- return str.substring(this.prefix.length, str.length - this.suffix.length);
+ let offset = this.selected ? this.selected[0].offset : this.start;
+ return commandline.command.slice(offset, this.caret);
},
- set completion(completion) {
- this.previewClear();
- // Change the completion text.
- // The second line is a hack to deal with some substring
- // preview corner cases.
- let value = this.prefix + completion + this.suffix;
- commandline.widgets.active.command.value = value;
- this.editor.selection.focusNode.textContent = value;
+ /**
+ * Updates the input field from *offset* to {@link #caret}
+ * with the value *value*. Afterward, the caret is moved
+ * just after the end of the updated text.
+ *
+ * @param {number} offset The offset in the original input
+ * string at which to insert *value*.
+ * @param {string} value The value to insert.
+ */
+ setCompletion: function setCompletion(offset, value) {
+ editor.withSavedValues(["skipSave"], function () {
+ editor.skipSave = true;
+ this.previewClear();
+
+ if (value == null)
+ var [input, caret] = [this.originalValue, this.originalCaret];
+ else {
+ input = this.getCompletion(offset, value);
+ caret = offset + value.length;
+ }
+
+ // Change the completion text.
+ // The second line is a hack to deal with some substring
+ // preview corner cases.
+ commandline.widgets.active.command.value = input;
+ this.editor.selection.focusNode.textContent = input;
- // Reset the caret to one position after the completion.
- this.caret = this.prefix.length + completion.length;
- this._caret = this.caret;
+ this.caret = caret;
+ this._caret = this.caret;
- this.input.dactylKeyPress = undefined;
+ this.input.dactylKeyPress = undefined;
+ }, this);
+ },
+
+ /**
+ * For a given offset and completion string, returns the
+ * full input value after selecting that item.
+ *
+ * @param {number} offset The offset at which to insert the
+ * completion.
+ * @param {string} value The value to insert.
+ * @returns {string};
+ */
+ getCompletion: function getCompletion(offset, value) {
+ return this.originalValue.substr(0, offset)
+ + value
+ + this.originalValue.substr(this.originalCaret);
+ },
+
+ get selected() this.itemList.selected,
+ set selected(tuple) {
+ if (!array.equals(tuple || [],
+ this.itemList.selected || []))
+ this.itemList.select(tuple);
+
+ if (!tuple)
+ this.setCompletion(null);
+ else {
+ let [ctxt, idx] = tuple;
+ this.setCompletion(ctxt.offset, ctxt.items[idx].result);
+ }
},
get caret() this.editor.selection.getRangeAt(0).startOffset,
set caret(offset) {
- this.editor.selection.getRangeAt(0).setStart(this.editor.rootElement.firstChild, offset);
- this.editor.selection.getRangeAt(0).setEnd(this.editor.rootElement.firstChild, offset);
+ this.editor.selection.collapse(this.editor.rootElement.firstChild, offset);
},
get start() this.context.allItems.start,
get wildtype() this.wildtypes[this.wildIndex] || "",
+ /**
+ * Cleanup resources used by this completion session. This
+ * instance should not be used again once this method is
+ * called.
+ */
+ cleanup: function cleanup() {
+ dactyl.unregisterObserver("events.doneFeeding", this.bound.onDoneFeeding);
+ this.previewClear();
+
+ this.tabTimer.reset();
+ this.autocompleteTimer.reset();
+ if (!this.onComplete)
+ this.context.cancelAll();
+
+ this.itemList.visible = false;
+ this.input.dactylKeyPress = undefined;
+ this.hasQuit = true;
+ },
+
+ /**
+ * Run the completer.
+ *
+ * @param {boolean} show Passed to {@link #reset}.
+ * @param {boolean} tabPressed Should be set to true if, and
+ * only if, this function is being called in response
+ * to a <Tab> press.
+ */
complete: function complete(show, tabPressed) {
this.session.ignoredCount = 0;
+
+ this.waiting = null;
this.context.reset();
this.context.tabPressed = tabPressed;
+
this.session.complete(this.context);
if (!this.session.active)
return;
- this.context.updateAsync = true;
+
this.reset(show, tabPressed);
this.wildIndex = 0;
this._caret = this.caret;
},
+ /**
+ * Clear any preview string and cancel any pending
+ * asynchronous context. Called when there is further input
+ * to be processed.
+ */
+ clear: function clear() {
+ this.context.cancelAll();
+ this.wildIndex = -1;
+ this.previewClear();
+ },
+
+ /**
+ * Saves the current input state. To be called before an
+ * item is selected in a new set of completion responses.
+ * @private
+ */
+ saveInput: function saveInput() {
+ this.originalValue = this.context.value;
+ this.originalCaret = this.caret;
+ },
+
+ /**
+ * Resets the completion state.
+ *
+ * @param {boolean} show If true and options allow the
+ * completion list to be shown, show it.
+ */
+ reset: function reset(show) {
+ this.waiting = null;
+ this.wildIndex = -1;
+
+ this.saveInput();
+
+ if (show) {
+ this.itemList.update();
+ this.context.updateAsync = true;
+ if (this.haveType("list"))
+ this.itemList.visible = true;
+ this.wildIndex = 0;
+ }
+
+ this.preview();
+ },
+
+ /**
+ * Calls when an asynchronous completion context has new
+ * results to return.
+ *
+ * @param {CompletionContext} context The changed context.
+ * @private
+ */
+ asyncUpdate: function asyncUpdate(context) {
+ if (this.hasQuit) {
+ let item = this.getItem(this.waiting);
+ if (item && this.waiting && this.onComplete) {
+ util.trapErrors("onComplete", this,
+ this.getCompletion(this.waiting[0].offset,
+ item.result));
+ this.waiting = null;
+ this.context.cancelAll();
+ }
+ return;
+ }
+
+ let value = this.editor.selection.focusNode.textContent;
+ this.saveInput();
+
+ if (this.itemList.visible)
+ this.itemList.updateContext(context);
+
+ if (this.waiting && this.waiting[0] == context)
+ this.select(this.waiting);
+ else if (!this.waiting) {
+ let cursor = this.selected;
+ if (cursor && cursor[0] == context) {
+ let item = this.getItem(cursor);
+ if (!item || this.completion != item.result)
+ this.itemList.select(null);
+ }
+
+ this.preview();
+ }
+ },
+
+ /**
+ * Returns true if the currently selected 'wildmode' index
+ * has the given completion type.
+ */
haveType: function haveType(type)
this.wildmode.checkHas(this.wildtype, type == "first" ? "" : type),
+ /**
+ * Returns the completion item for the given selection
+ * tuple.
+ *
+ * @param {[CompletionContext,number]} tuple The spec of the
+ * item to return.
+ * @default {@link #selected}
+ * @returns {object}
+ */
+ getItem: function getItem(tuple=this.selected)
+ tuple && tuple[0] && tuple[0].items[tuple[1]],
+
+ /**
+ * Returns a tuple representing the next item, at the given
+ * *offset*, from *tuple*.
+ *
+ * @param {[CompletionContext,number]} tuple The offset from
+ * which to search.
+ * @default {@link #selected}
+ * @param {number} offset The positive or negative offset to
+ * find.
+ * @default 1
+ * @param {boolean} noWrap If true, and the search would
+ * wrap, return null.
+ */
+ nextItem: function nextItem(tuple, offset, noWrap) {
+ if (tuple === undefined)
+ tuple = this.selected;
+
+ return this.itemList.getRelativeItem(offset || 1, tuple, noWrap);
+ },
+
+ /**
+ * The last previewed substring.
+ * @private
+ */
+ lastSubstring: "",
+
+ /**
+ * Displays a preview of the text provided by the next <Tab>
+ * press if the current input is an anchored substring of
+ * that result.
+ */
preview: function preview() {
this.previewClear();
- if (this.wildIndex < 0 || this.suffix || !this.items.length)
+ if (this.wildIndex < 0 || this.caret < this.input.value.length
+ || !this.activeContexts.length || this.waiting)
return;
let substring = "";
switch (this.wildtype.replace(/.*:/, "")) {
case "":
- substring = this.items[0].result;
+ var cursor = this.nextItem(null);
break;
case "longest":
if (this.items.length > 1) {
}
// Fallthrough
case "full":
- let item = this.items[this.selected != null ? this.selected + 1 : 0];
- if (item)
- substring = item.result;
+ cursor = this.nextItem();
break;
}
+ if (cursor)
+ substring = this.getItem(cursor).result;
// Don't show 1-character substrings unless we've just hit backspace
- if (substring.length < 2 && this.lastSubstring.indexOf(substring) !== 0)
+ if (substring.length < 2 && this.lastSubstring.indexOf(substring))
return;
this.lastSubstring = substring;
let value = this.completion;
if (util.compareIgnoreCase(value, substring.substr(0, value.length)))
return;
+
substring = substring.substr(value.length);
this.removeSubstring = substring;
- let node = util.xmlToDom(<span highlight="Preview">{substring}</span>,
- document);
- let start = this.caret;
- this.editor.insertNode(node, this.editor.rootElement, 1);
- this.caret = start;
+ let node = DOM.fromJSON(["span", { highlight: "Preview" }, substring],
+ document);
+
+ this.withSavedValues(["caret"], function () {
+ this.editor.insertNode(node, this.editor.rootElement, 1);
+ });
},
+ /**
+ * Clears the currently displayed next-<Tab> preview string.
+ */
previewClear: function previewClear() {
let node = this.editor.rootElement.firstChild;
if (node && node.nextSibling) {
try {
- this.editor.deleteNode(node.nextSibling);
+ DOM(node.nextSibling).remove();
}
catch (e) {
node.nextSibling.textContent = "";
delete this.removeSubstring;
},
- reset: function reset(show) {
- this.wildIndex = -1;
-
- this.prefix = this.context.value.substring(0, this.start);
- this.value = this.context.value.substring(this.start, this.caret);
- this.suffix = this.context.value.substring(this.caret);
-
- if (show) {
- this.itemList.reset();
- if (this.haveType("list"))
- this.itemList.visible = true;
- this.selected = null;
- this.wildIndex = 0;
- }
-
- this.preview();
- },
-
- _reset: function _reset() {
- let value = this.editor.selection.focusNode.textContent;
- this.prefix = value.substring(0, this.start);
- this.value = value.substring(this.start, this.caret);
- this.suffix = value.substring(this.caret);
+ /**
+ * Selects a completion based on the value of *idx*.
+ *
+ * @param {[CompletionContext,number]|const object} The
+ * (context,index) tuple of the item to select, or an
+ * offset constant from this object.
+ * @param {number} count When given an offset constant,
+ * select *count* units.
+ * @default 1
+ * @param {boolean} fromTab If true, this function was
+ * called by {@link #tab}.
+ * @default false
+ * @private
+ */
+ select: function select(idx, count=1, fromTab=false) {
+ switch (idx) {
+ case this.UP:
+ case this.DOWN:
+ idx = this.nextItem(this.waiting || this.selected,
+ idx == this.UP ? -count : count,
+ true);
+ break;
- this.itemList.reset();
- this.itemList.selectItem(this.selected);
+ case this.CTXT_UP:
+ case this.CTXT_DOWN:
+ let groups = this.itemList.activeGroups;
+ let i = Math.max(0, groups.indexOf(this.itemList.selectedGroup));
- this.preview();
- },
+ i += idx == this.CTXT_DOWN ? 1 : -1;
+ i %= groups.length;
+ if (i < 0)
+ i += groups.length;
- select: function select(idx) {
- switch (idx) {
- case this.UP:
- if (this.selected == null)
- idx = -2;
- else
- idx = this.selected - 1;
+ var position = 0;
+ idx = [groups[i].context, 0];
break;
- case this.DOWN:
- if (this.selected == null)
- idx = 0;
- else
- idx = this.selected + 1;
+
+ case this.PAGE_UP:
+ case this.PAGE_DOWN:
+ idx = this.itemList.getRelativePage(idx == this.PAGE_DOWN ? 1 : -1);
break;
+
case this.RESET:
idx = null;
break;
+
default:
- idx = Math.constrain(idx, 0, this.items.length - 1);
break;
}
- if (idx == -1 || this.items.length && idx >= this.items.length || idx == null) {
- // Wrapped. Start again.
- this.selected = null;
- this.completion = this.value;
- }
- else {
- // Wait for contexts to complete if necessary.
- // FIXME: Need to make idx relative to individual contexts.
- let list = this.context.contextList;
- if (idx == -2)
- list = list.slice().reverse();
- let n = 0;
- try {
- this.waiting = true;
- for (let [, context] in Iterator(list)) {
- let done = function done() !(idx >= n + context.items.length || idx == -2 && !context.items.length);
-
- util.waitFor(function () !context.incomplete || done());
- if (done())
- break;
+ if (!fromTab)
+ this.wildIndex = this.wildtypes.length - 1;
- n += context.items.length;
- }
- }
- finally {
- this.waiting = false;
+ if (idx && idx[1] >= idx[0].items.length) {
+ if (!idx[0].incomplete)
+ this.waiting = null;
+ else {
+ this.waiting = idx;
+ statusline.progress = _("completion.waitingForResults");
}
+ return;
+ }
- // See previous FIXME. This will break if new items in
- // a previous context come in.
- if (idx < 0)
- idx = this.items.length - 1;
- if (this.items.length == 0)
- return;
+ this.waiting = null;
- this.selected = idx;
- this.completion = this.items[idx].result;
- }
+ this.itemList.select(idx, null, position);
+ this.selected = idx;
- this.itemList.selectItem(idx);
- },
+ this.preview();
- tabs: [],
+ if (this.selected == null)
+ statusline.progress = "";
+ else
+ statusline.progress = _("completion.matchIndex",
+ this.itemList.getOffset(idx),
+ this.itemList.itemCount);
+ },
- tab: function tab(reverse, wildmode) {
+ /**
+ * Selects a completion result based on the 'wildmode'
+ * option, or the value of the *wildmode* parameter.
+ *
+ * @param {number} offset The positive or negative number of
+ * tab presses to process.
+ * @param {[string]} wildmode A 'wildmode' value to
+ * substitute for the value of the 'wildmode' option.
+ * @optional
+ */
+ tab: function tab(offset, wildmode) {
this.autocompleteTimer.flush();
this.ignoredCount = 0;
if (this.context.waitingForTab || this.wildIndex == -1)
this.complete(true, true);
- this.tabs.push([reverse, wildmode || options["wildmode"]]);
- if (this.waiting)
- return;
-
- while (this.tabs.length) {
- [reverse, this.wildtypes] = this.tabs.shift();
+ this.wildtypes = wildmode || options["wildmode"];
+ let count = Math.abs(offset);
+ let steps = Math.constrain(this.wildtypes.length - this.wildIndex,
+ 1, count);
+ count = Math.max(1, count - steps);
+ while (steps--) {
this.wildIndex = Math.min(this.wildIndex, this.wildtypes.length - 1);
switch (this.wildtype.replace(/.*:/, "")) {
case "":
- this.select(0);
+ this.select(this.nextItem(null));
break;
case "longest":
- if (this.items.length > 1) {
+ if (this.itemList.itemCount > 1) {
if (this.substring && this.substring.length > this.completion.length)
- this.completion = this.substring;
+ this.setCompletion(this.start, this.substring);
break;
}
// Fallthrough
case "full":
- this.select(reverse ? this.UP : this.DOWN);
+ let c = steps ? 1 : count;
+ this.select(offset < 0 ? this.UP : this.DOWN, c, true);
break;
}
this.itemList.visible = true;
this.wildIndex++;
- this.preview();
-
- if (this.selected == null)
- statusline.progress = "";
- else
- statusline.progress = _("completion.matchIndex", this.selected + 1, this.items.length);
}
- if (this.items.length == 0)
+ if (this.items.length == 0 && !this.waiting)
dactyl.beep();
}
}),
arg = dactyl.userEval(arg);
if (isObject(arg))
arg = util.objectToString(arg, useColor);
- else
- arg = String(arg);
+ else if (callable(arg))
+ arg = String.replace(arg, "/* use strict */ \n", "/* use strict */ ");
+ else if (!isString(arg) && useColor)
+ arg = template.highlight(arg);
return arg;
}
}, {
- commands: function init_commands() {
+ commands: function initCommands() {
[
{
name: "ec[ho]",
commandline.echo(message.message, message.highlight, commandline.FORCE_SINGLELINE);
}
else if (commandline._messageHistory.length > 1) {
- XML.ignoreWhitespace = false;
commandline.commandOutput(
- template.map(commandline._messageHistory.messages, function (message)
- <div highlight={message.highlight + " Message"}>{message.message}</div>));
+ template.map(commandline._messageHistory.messages, message =>
+ ["div", { highlight: message.highlight + " Message" },
+ message.message]));
}
},
{ argCount: "0" });
commands.add(["sil[ent]"],
"Run a command silently",
function (args) {
- commandline.runSilently(function () commands.execute(args[0] || "", null, true));
+ commandline.runSilently(() => { commands.execute(args[0] || "", null, true); });
}, {
completer: function (context) completion.ex(context),
literal: 0,
});
},
modes: function initModes() {
+ initModes.require("editor");
+
modes.addMode("COMMAND_LINE", {
char: "c",
description: "Active when the command line is focused",
});
modes.addMode("INPUT_MULTILINE", {
+ description: "Active when the command line's multiline input buffer is open",
bases: [modes.INSERT]
});
},
- mappings: function init_mappings() {
+ mappings: function initMappings() {
mappings.add([modes.COMMAND],
[":"], "Enter Command Line mode",
text = text.substring(1, index);
modes.pop();
- return function () self.callback.call(commandline, text);
+ return () => self.callback.call(commandline, text);
}
return Events.PASS;
});
- let bind = function bind()
- mappings.add.apply(mappings, [[modes.COMMAND_LINE]].concat(Array.slice(arguments)))
+ let bind = function bind(...args) mappings.add.apply(mappings, [[modes.COMMAND_LINE]].concat(args));
+
+ bind(["<Esc>", "<C-[>"], "Stop waiting for completions or exit Command Line mode",
+ function ({ self }) {
+ if (self.completions && self.completions.waiting)
+ self.completions.waiting = null;
+ else
+ return Events.PASS;
+ });
// Any "non-keyword" character triggers abbreviation expansion
// TODO: Add "<CR>" and "<Tab>" to this list
bind(["<Return>", "<C-j>", "<C-m>"], "Accept the current input",
function ({ self }) {
+ if (self.completions)
+ self.completions.tabTimer.flush();
+
let command = commandline.command;
self.accepted = true;
});
[
- [["<Up>", "<A-p>"], "previous matching", true, true],
- [["<S-Up>", "<C-p>", "<PageUp>"], "previous", true, false],
- [["<Down>", "<A-n>"], "next matching", false, true],
- [["<S-Down>", "<C-n>", "<PageDown>"], "next", false, false]
+ [["<Up>", "<A-p>", "<cmd-prev-match>"], "previous matching", true, true],
+ [["<S-Up>", "<C-p>", "<cmd-prev>"], "previous", true, false],
+ [["<Down>", "<A-n>", "<cmd-next-match>"], "next matching", false, true],
+ [["<S-Down>", "<C-n>", "<cmd-next>"], "next", false, false]
].forEach(function ([keys, desc, up, search]) {
bind(keys, "Recall the " + desc + " command line from the history list",
function ({ self }) {
});
});
- bind(["<A-Tab>", "<Tab>"], "Select the next matching completion item",
+ bind(["<A-Tab>", "<Tab>", "<A-compl-next>", "<compl-next>"],
+ "Select the next matching completion item",
+ function ({ keypressEvents, self }) {
+ dactyl.assert(self.completions);
+ self.completions.onTab(keypressEvents[0]);
+ });
+
+ bind(["<A-S-Tab>", "<S-Tab>", "<A-compl-prev>", "<compl-prev>"],
+ "Select the previous matching completion item",
+ function ({ keypressEvents, self }) {
+ dactyl.assert(self.completions);
+ self.completions.onTab(keypressEvents[0]);
+ });
+
+ bind(["<C-Tab>", "<A-f>", "<compl-next-group>"],
+ "Select the next matching completion group",
+ function ({ keypressEvents, self }) {
+ dactyl.assert(self.completions);
+ self.completions.tabTimer.flush();
+ self.completions.select(self.completions.CTXT_DOWN);
+ });
+
+ bind(["<C-S-Tab>", "<A-S-f>", "<compl-prev-group>"],
+ "Select the previous matching completion group",
+ function ({ keypressEvents, self }) {
+ dactyl.assert(self.completions);
+ self.completions.tabTimer.flush();
+ self.completions.select(self.completions.CTXT_UP);
+ });
+
+ bind(["<C-f>", "<PageDown>", "<compl-next-page>"],
+ "Select the next page of completions",
function ({ keypressEvents, self }) {
dactyl.assert(self.completions);
- self.completions.tabTimer.tell(keypressEvents[0]);
+ self.completions.tabTimer.flush();
+ self.completions.select(self.completions.PAGE_DOWN);
});
- bind(["<A-S-Tab>", "<S-Tab>"], "Select the previous matching completion item",
+ bind(["<C-b>", "<PageUp>", "<compl-prev-page>"],
+ "Select the previous page of completions",
function ({ keypressEvents, self }) {
dactyl.assert(self.completions);
- self.completions.tabTimer.tell(keypressEvents[0]);
+ self.completions.tabTimer.flush();
+ self.completions.select(self.completions.PAGE_UP);
});
bind(["<BS>", "<C-h>"], "Delete the previous character",
bind(["<C-]>", "<C-5>"], "Expand command line abbreviation",
function () { editor.expandAbbreviation(modes.COMMAND_LINE); });
},
- options: function init_options() {
+ options: function initOptions() {
options.add(["history", "hi"],
"Number of Ex commands and search patterns to store in the command-line history",
"number", 500,
"number", 100,
{ validator: function (value) value >= 0 });
},
- sanitizer: function init_sanitizer() {
+ sanitizer: function initSanitizer() {
sanitizer.addItem("commandline", {
description: "Command-line and search history",
persistent: true,
let store = commandline._store;
for (let [k, v] in store) {
if (k == "command")
- store.set(k, v.filter(function (item)
+ store.set(k, v.filter(item =>
!(timespan.contains(item.timestamp) && (!host || commands.hasDomain(item.value, host)))));
else if (!host)
- store.set(k, v.filter(function (item) !timespan.contains(item.timestamp)));
+ store.set(k, v.filter(item => !timespan.contains(item.timestamp)));
}
}
});
sanitizer.addItem("history", {
action: function (timespan, host) {
commandline._store.set("command",
- commandline._store.get("command", []).filter(function (item)
+ commandline._store.get("command", []).filter(item =>
!(timespan.contains(item.timestamp) && (host ? commands.hasDomain(item.value, host)
: item.privateData))));
- commandline._messageHistory.filter(function (item) !timespan.contains(item.timestamp * 1000) ||
- !item.domains && !item.privateData ||
- host && (!item.domains || !item.domains.some(function (d) util.isSubdomain(d, host))));
+ commandline._messageHistory.filter(item =>
+ ( !timespan.contains(item.timestamp * 1000)
+ || !item.domains && !item.privateData
+ || host && ( !item.domains
+ || !item.domains.some(d => util.isSubdomain(d, host)))));
}
});
sanitizer.addItem("messages", {
description: "Saved :messages",
action: function (timespan, host) {
- commandline._messageHistory.filter(function (item) !timespan.contains(item.timestamp * 1000) ||
- host && (!item.domains || !item.domains.some(function (d) util.isSubdomain(d, host))));
+ commandline._messageHistory.filter(item =>
+ ( !timespan.contains(item.timestamp * 1000)
+ || host && ( !item.domains
+ || !item.domains.some(d => util.isSubdomain(d, host)))));
}
});
}
});
/**
- * The list which is used for the completion box (and QuickFix window in
- * future).
+ * The list which is used for the completion box.
*
* @param {string} id The id of the iframe which will display the list. It
* must be in its own container element, whose height it will update as
* necessary.
*/
+
var ItemList = Class("ItemList", {
- init: function init(id) {
- this._completionElements = [];
-
- var iframe = document.getElementById(id);
-
- this._doc = iframe.contentDocument;
- this._win = iframe.contentWindow;
- this._container = iframe.parentNode;
-
- this._doc.documentElement.id = id + "-top";
- this._doc.body.id = id + "-content";
- this._doc.body.className = iframe.className + "-content";
- this._doc.body.appendChild(this._doc.createTextNode(""));
- this._doc.body.style.borderTop = "1px solid black"; // FIXME: For cases where completions/MOW are shown at once, or ls=0. Should use :highlight.
-
- this._items = null;
- this._startIndex = -1; // The index of the first displayed item
- this._endIndex = -1; // The index one *after* the last displayed item
- this._selIndex = -1; // The index of the currently selected element
- this._div = null;
- this._divNodes = {};
- this._minHeight = 0;
+ CONTEXT_LINES: 2,
+
+ init: function init(frame) {
+ this.frame = frame;
+
+ this.doc = frame.contentDocument;
+ this.win = frame.contentWindow;
+ this.body = this.doc.body;
+ this.container = frame.parentNode;
+
+ highlight.highlightNode(this.doc.body, "Comp");
+
+ this._onResize = Timer(20, 400, function _onResize(event) {
+ if (this.visible)
+ this.onResize(event);
+ }, this);
+ this._resize = Timer(20, 400, function _resize(flags) {
+ if (this.visible)
+ this.resize(flags);
+ }, this);
+
+ DOM(this.win).resize(this._onResize.bound.tell);
},
- _dom: function _dom(xml, map) util.xmlToDom(xml instanceof XML ? xml : <>{xml}</>, this._doc, map),
+ get rootXML()
+ ["div", { highlight: "Normal", style: "white-space: nowrap", key: "root" },
+ ["div", { key: "wrapper" },
+ ["div", { highlight: "Completions", key: "noCompletions" },
+ ["span", { highlight: "Title" },
+ _("completion.noCompletions")]],
+ ["div", { key: "completions" }]],
+
+ ["div", { highlight: "Completions" },
+ template.map(util.range(0, options["maxitems"] * 2), i =>
+ ["div", { highlight: "CompItem NonText" },
+ "~"])]],
+
+ get itemCount() this.context.contextList
+ .reduce((acc, ctxt) => acc + ctxt.items.length, 0),
+
+ get visible() !this.container.collapsed,
+ set visible(val) this.container.collapsed = !val,
+
+ get activeGroups() this.context.contextList
+ .filter(c => c.items.length || c.message || c.incomplete)
+ .map(this.getGroup, this),
+
+ get selected() let (g = this.selectedGroup) g && g.selectedIdx != null
+ ? [g.context, g.selectedIdx] : null,
+
+ getRelativeItem: function getRelativeItem(offset, tuple, noWrap) {
+ let groups = this.activeGroups;
+ if (!groups.length)
+ return null;
+
+ let group = this.selectedGroup || groups[0];
+ let start = group.selectedIdx || 0;
+ if (tuple === null) { // Kludge.
+ if (offset > 0)
+ tuple = [this.activeGroups[0], -1];
+ else {
+ let group = this.activeGroups.slice(-1)[0];
+ tuple = [group, group.itemCount];
+ }
+ }
+ if (tuple)
+ [group, start] = tuple;
+
+ group = this.getGroup(group);
- _autoSize: function _autoSize() {
- if (!this._div)
- return;
+ start = (group.offsets.start + start + offset);
+ if (!noWrap)
+ start %= this.itemCount || 1;
+ if (start < 0 && (!noWrap || arguments[1] === null))
+ start += this.itemCount;
- if (this._container.collapsed)
- this._div.style.minWidth = document.getElementById("dactyl-commandline").scrollWidth + "px";
+ if (noWrap && offset > 0) {
+ // Check if we've passed any incomplete contexts
- this._minHeight = Math.max(this._minHeight,
- this._win.scrollY + this._divNodes.completions.getBoundingClientRect().bottom);
+ let i = groups.indexOf(group);
+ util.assert(i >= 0, undefined, false);
+ for (; i < groups.length; i++) {
+ let end = groups[i].offsets.start + groups[i].itemCount;
+ if (start >= end && groups[i].context.incomplete)
+ return [groups[i].context, start - groups[i].offsets.start];
- if (this._container.collapsed)
- this._div.style.minWidth = "";
+ if (start >= end);
+ break;
+ }
+ }
- // FIXME: Belongs elsewhere.
- mow.resize(false, Math.max(0, this._minHeight - this._container.height));
+ if (start < 0 || start >= this.itemCount)
+ return null;
- this._container.height = this._minHeight;
- this._container.height -= mow.spaceNeeded;
- mow.resize(false);
- this.timeout(function () {
- this._container.height -= mow.spaceNeeded;
- });
+ group = groups.find(g => let (i = start - g.offsets.start) i >= 0 && i < g.itemCount);
+ return [group.context, start - group.offsets.start];
},
- _getCompletion: function _getCompletion(index) this._completionElements.snapshotItem(index - this._startIndex),
-
- _init: function _init() {
- this._div = this._dom(
- <div class="ex-command-output" highlight="Normal" style="white-space: nowrap">
- <div highlight="Completions" key="noCompletions"><span highlight="Title">{_("completion.noCompletions")}</span></div>
- <div key="completions"/>
- <div highlight="Completions">
- {
- template.map(util.range(0, options["maxitems"] * 2), function (i)
- <div highlight="CompItem NonText">
- <li>~</li>
- </div>)
- }
- </div>
- </div>, this._divNodes);
- this._doc.body.replaceChild(this._div, this._doc.body.firstChild);
- util.scrollIntoView(this._div, true);
-
- this._items.contextList.forEach(function init_eachContext(context) {
- delete context.cache.nodes;
- if (!context.items.length && !context.message && !context.incomplete)
- return;
- context.cache.nodes = [];
- this._dom(<div key="root" highlight="CompGroup">
- <div highlight="Completions">
- { context.createRow(context.title || [], "CompTitle") }
- </div>
- <div highlight="CompTitleSep"/>
- <div key="message" highlight="CompMsg"/>
- <div key="up" highlight="CompLess"/>
- <div key="items" highlight="Completions"/>
- <div key="waiting" highlight="CompMsg">{ItemList.WAITING_MESSAGE}</div>
- <div key="down" highlight="CompMore"/>
- </div>, context.cache.nodes);
- this._divNodes.completions.appendChild(context.cache.nodes.root);
- }, this);
-
- this.timeout(this._autoSize);
+ getRelativePage: function getRelativePage(offset, tuple, noWrap) {
+ offset *= this.maxItems;
+ // Try once with wrapping disabled.
+ let res = this.getRelativeItem(offset, tuple, true);
+
+ if (!res) {
+ // Wrapped.
+ let sign = offset / Math.abs(offset);
+
+ let off = this.getOffset(tuple === null ? null : tuple || this.selected);
+ if (off == null)
+ // Unselected. Defer to getRelativeItem.
+ res = this.getRelativeItem(offset, null, noWrap);
+ else if (~[0, this.itemCount - 1].indexOf(off))
+ // At start or end. Jump to other end.
+ res = this.getRelativeItem(sign, null, noWrap);
+ else
+ // Wrapped. Go to beginning or end.
+ res = this.getRelativeItem(-sign, null);
+ }
+ return res;
},
/**
- * Uses the entries in "items" to fill the listbox and does incremental
- * filling to speed up things.
+ * Initializes the ItemList for use with a new root completion
+ * context.
*
- * @param {number} offset Start at this index and show options["maxitems"].
+ * @param {CompletionContext} context The new root context.
*/
- _fill: function _fill(offset) {
- XML.ignoreWhiteSpace = false;
- let diff = offset - this._startIndex;
- if (this._items == null || offset == null || diff == 0 || offset < 0)
- return false;
+ open: function open(context) {
+ this.context = context;
+ this.nodes = {};
+ this.container.height = 0;
+ this.minHeight = 0;
+ this.maxItems = options["maxitems"];
+
+ DOM(this.rootXML, this.doc, this.nodes)
+ .appendTo(DOM(this.body).empty());
+
+ this.update();
+ },
- this._startIndex = offset;
- this._endIndex = Math.min(this._startIndex + options["maxitems"], this._items.allItems.items.length);
-
- let haveCompletions = false;
- let off = 0;
- let end = this._startIndex + options["maxitems"];
- function getRows(context) {
- function fix(n) Math.constrain(n, 0, len);
- let len = context.items.length;
- let start = off;
- end -= !!context.message + context.incomplete;
- off += len;
-
- let s = fix(offset - start), e = fix(end - start);
- return [s, e, context.incomplete && e >= offset && off - 1 < end];
+ /**
+ * Updates the absolute result indices of all groups after
+ * results have changed.
+ * @private
+ */
+ updateOffsets: function updateOffsets() {
+ let total = this.itemCount;
+ let count = 0;
+ for (let group in values(this.activeGroups)) {
+ group.offsets = { start: count, end: total - count - group.itemCount };
+ count += group.itemCount;
}
+ },
- this._items.contextList.forEach(function fill_eachContext(context) {
- let nodes = context.cache.nodes;
- if (!nodes)
- return;
- haveCompletions = true;
+ /**
+ * Updates the set and state of active groups for a new set of
+ * completion results.
+ */
+ update: function update() {
+ DOM(this.nodes.completions).empty();
+
+ let container = DOM(this.nodes.completions);
+ let groups = this.activeGroups;
+ for (let group in values(groups)) {
+ group.reset();
+ container.append(group.nodes.root);
+ }
- let root = nodes.root;
- let items = nodes.items;
- let [start, end, waiting] = getRows(context);
+ this.updateOffsets();
- if (context.message) {
- nodes.message.textContent = "";
- nodes.message.appendChild(this._dom(context.message));
- }
- nodes.message.style.display = context.message ? "block" : "none";
- nodes.waiting.style.display = waiting ? "block" : "none";
- nodes.up.style.opacity = "0";
- nodes.down.style.display = "none";
-
- for (let [i, row] in Iterator(context.getRows(start, end, this._doc)))
- nodes[i] = row;
- for (let [i, row] in array.iterItems(nodes)) {
- if (!row)
- continue;
- let display = (i >= start && i < end);
- if (display && row.parentNode != items) {
- do {
- var next = nodes[++i];
- if (next && next.parentNode != items)
- next = null;
- }
- while (!next && i < end)
- items.insertBefore(row, next);
- }
- else if (!display && row.parentNode == items)
- items.removeChild(row);
- }
- if (context.items.length == 0)
- return;
- nodes.up.style.opacity = (start == 0) ? "0" : "1";
- if (end != context.items.length)
- nodes.down.style.display = "block";
- else
- nodes.up.style.display = "block";
- if (start == end) {
- nodes.up.style.display = "none";
- nodes.down.style.display = "none";
- }
- }, this);
+ DOM(this.nodes.noCompletions).toggle(!groups.length);
+
+ this.startPos = null;
+ this.select(groups[0] && groups[0].context, null);
+
+ this._resize.tell();
+ },
- this._divNodes.noCompletions.style.display = haveCompletions ? "none" : "block";
+ /**
+ * Updates the group for *context* after an asynchronous update
+ * push.
+ *
+ * @param {CompletionContext} context The context which has
+ * changed.
+ */
+ updateContext: function updateContext(context) {
+ let group = this.getGroup(context);
+ this.updateOffsets();
- this._completionElements = util.evaluateXPath("//xhtml:div[@dactyl:highlight='CompItem']", this._doc);
+ if (~this.activeGroups.indexOf(group))
+ group.update();
+ else {
+ DOM(group.nodes.root).remove();
+ if (this.selectedGroup == group)
+ this.selectedGroup = null;
+ }
- return true;
+ let g = this.selectedGroup;
+ this.select(g, g && g.selectedIdx);
},
- clear: function clear() { this.setItems(); this._doc.body.innerHTML = ""; },
- get visible() !this._container.collapsed,
- set visible(val) this._container.collapsed = !val,
+ /**
+ * Updates the DOM to reflect the current state of all groups.
+ * @private
+ */
+ draw: function draw() {
+ for (let group in values(this.activeGroups))
+ group.draw();
+
+ // We need to collect all of the rescrolling functions in
+ // one go, as the height calculation that they need to do
+ // would force an expensive reflow after each call due to
+ // DOM modifications, otherwise.
+ this.activeGroups.filter(g => !g.collapsed)
+ .map(g => g.rescrollFunc)
+ .forEach(call);
+
+ if (!this.selected)
+ this.win.scrollTo(0, 0);
+
+ this._resize.tell(ItemList.RESIZE_BRIEF);
+ },
- reset: function reset(brief) {
- this._startIndex = this._endIndex = this._selIndex = -1;
- this._div = null;
- if (!brief)
- this.selectItem(-1);
+ onResize: function onResize() {
+ if (this.selectedGroup)
+ this.selectedGroup.rescrollFunc();
},
- // if @param selectedItem is given, show the list and select that item
- setItems: function setItems(newItems, selectedItem) {
- if (this._selItem > -1)
- this._getCompletion(this._selItem).removeAttribute("selected");
- if (this._container.collapsed) {
- this._minHeight = 0;
- this._container.height = 0;
- }
- this._startIndex = this._endIndex = this._selIndex = -1;
- this._items = newItems;
- this.reset(true);
- if (typeof selectedItem == "number") {
- this.selectItem(selectedItem);
- this.visible = true;
+ minHeight: 0,
+
+ /**
+ * Resizes the list after an update.
+ * @private
+ */
+ resize: function resize(flags) {
+ let { completions, root } = this.nodes;
+
+ if (!this.visible)
+ root.style.minWidth = document.getElementById("dactyl-commandline").scrollWidth + "px";
+
+ let { minHeight } = this;
+ if (mow.visible && this.isAboveMow) // Kludge.
+ minHeight -= mow.wantedHeight;
+
+ let needed = this.win.scrollY + DOM(completions).rect.bottom;
+ this.minHeight = Math.max(minHeight, needed);
+
+ if (!this.visible)
+ root.style.minWidth = "";
+
+ let height = this.visible ? parseFloat(this.container.height) : 0;
+ if (this.minHeight <= minHeight || !mow.visible)
+ this.container.height = Math.min(this.minHeight,
+ height + config.outputHeight - mow.spaceNeeded);
+ else {
+ // FIXME: Belongs elsewhere.
+ mow.resize(false, Math.max(0, this.minHeight - this.container.height));
+
+ this.container.height = this.minHeight - mow.spaceNeeded;
+ mow.resize(false);
+ this.timeout(function () {
+ this.container.height -= mow.spaceNeeded;
+ });
}
},
- // select index, refill list if necessary
- selectItem: function selectItem(index) {
- //let now = Date.now();
+ /**
+ * Selects the item at the given *group* and *index*.
+ *
+ * @param {CompletionContext|[CompletionContext,number]} *group* The
+ * completion context to select, or a tuple specifying the
+ * context and item index.
+ * @param {number} index The item index in *group* to select.
+ * @param {number} position If non-null, try to position the
+ * selected item at the *position*th row from the top of
+ * the screen. Note that at least {@link #CONTEXT_LINES}
+ * lines will be visible above and below the selected item
+ * unless there aren't enough results to make this possible.
+ * @optional
+ */
+ select: function select(group, index, position) {
+ if (isArray(group))
+ [group, index] = group;
- if (this._div == null)
- this._init();
+ group = this.getGroup(group);
- let sel = this._selIndex;
- let len = this._items.allItems.items.length;
- let newOffset = this._startIndex;
- let maxItems = options["maxitems"];
- let contextLines = Math.min(3, parseInt((maxItems - 1) / 2));
+ if (this.selectedGroup && (!group || group != this.selectedGroup))
+ this.selectedGroup.selectedIdx = null;
- if (index == -1 || index == null || index == len) { // wrapped around
- if (this._selIndex < 0)
- newOffset = 0;
- this._selIndex = -1;
- index = -1;
- }
- else {
- if (index <= this._startIndex + contextLines)
- newOffset = index - contextLines;
- if (index >= this._endIndex - contextLines)
- newOffset = index + contextLines - maxItems + 1;
+ this.selectedGroup = group;
- newOffset = Math.min(newOffset, len - maxItems);
- newOffset = Math.max(newOffset, 0);
+ if (group)
+ group.selectedIdx = index;
- this._selIndex = index;
- }
+ let groups = this.activeGroups;
- if (sel > -1)
- this._getCompletion(sel).removeAttribute("selected");
- this._fill(newOffset);
- if (index >= 0) {
- this._getCompletion(index).setAttribute("selected", "true");
- if (this._container.height != 0)
- util.scrollIntoView(this._getCompletion(index));
- }
+ if (position != null || !this.startPos && groups.length)
+ this.startPos = [group || groups[0], position || 0];
+
+ if (groups.length) {
+ group = group || groups[0];
+ let idx = groups.indexOf(group);
+
+ let start = this.startPos[0].getOffset(this.startPos[1]);
+ if (group) {
+ let idx = group.selectedIdx || 0;
+ let off = group.getOffset(idx);
+
+ start = Math.constrain(start,
+ off + Math.min(this.CONTEXT_LINES,
+ group.itemCount - idx + group.offsets.end)
+ - this.maxItems + 1,
+ off - Math.min(this.CONTEXT_LINES,
+ idx + group.offsets.start));
+ }
+
+ let count = this.maxItems;
+ for (let group in values(groups)) {
+ let off = Math.max(0, start - group.offsets.start);
+
+ group.count = Math.constrain(group.itemCount - off, 0, count);
+ count -= group.count;
- //if (index == 0)
- // this.start = now;
- //if (index == Math.min(len - 1, 100))
- // util.dump({ time: Date.now() - this.start });
+ group.collapsed = group.offsets.start >= start + this.maxItems
+ || group.offsets.start + group.itemCount < start;
+
+ group.range = ItemList.Range(off, off + group.count);
+
+ if (!startPos)
+ var startPos = [group, group.range.start];
+ }
+ this.startPos = startPos;
+ }
+ this.draw();
},
- onKeyPress: function onKeyPress(event) false
+ /**
+ * Returns an ItemList group for the given completion context,
+ * creating one if necessary.
+ *
+ * @param {CompletionContext} context
+ * @returns {ItemList.Group}
+ */
+ getGroup: function getGroup(context)
+ context instanceof ItemList.Group ? context
+ : context && context.getCache("itemlist-group",
+ () => ItemList.Group(this, context)),
+
+ getOffset: function getOffset(tuple) tuple && this.getGroup(tuple[0]).getOffset(tuple[1])
}, {
- WAITING_MESSAGE: _("completion.generating")
+ RESIZE_BRIEF: 1 << 0,
+
+ WAITING_MESSAGE: _("completion.generating"),
+
+ Group: Class("ItemList.Group", {
+ init: function init(parent, context) {
+ this.parent = parent;
+ this.context = context;
+ this.offsets = {};
+ this.range = ItemList.Range(0, 0);
+ },
+
+ get rootXML()
+ ["div", { key: "root", highlight: "CompGroup" },
+ ["div", { highlight: "Completions" },
+ this.context.createRow(this.context.title || [], "CompTitle")],
+ ["div", { highlight: "CompTitleSep" }],
+ ["div", { key: "contents" },
+ ["div", { key: "up", highlight: "CompLess" }],
+ ["div", { key: "message", highlight: "CompMsg" },
+ this.context.message || []],
+ ["div", { key: "itemsContainer", class: "completion-items-container" },
+ ["div", { key: "items", highlight: "Completions" }]],
+ ["div", { key: "waiting", highlight: "CompMsg" },
+ ItemList.WAITING_MESSAGE],
+ ["div", { key: "down", highlight: "CompMore" }]]],
+
+ get doc() this.parent.doc,
+ get win() this.parent.win,
+ get maxItems() this.parent.maxItems,
+
+ get itemCount() this.context.items.length,
+
+ /**
+ * Returns a function which will update the scroll offsets
+ * and heights of various DOM members.
+ * @private
+ */
+ get rescrollFunc() {
+ let container = this.nodes.itemsContainer;
+ let pos = DOM(container).rect.top;
+ let start = DOM(this.getRow(this.range.start)).rect.top;
+ let height = DOM(this.getRow(this.range.end - 1)).rect.bottom - start || 0;
+ let scroll = start + container.scrollTop - pos;
+
+ let win = this.win;
+ let row = this.selectedRow;
+ if (row && this.parent.minHeight) {
+ let { rect } = DOM(this.selectedRow);
+ var scrollY = this.win.scrollY + rect.bottom - this.win.innerHeight;
+ }
+
+ return function () {
+ container.style.height = height + "px";
+ container.scrollTop = scroll;
+ if (scrollY != null)
+ win.scrollTo(0, Math.max(scrollY, 0));
+ };
+ },
+
+ /**
+ * Reset this group for use with a new set of results.
+ */
+ reset: function reset() {
+ this.nodes = {};
+ this.generatedRange = ItemList.Range(0, 0);
+
+ DOM.fromJSON(this.rootXML, this.doc, this.nodes);
+ },
+
+ /**
+ * Update this group after an asynchronous results push.
+ */
+ update: function update() {
+ this.generatedRange = ItemList.Range(0, 0);
+ DOM(this.nodes.items).empty();
+
+ if (this.context.message)
+ DOM(this.nodes.message).empty()
+ .append(DOM.fromJSON(this.context.message, this.doc));
+
+ if (this.selectedIdx > this.itemCount)
+ this.selectedIdx = null;
+ },
+
+ /**
+ * Updates the DOM to reflect the current state of this
+ * group.
+ * @private
+ */
+ draw: function draw() {
+ DOM(this.nodes.contents).toggle(!this.collapsed);
+ if (this.collapsed)
+ return;
+
+ DOM(this.nodes.message).toggle(this.context.message && this.range.start == 0);
+ DOM(this.nodes.waiting).toggle(this.context.incomplete && this.range.end <= this.itemCount);
+ DOM(this.nodes.up).toggle(this.range.start > 0);
+ DOM(this.nodes.down).toggle(this.range.end < this.itemCount);
+
+ if (!this.generatedRange.contains(this.range)) {
+ if (this.generatedRange.end == 0)
+ var [start, end] = this.range;
+ else {
+ start = this.range.start - (this.range.start <= this.generatedRange.start
+ ? this.maxItems / 2 : 0);
+ end = this.range.end + (this.range.end > this.generatedRange.end
+ ? this.maxItems / 2 : 0);
+ }
+
+ let range = ItemList.Range(Math.max(0, start - start % 2),
+ Math.min(this.itemCount, end));
+
+ let first;
+ for (let [i, row] in this.context.getRows(this.generatedRange.start,
+ this.generatedRange.end,
+ this.doc))
+ if (!range.contains(i))
+ DOM(row).remove();
+ else if (!first)
+ first = row;
+
+ let container = DOM(this.nodes.items);
+ let before = first ? DOM(first).bound.before
+ : DOM(this.nodes.items).bound.append;
+
+ for (let [i, row] in this.context.getRows(range.start, range.end,
+ this.doc)) {
+ if (i < this.generatedRange.start)
+ before(row);
+ else if (i >= this.generatedRange.end)
+ container.append(row);
+ if (i == this.selectedIdx)
+ this.selectedIdx = this.selectedIdx;
+ }
+
+ this.generatedRange = range;
+ }
+ },
+
+ getRow: function getRow(idx) this.context.getRow(idx, this.doc),
+
+ getOffset: function getOffset(idx) this.offsets.start + (idx || 0),
+
+ get selectedRow() this.getRow(this._selectedIdx),
+
+ get selectedIdx() this._selectedIdx,
+ set selectedIdx(idx) {
+ if (this.selectedRow && this._selectedIdx != idx)
+ DOM(this.selectedRow).attr("selected", null);
+
+ this._selectedIdx = idx;
+
+ if (this.selectedRow)
+ DOM(this.selectedRow).attr("selected", true);
+ }
+ }),
+
+ Range: Class.Memoize(function () {
+ let Range = Struct("ItemList.Range", "start", "end");
+ update(Range.prototype, {
+ contains: function contains(idx)
+ typeof idx == "number" ? idx >= this.start && idx < this.end
+ : this.contains(idx.start) &&
+ idx.end >= this.start && idx.end <= this.end
+ });
+ return Range;
+ })
});
-// vim: set fdm=marker sw=4 ts=4 et:
+// vim: set fdm=marker sw=4 sts=4 ts=8 et: