]> git.donarmstrong.com Git - dactyl.git/blob - common/content/abbreviations.js
Import 1.0b7.1 supporting Firefox up to 8.*
[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-2011 by 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(function (_mode) _mode == 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(function (mode) this.inMode(mode), this),
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(function (m) m != mode).sort();
84     },
85
86     /**
87      * @property {string} The mode display characters associated with the
88      *     supported mode combination.
89      */
90     get modeChar() Abbreviation.modeChar(this.modes)
91 }, {
92     modeChar: function (_modes) {
93         let result = array.uniq(_modes.map(function (m) m.char)).join("");
94         if (result == "ci")
95             result = "!";
96         return result;
97     }
98 });
99
100 var AbbrevHive = Class("AbbrevHive", Contexts.Hive, {
101     init: function init(group) {
102         init.superapply(this, arguments);
103         this._store = {};
104     },
105
106     /** @property {boolean} True if there are no abbreviations. */
107     get empty() !values(this._store).nth(util.identity, 0),
108
109     /**
110      * Adds a new abbreviation.
111      *
112      * @param {Abbreviation} abbr The abbreviation to add.
113      */
114     add: function (abbr) {
115         if (!(abbr instanceof Abbreviation))
116             abbr = Abbreviation.apply(null, arguments);
117
118         for (let [, mode] in Iterator(abbr.modes)) {
119             if (!this._store[mode])
120                 this._store[mode] = {};
121             this._store[mode][abbr.lhs] = abbr;
122         }
123     },
124
125     /**
126      * Returns the abbreviation with *lhs* in the given *mode*.
127      *
128      * @param {Mode} mode The mode of the abbreviation.
129      * @param {string} lhs The LHS of the abbreviation.
130      * @returns {Abbreviation} The matching abbreviation.
131      */
132     get: function (mode, lhs) {
133         let abbrevs = this._store[mode];
134         return abbrevs && Set.has(abbrevs, lhs) ? abbrevs[lhs] : null;
135     },
136
137     /**
138      * @property {[Abbreviation]} The list of the abbreviations merged from
139      *     each mode.
140      */
141     get merged() {
142         // Wth? --Kris;
143         let map = values(this._store).map(Iterator).map(iter.toArray)
144                                      .flatten().toObject();
145         return Object.keys(map).sort().map(function (k) map[k]);
146     },
147
148     /**
149      * Remove the specified abbreviations.
150      *
151      * @param {Array} modes List of modes.
152      * @param {string} lhs The LHS of the abbreviation.
153      * @returns {boolean} Did the deleted abbreviation exist?
154      */
155     remove: function (modes, lhs) {
156         let result = false;
157         for (let [, mode] in Iterator(modes)) {
158             if ((mode in this._store) && (lhs in this._store[mode])) {
159                 result = true;
160                 this._store[mode][lhs].removeMode(mode);
161                 delete this._store[mode][lhs];
162             }
163         }
164         return result;
165     },
166
167     /**
168      * Removes all abbreviations specified in *modes*.
169      *
170      * @param {Array} modes List of modes.
171      */
172     clear: function (modes) {
173         for (let mode in values(modes)) {
174             for (let abbr in values(this._store[mode]))
175                 abbr.removeMode(mode);
176             delete this._store[mode];
177         }
178     }
179 });
180
181 var Abbreviations = Module("abbreviations", {
182     init: function () {
183
184         // (summarized from Vim's ":help abbreviations")
185         //
186         // There are three types of abbreviations.
187         //
188         // full-id: Consists entirely of keyword characters.
189         //          ("foo", "g3", "-1")
190         //
191         // end-id: Ends in a keyword character, but all other
192         //         are not keyword characters.
193         //         ("#i", "..f", "$/7")
194         //
195         // non-id: Ends in a non-keyword character, but the
196         //         others can be of any type other than space
197         //         and tab.
198         //         ("def#", "4/7$")
199         //
200         // Example strings that cannot be abbreviations:
201         //         "a.b", "#def", "a b", "_$r"
202         //
203         // For now, a keyword character is anything except for \s, ", or '
204         // (i.e., whitespace and quotes). In Vim, a keyword character is
205         // specified by the 'iskeyword' setting and is a much less inclusive
206         // list.
207         //
208         // TODO: Make keyword definition closer to Vim's default keyword
209         //       definition (which differs across platforms).
210
211         let params = { // This is most definitely not Vim compatible.
212             keyword:    /[^\s"']/,
213             nonkeyword: /[   "']/
214         };
215
216         this._match = util.regexp(<><![CDATA[
217             (^ | \s | <nonkeyword>) (<keyword>+             )$ | // full-id
218             (^ | \s | <keyword>   ) (<nonkeyword>+ <keyword>)$ | // end-id
219             (^ | \s               ) (\S* <nonkeyword>       )$   // non-id
220         ]]></>, "x", params);
221         this._check = util.regexp(<><![CDATA[
222             ^ (?:
223               <keyword>+              | // full-id
224               <nonkeyword>+ <keyword> | // end-id
225               \S* <nonkeyword>          // non-id
226             ) $
227         ]]></>, "x", params);
228     },
229
230     get: deprecated("group.abbrevs.get", { get: function get() this.user.closure.get }),
231     set: deprecated("group.abbrevs.set", { get: function set() this.user.closure.set }),
232     remove: deprecated("group.abbrevs.remove", { get: function remove() this.user.closure.remove }),
233     removeAll: deprecated("group.abbrevs.clear", { get: function removeAll() this.user.closure.clear }),
234
235     /**
236      * Returns the abbreviation for the given *mode* if *text* matches the
237      * abbreviation expansion criteria.
238      *
239      * @param {Mode} mode The mode to search.
240      * @param {string} text The string to test against the expansion criteria.
241      *
242      * @returns {Abbreviation}
243      */
244     match: function (mode, text) {
245         let match = this._match.exec(text);
246         if (match)
247             return this.hives.map(function (h) h.get(mode, match[2] || match[4] || match[6])).nth(util.identity, 0);
248         return null;
249     },
250
251     /**
252      * Lists all abbreviations matching *modes*, *lhs* and optionally *hives*.
253      *
254      * @param {Array} modes List of modes.
255      * @param {string} lhs The LHS of the abbreviation.
256      * @param {[Hive]} hives List of hives.
257      * @optional
258      */
259     list: function (modes, lhs, hives) {
260         let hives = hives || contexts.allGroups.abbrevs.filter(function (h) !h.empty);
261
262         function abbrevs(hive)
263             hive.merged.filter(function (abbr) (abbr.inModes(modes) && abbr.lhs.indexOf(lhs) == 0));
264
265         let list = <table>
266                 <tr highlight="Title">
267                     <td/>
268                     <td style="padding-right: 1em;">{_("title.Mode")}</td>
269                     <td style="padding-right: 1em;">{_("title.Abbrev")}</td>
270                     <td style="padding-right: 1em;">{_("title.Replacement")}</td>
271                 </tr>
272                 <col style="min-width: 6em; padding-right: 1em;"/>
273                 {
274                     template.map(hives, function (hive) let (i = 0)
275                         <tr style="height: .5ex;"/> +
276                         template.map(abbrevs(hive), function (abbrev)
277                             <tr>
278                                 <td highlight="Title">{!i++ ? hive.name : ""}</td>
279                                 <td>{abbrev.modeChar}</td>
280                                 <td>{abbrev.lhs}</td>
281                                 <td>{abbrev.rhs}</td>
282                             </tr>) +
283                         <tr style="height: .5ex;"/>)
284                 }
285                 </table>;
286
287         // TODO: Move this to an ItemList to show this automatically
288         if (list.*.length() === list.text().length() + 2)
289             dactyl.echomsg(_("abbreviation.none"));
290         else
291             commandline.commandOutput(list);
292     }
293
294 }, {
295 }, {
296     contexts: function initContexts(dactyl, modules, window) {
297         update(Abbreviations.prototype, {
298             hives: contexts.Hives("abbrevs", AbbrevHive),
299             user: contexts.hives.abbrevs.user
300         });
301     },
302     completion: function () {
303         completion.abbreviation = function abbreviation(context, modes, group) {
304             group = group || abbreviations.user;
305             let fn = modes ? function (abbr) abbr.inModes(modes) : util.identity;
306             context.keys = { text: "lhs" , description: "rhs" };
307             context.completions = group.merged.filter(fn);
308         };
309     },
310     commands: function () {
311         function addAbbreviationCommands(modes, ch, modeDescription) {
312             modes.sort();
313             modeDescription = modeDescription ? " in " + modeDescription + " mode" : "";
314
315             commands.add([ch ? ch + "a[bbreviate]" : "ab[breviate]"],
316                 "Abbreviate a key sequence" + modeDescription,
317                 function (args) {
318                     let [lhs, rhs] = args;
319                     dactyl.assert(!args.length || abbreviations._check.test(lhs),
320                                   _("error.invalidArgument"));
321
322                     if (!rhs) {
323                         let hives = args.explicitOpts["-group"] ? [args["-group"]] : null;
324                         abbreviations.list(modes, lhs || "", hives);
325                     }
326                     else {
327                         if (args["-javascript"])
328                             rhs = contexts.bindMacro({ literalArg: rhs }, "-javascript", ["editor"]);
329                         args["-group"].add(modes, lhs, rhs);
330                     }
331                 }, {
332                     identifier: "abbreviate",
333                     completer: function (context, args) {
334                         if (args.length == 1)
335                             return completion.abbreviation(context, modes, args["-group"]);
336                         else if (args["-javascript"])
337                             return completion.javascript(context);
338                     },
339                     hereDoc: true,
340                     literal: 1,
341                     options: [
342                         contexts.GroupFlag("abbrevs"),
343                         {
344                             names: ["-javascript", "-js", "-j"],
345                             description: "Expand this abbreviation by evaluating its right-hand-side as JavaScript"
346                         }
347                     ],
348                     serialize: function () [
349                         {
350                             command: this.name,
351                             arguments: [abbr.lhs],
352                             literalArg: abbr.rhs,
353                             options: callable(abbr.rhs) ? {"-javascript": null} : {}
354                         }
355                         for ([, abbr] in Iterator(abbreviations.user.merged))
356                         if (abbr.modesEqual(modes))
357                     ]
358                 });
359
360             commands.add([ch + "una[bbreviate]"],
361                 "Remove an abbreviation" + modeDescription,
362                 function (args) {
363                     util.assert(args.bang ^ !!args[0], _("error.argumentOrBang"));
364
365                     if (args.bang)
366                         args["-group"].clear(modes);
367                     else if (!args["-group"].remove(modes, args[0]))
368                         return dactyl.echoerr(_("abbreviation.noSuch"));
369                 }, {
370                     argCount: "?",
371                     bang: true,
372                     completer: function (context, args) completion.abbreviation(context, modes, args["-group"]),
373                     literal: 0,
374                     options: [contexts.GroupFlag("abbrevs")]
375                 });
376         }
377
378         addAbbreviationCommands([modes.INSERT, modes.COMMAND_LINE], "", "");
379         [modes.INSERT, modes.COMMAND_LINE].forEach(function (mode) {
380             addAbbreviationCommands([mode], mode.char, mode.displayName);
381         });
382     }
383 });
384
385 // vim: set fdm=marker sw=4 ts=4 et: