diff --git a/common/content/abbreviations.js b/common/content/abbreviations.js new file mode 100644 index 00000000..d41ccaff --- /dev/null +++ b/common/content/abbreviations.js @@ -0,0 +1,300 @@ +// Copyright (c) 2006-2009 by Martin Stubenschrott +// Copyright (c) 2010 by anekos +// Copyright (c) 2010 by Kris Maglion +// +// This work is licensed for reuse under an MIT license. Details are +// given in the License.txt file included with this file. + + +/** @scope modules */ + +const Abbreviation = Class("Abbreviation", { + init: function (modes, lhs, rhs) { + this.modes = modes.sort(); + this.lhs = lhs; + this.rhs = rhs; + }, + + equals: function (other) { + return this.lhs == other.lhs && this.rhs == other.rhs; + }, + + expand: function (editor) String(callable(this.rhs) ? this.rhs(editor) : this.rhs), + + modesEqual: function (modes) array.equals(this.modes, modes), + + inMode: function (mode) this.modes.some(function (_mode) _mode == mode), + + inModes: function (modes) modes.some(function (mode) this.inMode(mode), this), + + removeMode: function (mode) { + this.modes = this.modes.filter(function (m) m != mode).sort(); + }, + + get modeChar() Abbreviation.modeChar(this.modes) +}, { + modeChar: function (_modes) { + let result = ""; + for ([, mode] in Iterator(_modes)) + result += modes.getMode(mode).char; + if (/^(ic|ci)$/(result)) + result = "!"; + return result; + } +}); + +const Abbreviations = Module("abbreviations", { + init: function () { + this.abbrevs = {}; + + // (summarized from Vim's ":help abbreviations") + // + // There are three types of abbreviations. + // + // full-id: Consists entirely of keyword characters. + // ("foo", "g3", "-1") + // + // end-id: Ends in a keyword character, but all other + // are not keyword characters. + // ("#i", "..f", "$/7") + // + // non-id: Ends in a non-keyword character, but the + // others can be of any type other than space + // and tab. + // ("def#", "4/7$") + // + // Example strings that cannot be abbreviations: + // "a.b", "#def", "a b", "_$r" + // + // For now, a keyword character is anything except for \s, ", or ' + // (i.e., whitespace and quotes). In Vim, a keyword character is + // specified by the 'iskeyword' setting and is a much less inclusive + // list. + // + // TODO: Make keyword definition closer to Vim's default keyword + // definition (which differs across platforms). + + let nonkw = "\\s\"'"; + let keyword = "[^" + nonkw + "]"; + let nonkeyword = "[" + nonkw + "]"; + + let fullId = keyword + "+"; + let endId = nonkeyword + "+" + keyword; + let nonId = "\\S*" + nonkeyword; + + // Used in add and expand + this._match = fullId + "|" + endId + "|" + nonId; + }, + + /** + * Adds a new abbreviation. + * + * @param {Abbreviation} + */ + add: function (abbr) { + if (!(abbr instanceof Abbreviation)) + abbr = Abbreviation.apply(null, arguments); + + for (let [, mode] in Iterator(abbr.modes)) { + if (!this.abbrevs[mode]) + this.abbrevs[mode] = {}; + this.abbrevs[mode][abbr.lhs] = abbr; + } + }, + + /** + * Returns matched abbreviation. + * + * @param {mode} + * @param {string} + */ + get: function (mode, lhs) { + let abbrevs = this.abbrevs[mode]; + return abbrevs && abbrevs[lhs]; + }, + + /** + * Returns the abbreviation that matches the given text. + * + * @returns {Abbreviation} + */ + match: function (mode, text) { + let match = text.match(RegExp('(' + abbreviations._match + ')$')); + if (match) + return abbreviations.get(mode, match[0]); + return null; + }, + + /** + * The list of the abbreviations merged from each modes. + */ + get merged() { + let result = []; + let lhses = []; + let modes = [mode for (mode in this.abbrevs)]; + + for (let [, mabbrevs] in Iterator(this.abbrevs)) + lhses = lhses.concat([key for (key in mabbrevs)]); + lhses.sort(); + lhses = util.Array.uniq(lhses); + + for (let [, lhs] in Iterator(lhses)) { + let exists = {}; + for (let [, mabbrevs] in Iterator(this.abbrevs)) { + let abbr = mabbrevs[lhs]; + if (abbr && !exists[abbr.rhs]) { + exists[abbr.rhs] = 1; + result.push(abbr); + } + } + } + + return result; + }, + + /** + * Lists all abbreviations matching modes and lhs. + * + * @param {Array} list of mode. + * @param {string} lhs The LHS of the abbreviation. + */ + list: function (modes, lhs) { + let list = this.merged.filter(function (abbr) (abbr.inModes(modes) && abbr.lhs.indexOf(lhs) == 0)); + + if (!list.length) + dactyl.echomsg("No abbreviations found"); + else if (list.length == 1) { + let head = list[0]; + dactyl.echo(head.modeChar + " " + head.lhs + " " + head.rhs, commandline.FORCE_SINGLELINE); // 2 spaces, 3 spaces + } + else { + list = list.map(function (abbr) [abbr.modeChar, abbr.lhs, abbr.rhs]); + list = template.tabular(["", "LHS", "RHS"], [], list); + commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); + } + }, + + /** + * Remove the specified abbreviations. + * + * @param {Array} list of mode. + * @param {string} lhs of abbreviation. + * @returns {boolean} did the deleted abbreviation exist? + */ + remove: function (modes, lhs) { + let result = false; + for (let [, mode] in Iterator(modes)) { + if ((mode in this.abbrevs) && (lhs in this.abbrevs[mode])) { + result = true; + this.abbrevs[mode][lhs].removeMode(mode); + delete this.abbrevs[mode][lhs]; + } + } + return result; + }, + + /** + * Removes all abbreviations specified modes. + * + * @param {Array} list of mode. + */ + removeAll: function (modes) { + for (let [, mode] in modes) { + if (!(mode in this.abbrevs)) + return; + for (let [, abbr] in this.abbrevs[mode]) + abbr.removeMode(mode); + delete this.abbrevs[mode]; + } + } +}, { +}, { + completion: function () { + // TODO: shouldn't all of these have a standard signature (context, args, ...)? --djk + completion.abbreviation = function abbreviation(context, args, modes) { + if (args.completeArg == 0) { + let abbrevs = abbreviations.merged.filter(function (abbr) abbr.inModes(modes)); + context.completions = [[abbr.lhs, abbr.rhs] for ([, abbr] in Iterator(abbrevs))]; + } + }; + }, + + commands: function () { + function addAbbreviationCommands(modes, ch, modeDescription) { + modes.sort(); + modeDescription = modeDescription ? " in " + modeDescription + " mode" : ""; + + // Why? --Kris + function splitAbbrev(abbrev) abbrev.match(RegExp("^(\\s*)($|" + abbreviations._match + ")(?:\\s*$|(\\s+)(.*))")) || []; + + commands.add([ch ? ch + "a[bbrev]" : "ab[breviate]"], + "Abbreviate a key sequence" + modeDescription, + function (args) { + let [,, lhs,, rhs] = splitAbbrev(args[0]); + dactyl.assert(lhs, "E474: Invalid argument"); + + if (rhs) { + if (args["-javascript"]) { + let expr = rhs; + rhs = dactyl.userfunc("editor", expr); + rhs.toString = function () expr; + } + abbreviations.add(modes, lhs, rhs); + } + else { + abbreviations.list(modes, lhs || ""); + } + }, { + options: [ + { + names: ["-javascript", "-js", "-j"], + description: "Expand this abbreviation by evaluating its right-hand-side as JavaScript" + } + ], + completer: function (context, args) { + let [, sp1, lhs, sp2, rhs] = splitAbbrev(args[0]); + if (rhs == null) + return completion.abbreviation(context, args, modes) + context.advance((sp1 + lhs + sp2).length); + if (args["-javascript"]) + return completion.javascript(context); + }, + literal: 0, + serial: function () [ { + command: this.name, + arguments: [abbr.lhs], + literalArg: abbr.rhs, + options: callable(abbr.rhs) ? {"-javascript": null} : {} + } + for ([, abbr] in Iterator(abbreviations.merged)) + if (abbr.modesEqual(modes)) + ] + }); + + commands.add([ch ? ch + "una[bbrev]" : "una[bbreviate]"], + "Remove an abbreviation" + modeDescription, + function (args) { + let lhs = args.literalArg; + if (!lhs) + return dactyl.echoerr("E474: Invalid argument"); + if (!abbreviations.remove(modes, lhs)) + return dactyl.echoerr("E24: No such abbreviation"); + }, { + argCount: "1", + completer: function (context, args) completion.abbreviation(context, args, modes), + literal: 0 + }); + + commands.add([ch + "abc[lear]"], + "Remove all abbreviations" + modeDescription, + function () { abbreviations.removeAll(modes); }, + { argCount: "0" }); + } + + addAbbreviationCommands([modes.INSERT, modes.COMMAND_LINE], "", ""); + addAbbreviationCommands([modes.INSERT], "i", "insert"); + addAbbreviationCommands([modes.COMMAND_LINE], "c", "command line"); + } +}); + +// vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/commandline.js b/common/content/commandline.js index c5391df3..686d0590 100644 --- a/common/content/commandline.js +++ b/common/content/commandline.js @@ -1487,13 +1487,13 @@ const CommandLine = Module("commandline", { ["", '"', "'"], "Expand command line abbreviation", function () { commandline.resetCompletions(); - return editor.expandAbbreviation("c"); + return editor.expandAbbreviation(modes.COMMAND_LINE); }, { route: true }); mappings.add(myModes, ["", ""], "Expand command line abbreviation", - function () { editor.expandAbbreviation("c"); }); + function () { editor.expandAbbreviation(modes.COMMAND_LINE); }); mappings.add([modes.NORMAL], ["g<"], "Redisplay the last command output", diff --git a/common/content/dactyl-overlay.js b/common/content/dactyl-overlay.js index 219da375..c9e966f8 100644 --- a/common/content/dactyl-overlay.js +++ b/common/content/dactyl-overlay.js @@ -42,6 +42,7 @@ "modules", "storage", "util", + "abbreviations", "autocommands", "buffer", "commandline", diff --git a/common/content/editor.js b/common/content/editor.js index e43c8bfc..23c3470f 100644 --- a/common/content/editor.js +++ b/common/content/editor.js @@ -16,56 +16,6 @@ const Editor = Module("editor", { // this._lastFindChar = null; this._lastFindCharFunc = null; - // XXX: this strikes me as a rather odd ds; everyone's a critic --djk - this._abbreviations = {}; // this._abbreviations["lhr"][0]["{i,c,!}","rhs"] - - // (summarized from Vim's ":help this._abbreviations") - // - // There are three types of this._abbreviations: - // - // full-id: Consists entirely of keyword characters. - // ("foo", "g3", "-1") - // - // end-id: Ends in a keyword character, but all other - // are not keyword characters. - // ("#i", "..f", "$/7") - // - // non-id: Ends in a non-keyword character, but the - // others can be of any type other than space - // and tab. - // ("def#", "4/7$") - // - // Example strings that cannot be this._abbreviations: - // "a.b", "#def", "a b", "_$r" - // - // For now, a keyword character is anything except for \s, ", or ' - // (i.e., whitespace and quotes). In Vim, a keyword character is - // specified by the 'iskeyword' setting and is a much less inclusive - // list. - // - // TODO: Make keyword definition closer to Vim's default keyword - // definition (which differs across platforms). - // - - let nonkw = "\\s\"'"; - let keyword = "[^" + nonkw + "]"; - let nonkeyword = "[" + nonkw + "]"; - - let fullId = keyword + "+"; - let endId = nonkeyword + "+" + keyword; - let nonId = "\\S*" + nonkeyword; - - // Used in addAbbrevation and expandAbbreviation - this._abbrevmatch = fullId + "|" + endId + "|" + nonId; - - }, - - // For the record, some of this code I've just finished throwing - // away makes me want to pull someone else's hair out. --Kris - abbrevs: function () { - for (let [lhs, abbr] in Iterator(this._abbreviations)) - for (let [, rhs] in Iterator(abbr)) - yield [lhs, rhs]; }, line: function () { @@ -416,225 +366,31 @@ const Editor = Module("editor", { return; }, - // Abbreviations {{{ - - // NOTE: I think this comment block is trying to say something but no - // one is listening. In space, no one can hear you scream. --djk - // - // System for adding this._abbreviations: - // - // filter == ! delete all, and set first (END) - // - // if filter == ! remove all and add it as only END - // - // variant 1: rhs matches anywhere in loop - // - // 1 mod matches anywhere in loop - // a) simple replace and - // I) (maybe there's another rhs that matches? not possible) - // (when there's another item, it's opposite mod with different rhs) - // (so do nothing further but END) - // - // 2 mod does not match - // a) the opposite is there -> make a ! and put it as only and END - // (b) a ! is there. do nothing END) - // - // variant 2: rhs matches *no*were in loop and filter is c or i - // every kind of current combo is possible to 1 {c,i,!} or two {c and i} - // - // 1 mod is ! split into two i + c END - // 1 not !: opposite mode (first), add/change 'second' and END - // 1 not !: same mode (first), overwrite first this END - // - // TODO: I don't like these funky filters, I am a funky filter hater. --djk - // : make this a separate object - // : use Struct for individual this._abbreviations - // : rename "filter" arg "mode" - /** - * Adds a new abbreviation. Abbreviations consist of a LHS (the text - * that is replaced when the abbreviation is expanded) and a RHS (the - * replacement text). - * - * @param {string} filter The mode filter. This specifies the modes in - * which this abbreviation is available. Either: - * "c" - applies in command-line mode - * "i" - applies in insert mode - * "!" - applies in both command-line and insert modes - * @param {string} lhs The LHS of the abbreviation. - * @param {string} rhs The RHS of the abbreviation. - */ - addAbbreviation: function (filter, lhs, rhs) { - if (!this._abbreviations[lhs]) { - this._abbreviations[lhs] = []; - this._abbreviations[lhs][0] = [filter, rhs]; - return; - } - - if (filter == "!") { - if (this._abbreviations[lhs][1]) - this._abbreviations[lhs][1] = ""; - this._abbreviations[lhs][0] = [filter, rhs]; - return; - } - - for (let i = 0; i < this._abbreviations[lhs].length; i++) { - if (this._abbreviations[lhs][i][1] == rhs) { - if (this._abbreviations[lhs][i][0] == filter) { - this._abbreviations[lhs][i] = [filter, rhs]; - return; - } - else { - if (this._abbreviations[lhs][i][0] != "!") { - if (this._abbreviations[lhs][1]) - this._abbreviations[lhs][1] = ""; - this._abbreviations[lhs][0] = ["!", rhs]; - return; - } - else - return; - } - } - } - - if (this._abbreviations[lhs][0][0] == "!") { - let tmpOpp = ("i" == filter) ? "c" : "i"; - this._abbreviations[lhs][1] = [tmpOpp, this._abbreviations[lhs][0][1]]; - this._abbreviations[lhs][0] = [filter, rhs]; - return; - } - - if (this._abbreviations[lhs][0][0] != filter) - this._abbreviations[lhs][1] = [filter, rhs]; - else - this._abbreviations[lhs][0] = [filter, rhs]; - }, - /** * Expands an abbreviation in the currently active textbox. * - * @param {string} filter The mode filter. + * @param {string} mode The mode filter. * @see #addAbbreviation */ - expandAbbreviation: function (filter) { + expandAbbreviation: function (mode) { let textbox = Editor.getEditor(); if (!textbox) return false; let text = textbox.value; let currStart = textbox.selectionStart; let currEnd = textbox.selectionEnd; - let foundWord = text.substring(0, currStart).replace(RegExp("^(.|\\n)*?\\s*(" + this._abbrevmatch + ")$", "m"), "$2"); // get last word \b word boundary - if (!foundWord) - return true; - - for (let lhs in this._abbreviations) { - for (let i = 0; i < this._abbreviations[lhs].length; i++) { - if (lhs == foundWord && (this._abbreviations[lhs][i][0] == filter || this._abbreviations[lhs][i][0] == "!")) { - // if found, replace accordingly - let len = foundWord.length; - let abbrText = this._abbreviations[lhs][i][1]; - text = text.substring(0, currStart - len) + abbrText + text.substring(currStart); - textbox.value = text; - textbox.selectionStart = currStart - len + abbrText.length; - textbox.selectionEnd = currEnd - len + abbrText.length; - break; - } - } + let abbrev = abbreviations.match(mode, text.substring(0, currStart).replace(/.*\s/g, "")); + if (abbrev) { + let len = abbrev.lhs.length; + let abbrText = abbrev.expand(textbox); + text = text.substring(0, currStart - len) + abbrText + text.substring(currStart); + textbox.value = text; + textbox.selectionStart = currStart - len + abbrText.length; + textbox.selectionEnd = currEnd - len + abbrText.length; } + return true; }, - - /** - * Returns all abbreviations matching filter and lhs. - * - * @param {string} filter The mode filter. - * @param {string} lhs The LHS of the abbreviation. - * @returns {Array} The matching abbreviations [mode, lhs, rhs] - * @see #addAbbreviation - */ - getAbbreviations: function (filter, lhs) { - // ! -> list all, on c or i ! matches too - let searchFilter = (filter == "!") ? "!ci" : filter + "!"; - return [[mode, left, right] for ([left, [mode, right]] in this.abbrevs()) - if (searchFilter.indexOf(mode) >= 0 && left.indexOf(lhs || "") == 0)]; - }, - - /** - * Lists all abbreviations matching filter and lhs. - * - * @param {string} filter The mode filter. - * @param {string} lhs The LHS of the abbreviation. - * @see #addAbbreviation - */ - listAbbreviations: function (filter, lhs) { - let list = this.getAbbreviations(filter, lhs); - - if (!list.length) - dactyl.echomsg("No abbreviations found"); - else if (list.length == 1) { - let [mode, lhs, rhs] = list[0]; - - dactyl.echo(mode + " " + lhs + " " + rhs, commandline.FORCE_SINGLELINE); // 2 spaces, 3 spaces - } - else - commandline.commandOutput(template.tabular(["", "LHS", "RHS"], [], list)); - }, - - /** - * Deletes all abbreviations matching filter and lhs. - * - * @param {string} filter The mode filter. - * @param {string} lhs The LHS of the abbreviation. - * @see #addAbbreviation - */ - removeAbbreviation: function (filter, lhs) { - if (!lhs) { - dactyl.echoerr("E474: Invalid argument"); - return false; - } - - if (this._abbreviations[lhs]) { // this._abbreviations exists - if (filter == "!") { - this._abbreviations[lhs] = ""; - return true; - } - else { - if (!this._abbreviations[lhs][1]) { // only one exists - if (this._abbreviations[lhs][0][0] == "!") { // exists as ! -> no 'full' delete - this._abbreviations[lhs][0][0] = (filter == "i") ? "c" : "i"; // ! - i = c; ! - c = i - return true; - } - else if (this._abbreviations[lhs][0][0] == filter) { - this._abbreviations[lhs] = ""; - return true; - } - } - else { // two this._abbreviations exist ( 'i' or 'c' (filter as well)) - if (this._abbreviations[lhs][0][0] == "c" && filter == "c") - this._abbreviations[lhs][0] = this._abbreviations[lhs][1]; - - this._abbreviations[lhs][1] = ""; - - return true; - } - } - } - - dactyl.echoerr("E24: No such abbreviation"); - return false; - }, - - /** - * Removes all abbreviations matching filter. - * - * @param {string} filter The mode filter. - * @see #addAbbreviation - */ - removeAllAbbreviations: function (filter) { - let searchFilter = (filter == "!") ? "!ci" : filter + "!"; - for (let [lhs, [mode, rhs]] in this.abbrevs()) - if (searchFilter.indexOf(mode) >= 0) - this.removeAbbreviation(filter, lhs); - } }, { getEditor: function () dactyl.focus, @@ -646,67 +402,6 @@ const Editor = Module("editor", { return ed.controllers.getControllerForCommand("cmd_beginLine"); } }, { - commands: function () { - // mode = "i" -> add :iabbrev, :iabclear and :iunabbrev commands - function addAbbreviationCommands(ch, modeDescription) { - let mode = ch || "!"; - modeDescription = modeDescription ? " in " + modeDescription + " mode" : ""; - - commands.add([ch ? ch + "a[bbrev]" : "ab[breviate]"], - "Abbreviate a key sequence" + modeDescription, - function (args) { - let matches = args.string.match(RegExp("^\\s*($|" + editor._abbrevmatch + ")(?:\\s*$|\\s+(.*))")); - dactyl.assert(matches, "E474: Invalid argument"); - - let [, lhs, rhs] = matches; - if (rhs) - editor.addAbbreviation(mode, lhs, rhs); - else - editor.listAbbreviations(mode, lhs || ""); - }, { - completer: function (context, args) completion.abbreviation(context, args, mode), - literal: 0, - serialize: function () [ { - command: this.name, - arguments: [lhs], - literalArg: abbr[1] - } - for ([lhs, abbr] in editor.abbrevs()) - if (abbr[0] == mode) - ] - }); - - commands.add([ch ? ch + "una[bbrev]" : "una[bbreviate]"], - "Remove an abbreviation" + modeDescription, - function (args) { editor.removeAbbreviation(mode, args.literalArg); }, { - argCount: "1", - completer: function (context, args) completion.abbreviation(context, args, mode), - literal: 0 - }); - - commands.add([ch + "abc[lear]"], - "Remove all this._abbreviations" + modeDescription, - function () { editor.removeAllAbbreviations(mode); }, - { argCount: "0" }); - } - - addAbbreviationCommands("", ""); - addAbbreviationCommands("i", "insert"); - addAbbreviationCommands("c", "command line"); - }, - - completion: function () { - // TODO: shouldn't all of these have a standard signature (context, args, ...)? --djk - completion.abbreviation = function abbreviation(context, args, mode) { - mode = mode || "!"; - - if (args.completeArg == 0) { - let abbreviations = editor.getAbbreviations(mode); - context.completions = [[lhs, ""] for ([, [, lhs,]] in Iterator(abbreviations))]; - } - }; - }, - mappings: function () { var myModes = [modes.INSERT, modes.COMMAND_LINE]; @@ -866,16 +561,16 @@ const Editor = Module("editor", { mappings.add([modes.INSERT], ["", ""], "Expand insert mode abbreviation", - function () { editor.expandAbbreviation("i"); }, + function () { editor.expandAbbreviation(modes.INSERT); }, { route: true }); mappings.add([modes.INSERT], [""], "Expand insert mode abbreviation", - function () { editor.expandAbbreviation("i"); document.commandDispatcher.advanceFocus(); }); + function () { editor.expandAbbreviation(modes.INSERT); document.commandDispatcher.advanceFocus(); }); mappings.add([modes.INSERT], ["", ""], "Expand insert mode abbreviation", - function () { editor.expandAbbreviation("i"); }); + function () { editor.expandAbbreviation(modes.INSERT); }); // textarea mode mappings.add([modes.TEXTAREA], diff --git a/common/locale/en-US/map.xml b/common/locale/en-US/map.xml index 13754b8b..4f859089 100644 --- a/common/locale/en-US/map.xml +++ b/common/locale/en-US/map.xml @@ -416,6 +416,17 @@ lhs. If no arguments are given, list all abbreviations.

+ +

+ If the -javascript (short names -js, + -j) option is given, lhs is expanded to + the value returned by the JavaScript code + rhs. The code is evaluated with the variable + editor set to the editable element that the + abbreviation is currently being expanded in. The code + should not make any changes to the contents of + the editor. +

diff --git a/pentadactyl/NEWS b/pentadactyl/NEWS index 6ccbdbab..4cf5f3bf 100644 --- a/pentadactyl/NEWS +++ b/pentadactyl/NEWS @@ -28,6 +28,7 @@ * Added ‘transliterated’ option to 'hintmatching' * Added 'autocomplete' option for specifying which completion contexts should be autocompleted + * Added -javascript option to :abbrev and :map * Added several new options to :map * Removed the :source line at the end of files generated by :mkpentadactylrc