diff --git a/common/content/commandline.js b/common/content/commandline.js index ccb83125..3f8940bb 100644 --- a/common/content/commandline.js +++ b/common/content/commandline.js @@ -1759,8 +1759,7 @@ var ItemList = Class("ItemList", { set visible(val) this.container.collapsed = !val, get activeGroups() this.context.contextList - .filter(function (c) c.message || c.incomplete - || c.hasItems && c.items.length) + .filter(function (c) c.message || c.incomplete || c.items.length) .map(this.getGroup, this), get selected() let (g = this.selectedGroup) g && g.selectedIdx != null && diff --git a/common/content/dactyl.js b/common/content/dactyl.js index 4b64f2d7..f291aa17 100644 --- a/common/content/dactyl.js +++ b/common/content/dactyl.js @@ -339,16 +339,20 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), { * This is same as Firefox's readFromClipboard function, but is needed for * apps like Thunderbird which do not provide it. * + * @param {string} which Which clipboard to write to. Either + * "global" or "selection". If not provided, both clipboards are + * updated. + * @optional * @returns {string} */ - clipboardRead: function clipboardRead(getClipboard) { + clipboardRead: function clipboardRead(which) { try { const { clipboard } = services; let transferable = services.Transferable(); transferable.addDataFlavor("text/unicode"); - let source = clipboard[getClipboard || !clipboard.supportsSelectionClipboard() ? + let source = clipboard[which == "global" || !clipboard.supportsSelectionClipboard() ? "kGlobalClipboard" : "kSelectionClipboard"]; clipboard.getData(transferable, source); @@ -375,7 +379,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), { * @optional */ clipboardWrite: function clipboardWrite(str, verbose, which) { - if (!which) + if (which == null) services.clipboardHelper.copyString(str); else if (which == "selection" && !services.clipboard.supportsSelectionClipboard()) return; diff --git a/common/content/editor.js b/common/content/editor.js index 28c19af1..55553ae2 100644 --- a/common/content/editor.js +++ b/common/content/editor.js @@ -11,7 +11,7 @@ // 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; @@ -26,6 +26,97 @@ var Editor = Module("editor", { }); }, + signals: { + "mappings.willExecute": function mappings_willExecute(map) { + if (this.currentRegister && !(this._currentMap && this._wait == util.yielders)) { + this._currentMap = map; + this._wait = util.yielders; + } + }, + "mappings.executed": function mappings_executed(map) { + if (this._currentMap == map) { + this.currentRegister = null; + this._wait = util.yielders; + this._currentMap = null; + } + } + }, + + get registers() storage.newMap("registers", { privateData: true, store: true }), + get registerRing() storage.newArray("register-ring", { privateData: true, store: true }), + + // Fixme: Move off this object. + currentRegister: null, + + 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) { + 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 }; + + if (name == '"') + name = 0; + if (name == "_") + ; + else if (Set.has(this.selectionRegisters, name)) + dactyl.clipboardWrite(value.text, false, 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, @@ -72,30 +163,44 @@ var Editor = Module("editor", { this.editor.setShouldTxnSetSelection(!val); }, - pasteClipboard: function pasteClipboard(clipboard) { - let elem = this.element; + copy: function copy(range, name) { + range = range || this.selection; - let text = dactyl.clipboardRead(clipboard); + if (!range.collapsed) + this.setRegister(name, range); + }, + + cut: function cut(range, name) { + if (range) + this.selectedRange = range; + + if (!this.selection.isCollapsed) + this.setRegister(name, this.selection); + + this.editor.deleteSelection(0); + }, + + paste: function paste(name) { + let text = this.getRegister(name); dactyl.assert(text && this.editor instanceof Ci.nsIPlaintextEditor); this.editor.insertText(text); }, // count is optional, defaults to 1 - executeCommand: function (cmd, count) { - let controller = this.getController(cmd); - 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; - if (!callable(cmd)) - cmd = bind("doCommand", controller, cmd); - let didCommand = false; while (count--) { // some commands need this try/catch workaround, because a cmd_charPrevious triggered @@ -363,7 +468,21 @@ var Editor = Module("editor", { range.setStart(range.startContainer, range.endOffset - abbrev.lhs.length); this.mungeRange(range, function () abbrev.expand(this.element), true); } - } + }, + + // nsIEditActionListener: + WillDeleteNode: util.wrapCallback(function WillDeleteNode(node) { + if (node.textContent) + this.setRegister(0, node); + }), + WillDeleteSelection: util.wrapCallback(function WillDeleteSelection(selection) { + if (!selection.isCollapsed) + this.setRegister(0, selection); + }), + WillDeleteText: util.wrapCallback(function WillDeleteText(node, start, length) { + if (length) + this.setRegister(0, node.textContent.substr(start, length)); + }) }, { TextsIterator: Class("TextsIterator", { init: function init(range, context, after) { @@ -575,6 +694,38 @@ var Editor = Module("editor", { 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 }; + + context.match = function (r) !this.filter || ~this.filter.indexOf(r); + + 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() { Map.types["editor"] = { @@ -676,9 +827,9 @@ var Editor = Module("editor", { editor.executeCommand("cmd_selectLineNext"); } - function updateRange(editor, forward, re, modify) { + function updateRange(editor, forward, re, modify, sameWord) { let range = Editor.extendRange(editor.selection.getRangeAt(0), - forward, re, false, editor.rootElement); + forward, re, sameWord, editor.rootElement); modify(range); editor.selection.removeAllRanges(); editor.selection.addRange(range); @@ -694,10 +845,11 @@ var Editor = Module("editor", { parent.input(); } - function move(forward, re) + function move(forward, re, sameWord) function _move(editor) { updateRange(editor, forward, re, - function (range) { range.collapse(!forward); }); + function (range) { range.collapse(!forward); }, + sameWord); } function select(forward, re) function _select(editor) { @@ -706,7 +858,7 @@ var Editor = Module("editor", { } function beginLine(editor_) { editor.executeCommand("cmd_beginLine"); - move(true, /\S/)(editor_); + move(true, /\s/, true)(editor_); } // COUNT CARET TEXT_EDIT VISUAL_TEXT_EDIT @@ -769,6 +921,7 @@ var Editor = Module("editor", { function ({ command, count, motion }) { let start = editor.selectedRange.cloneRange(); + editor._currentMap = null; modes.push(modes.OPERATOR, null, { forCommand: command, @@ -786,6 +939,7 @@ var Editor = Module("editor", { doTxn(range, editor); }); + editor.currentRegister = null; modes.delay(function () { if (mode) modes.push(mode); @@ -804,9 +958,9 @@ var Editor = Module("editor", { { count: true, type: "motion" }); } - 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(String(range)) }); + 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); }); addMotionMap(["gu"], "Lowercase text", false, function (editor, range) { @@ -822,13 +976,13 @@ var Editor = Module("editor", { ["c", "d", "y"], "Select the entire line", function ({ command, count }) { dactyl.assert(command == modes.getStack(0).params.forCommand); - editor.executeCommand("cmd_beginLine", 1); - editor.executeCommand("cmd_selectLineNext", count || 1); - let range = editor.selectedRange; - if (command == "c" && !range.collapsed) // Hack. - if (range.endContainer instanceof Text && - range.endContainer.textContent[range.endOffset - 1] == "\n") - editor.executeCommand("cmd_selectCharPrevious", 1); + + let sel = editor.selection; + sel.modify("move", "backward", "lineboundary"); + sel.modify("extend", "forward", "lineboundary"); + + if (command != "c") + sel.modify("extend", "forward", "character"); }, { count: true, type: "operator" }); @@ -875,7 +1029,7 @@ var Editor = Module("editor", { function () { editor.executeCommand("cmd_deleteCharForward", 1); }); bind([""], "Insert clipboard/selection", - function () { editor.pasteClipboard(); }); + function () { editor.paste(); }); mappings.add([modes.INPUT], [""], "Edit text field with an external editor", @@ -1005,11 +1159,25 @@ var Editor = Module("editor", { 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 }); + mappings.add([modes.TEXT_EDIT, modes.VISUAL], + ['"'], "Bind a register to the next command", + function ({ arg }) { + editor.currentRegister = arg; + }, + { arg: true }); + + mappings.add([modes.INPUT], + ["", ''], "Bind a register to the next command", + function ({ arg }) { + editor.currentRegister = arg; + }, + { arg: true }); + let bind = function bind(names, description, action, params) mappings.add([modes.TEXT_EDIT, modes.OPERATOR, modes.VISUAL], names, description, diff --git a/common/content/events.js b/common/content/events.js index 3bba8ef8..1b655253 100644 --- a/common/content/events.js +++ b/common/content/events.js @@ -191,6 +191,13 @@ var Events = Module("events", { this.listen(window, this.popups, "events", true); }, + cleanup: function cleanup() { + let elem = dactyl.focusedElement; + if (DOM(elem).isEditable) + util.trapErrors("removeEditActionListener", + DOM(elem).editor, editor); + }, + signals: { "browser.locationChange": function (webProgress, request, uri) { options.get("passkeys").flush(); @@ -572,6 +579,10 @@ var Events = Module("events", { events: { blur: function onBlur(event) { let elem = event.originalTarget; + if (DOM(elem).isEditable) + util.trapErrors("removeEditActionListener", + DOM(elem).editor, editor); + if (elem instanceof Window && services.focus.activeWindow == null && document.commandDispatcher.focusedWindow !== window) { // Deals with circumstances where, after the main window @@ -592,6 +603,9 @@ var Events = Module("events", { // TODO: Merge with onFocusChange focus: function onFocus(event) { let elem = event.originalTarget; + if (DOM(elem).isEditable) + util.trapErrors("addEditActionListener", + DOM(elem).editor, editor); if (elem == window) overlay.activeWindow = window; diff --git a/common/content/mappings.js b/common/content/mappings.js index bc253656..f1293e3f 100644 --- a/common/content/mappings.js +++ b/common/content/mappings.js @@ -134,6 +134,7 @@ var Map = Class("Map", { false); try { + dactyl.triggerObserver("mappings.willExecute", this, args); this.preExecute(args); this.executing = true; var res = repeat(); diff --git a/common/modules/completion.jsm b/common/modules/completion.jsm index f19f9406..4dea7371 100644 --- a/common/modules/completion.jsm +++ b/common/modules/completion.jsm @@ -6,8 +6,6 @@ // given in the LICENSE.txt file included with this file. "use strict"; -try { - Components.utils.import("resource://dactyl/bootstrap.jsm"); defineModule("completion", { exports: ["CompletionContext", "Completion", "completion"] @@ -221,7 +219,7 @@ var CompletionContext = Class("CompletionContext", { }, get title() this.__title, - get activeContexts() this.contextList.filter(function (c) c.hasItems && c.items.length), + get activeContexts() this.contextList.filter(function (c) c.items.length), // Temporary /** @@ -356,6 +354,7 @@ var CompletionContext = Class("CompletionContext", { yield ["context", function () self]; yield ["result", quote ? function () quote[0] + util.trapErrors(1, quote, this.text) + quote[2] : function () this.text]; + yield ["texts", function () Array.concat(this.text)]; }; for (let i in iter(this.keys, result(this.quote))) { @@ -862,10 +861,9 @@ var CompletionContext = Class("CompletionContext", { Filter: { text: function (item) { - let text = item.texts || Array.concat(item.text); + let text = item.texts; for (let [i, str] in Iterator(text)) { if (this.match(String(str))) { - item.texts = text; item.text = String(text[i]); return true; } @@ -1063,11 +1061,13 @@ var Completion = Module("completion", { context.title[0] += " " + _("completion.additional"); context.filter = context.parent.filter; // FIXME context.completions = context.parent.completions; + // For items whose URL doesn't exactly match the filter, // accept them if all tokens match either the URL or the title. // Filter out all directly matching strings. let match = context.filters[0]; context.filters[0] = function (item) !match.call(this, item); + // and all that don't match the tokens. let tokens = context.filter.split(/\s+/); context.filters.push(function (item) tokens.every( @@ -1209,6 +1209,6 @@ var Completion = Module("completion", { endModule(); -} catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); } +// catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); } // vim: set fdm=marker sw=4 ts=4 et ft=javascript: diff --git a/common/modules/finder.jsm b/common/modules/finder.jsm index 019b4f33..cffa39d6 100644 --- a/common/modules/finder.jsm +++ b/common/modules/finder.jsm @@ -819,7 +819,7 @@ var RangeFind = Class("RangeFind", { let start = a.compareBoundaryPoints(a.START_TO_START, b) < 0 ? a : b; let end = a.compareBoundaryPoints(a.END_TO_END, b) > 0 ? a : b; let res = start.cloneRange(); - res.setEnd(end.startContainer, end.endOffset); + res.setEnd(end.endContainer, end.endOffset); return res; } }); diff --git a/common/modules/storage.jsm b/common/modules/storage.jsm index 803cb00a..8604d2e6 100644 --- a/common/modules/storage.jsm +++ b/common/modules/storage.jsm @@ -93,12 +93,32 @@ var ArrayStore = Class("ArrayStore", StoreBase, { this.fireEvent("push", this._object.length); }, - pop: function pop(value) { - var res = this._object.pop(); - this.fireEvent("pop", this._object.length); + pop: function pop(value, ord) { + if (ord == null) + var res = this._object.pop(); + else + res = this._object.splice(ord, 1)[0]; + + this.fireEvent("pop", this._object.length, ord); return res; }, + shift: function shift(value) { + var res = this._object.shift(); + this.fireEvent("shift", this._object.length); + return res; + }, + + insert: function insert(value, ord) { + if (ord == 0) + this._object.unshift(value); + else + this._object = this._object.slice(0, ord) + .concat([value]) + .concat(this._object.slice(ord)); + this.fireEvent("insert", this._object.length, ord); + }, + truncate: function truncate(length, fromEnd) { var res = this._object.length; if (this._object.length > length) { diff --git a/pentadactyl/NEWS b/pentadactyl/NEWS index 4e67b692..4c2129a3 100644 --- a/pentadactyl/NEWS +++ b/pentadactyl/NEWS @@ -45,6 +45,8 @@ • Text editing improvements, including: - Added t_gu, t_gU, and v_o mappings. [b8] - Added o_c, o_d, and o_y mappings. [b8] + - Added register and basic kill ring support, t_" and I_ + mappings, and :registers command. [b8] - Added operator modes and proper first class motion maps. [b8] - Improved undo support for most mappings. [b8] • General completion improvements diff --git a/pentadactyl/TODO b/pentadactyl/TODO index 22ca8c1a..af56b4f1 100644 --- a/pentadactyl/TODO +++ b/pentadactyl/TODO @@ -13,15 +13,12 @@ BUGS: FEATURES: 9 Add more tests. -9 / should work as in Vim (i.e., save page positions as well as - locations in the history list). 9 clean up error message codes and document 9 option groups 9 global, window-local, tab-local, buffer-local, script-local groups 9 add [count] support to :b* and :tab* commands where missing 8 wherever possible: get rid of dialogs and ask console-like dialog questions or write error prompts directly on the webpage or with :echo() -8 registers 8 add support for filename special characters such as % 8 :redir and 'verbosefile' 8 Add information to dactyl/HACKING file about testing and optimization