1 // Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2010 by anekos <anekos@snca.net>
3 // Copyright (c) 2010-2013 Kris Maglione <maglione.k at Gmail>
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
12 * A user-defined input mode binding of a typed string to an automatically
13 * inserted expansion string.
15 * Abbreviations have a left-hand side (LHS) whose text is replaced by that of
16 * the right-hand side (RHS) when triggered by an Input mode expansion key.
17 * E.g. an abbreviation with a LHS of "gop" and RHS of "Grand Old Party" will
18 * replace the former with the latter.
20 * @param {[Mode]} modes The modes in which this abbreviation is active.
21 * @param {string} lhs The left hand side of the abbreviation; the text to
23 * @param {string|function(nsIEditor):string} rhs The right hand side of
24 * the abbreviation; the replacement text. This may either be a string
25 * literal or a function that will be passed the appropriate nsIEditor.
28 var Abbreviation = Class("Abbreviation", {
29 init: function (modes, lhs, rhs) {
30 this.modes = modes.sort();
36 * Returns true if this abbreviation's LHS and RHS are equal to those in
39 * @param {Abbreviation} other The abbreviation to test.
40 * @returns {boolean} The result of the comparison.
42 equals: function (other) this.lhs == other.lhs && this.rhs == other.rhs,
45 * Returns the abbreviation's expansion text.
47 * @param {nsIEditor} editor The editor in which abbreviation expansion is
51 expand: function (editor) String(callable(this.rhs) ? this.rhs(editor) : this.rhs),
54 * Returns true if this abbreviation is defined for all *modes*.
56 * @param {[Mode]} modes The modes to test.
57 * @returns {boolean} The result of the comparison.
59 modesEqual: function (modes) array.equals(this.modes, modes),
62 * Returns true if this abbreviation is defined for *mode*.
64 * @param {Mode} mode The mode to test.
65 * @returns {boolean} The result of the comparison.
67 inMode: function (mode) this.modes.some(m => m == mode),
70 * Returns true if this abbreviation is defined in any of *modes*.
72 * @param {[Modes]} modes The modes to test.
73 * @returns {boolean} The result of the comparison.
75 inModes: function (modes) modes.some(mode => this.inMode(mode)),
78 * Remove *mode* from the list of supported modes for this abbreviation.
80 * @param {Mode} mode The mode to remove.
82 removeMode: function (mode) {
83 this.modes = this.modes.filter(m => m != mode)
88 * @property {string} The mode display characters associated with the
89 * supported mode combination.
91 get modeChar() Abbreviation.modeChar(this.modes)
93 modeChar: function (_modes) {
94 let result = array.uniq(_modes.map(m => m.char)).join("");
101 var AbbrevHive = Class("AbbrevHive", Contexts.Hive, {
102 init: function init(group) {
103 init.superapply(this, arguments);
107 /** @property {boolean} True if there are no abbreviations. */
108 get empty() !values(this._store).nth(util.identity, 0),
111 * Adds a new abbreviation.
113 * @param {Abbreviation} abbr The abbreviation to add.
115 add: function (abbr) {
116 if (!(abbr instanceof Abbreviation))
117 abbr = Abbreviation.apply(null, arguments);
119 for (let [, mode] in Iterator(abbr.modes)) {
120 if (!this._store[mode])
121 this._store[mode] = {};
122 this._store[mode][abbr.lhs] = abbr;
127 * Returns the abbreviation with *lhs* in the given *mode*.
129 * @param {Mode} mode The mode of the abbreviation.
130 * @param {string} lhs The LHS of the abbreviation.
131 * @returns {Abbreviation} The matching abbreviation.
133 get: function (mode, lhs) {
134 let abbrevs = this._store[mode];
135 return abbrevs && Set.has(abbrevs, lhs) ? abbrevs[lhs] : null;
139 * @property {[Abbreviation]} The list of the abbreviations merged from
144 let map = values(this._store).map(Iterator).map(iter.toArray)
145 .flatten().toObject();
146 return Object.keys(map).sort().map(k => map[k]);
150 * Remove the specified abbreviations.
152 * @param {Array} modes List of modes.
153 * @param {string} lhs The LHS of the abbreviation.
154 * @returns {boolean} Did the deleted abbreviation exist?
156 remove: function (modes, lhs) {
158 for (let [, mode] in Iterator(modes)) {
159 if ((mode in this._store) && (lhs in this._store[mode])) {
161 this._store[mode][lhs].removeMode(mode);
162 delete this._store[mode][lhs];
169 * Removes all abbreviations specified in *modes*.
171 * @param {Array} modes List of modes.
173 clear: function (modes) {
174 for (let mode in values(modes)) {
175 for (let abbr in values(this._store[mode]))
176 abbr.removeMode(mode);
177 delete this._store[mode];
182 var Abbreviations = Module("abbreviations", {
185 // (summarized from Vim's ":help abbreviations")
187 // There are three types of abbreviations.
189 // full-id: Consists entirely of keyword characters.
190 // ("foo", "g3", "-1")
192 // end-id: Ends in a keyword character, but all other
193 // are not keyword characters.
194 // ("#i", "..f", "$/7")
196 // non-id: Ends in a non-keyword character, but the
197 // others can be of any type other than space
201 // Example strings that cannot be abbreviations:
202 // "a.b", "#def", "a b", "_$r"
204 // For now, a keyword character is anything except for \s, ", or '
205 // (i.e., whitespace and quotes). In Vim, a keyword character is
206 // specified by the 'iskeyword' setting and is a much less inclusive
209 // TODO: Make keyword definition closer to Vim's default keyword
210 // definition (which differs across platforms).
212 let params = { // This is most definitely not Vim compatible.
217 this._match = util.regexp(literal(/*
218 (^ | \s | <nonkeyword>) (<keyword>+ )$ | // full-id
219 (^ | \s | <keyword> ) (<nonkeyword>+ <keyword>)$ | // end-id
220 (^ | \s ) (\S* <nonkeyword> )$ // non-id
222 this._check = util.regexp(literal(/*
224 <keyword>+ | // full-id
225 <nonkeyword>+ <keyword> | // end-id
226 \S* <nonkeyword> // non-id
231 get allHives() contexts.allGroups.abbrevs,
233 get userHives() this.allHives.filter(h => h !== this.builtin),
235 get: deprecated("group.abbrevs.get", { get: function get() this.user.closure.get }),
236 set: deprecated("group.abbrevs.set", { get: function set() this.user.closure.set }),
237 remove: deprecated("group.abbrevs.remove", { get: function remove() this.user.closure.remove }),
238 removeAll: deprecated("group.abbrevs.clear", { get: function removeAll() this.user.closure.clear }),
241 * Returns the abbreviation for the given *mode* if *text* matches the
242 * abbreviation expansion criteria.
244 * @param {Mode} mode The mode to search.
245 * @param {string} text The string to test against the expansion criteria.
247 * @returns {Abbreviation}
249 match: function (mode, text) {
250 let match = this._match.exec(text);
252 return this.hives.map(h => h.get(mode, match[2] || match[4] || match[6]))
253 .nth(util.identity, 0);
258 * Lists all abbreviations matching *modes*, *lhs* and optionally *hives*.
260 * @param {Array} modes List of modes.
261 * @param {string} lhs The LHS of the abbreviation.
262 * @param {[Hive]} hives List of hives.
265 list: function (modes, lhs, hives) {
266 let hives = (hives || this.userHives).filter(h => !h.empty);
268 function abbrevs(hive)
269 hive.merged.filter(ab => (ab.inModes(modes) && ab.lhs.indexOf(lhs) == 0));
271 let list = ["table", {},
272 ["tr", { highlight: "Title" },
274 ["td", { style: "padding-right: 1em;" }, _("title.Mode")],
275 ["td", { style: "padding-right: 1em;" }, _("title.Abbrev")],
276 ["td", { style: "padding-right: 1em;" }, _("title.Replacement")]],
277 ["col", { style: "min-width: 6em; padding-right: 1em;" }],
278 hives.map(hive => let (i = 0) [
279 ["tr", { style: "height: .5ex;" }],
280 abbrevs(hive).map(abbrev =>
282 ["td", { highlight: "Title" }, !i++ ? String(hive.name) : ""],
283 ["td", {}, abbrev.modeChar],
284 ["td", {}, abbrev.lhs],
285 ["td", {}, abbrev.rhs]]),
286 ["tr", { style: "height: .5ex;" }]])];
289 // // TODO: Move this to an ItemList to show this automatically
290 // if (list.*.length() === list.text().length() + 2)
291 // dactyl.echomsg(_("abbreviation.none"));
293 commandline.commandOutput(list);
298 contexts: function initContexts(dactyl, modules, window) {
299 update(Abbreviations.prototype, {
300 hives: contexts.Hives("abbrevs", AbbrevHive),
301 user: contexts.hives.abbrevs.user
304 completion: function initCompletion() {
305 completion.abbreviation = function abbreviation(context, modes, group) {
306 group = group || abbreviations.user;
307 let fn = modes ? abbr => abbr.inModes(modes)
309 context.keys = { text: "lhs" , description: "rhs" };
310 context.completions = group.merged.filter(fn);
313 commands: function initCommands() {
314 function addAbbreviationCommands(modes, ch, modeDescription) {
316 modeDescription = modeDescription ? " in " + modeDescription + " mode" : "";
318 commands.add([ch ? ch + "a[bbreviate]" : "ab[breviate]"],
319 "Abbreviate a key sequence" + modeDescription,
321 let [lhs, rhs] = args;
322 dactyl.assert(!args.length || abbreviations._check.test(lhs),
323 _("error.invalidArgument"));
326 let hives = args.explicitOpts["-group"] ? [args["-group"]] : null;
327 abbreviations.list(modes, lhs || "", hives);
330 if (args["-javascript"])
331 rhs = contexts.bindMacro({ literalArg: rhs }, "-javascript", ["editor"]);
332 args["-group"].add(modes, lhs, rhs);
335 identifier: "abbreviate",
336 completer: function (context, args) {
337 if (args.length == 1)
338 return completion.abbreviation(context, modes, args["-group"]);
339 else if (args["-javascript"])
340 return completion.javascript(context);
345 contexts.GroupFlag("abbrevs"),
347 names: ["-javascript", "-js", "-j"],
348 description: "Expand this abbreviation by evaluating its right-hand-side as JavaScript"
351 serialize: function () array(abbreviations.userHives)
352 .filter(h => h.persist)
356 arguments: [abbr.lhs],
357 literalArg: abbr.rhs,
359 "-group": hive.name == "user" ? undefined : hive.name,
360 "-javascript": callable(abbr.rhs) ? null : undefined
363 for ([, abbr] in Iterator(hive.merged))
364 if (abbr.modesEqual(modes))
369 commands.add([ch + "una[bbreviate]"],
370 "Remove an abbreviation" + modeDescription,
372 util.assert(args.bang ^ !!args[0], _("error.argumentOrBang"));
375 args["-group"].clear(modes);
376 else if (!args["-group"].remove(modes, args[0]))
377 return dactyl.echoerr(_("abbreviation.noSuch"));
381 completer: function (context, args) completion.abbreviation(context, modes, args["-group"]),
383 options: [contexts.GroupFlag("abbrevs")]
387 addAbbreviationCommands([modes.INSERT, modes.COMMAND_LINE], "", "");
388 [modes.INSERT, modes.COMMAND_LINE].forEach(function (mode) {
389 addAbbreviationCommands([mode], mode.char, mode.displayName);
394 // vim: set fdm=marker sw=4 sts=4 ts=8 et: