X-Git-Url: https://git.donarmstrong.com/?a=blobdiff_plain;f=common%2Fcontent%2Feditor.js;h=eef3c1ded03f38f4fecd42e038b8c192b47dafa3;hb=8b6fcae7eaa413bc62d645d2d0c99835c47265e6;hp=d2fe3224b9f063b010fe42eae926f1418a41df07;hpb=70740024f9c028c1fd63e1a1850ab062ff956054;p=dactyl.git diff --git a/common/content/editor.js b/common/content/editor.js index d2fe322..eef3c1d 100644 --- a/common/content/editor.js +++ b/common/content/editor.js @@ -11,77 +11,203 @@ // 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. - // 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, noStrip) { + 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, this.editor[noStrip ? "eNoStrip" : "eStrip"]); + }, - 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) { @@ -92,133 +218,135 @@ var Editor = Module("editor", { } }, - // 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 findChar(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"); }, /** @@ -249,7 +377,11 @@ var Editor = Module("editor", { 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") + " ", @@ -262,18 +394,32 @@ var Editor = Module("editor", { 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(); @@ -290,7 +436,9 @@ var Editor = Module("editor", { 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; @@ -307,10 +455,12 @@ var Editor = Module("editor", { 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 (true) { + let elem = DOM(textBox); + elem.attrNS(NS, "modifiable", true) + .style.MozUserInput; + elem.input().attrNS(NS, "modifiable", null); + } } else { while (editor_.rootElement.firstChild) @@ -325,8 +475,9 @@ var Editor = Module("editor", { 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)) @@ -349,38 +500,167 @@ var Editor = Module("editor", { * @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); @@ -405,93 +685,183 @@ var Editor = Module("editor", { 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 initModes() { + 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 initCommands() { + commands.add(["reg[isters]"], + "List the contents of known registers", + function (args) { + completion.listCompleter("register", args[0]); + }, + { argCount: "*" }); }, + completion: function initCompletion() { + 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 initMappings() { + + Map.types["editor"] = { + preExecute: function preExecute(args) { + if (editor.editor && !this.editor) { + this.editor = editor.editor; + if (!this.noTransaction) + this.editor.beginTransaction(); + } + editor.inEditMap = true; + }, + postExecute: function preExecute(args) { + editor.inEditMap = false; + if (this.editor) { + if (!this.noTransaction) + 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(); + } + }; // 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)); - } - - let controller = buffer.selectionController; + 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); } @@ -500,42 +870,62 @@ var Editor = Module("editor", { 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 @@ -548,9 +938,9 @@ var Editor = Module("editor", { addMovementMap(["l", "", ""], "Move right one character", true, "characterMove", true, "cmd_charNext", "cmd_selectCharNext"); addMovementMap(["b", ""], "Move left one word", - true, "wordMove", false, "cmd_wordPrevious", "cmd_selectWordPrevious"); + true, "wordMove", false, move(false, /\w/), select(false, /\w/)); addMovementMap(["w", ""], "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", @@ -582,77 +972,158 @@ var Editor = Module("editor", { 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"); + addMotionMap(["d", "x"], "Delete text", true, function (editor) { editor.cut(); }); + addMotionMap(["c"], "Change text", true, function (editor) { editor.cut(null, null, true); }, modes.INSERT); + addMotionMap(["y"], "Yank text", false, function (editor, range) { editor.copy(range); }, null, true); - mappings.add([modes.INPUT], - [""], "Delete previous word", - function () { editor.executeCommand("cmd_deleteWordBackward", 1); }); - - mappings.add([modes.INPUT], - [""], "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], - [""], "Delete until end of current line", - function () { editor.executeCommand("cmd_deleteToEndOfLine", 1); }); - - mappings.add([modes.INPUT], - [""], "Move cursor to beginning of current line", - function () { editor.executeCommand("cmd_beginLine", 1); }); - - mappings.add([modes.INPUT], - [""], "Move cursor to end of current line", - function () { editor.executeCommand("cmd_endLine", 1); }); - - mappings.add([modes.INPUT], - [""], "Delete character to the left", - function () { events.feedkeys("", true); }); - - mappings.add([modes.INPUT], - [""], "Delete character to the right", - function () { editor.executeCommand("cmd_deleteCharForward", 1); }); - - mappings.add([modes.INPUT], - [""], "Insert clipboard/selection", - function () { editor.pasteClipboard(); }); + 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, modes.TEXT_EDIT], - [""], "Edit text field with an external editor", - function () { editor.editFieldExternally(); }); + let sel = editor.selection; + sel.modify("move", "backward", "lineboundary"); + sel.modify("extend", "forward", "lineboundary"); - mappings.add([modes.INPUT], - [""], "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([""], "Delete previous word", + function () { + if (editor.editor) + clear(false, /\w/)(editor.editor); + else + editor.executeCommand("cmd_deleteWordBackward", 1); + }); + + bind([""], "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([""], "Delete until end of current line", + function () { editor.executeCommand("cmd_deleteToEndOfLine", 1); }); + + bind([""], "Move cursor to beginning of current line", + function () { editor.executeCommand("cmd_beginLine", 1); }); + + bind([""], "Move cursor to end of current line", + function () { editor.executeCommand("cmd_endLine", 1); }); + + bind([""], "Delete character to the left", + function () { events.feedkeys("", true); }); + + bind([""], "Delete character to the right", + function () { editor.executeCommand("cmd_deleteCharForward", 1); }); + + bind([""], "Insert clipboard/selection", + function () { editor.paste(); }); + + bind([""], "Edit text field with an external editor", + function () { editor.editFieldExternally(); }); + + bind([""], "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], @@ -673,52 +1144,58 @@ var Editor = Module("editor", { ["", ""], "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], - [""], "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(""); - }); + bind([""], "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(""); - editor.executeCommand("cmd_linePrevious", 1); - }); + bind([""], "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.editor.undo(Math.max(args.count, 1)); + editor.deselect(); + }, + { count: true, noTransaction: true }); + + bind([""], "Redo undone changes", + function (args) { + editor.editor.redo(Math.max(args.count, 1)); + editor.deselect(); + }, + { count: true, noTransaction: 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(""); + }); + + bind(["O"], "Open line above current", + function () { + editor.executeCommand("cmd_beginLine", 1); + modes.push(modes.INSERT); + events.feedkeys(""); + 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 @@ -730,16 +1207,15 @@ var Editor = Module("editor", { ["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"); @@ -747,95 +1223,110 @@ var Editor = Module("editor", { }); 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], + ["", ''], "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([""], "Return to Insert mode", function () Events.PASS_THROUGH); @@ -855,8 +1346,7 @@ var Editor = Module("editor", { bind([""], "Select the next autocomplete result", function () { events.feedkeys("", { skipmap: true }); }); }, - - options: function () { + options: function initOptions() { options.add(["editor"], "The external text editor", "string", 'gvim -f + +"sil! call cursor(0, )" ', { @@ -878,6 +1368,42 @@ var Editor = Module("editor", { 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 initSanitizer() { + 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); + } + } + }); } });