]> git.donarmstrong.com Git - dactyl.git/blob - common/content/abbreviations.js
9a76fa372df310d733ca85f6326a66ec501a7730
[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 var Abbreviation = Class("Abbreviation", {
12     init: function (modes, lhs, rhs) {
13         this.modes = modes.sort();
14         this.lhs = lhs;
15         this.rhs = rhs;
16     },
17
18     equals: function (other) this.lhs == other.lhs && this.rhs == other.rhs,
19
20     expand: function (editor) String(callable(this.rhs) ? this.rhs(editor) : this.rhs),
21
22     modesEqual: function (modes) array.equals(this.modes, modes),
23
24     inMode: function (mode) this.modes.some(function (_mode) _mode == mode),
25
26     inModes: function (modes) modes.some(function (mode) this.inMode(mode), this),
27
28     removeMode: function (mode) {
29         this.modes = this.modes.filter(function (m) m != mode).sort();
30     },
31
32     get modeChar() Abbreviation.modeChar(this.modes)
33 }, {
34     modeChar: function (_modes) {
35         let result = array.uniq(_modes.map(function (m) m.char)).join("");
36         if (result == "ci")
37             result = "!";
38         return result;
39     }
40 });
41
42 var AbbrevHive = Class("AbbrevHive", Contexts.Hive, {
43     init: function init(group) {
44         init.superapply(this, arguments);
45         this._store = {};
46     },
47
48     get empty() !values(this._store).nth(util.identity, 0),
49
50     /**
51      * Adds a new abbreviation.
52      *
53      * @param {Abbreviation} abbr The abbreviation to add.
54      */
55     add: function (abbr) {
56         if (!(abbr instanceof Abbreviation))
57             abbr = Abbreviation.apply(null, arguments);
58
59         for (let [, mode] in Iterator(abbr.modes)) {
60             if (!this._store[mode])
61                 this._store[mode] = {};
62             this._store[mode][abbr.lhs] = abbr;
63         }
64     },
65
66     /**
67      * Returns the abbreviation with *lhs* in the given *mode*.
68      *
69      * @param {Mode} mode The mode of the abbreviation.
70      * @param {string} lhs The LHS of the abbreviation.
71      */
72     get: function (mode, lhs) {
73         let abbrevs = this._store[mode];
74         return abbrevs && set.has(abbrevs, lhs) ? abbrevs[lhs] : null;
75     },
76
77     /**
78      * @property {Abbreviation[]} The list of the abbreviations merged from
79      *     each mode.
80      */
81     get merged() {
82         // Wth? --Kris;
83         let map = values(this._store).map(Iterator).map(iter.toArray)
84                                      .flatten().toObject();
85         return Object.keys(map).sort().map(function (k) map[k]);
86     },
87
88     /**
89      * Remove the specified abbreviations.
90      *
91      * @param {Array} modes List of modes.
92      * @param {string} lhs The LHS of the abbreviation.
93      * @returns {boolean} Did the deleted abbreviation exist?
94      */
95     remove: function (modes, lhs) {
96         let result = false;
97         for (let [, mode] in Iterator(modes)) {
98             if ((mode in this._store) && (lhs in this._store[mode])) {
99                 result = true;
100                 this._store[mode][lhs].removeMode(mode);
101                 delete this._store[mode][lhs];
102             }
103         }
104         return result;
105     },
106
107     /**
108      * Removes all abbreviations specified in *modes*.
109      *
110      * @param {Array} modes List of modes.
111      */
112     clear: function (modes) {
113         for (let mode in values(modes)) {
114             for (let abbr in values(this._store[mode]))
115                 abbr.removeMode(mode);
116             delete this._store[mode];
117         }
118     }
119 });
120
121 var Abbreviations = Module("abbreviations", {
122     init: function () {
123
124         // (summarized from Vim's ":help abbreviations")
125         //
126         // There are three types of abbreviations.
127         //
128         // full-id: Consists entirely of keyword characters.
129         //          ("foo", "g3", "-1")
130         //
131         // end-id: Ends in a keyword character, but all other
132         //         are not keyword characters.
133         //         ("#i", "..f", "$/7")
134         //
135         // non-id: Ends in a non-keyword character, but the
136         //         others can be of any type other than space
137         //         and tab.
138         //         ("def#", "4/7$")
139         //
140         // Example strings that cannot be abbreviations:
141         //         "a.b", "#def", "a b", "_$r"
142         //
143         // For now, a keyword character is anything except for \s, ", or '
144         // (i.e., whitespace and quotes). In Vim, a keyword character is
145         // specified by the 'iskeyword' setting and is a much less inclusive
146         // list.
147         //
148         // TODO: Make keyword definition closer to Vim's default keyword
149         //       definition (which differs across platforms).
150
151         let params = { // This is most definitely not Vim compatible.
152             keyword:    /[^\s"']/,
153             nonkeyword: /[   "']/
154         };
155
156         this._match = util.regexp(<><![CDATA[
157             (^ | \s | <nonkeyword>) (<keyword>+             )$ | // full-id
158             (^ | \s | <keyword>   ) (<nonkeyword>+ <keyword>)$ | // end-id
159             (^ | \s               ) (\S* <nonkeyword>       )$   // non-id
160         ]]></>, "x", params);
161         this._check = util.regexp(<><![CDATA[
162             ^ (?:
163               <keyword>+              | // full-id
164               <nonkeyword>+ <keyword> | // end-id
165               \S* <nonkeyword>          // non-id
166             ) $
167         ]]></>, "x", params);
168     },
169
170     get: deprecated("group.abbrevs.get", { get: function get() this.user.closure.get }),
171     set: deprecated("group.abbrevs.set", { get: function set() this.user.closure.set }),
172     remove: deprecated("group.abbrevs.remove", { get: function remove() this.user.closure.remove }),
173     removeAll: deprecated("group.abbrevs.clear", { get: function removeAll() this.user.closure.clear }),
174
175     /**
176      * Returns the abbreviation for the given *mode* if *text* matches the
177      * abbreviation expansion criteria.
178      *
179      * @param {Mode} mode The mode to search.
180      * @param {string} text The string to test against the expansion criteria.
181      *
182      * @returns {Abbreviation}
183      */
184     match: function (mode, text) {
185         let match = this._match.exec(text);
186         if (match)
187             return this.hives.map(function (h) h.get(mode, match[2] || match[4] || match[6])).nth(util.identity, 0);
188         return null;
189     },
190
191     /**
192      * Lists all abbreviations matching *modes* and *lhs*.
193      *
194      * @param {Array} modes List of modes.
195      * @param {string} lhs The LHS of the abbreviation.
196      */
197     list: function (modes, lhs) {
198         let hives = contexts.allGroups.abbrevs.filter(function (h) !h.empty);
199
200         function abbrevs(hive)
201             hive.merged.filter(function (abbr) (abbr.inModes(modes) && abbr.lhs.indexOf(lhs) == 0));
202
203         let list = <table>
204                 <tr highlight="Title">
205                     <td/>
206                     <td style="padding-right: 1em;">Mode</td>
207                     <td style="padding-right: 1em;">Abbrev</td>
208                     <td style="padding-right: 1em;">Replacement</td>
209                 </tr>
210                 <col style="min-width: 6em; padding-right: 1em;"/>
211                 {
212                     template.map(hives, function (hive) let (i = 0)
213                         <tr style="height: .5ex;"/> +
214                         template.map(abbrevs(hive), function (abbrev)
215                             <tr>
216                                 <td highlight="Title">{!i++ ? hive.name : ""}</td>
217                                 <td>{abbrev.modeChar}</td>
218                                 <td>{abbrev.lhs}</td>
219                                 <td>{abbrev.rhs}</td>
220                             </tr>) +
221                         <tr style="height: .5ex;"/>)
222                 }
223                 </table>;
224
225         // TODO: Move this to an ItemList to show this automatically
226         if (list.*.length() === list.text().length() + 2)
227             dactyl.echomsg(_("abbrev.none"));
228         else
229             commandline.commandOutput(list);
230     }
231
232 }, {
233 }, {
234     contexts: function initContexts(dactyl, modules, window) {
235         update(Abbreviations.prototype, {
236             hives: contexts.Hives("abbrevs", AbbrevHive),
237             user: contexts.hives.abbrevs.user
238         });
239     },
240     completion: function () {
241         completion.abbreviation = function abbreviation(context, modes, group) {
242             group = group || abbreviations.user;
243             let fn = modes ? function (abbr) abbr.inModes(modes) : util.identity;
244             context.keys = { text: "lhs" , description: "rhs" };
245             context.completions = group.merged.filter(fn);
246         };
247     },
248
249     commands: function () {
250         function addAbbreviationCommands(modes, ch, modeDescription) {
251             modes.sort();
252             modeDescription = modeDescription ? " in " + modeDescription + " mode" : "";
253
254             commands.add([ch ? ch + "a[bbreviate]" : "ab[breviate]"],
255                 "Abbreviate a key sequence" + modeDescription,
256                 function (args) {
257                     let [lhs, rhs] = args;
258                     dactyl.assert(!args.length || abbreviations._check.test(lhs),
259                                   _("error.invalidArgument"));
260
261                     if (!rhs)
262                         abbreviations.list(modes, lhs || "");
263                     else {
264                         if (args["-javascript"])
265                             rhs = contexts.bindMacro({ literalArg: rhs }, "-javascript", ["editor"]);
266                         args["-group"].add(modes, lhs, rhs);
267                     }
268                 }, {
269                     completer: function (context, args) {
270                         if (args.length == 1)
271                             return completion.abbreviation(context, modes, args["-group"]);
272                         else if (args["-javascript"])
273                             return completion.javascript(context);
274                     },
275                     hereDoc: true,
276                     literal: 1,
277                     options: [
278                         contexts.GroupFlag("abbrevs"),
279                         {
280                             names: ["-javascript", "-js", "-j"],
281                             description: "Expand this abbreviation by evaluating its right-hand-side as JavaScript"
282                         }
283                     ],
284                     serialize: function () [
285                         {
286                             command: this.name,
287                             arguments: [abbr.lhs],
288                             literalArg: abbr.rhs,
289                             options: callable(abbr.rhs) ? {"-javascript": null} : {}
290                         }
291                         for ([, abbr] in Iterator(abbreviations.user.merged))
292                         if (abbr.modesEqual(modes))
293                     ]
294                 });
295
296             commands.add([ch + "una[bbreviate]"],
297                 "Remove an abbreviation" + modeDescription,
298                 function (args) {
299                     util.assert(args.bang ^ !!args[0], _("error.argumentOrBang"));
300
301                     if (args.bang)
302                         args["-group"].clear(modes);
303                     else if (!args["-group"].remove(modes, args[0]))
304                         return dactyl.echoerr(_("abbrev.noSuch"));
305                 }, {
306                     argCount: "?",
307                     bang: true,
308                     completer: function (context, args) completion.abbreviation(context, modes, args["-group"]),
309                     literal: 0,
310                     options: [contexts.GroupFlag("abbrevs")]
311                 });
312         }
313
314         addAbbreviationCommands([modes.INSERT, modes.COMMAND_LINE], "", "");
315         addAbbreviationCommands([modes.INSERT], "i", "insert");
316         addAbbreviationCommands([modes.COMMAND_LINE], "c", "command line");
317     }
318 });
319
320 // vim: set fdm=marker sw=4 ts=4 et: