//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
/** @scope modules */
// http://developer.mozilla.org/en/docs/Editor_Embedding_Guide
/** @instance editor */
-var Editor = Module("editor", {
+var Editor = Module("editor", XPCOM(Ci.nsIEditActionListener, ModuleBase), {
+ init: function init(elem) {
+ if (elem)
+ this.element = elem;
+ else
+ this.__defineGetter__("element", function () {
+ let elem = dactyl.focusedElement;
+ if (elem)
+ return elem.inputField || elem;
+
+ let win = document.commandDispatcher.focusedWindow;
+ return DOM(win).isEditable && win || null;
+ });
+ },
+
+ get registers() storage.newMap("registers", { privateData: true, store: true }),
+ get registerRing() storage.newArray("register-ring", { privateData: true, store: true }),
+
+ skipSave: false,
+
+ // Fixme: Move off this object.
+ currentRegister: null,
+
+ /**
+ * Temporarily set the default register for the span of the next
+ * mapping.
+ */
+ pushRegister: function pushRegister(arg) {
+ let restore = this.currentRegister;
+ this.currentRegister = arg;
+ mappings.afterCommands(2, function () {
+ this.currentRegister = restore;
+ }, this);
+ },
+
+ defaultRegister: "*",
+
+ selectionRegisters: {
+ "*": "selection",
+ "+": "global"
+ },
+
+ /**
+ * Get the value of the register *name*.
+ *
+ * @param {string|number} name The name of the register to get.
+ * @returns {string|null}
+ * @see #setRegister
+ */
+ getRegister: function getRegister(name) {
+ if (name == null)
+ name = editor.currentRegister || editor.defaultRegister;
+
+ if (name == '"')
+ name = 0;
+ if (name == "_")
+ var res = null;
+ else if (Set.has(this.selectionRegisters, name))
+ res = { text: dactyl.clipboardRead(this.selectionRegisters[name]) || "" };
+ else if (!/^[0-9]$/.test(name))
+ res = this.registers.get(name);
+ else
+ res = this.registerRing.get(name);
+
+ return res != null ? res.text : res;
+ },
+
+ /**
+ * Sets the value of register *name* to value. The following
+ * registers have special semantics:
+ *
+ * * - Tied to the PRIMARY selection value on X11 systems.
+ * + - Tied to the primary global clipboard.
+ * _ - The null register. Never has any value.
+ * " - Equivalent to 0.
+ * 0-9 - These act as a kill ring. Setting any of them pushes the
+ * values of higher numbered registers up one slot.
+ *
+ * @param {string|number} name The name of the register to set.
+ * @param {string|Range|Selection|Node} value The value to save to
+ * the register.
+ */
+ setRegister: function setRegister(name, value, verbose) {
+ if (name == null)
+ name = editor.currentRegister || editor.defaultRegister;
+
+ if (isinstance(value, [Ci.nsIDOMRange, Ci.nsIDOMNode, Ci.nsISelection]))
+ value = DOM.stringify(value);
+ value = { text: value, isLine: modes.extended & modes.LINE, timestamp: Date.now() * 1000 };
+
+ if (name == '"')
+ name = 0;
+ if (name == "_")
+ ;
+ else if (Set.has(this.selectionRegisters, name))
+ dactyl.clipboardWrite(value.text, verbose, this.selectionRegisters[name]);
+ else if (!/^[0-9]$/.test(name))
+ this.registers.set(name, value);
+ else {
+ this.registerRing.insert(value, name);
+ this.registerRing.truncate(10);
+ }
+ },
+
get isCaret() modes.getStack(1).main == modes.CARET,
get isTextEdit() modes.getStack(1).main == modes.TEXT_EDIT,
- unselectText: function (toEnd) {
- try {
- Editor.getEditor(null).selection[toEnd ? "collapseToEnd" : "collapseToStart"]();
+ get editor() DOM(this.element).editor,
+
+ getController: function getController(cmd) {
+ let controllers = this.element && this.element.controllers;
+ dactyl.assert(controllers);
+
+ return controllers.getControllerForCommand(cmd || "cmd_beginLine");
+ },
+
+ get selection() this.editor && this.editor.selection || null,
+ get selectionController() this.editor && this.editor.selectionController || null,
+
+ deselect: function () {
+ if (this.selection && this.selection.focusNode)
+ this.selection.collapse(this.selection.focusNode,
+ this.selection.focusOffset);
+ },
+
+ get selectedRange() {
+ if (!this.selection)
+ return null;
+
+ if (!this.selection.rangeCount) {
+ let range = RangeFind.nodeContents(this.editor.rootElement.ownerDocument);
+ range.collapse(true);
+ this.selectedRange = range;
}
- catch (e) {}
+ return this.selection.getRangeAt(0);
+ },
+ set selectedRange(range) {
+ this.selection.removeAllRanges();
+ if (range != null)
+ this.selection.addRange(range);
},
- selectedText: function () String(Editor.getEditor(null).selection),
+ get selectedText() String(this.selection),
- pasteClipboard: function (clipboard, toStart) {
- let elem = dactyl.focusedElement;
- if (elem.inputField)
- elem = elem.inputField;
+ get preserveSelection() this.editor && !this.editor.shouldTxnSetSelection,
+ set preserveSelection(val) {
+ if (this.editor)
+ this.editor.setShouldTxnSetSelection(!val);
+ },
- if (elem.setSelectionRange) {
- let text = dactyl.clipboardRead(clipboard);
- if (!text)
- return;
- if (isinstance(elem, [HTMLInputElement, XULTextBoxElement]))
- text = text.replace(/\n+/g, "");
+ copy: function copy(range, name) {
+ range = range || this.selection;
- // This is a hacky fix - but it works.
- // <s-insert> in the bottom of a long textarea bounces up
- let top = elem.scrollTop;
- let left = elem.scrollLeft;
+ if (!range.collapsed)
+ this.setRegister(name, range);
+ },
- let start = elem.selectionStart; // caret position
- let end = elem.selectionEnd;
- let value = elem.value.substring(0, start) + text + elem.value.substring(end);
- elem.value = value;
+ cut: function cut(range, name) {
+ if (range)
+ this.selectedRange = range;
- if (/^(search|text)$/.test(elem.type))
- Editor.getEditor(elem).rootElement.firstChild.textContent = value;
+ if (!this.selection.isCollapsed)
+ this.setRegister(name, this.selection);
- elem.selectionStart = Math.min(start + (toStart ? 0 : text.length), elem.value.length);
- elem.selectionEnd = elem.selectionStart;
+ this.editor.deleteSelection(0);
+ },
- elem.scrollTop = top;
- elem.scrollLeft = left;
+ paste: function paste(name) {
+ let text = this.getRegister(name);
+ dactyl.assert(text && this.editor instanceof Ci.nsIPlaintextEditor);
- events.dispatch(elem, events.create(elem.ownerDocument, "input"));
- }
+ this.editor.insertText(text);
},
// count is optional, defaults to 1
- executeCommand: function (cmd, count) {
- let editor = Editor.getEditor(null);
- let controller = Editor.getController();
- dactyl.assert(callable(cmd) ||
- controller &&
- controller.supportsCommand(cmd) &&
- controller.isCommandEnabled(cmd));
+ executeCommand: function executeCommand(cmd, count) {
+ if (!callable(cmd)) {
+ var controller = this.getController(cmd);
+ util.assert(controller &&
+ controller.supportsCommand(cmd) &&
+ controller.isCommandEnabled(cmd));
+ cmd = bind("doCommand", controller, cmd);
+ }
// XXX: better as a precondition
if (count == null)
- count = 1;
+ count = 1;
let didCommand = false;
while (count--) {
// some commands need this try/catch workaround, because a cmd_charPrevious triggered
// at the beginning of the textarea, would hang the doCommand()
// good thing is, we need this code anyway for proper beeping
+
+ // What huh? --Kris
try {
- if (callable(cmd))
- cmd(editor, controller);
- else
- controller.doCommand(cmd);
+ cmd(this.editor, controller);
didCommand = true;
}
catch (e) {
}
},
- // cmd = y, d, c
- // motion = b, 0, gg, G, etc.
- selectMotion: function selectMotion(cmd, motion, count) {
- // XXX: better as a precondition
- if (count == null)
- count = 1;
+ moveToPosition: function (pos, select) {
+ if (isObject(pos))
+ var { startContainer, startOffset } = pos;
+ else
+ [startOffset, startOffset] = [this.selection.focusNode, pos];
+ this.selection[select ? "extend" : "collapse"](startContainer, startOffset);
+ },
- if (cmd == motion) {
- motion = "j";
- count--;
- }
+ mungeRange: function mungeRange(range, munger, selectEnd) {
+ let { editor } = this;
+ editor.beginPlaceHolderTransaction(null);
- if (modes.main != modes.VISUAL)
- modes.push(modes.VISUAL);
-
- switch (motion) {
- case "j":
- this.executeCommand("cmd_beginLine", 1);
- this.executeCommand("cmd_selectLineNext", count + 1);
- break;
- case "k":
- this.executeCommand("cmd_beginLine", 1);
- this.executeCommand("cmd_lineNext", 1);
- this.executeCommand("cmd_selectLinePrevious", count + 1);
- break;
- case "h":
- this.executeCommand("cmd_selectCharPrevious", count);
- break;
- case "l":
- this.executeCommand("cmd_selectCharNext", count);
- break;
- case "e":
- case "w":
- this.executeCommand("cmd_selectWordNext", count);
- break;
- case "b":
- this.executeCommand("cmd_selectWordPrevious", count);
- break;
- case "0":
- case "^":
- this.executeCommand("cmd_selectBeginLine", 1);
- break;
- case "$":
- this.executeCommand("cmd_selectEndLine", 1);
- break;
- case "gg":
- this.executeCommand("cmd_endLine", 1);
- this.executeCommand("cmd_selectTop", 1);
- this.executeCommand("cmd_selectBeginLine", 1);
- break;
- case "G":
- this.executeCommand("cmd_beginLine", 1);
- this.executeCommand("cmd_selectBottom", 1);
- this.executeCommand("cmd_selectEndLine", 1);
- break;
-
- default:
- dactyl.beep();
- return;
- }
- },
+ let [container, offset] = ["startContainer", "startOffset"];
+ if (selectEnd)
+ [container, offset] = ["endContainer", "endOffset"];
- // This function will move/select up to given "pos"
- // Simple setSelectionRange() would be better, but we want to maintain the correct
- // order of selectionStart/End (a Gecko bug always makes selectionStart <= selectionEnd)
- // Use only for small movements!
- moveToPosition: function (pos, forward, select) {
- if (!select) {
- Editor.getEditor().setSelectionRange(pos, pos);
- return;
- }
+ try {
+ // :(
+ let idx = range[offset];
+ let parent = range[container].parentNode;
+ let parentIdx = Array.indexOf(parent.childNodes,
+ range[container]);
+
+ let delta = 0;
+ for (let node in Editor.TextsIterator(range)) {
+ let text = node.textContent;
+ let start = 0, end = text.length;
+ if (node == range.startContainer)
+ start = range.startOffset;
+ if (node == range.endContainer)
+ end = range.endOffset;
+
+ if (start == 0 && end == text.length)
+ text = munger(text);
+ else
+ text = text.slice(0, start)
+ + munger(text.slice(start, end))
+ + text.slice(end);
- if (forward) {
- if (pos <= Editor.getEditor().selectionEnd || pos > Editor.getEditor().value.length)
- return;
+ if (text == node.textContent)
+ continue;
- do { // TODO: test code for endless loops
- this.executeCommand("cmd_selectCharNext", 1);
- }
- while (Editor.getEditor().selectionEnd != pos);
- }
- else {
- if (pos >= Editor.getEditor().selectionStart || pos < 0)
- return;
+ if (selectEnd)
+ delta = text.length - node.textContent.length;
- do { // TODO: test code for endless loops
- this.executeCommand("cmd_selectCharPrevious", 1);
+ if (editor instanceof Ci.nsIPlaintextEditor) {
+ this.selectedRange = RangeFind.nodeContents(node);
+ editor.insertText(text);
+ }
+ else
+ node.textContent = text;
}
- while (Editor.getEditor().selectionStart != pos);
+ let node = parent.childNodes[parentIdx];
+ if (node instanceof Text)
+ idx = Math.constrain(idx + delta, 0, node.textContent.length);
+ this.selection.collapse(node, idx);
+ }
+ finally {
+ editor.endPlaceHolderTransaction();
}
},
- findChar: function (key, count, backward) {
+ findChar: function findNumber(key, count, backward, offset) {
+ count = count || 1; // XXX ?
+ offset = (offset || 0) - !!backward;
- let editor = Editor.getEditor();
- if (!editor)
- return -1;
+ // Grab the charcode of the key spec. Using the key name
+ // directly will break keys like <
+ let code = DOM.Event.parse(key)[0].charCode;
+ let char = String.fromCharCode(code);
+ util.assert(code);
- // XXX
- if (count == null)
- count = 1;
+ let range = this.selectedRange.cloneRange();
+ let collapse = DOM(this.element).whiteSpace == "normal";
- let code = events.fromString(key)[0].charCode;
- util.assert(code);
- let char = String.fromCharCode(code);
+ // Find the *count*th occurance of *char* before a non-collapsed
+ // \n, ignoring the character at the caret.
+ let i = 0;
+ function test(c) (collapse || c != "\n") && !!(!i++ || c != char || --count)
- let text = editor.value;
- let caret = editor.selectionEnd;
- if (backward) {
- let end = text.lastIndexOf("\n", caret);
- while (caret > end && caret >= 0 && count--)
- caret = text.lastIndexOf(char, caret - 1);
- }
- else {
- let end = text.indexOf("\n", caret);
- if (end == -1)
- end = text.length;
+ Editor.extendRange(range, !backward, { test: test }, true);
+ dactyl.assert(count == 0);
+ range.collapse(backward);
+
+ // Skip to any requested offset.
+ count = Math.abs(offset);
+ Editor.extendRange(range, offset > 0, { test: function (c) !!count-- }, true);
+ range.collapse(offset < 0);
+
+ return range;
+ },
+
+ findNumber: function findNumber(range) {
+ if (!range)
+ range = this.selectedRange.cloneRange();
- while (caret < end && caret >= 0 && count--)
- caret = text.indexOf(char, caret + 1);
+ // Find digit (or \n).
+ Editor.extendRange(range, true, /[^\n\d]/, true);
+ range.collapse(false);
+ // Select entire number.
+ Editor.extendRange(range, true, /\d/, true);
+ Editor.extendRange(range, false, /\d/, true);
+
+ // Sanity check.
+ dactyl.assert(/^\d+$/.test(range));
+
+ if (false) // Skip for now.
+ if (range.startContainer instanceof Text && range.startOffset > 2) {
+ if (range.startContainer.textContent.substr(range.startOffset - 2, 2) == "0x")
+ range.setStart(range.startContainer, range.startOffset - 2);
}
- if (count > 0)
- caret = -1;
- if (caret == -1)
- dactyl.beep();
- return caret;
+ // Grab the sign, if it's there.
+ Editor.extendRange(range, false, /[+-]/, true);
+
+ return range;
+ },
+
+ modifyNumber: function modifyNumber(delta, range) {
+ range = this.findNumber(range);
+ let number = parseInt(range) + delta;
+ if (/^[+-]?0x/.test(range))
+ number = number.toString(16).replace(/^[+-]?/, "$&0x");
+ else if (/^[+-]?0\d/.test(range))
+ number = number.toString(8).replace(/^[+-]?/, "$&0");
+
+ this.selectedRange = range;
+ this.editor.insertText(String(number));
+ this.selection.modify("move", "backward", "character");
},
/**
return;
let textBox = config.isComposeWindow ? null : dactyl.focusedElement;
+ if (!DOM(textBox).isInput)
+ textBox = null;
+
let line, column;
+ let keepFocus = modes.stack.some(function (m) isinstance(m.main, modes.COMMAND_LINE));
if (!forceEditing && textBox && textBox.type == "password") {
commandline.input(_("editor.prompt.editPassword") + " ",
if (textBox) {
var text = textBox.value;
- let pre = text.substr(0, textBox.selectionStart);
- line = 1 + pre.replace(/[^\n]/g, "").length;
- column = 1 + pre.replace(/[^]*\n/, "").length;
+ var pre = text.substr(0, textBox.selectionStart);
}
else {
var editor_ = window.GetCurrentEditor ? GetCurrentEditor()
: Editor.getEditor(document.commandDispatcher.focusedWindow);
dactyl.assert(editor_);
- text = Array.map(editor_.rootElement.childNodes, function (e) util.domToString(e, true)).join("");
+ text = Array.map(editor_.rootElement.childNodes, function (e) DOM.stringify(e, true)).join("");
+
+ if (!editor_.selection.rangeCount)
+ var sel = "";
+ else {
+ let range = RangeFind.nodeContents(editor_.rootElement);
+ let end = editor_.selection.getRangeAt(0);
+ range.setEnd(end.startContainer, end.startOffset);
+ pre = DOM.stringify(range, true);
+ if (range.startContainer instanceof Text)
+ pre = pre.replace(/^(?:<[^>"]+>)+/, "");
+ if (range.endContainer instanceof Text)
+ pre = pre.replace(/(?:<\/[^>"]+>)+$/, "");
+ }
}
- let origGroup = textBox && textBox.getAttributeNS(NS, "highlight") || "";
+ line = 1 + pre.replace(/[^\n]/g, "").length;
+ column = 1 + pre.replace(/[^]*\n/, "").length;
+
+ let origGroup = DOM(textBox).highlight.toString();
let cleanup = util.yieldable(function cleanup(error) {
if (timer)
timer.cancel();
tmpfile.remove(false);
if (textBox) {
- dactyl.focus(textBox);
+ DOM(textBox).highlight.remove("EditorEditing");
+ if (!keepFocus)
+ dactyl.focus(textBox);
for (let group in values(blink.concat(blink, ""))) {
highlight.highlightNode(textBox, origGroup + " " + group);
yield 100;
if (textBox) {
textBox.value = val;
- textBox.setAttributeNS(NS, "modifiable", true);
- util.computedStyle(textBox).MozUserInput;
- events.dispatch(textBox, events.create(textBox.ownerDocument, "input", {}));
- textBox.removeAttributeNS(NS, "modifiable");
+ if (false) {
+ let elem = DOM(textBox);
+ elem.attrNS(NS, "modifiable", true)
+ .style.MozUserInput;
+ elem.input().attrNS(NS, "modifiable", null);
+ }
}
else {
while (editor_.rootElement.firstChild)
throw Error(_("io.cantCreateTempFile"));
if (textBox) {
- highlight.highlightNode(textBox, origGroup + " EditorEditing");
- textBox.blur();
+ if (!keepFocus)
+ textBox.blur();
+ DOM(textBox).highlight.add("EditorEditing");
}
if (!tmpfile.write(text))
* @see Abbreviation#expand
*/
expandAbbreviation: function (mode) {
- let elem = dactyl.focusedElement;
- if (!(elem && elem.value))
+ if (!this.selection)
return;
- let text = elem.value;
- let start = elem.selectionStart;
- let end = elem.selectionEnd;
- let abbrev = abbreviations.match(mode, text.substring(0, start).replace(/.*\s/g, ""));
+ let range = this.selectedRange.cloneRange();
+ if (!range.collapsed)
+ return;
+
+ Editor.extendRange(range, false, /\S/, true);
+ let abbrev = abbreviations.match(mode, String(range));
if (abbrev) {
- let len = abbrev.lhs.length;
- let rhs = abbrev.expand(elem);
- elem.value = text.substring(0, start - len) + rhs + text.substring(start);
- elem.selectionStart = start - len + rhs.length;
- elem.selectionEnd = end - len + rhs.length;
+ range.setStart(range.startContainer, range.endOffset - abbrev.lhs.length);
+ this.selectedRange = range;
+ this.editor.insertText(abbrev.expand(this.element));
}
},
+
+ // nsIEditActionListener:
+ WillDeleteNode: util.wrapCallback(function WillDeleteNode(node) {
+ if (!editor.skipSave && node.textContent)
+ this.setRegister(0, node);
+ }),
+ WillDeleteSelection: util.wrapCallback(function WillDeleteSelection(selection) {
+ if (!editor.skipSave && !selection.isCollapsed)
+ this.setRegister(0, selection);
+ }),
+ WillDeleteText: util.wrapCallback(function WillDeleteText(node, start, length) {
+ if (!editor.skipSave && length)
+ this.setRegister(0, node.textContent.substr(start, length));
+ })
}, {
- extendRange: function extendRange(range, forward, re, sameWord) {
+ TextsIterator: Class("TextsIterator", {
+ init: function init(range, context, after) {
+ this.after = after;
+ this.start = context || range[after ? "endContainer" : "startContainer"];
+ if (after)
+ this.context = this.start;
+ this.range = range;
+ },
+
+ __iterator__: function __iterator__() {
+ while (this.nextNode())
+ yield this.context;
+ },
+
+ prevNode: function prevNode() {
+ if (!this.context)
+ return this.context = this.start;
+
+ var node = this.context;
+ if (!this.after)
+ node = node.previousSibling;
+
+ if (!node)
+ node = this.context.parentNode;
+ else
+ while (node.lastChild)
+ node = node.lastChild;
+
+ if (!node || !RangeFind.containsNode(this.range, node, true))
+ return null;
+ this.after = false;
+ return this.context = node;
+ },
+
+ nextNode: function nextNode() {
+ if (!this.context)
+ return this.context = this.start;
+
+ if (!this.after)
+ var node = this.context.firstChild;
+
+ if (!node) {
+ node = this.context;
+ while (node.parentNode && node != this.range.endContainer
+ && !node.nextSibling)
+ node = node.parentNode;
+
+ node = node.nextSibling;
+ }
+
+ if (!node || !RangeFind.containsNode(this.range, node, true))
+ return null;
+ this.after = false;
+ return this.context = node;
+ },
+
+ getPrev: function getPrev() {
+ return this.filter("prevNode");
+ },
+
+ getNext: function getNext() {
+ return this.filter("nextNode");
+ },
+
+ filter: function filter(meth) {
+ let node;
+ while (node = this[meth]())
+ if (node instanceof Ci.nsIDOMText &&
+ DOM(node).isVisible &&
+ DOM(node).style.MozUserSelect != "none")
+ return node;
+ }
+ }),
+
+ extendRange: function extendRange(range, forward, re, sameWord, root, end) {
function advance(positive) {
- let idx = range.endOffset;
- while (idx < text.length && re.test(text[idx++]) == positive)
- range.setEnd(range.endContainer, idx);
+ while (true) {
+ while (idx == text.length && (node = iterator.getNext())) {
+ if (node == iterator.start)
+ idx = range[offset];
+
+ start = text.length;
+ text += node.textContent;
+ range[set](node, idx - start);
+ }
+
+ if (idx >= text.length || re.test(text[idx]) != positive)
+ break;
+ range[set](range[container], ++idx - start);
+ }
}
function retreat(positive) {
- let idx = range.startOffset;
- while (idx > 0 && re.test(text[--idx]) == positive)
- range.setStart(range.startContainer, idx);
+ while (true) {
+ while (idx == 0 && (node = iterator.getPrev())) {
+ let str = node.textContent;
+ if (node == iterator.start)
+ idx = range[offset];
+ else
+ idx = str.length;
+
+ text = str + text;
+ range[set](node, idx);
+ }
+ if (idx == 0 || re.test(text[idx - 1]) != positive)
+ break;
+ range[set](range[container], --idx);
+ }
}
- let nodeRange = range.cloneRange();
- nodeRange.selectNodeContents(range.startContainer);
- let text = String(nodeRange);
+ if (end == null)
+ end = forward ? "end" : "start";
+ let [container, offset, set] = [end + "Container", end + "Offset",
+ "set" + util.capitalize(end)];
+
+ if (!root)
+ for (root = range[container];
+ root.parentNode instanceof Element && !DOM(root).isEditable;
+ root = root.parentNode)
+ ;
+ if (root instanceof Ci.nsIDOMNSEditableElement)
+ root = root.editor;
+ if (root instanceof Ci.nsIEditor)
+ root = root.rootElement;
+
+ let node = range[container];
+ let iterator = Editor.TextsIterator(RangeFind.nodeContents(root),
+ node, !forward);
+
+ let text = "";
+ let idx = 0;
+ let start = 0;
if (forward) {
advance(true);
elem = dactyl.focusedElement || document.commandDispatcher.focusedWindow;
dactyl.assert(elem);
- try {
- if (elem instanceof Element)
- return elem.QueryInterface(Ci.nsIDOMNSEditableElement).editor;
- return elem.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
- .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIEditingSession)
- .getEditorForWindow(elem);
- }
- catch (e) {
- return null;
- }
+ return DOM(elem).editor;
+ }
+}, {
+ modes: function init_modes() {
+ modes.addMode("OPERATOR", {
+ char: "o",
+ description: "Mappings which move the cursor",
+ bases: []
+ });
+ modes.addMode("VISUAL", {
+ char: "v",
+ description: "Active when text is selected",
+ display: function () "VISUAL" + (this._extended & modes.LINE ? " LINE" : ""),
+ bases: [modes.COMMAND],
+ ownsFocus: true
+ }, {
+ enter: function (stack) {
+ if (editor.selectionController)
+ editor.selectionController.setCaretVisibilityDuringSelection(true);
+ },
+ leave: function (stack, newMode) {
+ if (newMode.main == modes.CARET) {
+ let selection = content.getSelection();
+ if (selection && !selection.isCollapsed)
+ selection.collapseToStart();
+ }
+ else if (stack.pop)
+ editor.deselect();
+ }
+ });
+ modes.addMode("TEXT_EDIT", {
+ char: "t",
+ description: "Vim-like editing of input elements",
+ bases: [modes.COMMAND],
+ ownsFocus: true
+ }, {
+ onKeyPress: function (eventList) {
+ const KILL = false, PASS = true;
+
+ // Hack, really.
+ if (eventList[0].charCode || /^<(?:.-)*(?:BS|Del|C-h|C-w|C-u|C-k)>$/.test(DOM.Event.stringify(eventList[0]))) {
+ dactyl.beep();
+ return KILL;
+ }
+ return PASS;
+ }
+ });
+
+ modes.addMode("INSERT", {
+ char: "i",
+ description: "Active when an input element is focused",
+ insert: true,
+ ownsFocus: true
+ });
+ modes.addMode("AUTOCOMPLETE", {
+ description: "Active when an input autocomplete pop-up is active",
+ display: function () "AUTOCOMPLETE (insert)",
+ bases: [modes.INSERT]
+ });
+ },
+ commands: function init_commands() {
+ commands.add(["reg[isters]"],
+ "List the contents of known registers",
+ function (args) {
+ completion.listCompleter("register", args[0]);
+ },
+ { argCount: "*" });
},
+ completion: function init_completion() {
+ completion.register = function complete_register(context) {
+ context = context.fork("registers");
+ context.keys = { text: util.identity, description: editor.closure.getRegister };
- getController: function () {
- let ed = dactyl.focusedElement;
- if (!ed || !ed.controllers)
- return null;
+ context.match = function (r) !this.filter || ~this.filter.indexOf(r);
- return ed.controllers.getControllerForCommand("cmd_beginLine");
- }
-}, {
- mappings: function () {
+ context.fork("clipboard", 0, this, function (ctxt) {
+ ctxt.match = context.match;
+ ctxt.title = ["Clipboard Registers"];
+ ctxt.completions = Object.keys(editor.selectionRegisters);
+ });
+ context.fork("kill-ring", 0, this, function (ctxt) {
+ ctxt.match = context.match;
+ ctxt.title = ["Kill Ring Registers"];
+ ctxt.completions = Array.slice("0123456789");
+ });
+ context.fork("user", 0, this, function (ctxt) {
+ ctxt.match = context.match;
+ ctxt.title = ["User Defined Registers"];
+ ctxt.completions = editor.registers.keys();
+ });
+ };
+ },
+ mappings: function init_mappings() {
- // add mappings for commands like h,j,k,l,etc. in CARET, VISUAL and TEXT_EDIT mode
- function addMovementMap(keys, description, hasCount, caretModeMethod, caretModeArg, textEditCommand, visualTextEditCommand) {
- let extraInfo = {};
- if (hasCount)
- extraInfo.count = true;
-
- function caretExecute(arg, again) {
- function fixSelection() {
- sel.removeAllRanges();
- sel.addRange(RangeFind.endpoint(
- RangeFind.nodeRange(buffer.focusedFrame.document.documentElement),
- true));
+ Map.types["editor"] = {
+ preExecute: function preExecute(args) {
+ if (editor.editor && !this.editor) {
+ this.editor = editor.editor;
+ this.editor.beginTransaction();
+ }
+ editor.inEditMap = true;
+ },
+ postExecute: function preExecute(args) {
+ editor.inEditMap = false;
+ if (this.editor) {
+ this.editor.endTransaction();
+ this.editor = null;
}
+ },
+ };
+ Map.types["operator"] = {
+ preExecute: function preExecute(args) {
+ editor.inEditMap = true;
+ },
+ postExecute: function preExecute(args) {
+ editor.inEditMap = true;
+ if (modes.main == modes.OPERATOR)
+ modes.pop();
+ }
+ };
- let controller = buffer.selectionController;
+ // add mappings for commands like h,j,k,l,etc. in CARET, VISUAL and TEXT_EDIT mode
+ function addMovementMap(keys, description, hasCount, caretModeMethod, caretModeArg, textEditCommand, visualTextEditCommand) {
+ let extraInfo = {
+ count: !!hasCount,
+ type: "operator"
+ };
+
+ function caretExecute(arg) {
+ let win = document.commandDispatcher.focusedWindow;
+ let controller = util.selectionController(win);
let sel = controller.getSelection(controller.SELECTION_NORMAL);
+
+ let buffer = Buffer(win);
if (!sel.rangeCount) // Hack.
- fixSelection();
+ buffer.resetCaret();
- try {
- controller[caretModeMethod](caretModeArg, arg);
- }
- catch (e) {
- dactyl.assert(again && e.result === Cr.NS_ERROR_FAILURE);
- fixSelection();
- caretExecute(arg, false);
+ if (caretModeMethod == "pageMove") { // Grr.
+ buffer.scrollVertical("pages", caretModeArg ? 1 : -1);
+ buffer.resetCaret();
}
+ else
+ controller[caretModeMethod](caretModeArg, arg);
}
- mappings.add([modes.CARET], keys, description,
- function ({ count }) {
- if (!count)
- count = 1;
-
- while (count--)
- caretExecute(false, true);
- },
- extraInfo);
-
mappings.add([modes.VISUAL], keys, description,
function ({ count }) {
- if (!count)
- count = 1;
+ count = count || 1;
- let editor_ = Editor.getEditor(null);
+ let caret = !dactyl.focusedElement;
let controller = buffer.selectionController;
+
while (count-- && modes.main == modes.VISUAL) {
- if (editor.isTextEdit) {
+ if (caret)
+ caretExecute(true, true);
+ else {
if (callable(visualTextEditCommand))
- visualTextEditCommand(editor_);
+ visualTextEditCommand(editor.editor);
else
editor.executeCommand(visualTextEditCommand);
}
- else
- caretExecute(true, true);
}
},
extraInfo);
- mappings.add([modes.TEXT_EDIT], keys, description,
+ mappings.add([modes.CARET, modes.TEXT_EDIT, modes.OPERATOR], keys, description,
function ({ count }) {
- if (!count)
- count = 1;
+ count = count || 1;
- editor.executeCommand(textEditCommand, count);
+ if (editor.editor)
+ editor.executeCommand(textEditCommand, count);
+ else {
+ while (count--)
+ caretExecute(false);
+ }
},
extraInfo);
}
function addBeginInsertModeMap(keys, commands, description) {
mappings.add([modes.TEXT_EDIT], keys, description || "",
function () {
- commands.forEach(function (cmd)
- editor.executeCommand(cmd, 1));
+ commands.forEach(function (cmd) { editor.executeCommand(cmd, 1) });
modes.push(modes.INSERT);
- });
+ },
+ { type: "editor" });
}
function selectPreviousLine() {
editor.executeCommand("cmd_selectLinePrevious");
- if ((modes.extended & modes.LINE) && !editor.selectedText())
+ if ((modes.extended & modes.LINE) && !editor.selectedText)
editor.executeCommand("cmd_selectLinePrevious");
}
function selectNextLine() {
editor.executeCommand("cmd_selectLineNext");
- if ((modes.extended & modes.LINE) && !editor.selectedText())
+ if ((modes.extended & modes.LINE) && !editor.selectedText)
editor.executeCommand("cmd_selectLineNext");
}
- function updateRange(editor, forward, re, modify) {
- let range = Editor.extendRange(editor.selection.getRangeAt(0),
- forward, re, false);
+ function updateRange(editor, forward, re, modify, sameWord) {
+ let sel = editor.selection;
+ let range = sel.getRangeAt(0);
+
+ let end = range.endContainer == sel.focusNode && range.endOffset == sel.focusOffset;
+ if (range.collapsed)
+ end = forward;
+
+ Editor.extendRange(range, forward, re, sameWord,
+ editor.rootElement, end ? "end" : "start");
modify(range);
- editor.selection.removeAllRanges();
- editor.selection.addRange(range);
+ editor.selectionController.repaintSelection(editor.selectionController.SELECTION_NORMAL);
}
- function move(forward, re)
+
+ function clear(forward, re)
+ function _clear(editor) {
+ updateRange(editor, forward, re, function (range) {});
+ dactyl.assert(!editor.selection.isCollapsed);
+ editor.selection.deleteFromDocument();
+ let parent = DOM(editor.rootElement.parentNode);
+ if (parent.isInput)
+ parent.input();
+ }
+
+ function move(forward, re, sameWord)
function _move(editor) {
- updateRange(editor, forward, re, function (range) { range.collapse(!forward); });
+ updateRange(editor, forward, re,
+ function (range) { range.collapse(!forward); },
+ sameWord);
}
function select(forward, re)
function _select(editor) {
- updateRange(editor, forward, re, function (range) {});
+ updateRange(editor, forward, re,
+ function (range) {});
}
function beginLine(editor_) {
editor.executeCommand("cmd_beginLine");
- move(true, /\S/)(editor_);
+ move(true, /\s/, true)(editor_);
}
// COUNT CARET TEXT_EDIT VISUAL_TEXT_EDIT
addMovementMap(["l", "<Right>", "<Space>"], "Move right one character",
true, "characterMove", true, "cmd_charNext", "cmd_selectCharNext");
addMovementMap(["b", "<C-Left>"], "Move left one word",
- true, "wordMove", false, "cmd_wordPrevious", "cmd_selectWordPrevious");
+ true, "wordMove", false, move(false, /\w/), select(false, /\w/));
addMovementMap(["w", "<C-Right>"], "Move right one word",
- true, "wordMove", true, "cmd_wordNext", "cmd_selectWordNext");
+ true, "wordMove", true, move(true, /\w/), select(true, /\w/));
addMovementMap(["B"], "Move left to the previous white space",
true, "wordMove", false, move(false, /\S/), select(false, /\S/));
addMovementMap(["W"], "Move right to just beyond the next white space",
addBeginInsertModeMap(["S"], ["cmd_deleteToEndOfLine", "cmd_deleteToBeginningOfLine"], "Delete the current line and start insert");
addBeginInsertModeMap(["C"], ["cmd_deleteToEndOfLine"], "Delete from the cursor to the end of the line and start insert");
- function addMotionMap(key, desc, cmd, mode) {
- mappings.add([modes.TEXT_EDIT], [key],
+ function addMotionMap(key, desc, select, cmd, mode, caretOk) {
+ function doTxn(range, editor) {
+ try {
+ editor.editor.beginTransaction();
+ cmd(editor, range, editor.editor);
+ }
+ finally {
+ editor.editor.endTransaction();
+ }
+ }
+
+ mappings.add([modes.TEXT_EDIT], key,
+ desc,
+ function ({ command, count, motion }) {
+ let start = editor.selectedRange.cloneRange();
+
+ mappings.pushCommand();
+ modes.push(modes.OPERATOR, null, {
+ forCommand: command,
+
+ count: count,
+
+ leave: function leave(stack) {
+ try {
+ if (stack.push || stack.fromEscape)
+ return;
+
+ editor.withSavedValues(["inEditMap"], function () {
+ this.inEditMap = true;
+
+ let range = RangeFind.union(start, editor.selectedRange);
+ editor.selectedRange = select ? range : start;
+ doTxn(range, editor);
+ });
+
+ editor.currentRegister = null;
+ modes.delay(function () {
+ if (mode)
+ modes.push(mode);
+ });
+ }
+ finally {
+ if (!stack.push)
+ mappings.popCommand();
+ }
+ }
+ });
+ },
+ { count: true, type: "motion" });
+
+ mappings.add([modes.VISUAL], key,
desc,
function ({ count, motion }) {
- editor.selectMotion(key, motion, Math.max(count, 1));
- if (callable(cmd))
- cmd.call(events, Editor.getEditor(null));
- else {
- editor.executeCommand(cmd, 1);
- modes.pop(modes.TEXT_EDIT);
- }
- if (mode)
- modes.push(mode);
+ dactyl.assert(caretOk || editor.isTextEdit);
+ if (editor.isTextEdit)
+ doTxn(editor.selectedRange, editor);
+ else
+ cmd(editor, buffer.selection.getRangeAt(0));
},
- { count: true, motion: true });
+ { count: true, type: "motion" });
}
- addMotionMap("d", "Delete motion", "cmd_delete");
- addMotionMap("c", "Change motion", "cmd_delete", modes.INSERT);
- addMotionMap("y", "Yank motion", "cmd_copy");
-
- mappings.add([modes.INPUT],
- ["<C-w>"], "Delete previous word",
- function () { editor.executeCommand("cmd_deleteWordBackward", 1); });
+ addMotionMap(["d", "x"], "Delete text", true, function (editor) { editor.cut(); });
+ addMotionMap(["c"], "Change text", true, function (editor) { editor.cut(); }, modes.INSERT);
+ addMotionMap(["y"], "Yank text", false, function (editor, range) { editor.copy(range); }, null, true);
- mappings.add([modes.INPUT],
- ["<C-u>"], "Delete until beginning of current line",
- function () {
- // Deletes the whole line. What the hell.
- // editor.executeCommand("cmd_deleteToBeginningOfLine", 1);
+ addMotionMap(["gu"], "Lowercase text", false,
+ function (editor, range) {
+ editor.mungeRange(range, String.toLocaleLowerCase);
+ });
- editor.executeCommand("cmd_selectBeginLine", 1);
- if (Editor.getController().isCommandEnabled("cmd_delete"))
- editor.executeCommand("cmd_delete", 1);
+ addMotionMap(["gU"], "Uppercase text", false,
+ function (editor, range) {
+ editor.mungeRange(range, String.toLocaleUpperCase);
});
- mappings.add([modes.INPUT],
- ["<C-k>"], "Delete until end of current line",
- function () { editor.executeCommand("cmd_deleteToEndOfLine", 1); });
+ mappings.add([modes.OPERATOR],
+ ["c", "d", "y"], "Select the entire line",
+ function ({ command, count }) {
+ dactyl.assert(command == modes.getStack(0).params.forCommand);
- mappings.add([modes.INPUT],
- ["<C-a>"], "Move cursor to beginning of current line",
- function () { editor.executeCommand("cmd_beginLine", 1); });
+ let sel = editor.selection;
+ sel.modify("move", "backward", "lineboundary");
+ sel.modify("extend", "forward", "lineboundary");
- mappings.add([modes.INPUT],
- ["<C-e>"], "Move cursor to end of current line",
- function () { editor.executeCommand("cmd_endLine", 1); });
-
- mappings.add([modes.INPUT],
- ["<C-h>"], "Delete character to the left",
- function () { events.feedkeys("<BS>", true); });
-
- mappings.add([modes.INPUT],
- ["<C-d>"], "Delete character to the right",
- function () { editor.executeCommand("cmd_deleteCharForward", 1); });
-
- mappings.add([modes.INPUT],
- ["<S-Insert>"], "Insert clipboard/selection",
- function () { editor.pasteClipboard(); });
-
- mappings.add([modes.INPUT, modes.TEXT_EDIT],
- ["<C-i>"], "Edit text field with an external editor",
- function () { editor.editFieldExternally(); });
-
- mappings.add([modes.INPUT],
- ["<C-t>"], "Edit text field in Vi mode",
- function () {
- dactyl.assert(dactyl.focusedElement);
- dactyl.assert(!editor.isTextEdit);
- modes.push(modes.TEXT_EDIT);
- });
+ if (command != "c")
+ sel.modify("extend", "forward", "character");
+ },
+ { count: true, type: "operator" });
+
+ let bind = function bind(names, description, action, params)
+ mappings.add([modes.INPUT], names, description,
+ action, update({ type: "editor" }, params));
+
+ bind(["<C-w>"], "Delete previous word",
+ function () {
+ if (editor.editor)
+ clear(false, /\w/)(editor.editor);
+ else
+ editor.executeCommand("cmd_deleteWordBackward", 1);
+ });
+
+ bind(["<C-u>"], "Delete until beginning of current line",
+ function () {
+ // Deletes the whole line. What the hell.
+ // editor.executeCommand("cmd_deleteToBeginningOfLine", 1);
+
+ editor.executeCommand("cmd_selectBeginLine", 1);
+ if (editor.selection && editor.selection.isCollapsed) {
+ editor.executeCommand("cmd_deleteCharBackward", 1);
+ editor.executeCommand("cmd_selectBeginLine", 1);
+ }
+
+ if (editor.getController("cmd_delete").isCommandEnabled("cmd_delete"))
+ editor.executeCommand("cmd_delete", 1);
+ });
+
+ bind(["<C-k>"], "Delete until end of current line",
+ function () { editor.executeCommand("cmd_deleteToEndOfLine", 1); });
+
+ bind(["<C-a>"], "Move cursor to beginning of current line",
+ function () { editor.executeCommand("cmd_beginLine", 1); });
+
+ bind(["<C-e>"], "Move cursor to end of current line",
+ function () { editor.executeCommand("cmd_endLine", 1); });
+
+ bind(["<C-h>"], "Delete character to the left",
+ function () { events.feedkeys("<BS>", true); });
+
+ bind(["<C-d>"], "Delete character to the right",
+ function () { editor.executeCommand("cmd_deleteCharForward", 1); });
+
+ bind(["<S-Insert>"], "Insert clipboard/selection",
+ function () { editor.paste(); });
+
+ bind(["<C-i>"], "Edit text field with an external editor",
+ function () { editor.editFieldExternally(); });
+
+ bind(["<C-t>"], "Edit text field in Text Edit mode",
+ function () {
+ dactyl.assert(!editor.isTextEdit && editor.editor);
+ dactyl.assert(dactyl.focusedElement ||
+ // Sites like Google like to use a
+ // hidden, editable window for keyboard
+ // focus and use their own WYSIWYG editor
+ // implementations for the visible area,
+ // which we can't handle.
+ let (f = document.commandDispatcher.focusedWindow.frameElement)
+ f && Hints.isVisible(f, true));
+
+ modes.push(modes.TEXT_EDIT);
+ });
// Ugh.
mappings.add([modes.INPUT, modes.CARET],
["<C-]>", "<C-5>"], "Expand Insert mode abbreviation",
function () { editor.expandAbbreviation(modes.INSERT); });
- // text edit mode
- mappings.add([modes.TEXT_EDIT],
- ["u"], "Undo changes",
- function (args) {
- editor.executeCommand("cmd_undo", Math.max(args.count, 1));
- editor.unselectText();
- },
- { count: true });
-
- mappings.add([modes.TEXT_EDIT],
- ["<C-r>"], "Redo undone changes",
- function (args) {
- editor.executeCommand("cmd_redo", Math.max(args.count, 1));
- editor.unselectText();
- },
- { count: true });
+ let bind = function bind(names, description, action, params)
+ mappings.add([modes.TEXT_EDIT], names, description,
+ action, update({ type: "editor" }, params));
- mappings.add([modes.TEXT_EDIT],
- ["D"], "Delete the characters under the cursor until the end of the line",
- function () { editor.executeCommand("cmd_deleteToEndOfLine"); });
- mappings.add([modes.TEXT_EDIT],
- ["o"], "Open line below current",
- function () {
- editor.executeCommand("cmd_endLine", 1);
- modes.push(modes.INSERT);
- events.feedkeys("<Return>");
- });
+ bind(["<C-a>"], "Increment the next number",
+ function ({ count }) { editor.modifyNumber(count || 1) },
+ { count: true });
- mappings.add([modes.TEXT_EDIT],
- ["O"], "Open line above current",
- function () {
- editor.executeCommand("cmd_beginLine", 1);
- modes.push(modes.INSERT);
- events.feedkeys("<Return>");
- editor.executeCommand("cmd_linePrevious", 1);
- });
+ bind(["<C-x>"], "Decrement the next number",
+ function ({ count }) { editor.modifyNumber(-(count || 1)) },
+ { count: true });
- mappings.add([modes.TEXT_EDIT],
- ["X"], "Delete character to the left",
- function (args) { editor.executeCommand("cmd_deleteCharBackward", Math.max(args.count, 1)); },
+ // text edit mode
+ bind(["u"], "Undo changes",
+ function (args) {
+ editor.executeCommand("cmd_undo", Math.max(args.count, 1));
+ editor.deselect();
+ },
+ { count: true });
+
+ bind(["<C-r>"], "Redo undone changes",
+ function (args) {
+ editor.executeCommand("cmd_redo", Math.max(args.count, 1));
+ editor.deselect();
+ },
+ { count: true });
+
+ bind(["D"], "Delete characters from the cursor to the end of the line",
+ function () { editor.executeCommand("cmd_deleteToEndOfLine"); });
+
+ bind(["o"], "Open line below current",
+ function () {
+ editor.executeCommand("cmd_endLine", 1);
+ modes.push(modes.INSERT);
+ events.feedkeys("<Return>");
+ });
+
+ bind(["O"], "Open line above current",
+ function () {
+ editor.executeCommand("cmd_beginLine", 1);
+ modes.push(modes.INSERT);
+ events.feedkeys("<Return>");
+ editor.executeCommand("cmd_linePrevious", 1);
+ });
+
+ bind(["X"], "Delete character to the left",
+ function (args) { editor.executeCommand("cmd_deleteCharBackward", Math.max(args.count, 1)); },
{ count: true });
- mappings.add([modes.TEXT_EDIT],
- ["x"], "Delete character to the right",
- function (args) { editor.executeCommand("cmd_deleteCharForward", Math.max(args.count, 1)); },
+ bind(["x"], "Delete character to the right",
+ function (args) { editor.executeCommand("cmd_deleteCharForward", Math.max(args.count, 1)); },
{ count: true });
// visual mode
["v", "V"], "End Visual mode",
function () { modes.pop(); });
- mappings.add([modes.TEXT_EDIT],
- ["V"], "Start Visual Line mode",
- function () {
- modes.push(modes.VISUAL, modes.LINE);
- editor.executeCommand("cmd_beginLine", 1);
- editor.executeCommand("cmd_selectLineNext", 1);
- });
+ bind(["V"], "Start Visual Line mode",
+ function () {
+ modes.push(modes.VISUAL, modes.LINE);
+ editor.executeCommand("cmd_beginLine", 1);
+ editor.executeCommand("cmd_selectLineNext", 1);
+ });
mappings.add([modes.VISUAL],
- ["c", "s"], "Change selected text",
+ ["s"], "Change selected text",
function () {
dactyl.assert(editor.isTextEdit);
editor.executeCommand("cmd_cut");
});
mappings.add([modes.VISUAL],
- ["d", "x"], "Delete selected text",
+ ["o"], "Move cursor to the other end of the selection",
function () {
- dactyl.assert(editor.isTextEdit);
- editor.executeCommand("cmd_cut");
- });
-
- mappings.add([modes.VISUAL],
- ["y"], "Yank selected text",
- function () {
- if (editor.isTextEdit) {
- editor.executeCommand("cmd_copy");
- modes.pop();
- }
+ if (editor.isTextEdit)
+ var selection = editor.selection;
else
- dactyl.clipboardWrite(buffer.currentWord, true);
+ selection = buffer.focusedFrame.getSelection();
+
+ util.assert(selection.focusNode);
+ let { focusOffset, anchorOffset, focusNode, anchorNode } = selection;
+ selection.collapse(focusNode, focusOffset);
+ selection.extend(anchorNode, anchorOffset);
});
- mappings.add([modes.VISUAL, modes.TEXT_EDIT],
- ["p"], "Paste clipboard contents",
- function ({ count }) {
+ bind(["p"], "Paste clipboard contents",
+ function ({ count }) {
dactyl.assert(!editor.isCaret);
- editor.executeCommand("cmd_paste", count || 1);
- modes.pop(modes.TEXT_EDIT);
+ editor.executeCommand(modules.bind("paste", editor, null),
+ count || 1);
},
{ count: true });
- // finding characters
- mappings.add([modes.TEXT_EDIT, modes.VISUAL],
- ["f"], "Move to a character on the current line after the cursor",
- function ({ arg, count }) {
- let pos = editor.findChar(arg, Math.max(count, 1));
- if (pos >= 0)
- editor.moveToPosition(pos, true, modes.main == modes.VISUAL);
+ mappings.add([modes.COMMAND],
+ ['"'], "Bind a register to the next command",
+ function ({ arg }) {
+ editor.pushRegister(arg);
},
- { arg: true, count: true });
+ { arg: true });
- mappings.add([modes.TEXT_EDIT, modes.VISUAL],
- ["F"], "Move to a character on the current line before the cursor",
- function ({ arg, count }) {
- let pos = editor.findChar(arg, Math.max(count, 1), true);
- if (pos >= 0)
- editor.moveToPosition(pos, false, modes.main == modes.VISUAL);
+ mappings.add([modes.INPUT],
+ ["<C-'>", '<C-">'], "Bind a register to the next command",
+ function ({ arg }) {
+ editor.pushRegister(arg);
},
- { arg: true, count: true });
+ { arg: true });
- mappings.add([modes.TEXT_EDIT, modes.VISUAL],
- ["t"], "Move before a character on the current line",
- function ({ arg, count }) {
- let pos = editor.findChar(arg, Math.max(count, 1));
- if (pos >= 0)
- editor.moveToPosition(pos - 1, true, modes.main == modes.VISUAL);
- },
- { arg: true, count: true });
+ let bind = function bind(names, description, action, params)
+ mappings.add([modes.TEXT_EDIT, modes.OPERATOR, modes.VISUAL],
+ names, description,
+ action, update({ type: "editor" }, params));
- mappings.add([modes.TEXT_EDIT, modes.VISUAL],
- ["T"], "Move before a character on the current line, backwards",
- function ({ arg, count }) {
- let pos = editor.findChar(arg, Math.max(count, 1), true);
- if (pos >= 0)
- editor.moveToPosition(pos + 1, false, modes.main == modes.VISUAL);
- },
- { arg: true, count: true });
+ // finding characters
+ function offset(backward, before, pos) {
+ if (!backward && modes.main != modes.TEXT_EDIT)
+ return before ? 0 : 1;
+ if (before)
+ return backward ? +1 : -1;
+ return 0;
+ }
+
+ bind(["f"], "Find a character on the current line, forwards",
+ function ({ arg, count }) {
+ editor.moveToPosition(editor.findChar(arg, Math.max(count, 1), false,
+ offset(false, false)),
+ modes.main == modes.VISUAL);
+ },
+ { arg: true, count: true, type: "operator" });
+
+ bind(["F"], "Find a character on the current line, backwards",
+ function ({ arg, count }) {
+ editor.moveToPosition(editor.findChar(arg, Math.max(count, 1), true,
+ offset(true, false)),
+ modes.main == modes.VISUAL);
+ },
+ { arg: true, count: true, type: "operator" });
+
+ bind(["t"], "Find a character on the current line, forwards, and move to the character before it",
+ function ({ arg, count }) {
+ editor.moveToPosition(editor.findChar(arg, Math.max(count, 1), false,
+ offset(false, true)),
+ modes.main == modes.VISUAL);
+ },
+ { arg: true, count: true, type: "operator" });
+
+ bind(["T"], "Find a character on the current line, backwards, and move to the character after it",
+ function ({ arg, count }) {
+ editor.moveToPosition(editor.findChar(arg, Math.max(count, 1), true,
+ offset(true, true)),
+ modes.main == modes.VISUAL);
+ },
+ { arg: true, count: true, type: "operator" });
// text edit and visual mode
mappings.add([modes.TEXT_EDIT, modes.VISUAL],
["~"], "Switch case of the character under the cursor and move the cursor to the right",
function ({ count }) {
- if (modes.main == modes.VISUAL)
- count = Editor.getEditor().selectionEnd - Editor.getEditor().selectionStart;
- count = Math.max(count, 1);
-
- // FIXME: do this in one pass?
- while (count-- > 0) {
- let text = Editor.getEditor().value;
- let pos = Editor.getEditor().selectionStart;
- dactyl.assert(pos < text.length);
-
- let chr = text[pos];
- Editor.getEditor().value = text.substring(0, pos) +
- (chr == chr.toLocaleLowerCase() ? chr.toLocaleUpperCase() : chr.toLocaleLowerCase()) +
- text.substring(pos + 1);
- editor.moveToPosition(pos + 1, true, false);
+ function munger(range)
+ String(range).replace(/./g, function (c) {
+ let lc = c.toLocaleLowerCase();
+ return c == lc ? c.toLocaleUpperCase() : lc;
+ });
+
+ var range = editor.selectedRange;
+ if (range.collapsed) {
+ count = count || 1;
+ Editor.extendRange(range, true, { test: function (c) !!count-- }, true);
}
+ editor.mungeRange(range, munger, count != null);
+
modes.pop(modes.TEXT_EDIT);
},
{ count: true });
- function bind() mappings.add.apply(mappings,
- [[modes.AUTOCOMPLETE]].concat(Array.slice(arguments)))
+ let bind = function bind() mappings.add.apply(mappings,
+ [[modes.AUTOCOMPLETE]].concat(Array.slice(arguments)))
bind(["<Esc>"], "Return to Insert mode",
function () Events.PASS_THROUGH);
bind(["<C-n>"], "Select the next autocomplete result",
function () { events.feedkeys("<Down>", { skipmap: true }); });
},
-
- options: function () {
+ options: function init_options() {
options.add(["editor"],
"The external text editor",
"string", 'gvim -f +<line> +"sil! call cursor(0, <column>)" <file>', {
options.add(["insertmode", "im"],
"Enter Insert mode rather than Text Edit mode when focusing text areas",
"boolean", true);
+
+ options.add(["spelllang", "spl"],
+ "The language used by the spell checker",
+ "string", config.locale,
+ {
+ initValue: function () {},
+ getter: function getter() {
+ try {
+ return services.spell.dictionary || "";
+ }
+ catch (e) {
+ return "";
+ }
+ },
+ setter: function setter(val) { services.spell.dictionary = val; },
+ completer: function completer(context) {
+ let res = {};
+ services.spell.getDictionaryList(res, {});
+ context.completions = res.value;
+ context.keys = { text: util.identity, description: util.identity };
+ }
+ });
+ },
+ sanitizer: function () {
+ sanitizer.addItem("registers", {
+ description: "Register values",
+ persistent: true,
+ action: function (timespan, host) {
+ if (!host) {
+ for (let [k, v] in editor.registers)
+ if (timespan.contains(v.timestamp))
+ editor.registers.remove(k);
+ editor.registerRing.truncate(0);
+ }
+ }
+ });
}
});