diff --git a/common/content/editor.js b/common/content/editor.js index 333e0f72..a58552c0 100644 --- a/common/content/editor.js +++ b/common/content/editor.js @@ -46,8 +46,28 @@ var Editor = Module("editor", { this.selection.focusOffset); }, + get selectionRange() { + if (!this.selection.rangeCount) { + let range = RangeFind.nodeContents(this.editor.rootElement.ownerDocument); + range.collapse(true); + this.selectionRange = range; + } + return this.selection.getRangeAt(0); + }, + set selectionRange(range) { + this.selection.removeAllRanges(); + if (range != null) + this.selection.addRange(range); + }, + get selectedText() String(this.selection), + get preserveSelection() this.editor && !this.editor.shouldTxnSetSelection, + set preserveSelection(val) { + if (this.editor) + this.editor.setShouldTxnSetSelection(!val); + }, + pasteClipboard: function (clipboard, toStart) { let elem = this.element; @@ -69,7 +89,7 @@ var Editor = Module("editor", { elem.value = value; if (/^(search|text)$/.test(elem.type)) - DOM(elem).rootElement.firstChild.textContent = value; + DOM(elem).editor.rootElement.firstChild.textContent = value; elem.selectionStart = Math.min(start + (toStart ? 0 : text.length), elem.value.length); elem.selectionEnd = elem.selectionStart; @@ -118,6 +138,51 @@ var Editor = Module("editor", { this.selection[select ? "extend" : "collapse"](node, pos); }, + + mungeRange: function mungeRange(range, munger, selectEnd) { + let { editor } = this; + editor.beginPlaceHolderTransaction(null); + + let [container, offset] = ["startContainer", "startOffset"]; + if (selectEnd) + [container, offset] = ["endContainer", "endOffset"]; + + try { + // :( + let idx = range[offset]; + let parent = range[container].parentNode; + let parentIdx = Array.indexOf(parent.childNodes, + range[container]); + + 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 = text.slice(0, start) + + munger(text.slice(start, end)) + + text.slice(end); + else + text = munger(text); + + if (editor instanceof Ci.nsIPlaintextEditor) { + this.selectionRange = RangeFind.nodeContents(node); + editor.insertText(text); + } + else + node.textContent = text; + } + this.selection.collapse(parent.childNodes[parentIdx], idx); + } + finally { + editor.endPlaceHolderTransaction(); + } + }, + findChar: function (key, count, backward) { util.assert(DOM(this.element).isInput); @@ -322,35 +387,58 @@ var Editor = Module("editor", { }, }, { TextsIterator: Class("TextsIterator", { - init: function init(root, context) { - this.context = context; - this.root = root; + 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 == this.root) - return null; + if (!this.context) + return this.context = this.start; + + var node = this.context; + if (!this.after) + node = node.previousSibling; - var node = this.context.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() { - var node = this.context.firstChild; + if (!this.context) + return this.context = this.start; + + if (!this.after) + var node = this.context.firstChild; + if (!node) { node = this.context; - while (node != this.root && !node.nextSibling) + while (node.parentNode && node != this.range.endContainer + && !node.nextSibling) node = node.parentNode; node = node.nextSibling; } - if (node == this.root || node == null) + + if (!node || !RangeFind.containsNode(this.range, node, true)) return null; + this.after = false; return this.context = node; }, @@ -379,9 +467,12 @@ var Editor = Module("editor", { function advance(positive) { while (true) { while (idx == text.length && (node = iterator.getNext())) { + if (node == iterator.start) + idx = range.endOffset; + offset = text.length; text += node.textContent; - range.setEnd(node, 0); + range.setEnd(node, idx - offset); } if (idx >= text.length || re.test(text[idx]) != positive) @@ -393,9 +484,13 @@ var Editor = Module("editor", { while (true) { while (idx == 0 && (node = iterator.getPrev())) { let str = node.textContent; - idx += str.length; + if (node == iterator.start) + idx = range.endOffset; + else + idx = str.length; + text = str + text; - range.setStart(node, str.length); + range.setStart(node, idx); } if (idx == 0 || re.test(text[idx - 1]) != positive) break; @@ -403,19 +498,22 @@ var Editor = Module("editor", { } } + if (!root) + for (root = range.startContainer; + 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[forward ? "endContainer" : "startContainer"]; - let iterator = Editor.TextsIterator(root || node.ownerDocument.documentElement, - node); + let iterator = Editor.TextsIterator(RangeFind.nodeContents(root), + node, !forward); - if (!(node instanceof Ci.nsIDOMText)) { - node = iterator[forward ? "getNext" : "getPrev"](); - dactyl.assert(node); - range[forward ? "setEnd" : "setStart"](node, forward ? 0 : node.textContent.length); - } - - - let text = range[forward ? "endContainer" : "startContainer"].textContent; - let idx = range[forward ? "endOffset" : "startOffset"]; + let text = ""; + let idx = 0; let offset = 0; if (forward) { @@ -621,9 +719,21 @@ var Editor = Module("editor", { addBeginInsertModeMap(["C"], ["cmd_deleteToEndOfLine"], "Delete from the cursor to the end of the line and start insert"); function addMotionMap(key, desc, select, cmd, mode) { - mappings.add([modes.TEXT_EDIT], [key], + 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 ({ count, motion }) { + let start = editor.selectionRange.cloneRange(); + modes.push(modes.OPERATOR, null, { count: count, @@ -631,17 +741,9 @@ var Editor = Module("editor", { if (stack.push || stack.fromEscape) return; - try { - editor_.beginTransaction(); - - let range = RangeFind.union(start, sel.getRangeAt(0)); - sel.removeAllRanges(); - sel.addRange(select ? range : start); - cmd(editor_, range); - } - finally { - editor_.endTransaction(); - } + let range = RangeFind.union(start, editor.selectionRange); + editor.selectionRange = select ? range : start; + doTxn(range, editor); modes.delay(function () { if (mode) @@ -649,17 +751,31 @@ var Editor = Module("editor", { }); } }); - - let editor_ = editor.editor; - let sel = editor.selection; - let start = sel.getRangeAt(0).cloneRange(); }, { count: true, type: "motion" }); + + mappings.add([modes.VISUAL], key, + desc, + function ({ count, motion }) { + dactyl.assert(editor.isTextEdit); + doTxn(editor.selectionRange, editor); + }, + { count: true, type: "motion" }); } - addMotionMap("d", "Delete motion", true, function (editor) { editor.cut(); }); - addMotionMap("c", "Change motion", true, function (editor) { editor.cut(); }, modes.INSERT); - addMotionMap("y", "Yank motion", false, function (editor, range) { dactyl.clipboardWrite(DOM.stringify(range)) }); + addMotionMap(["d", "x"], "Delete text", true, function (editor) { editor.editor.cut(); }); + addMotionMap(["c"], "Change text", true, function (editor) { editor.editor.cut(); }, modes.INSERT); + addMotionMap(["y"], "Yank text", false, function (editor, range) { dactyl.clipboardWrite(DOM.stringify(range)) }); + + addMotionMap(["gu"], "Lowercase text", false, + function (editor, range) { + editor.mungeRange(range, String.toLocaleLowerCase); + }); + + addMotionMap(["gU"], "Uppercase text", false, + function (editor, range) { + editor.mungeRange(range, String.toLocaleUpperCase); + }); let bind = function bind(names, description, action, params) mappings.add([modes.INPUT], names, description, @@ -811,31 +927,13 @@ var Editor = Module("editor", { }); mappings.add([modes.VISUAL], - ["c", "s"], "Change selected text", + ["s"], "Change selected text", function () { dactyl.assert(editor.isTextEdit); editor.executeCommand("cmd_cut"); modes.push(modes.INSERT); }); - mappings.add([modes.VISUAL], - ["d", "x"], "Delete selected text", - 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(); - } - else - dactyl.clipboardWrite(buffer.currentWord, true); - }); - bind(["p"], "Paste clipboard contents", function ({ count }) { dactyl.assert(!editor.isCaret); @@ -894,19 +992,6 @@ var Editor = Module("editor", { }, { arg: true, count: true, type: "operator" }); - function mungeRange(range, munger) { - let text = munger(range); - let { startOffset, endOffset } = range; - let root = range.startContainer.parentNode; - - range.deleteContents(); - range.insertNode(range.startContainer.ownerDocument - .createTextNode(text)); - root.normalize(); - range.setStart(root.firstChild, startOffset); - range.setEnd(root.firstChild, endOffset); - } - // 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", @@ -917,21 +1002,14 @@ var Editor = Module("editor", { return c == lc ? c.toLocaleUpperCase() : lc; }); - var range = editor.selection.getRangeAt(0); - // Ugh. TODO: Utility. - if (!(range.startContainer instanceof Ci.nsIDOMText)) { - range = RangeFind.nodeContants(node.startContainer); - range.collapse(false); - } - + var range = editor.selectionRange; if (range.collapsed) { count = count || 1; range.setEnd(range.startContainer, range.startOffset + count); } - mungeRange(range, munger); - editor.selection.collapse(range.startContainer, range.endOffset); + editor.mungeRange(range, munger, count != null); modes.pop(modes.TEXT_EDIT); }, diff --git a/common/content/events.js b/common/content/events.js index e8d1080b..fdfa231b 100644 --- a/common/content/events.js +++ b/common/content/events.js @@ -867,7 +867,7 @@ var Events = Module("events", { if (!haveInput) if (options["insertmode"]) modes.push(modes.INSERT); - else { + else if (!isinstance(modes.main, [modes.TEXT_EDIT, modes.VISUAL])) { modes.push(modes.TEXT_EDIT); if (elem.selectionEnd - elem.selectionStart > 0) modes.push(modes.VISUAL); diff --git a/common/modules/finder.jsm b/common/modules/finder.jsm index 48549f7f..019b4f33 100644 --- a/common/modules/finder.jsm +++ b/common/modules/finder.jsm @@ -476,7 +476,7 @@ var RangeFind = Class("RangeFind", { } }, - indexIter: function (private_) { + indexIter: function indexIter(private_) { let idx = this.range.index; if (this.backward) var groups = [util.range(idx + 1, 0, -1), util.range(this.ranges.length, idx, -1)]; @@ -494,7 +494,7 @@ var RangeFind = Class("RangeFind", { } }, - iter: function (word) { + iter: function iter(word) { let saved = ["lastRange", "lastString", "range", "regexp"].map(function (s) [s, this[s]], this); let res; try { @@ -525,7 +525,7 @@ var RangeFind = Class("RangeFind", { } }, - makeFrameList: function (win) { + makeFrameList: function makeFrameList(win) { const self = this; win = win.top; let frames = []; @@ -581,7 +581,7 @@ var RangeFind = Class("RangeFind", { return frames; }, - reset: function () { + reset: function reset() { this.ranges = this.makeFrameList(this.content); this.startRange = this.selectedRange; @@ -594,7 +594,7 @@ var RangeFind = Class("RangeFind", { this.found = false; }, - find: function (pattern, reverse, private_) { + find: function find(pattern, reverse, private_) { if (!private_ && this.lastRange && !RangeFind.equal(this.selectedRange, this.lastRange)) this.reset(); @@ -685,18 +685,18 @@ var RangeFind = Class("RangeFind", { get stale() this._stale || this.baseDocument.get() != this.content.document, set stale(val) this._stale = val, - addListeners: function () { + addListeners: function addListeners() { for (let range in array.iterValues(this.ranges)) range.window.addEventListener("unload", this.closure.onUnload, true); }, - purgeListeners: function () { + purgeListeners: function purgeListeners() { for (let range in array.iterValues(this.ranges)) try { range.window.removeEventListener("unload", this.closure.onUnload, true); } catch (e if e.result === Cr.NS_ERROR_FAILURE) {} }, - onUnload: function (event) { + onUnload: function onUnload(event) { this.purgeListeners(); if (this.highlighted) this.highlight(true); @@ -704,7 +704,7 @@ var RangeFind = Class("RangeFind", { } }, { Range: Class("RangeFind.Range", { - init: function (range, index) { + init: function init(range, index) { this.index = index; this.range = range; @@ -720,7 +720,7 @@ var RangeFind = Class("RangeFind", { intersects: function (range) RangeFind.intersects(this.range, range), - save: function () { + save: function save() { this.scroll = Point(this.window.pageXOffset, this.window.pageYOffset); this.initialSelection = null; @@ -728,11 +728,11 @@ var RangeFind = Class("RangeFind", { this.initialSelection = this.selection.getRangeAt(0); }, - descroll: function () { + descroll: function descroll() { this.window.scrollTo(this.scroll.x, this.scroll.y); }, - deselect: function () { + deselect: function deselect() { if (this.selection) { this.selection.removeAllRanges(); if (this.initialSelection) @@ -752,17 +752,19 @@ var RangeFind = Class("RangeFind", { } } }), - contains: function (range, r) { + contains: function contains(range, r, quiet) { try { return range.compareBoundaryPoints(range.START_TO_END, r) >= 0 && range.compareBoundaryPoints(range.END_TO_START, r) <= 0; } catch (e) { - util.reportError(e, true); + if (e.result != Cr.NS_ERROR_DOM_WRONG_DOCUMENT_ERR && !quiet) + util.reportError(e, true); return false; } }, - intersects: function (range, r) { + containsNode: function containsNode(range, n, quiet) n.ownerDocument && this.contains(range, RangeFind.nodeRange(n), quiet), + intersects: function intersects(range, r) { try { return r.compareBoundaryPoints(range.START_TO_END, range) >= 0 && r.compareBoundaryPoints(range.END_TO_START, range) <= 0; @@ -772,12 +774,12 @@ var RangeFind = Class("RangeFind", { return false; } }, - endpoint: function (range, before) { + endpoint: function endpoint(range, before) { range = range.cloneRange(); range.collapse(before); return range; }, - equal: function (r1, r2) { + equal: function equal(r1, r2) { try { return !r1.compareBoundaryPoints(r1.START_TO_START, r2) && !r1.compareBoundaryPoints(r1.END_TO_END, r2); } @@ -785,7 +787,7 @@ var RangeFind = Class("RangeFind", { return false; } }, - nodeContents: function (node) { + nodeContents: function nodeContents(node) { let range = node.ownerDocument.createRange(); try { range.selectNodeContents(node); @@ -793,7 +795,7 @@ var RangeFind = Class("RangeFind", { catch (e) {} return range; }, - nodeRange: function (node) { + nodeRange: function nodeRange(node) { let range = node.ownerDocument.createRange(); try { range.selectNode(node); @@ -801,7 +803,7 @@ var RangeFind = Class("RangeFind", { catch (e) {} return range; }, - sameDocument: function (r1, r2) { + sameDocument: function sameDocument(r1, r2) { if (!(r1 && r2 && r1.endContainer.ownerDocument == r2.endContainer.ownerDocument)) return false; try { diff --git a/pentadactyl/NEWS b/pentadactyl/NEWS index c2d311b3..e0a3c307 100644 --- a/pentadactyl/NEWS +++ b/pentadactyl/NEWS @@ -30,17 +30,44 @@ everywhere from command-line and message history to option values and cookies. [b1] • New and much more powerful incremental search implementation. - Improvements over the standard Firefox find include: [b1] + Improvements over the standard Firefox find include: - Starts at the cursor position in the currently selected frame, unlike Firefox, which always starts at the start of - the first frame. + the first frame. [b1] - Returns the cursor and viewport to their original position - on cancel. + on cancel. [b1] - Backtracks to the first successful match after pressing - backspace. - - Supports reverse incremental search. - - Input boxes are not focused when matches are highlighted. + backspace. [b1] + - Supports reverse incremental search. [b1] + - Input boxes are not focused when matches are highlighted. [b1] - Crude regular expression search is supported. [b8] + - New searches now start within the current viewport where possible. [b8] + • Text editing improvements, including: + - Added t_gu and t_gU. [b8] + - Added operator modes and proper first class motion maps. [b8] + - Improved undo support for most mappings. [b8] + • General completion improvements + - Greatly improved completion rendering performance, especially + while scrolling. [b8] + - Added c_, c_, c_, and c_ for scrolling + the completion list in increments larger than one line. [b8] + - Improved handling of asynchronous completions, including: [b8] + + Pressing after tabbing past the end of already received + completions will execute the command after the desired result has + arrived. + + Tabbing past the end of available completions more reliably selects + the desired completion when it is available. + + Late arriving completion results no longer interfere with typing. + + It is now possible to skip past the end of incomplete completion + groups via the c_ and c_ keys. + - JavaScript completion improvements, including: [b2] + + The prototype of the function whose arguments are currently + being typed is displayed during completion. + + Non-enumerable global properties are now completed for the + global object, including XMLHttpRequest and encodeURI. + - The concept of completion contexts is now exposed to the user + (see :contexts), allowing for powerful and fine-grained + completion system customization. [b1] • Ex command parsing improvements, including: - Multiple Ex commands may now be separated by | [b1] - Commands can continue over multiple lines in RC files by @@ -61,15 +88,6 @@ - Added ;a and ;S modes for creating bookmarks and search keywords. [b4][b3] - Added 'hintkeys' option. [b2] - Added "transliterated" option to 'hintmatching'. [b1] - • General completion improvements - - JavaScript completion improvements, including: [b2] - + The prototype of the function whose arguments are currently - being typed is displayed during completion. - + Non-enumerable global properties are now completed for the - global object, including XMLHttpRequest and encodeURI. - - The concept of completion contexts is now exposed to the user - (see :contexts), allowing for powerful and fine-grained - completion system customization. [b1] • The external editor can now be configured to open to a given line number and column, used for opening source links and editing input fields with i_. See :h 'editor'. [b4]