]> git.donarmstrong.com Git - dactyl.git/blob - common/content/abbreviations.js
f0cd06d17747011ec4441a5505354031505086cc
[dactyl.git] / common / content / abbreviations.js
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>
4 //
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
7 "use strict";
8
9 /** @scope modules */
10
11 /**
12  * A user-defined input mode binding of a typed string to an automatically
13  * inserted expansion string.
14  *
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.
19  *
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
22  *     be replaced.
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.
26  * @private
27  */
28 var Abbreviation = Class("Abbreviation", {
29     init: function (modes, lhs, rhs) {
30         this.modes = modes.sort();
31         this.lhs = lhs;
32         this.rhs = rhs;
33     },
34
35     /**
36      * Returns true if this abbreviation's LHS and RHS are equal to those in
37      * *other*.
38      *
39      * @param {Abbreviation} other The abbreviation to test.
40      * @returns {boolean} The result of the comparison.
41      */
42     equals: function (other) this.lhs == other.lhs && this.rhs == other.rhs,
43
44     /**
45      * Returns the abbreviation's expansion text.
46      *
47      * @param {nsIEditor} editor The editor in which abbreviation expansion is
48      *     occurring.
49      * @returns {string}
50      */
51     expand: function (editor) String(callable(this.rhs) ? this.rhs(editor) : this.rhs),
52
53     /**
54      * Returns true if this abbreviation is defined for all *modes*.
55      *
56      * @param {[Mode]} modes The modes to test.
57      * @returns {boolean} The result of the comparison.
58      */
59     modesEqual: function (modes) array.equals(this.modes, modes),
60
61     /**
62      * Returns true if this abbreviation is defined for *mode*.
63      *
64      * @param {Mode} mode The mode to test.
65      * @returns {boolean} The result of the comparison.
66      */
67     inMode: function (mode) this.modes.some(m => m == mode),
68
69     /**
70      * Returns true if this abbreviation is defined in any of *modes*.
71      *
72      * @param {[Modes]} modes The modes to test.
73      * @returns {boolean} The result of the comparison.
74      */
75     inModes: function (modes) modes.some(mode => this.inMode(mode)),
76
77     /**
78      * Remove *mode* from the list of supported modes for this abbreviation.
79      *
80      * @param {Mode} mode The mode to remove.
81      */
82     removeMode: function (mode) {
83         this.modes = this.modes.filter(m => m != mode)
84                                .sort();
85     },
86
87     /**
88      * @property {string} The mode display characters associated with the
89      *     supported mode combination.
90      */
91     get modeChar() Abbreviation.modeChar(this.modes)
92 }, {
93     modeChar: function (_modes) {
94         let result = array.uniq(_modes.map(m => m.char)).join("");
95         if (result == "ci")
96             result = "!";
97         return result;
98     }
99 });
100
101 var AbbrevHive = Class("AbbrevHive", Contexts.Hive, {
102     init: function init(group) {
103         init.superapply(this, arguments);
104         this._store = {};
105     },
106
107     /** @property {boolean} True if there are no abbreviations. */
108     get empty() !values(this._store).nth(util.identity, 0),
109
110     /**
111      * Adds a new abbreviation.
112      *
113      * @param {Abbreviation} abbr The abbreviation to add.
114      */
115     add: function (abbr) {
116         if (!(abbr instanceof Abbreviation))
117             abbr = Abbreviation.apply(null, arguments);
118
119         for (let [, mode] in Iterator(abbr.modes)) {
120             if (!this._store[mode])
121                 this._store[mode] = {};
122             this._store[mode][abbr.lhs] = abbr;
123         }
124     },
125
126     /**
127      * Returns the abbreviation with *lhs* in the given *mode*.
128      *
129      * @param {Mode} mode The mode of the abbreviation.
130      * @param {string} lhs The LHS of the abbreviation.
131      * @returns {Abbreviation} The matching abbreviation.
132      */
133     get: function (mode, lhs) {
134         let abbrevs = this._store[mode];
135         return abbrevs && Set.has(abbrevs, lhs) ? abbrevs[lhs] : null;
136     },
137
138     /**
139      * @property {[Abbreviation]} The list of the abbreviations merged from
140      *     each mode.
141      */
142     get merged() {
143         // Wth? --Kris;
144         let map = values(this._store).map(Iterator).map(iter.toArray)
145                                      .flatten().toObject();
146         return Object.keys(map).sort().map(k => map[k]);
147     },
148
149     /**
150      * Remove the specified abbreviations.
151      *
152      * @param {Array} modes List of modes.
153      * @param {string} lhs The LHS of the abbreviation.
154      * @returns {boolean} Did the deleted abbreviation exist?
155      */
156     remove: function (modes, lhs) {
157         let result = false;
158         for (let [, mode] in Iterator(modes)) {
159             if ((mode in this._store) && (lhs in this._store[mode])) {
160                 result = true;
161                 this._store[mode][lhs].removeMode(mode);
162                 delete this._store[mode][lhs];
163             }
164         }
165         return result;
166     },
167
168     /**
169      * Removes all abbreviations specified in *modes*.
170      *
171      * @param {Array} modes List of modes.
172      */
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];
178         }
179     }
180 });
181
182 var Abbreviations = Module("abbreviations", {
183     init: function () {
184
185         // (summarized from Vim's ":help abbreviations")
186         //
187         // There are three types of abbreviations.
188         //
189         // full-id: Consists entirely of keyword characters.
190         //          ("foo", "g3", "-1")
191         //
192         // end-id: Ends in a keyword character, but all other
193         //         are not keyword characters.
194         //         ("#i", "..f", "$/7")
195         //
196         // non-id: Ends in a non-keyword character, but the
197         //         others can be of any type other than space
198         //         and tab.
199         //         ("def#", "4/7$")
200         //
201         // Example strings that cannot be abbreviations:
202         //         "a.b", "#def", "a b", "_$r"
203         //
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
207         // list.
208         //
209         // TODO: Make keyword definition closer to Vim's default keyword
210         //       definition (which differs across platforms).
211
212         let params = { // This is most definitely not Vim compatible.
213             keyword:    /[^\s"']/,
214             nonkeyword: /[   "']/
215         };
216
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
221         */), "x", params);
222         this._check = util.regexp(literal(/*
223             ^ (?:
224               <keyword>+              | // full-id
225               <nonkeyword>+ <keyword> | // end-id
226               \S* <nonkeyword>          // non-id
227             ) $
228         */), "x", params);
229     },
230
231     get allHives() contexts.allGroups.abbrevs,
232
233     get userHives() this.allHives.filter(h => h !== this.builtin),
234
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 }),
239
240     /**
241      * Returns the abbreviation for the given *mode* if *text* matches the
242      * abbreviation expansion criteria.
243      *
244      * @param {Mode} mode The mode to search.
245      * @param {string} text The string to test against the expansion criteria.
246      *
247      * @returns {Abbreviation}
248      */
249     match: function (mode, text) {
250         let match = this._match.exec(text);
251         if (match)
252             return this.hives.map(h => h.get(mode, match[2] || match[4] || match[6]))
253                        .nth(util.identity, 0);
254         return null;
255     },
256
257     /**
258      * Lists all abbreviations matching *modes*, *lhs* and optionally *hives*.
259      *
260      * @param {Array} modes List of modes.
261      * @param {string} lhs The LHS of the abbreviation.
262      * @param {[Hive]} hives List of hives.
263      * @optional
264      */
265     list: function (modes, lhs, hives) {
266         let hives = (hives || this.userHives).filter(h => !h.empty);
267
268         function abbrevs(hive)
269             hive.merged.filter(ab => (ab.inModes(modes) && ab.lhs.indexOf(lhs) == 0));
270
271         let list = ["table", {},
272                 ["tr", { highlight: "Title" },
273                     ["td"],
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 =>
281                         ["tr", {},
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;" }]])];
287
288         // FIXME?
289         // // TODO: Move this to an ItemList to show this automatically
290         // if (list.*.length() === list.text().length() + 2)
291         //     dactyl.echomsg(_("abbreviation.none"));
292         // else
293         commandline.commandOutput(list);
294     }
295
296 }, {
297 }, {
298     contexts: function initContexts(dactyl, modules, window) {
299         update(Abbreviations.prototype, {
300             hives: contexts.Hives("abbrevs", AbbrevHive),
301             user: contexts.hives.abbrevs.user
302         });
303     },
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)
308                            : abbr => abbr;
309             context.keys = { text: "lhs" , description: "rhs" };
310             context.completions = group.merged.filter(fn);
311         };
312     },
313     commands: function initCommands() {
314         function addAbbreviationCommands(modes, ch, modeDescription) {
315             modes.sort();
316             modeDescription = modeDescription ? " in " + modeDescription + " mode" : "";
317
318             commands.add([ch ? ch + "a[bbreviate]" : "ab[breviate]"],
319                 "Abbreviate a key sequence" + modeDescription,
320                 function (args) {
321                     let [lhs, rhs] = args;
322                     dactyl.assert(!args.length || abbreviations._check.test(lhs),
323                                   _("error.invalidArgument"));
324
325                     if (!rhs) {
326                         let hives = args.explicitOpts["-group"] ? [args["-group"]] : null;
327                         abbreviations.list(modes, lhs || "", hives);
328                     }
329                     else {
330                         if (args["-javascript"])
331                             rhs = contexts.bindMacro({ literalArg: rhs }, "-javascript", ["editor"]);
332                         args["-group"].add(modes, lhs, rhs);
333                     }
334                 }, {
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);
341                     },
342                     hereDoc: true,
343                     literal: 1,
344                     options: [
345                         contexts.GroupFlag("abbrevs"),
346                         {
347                             names: ["-javascript", "-js", "-j"],
348                             description: "Expand this abbreviation by evaluating its right-hand-side as JavaScript"
349                         }
350                     ],
351                     serialize: function () array(abbreviations.userHives)
352                         .filter(h => h.persist)
353                         .map(hive => [
354                             {
355                                 command: this.name,
356                                 arguments: [abbr.lhs],
357                                 literalArg: abbr.rhs,
358                                 options: {
359                                     "-group": hive.name == "user" ? undefined : hive.name,
360                                     "-javascript": callable(abbr.rhs) ? null : undefined
361                                 }
362                             }
363                             for ([, abbr] in Iterator(hive.merged))
364                             if (abbr.modesEqual(modes))
365                         ]).
366                         flatten().array
367                 });
368
369             commands.add([ch + "una[bbreviate]"],
370                 "Remove an abbreviation" + modeDescription,
371                 function (args) {
372                     util.assert(args.bang ^ !!args[0], _("error.argumentOrBang"));
373
374                     if (args.bang)
375                         args["-group"].clear(modes);
376                     else if (!args["-group"].remove(modes, args[0]))
377                         return dactyl.echoerr(_("abbreviation.noSuch"));
378                 }, {
379                     argCount: "?",
380                     bang: true,
381                     completer: function (context, args) completion.abbreviation(context, modes, args["-group"]),
382                     literal: 0,
383                     options: [contexts.GroupFlag("abbrevs")]
384                 });
385         }
386
387         addAbbreviationCommands([modes.INSERT, modes.COMMAND_LINE], "", "");
388         [modes.INSERT, modes.COMMAND_LINE].forEach(function (mode) {
389             addAbbreviationCommands([mode], mode.char, mode.displayName);
390         });
391     }
392 });
393
394 // vim: set fdm=marker sw=4 sts=4 ts=8 et: