]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/contexts.jsm
Import r6923 from upstream hg supporting Firefox up to 22.0a1
[dactyl.git] / common / modules / contexts.jsm
1 // Copyright (c) 2010-2012 Kris Maglione <maglione.k@gmail.com>
2 //
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
5 "use strict";
6
7 defineModule("contexts", {
8     exports: ["Contexts", "Group", "contexts"],
9     require: ["services", "util"]
10 });
11
12 lazyRequire("commands", ["ArgType", "CommandOption", "commands"]);
13 lazyRequire("options", ["Option"]);
14 lazyRequire("overlay", ["overlay"]);
15 lazyRequire("storage", ["File"]);
16 lazyRequire("template", ["template"]);
17
18 var Const = function Const(val) Class.Property({ enumerable: true, value: val });
19
20 var Group = Class("Group", {
21     init: function init(name, description, filter, persist) {
22         const self = this;
23
24         this.name = name;
25         this.description = description;
26         this.filter = filter || this.constructor.defaultFilter;
27         this.persist = persist || false;
28         this.hives = [];
29         this.children = [];
30     },
31
32     get contexts() this.modules.contexts,
33
34     set lastDocument(val) { this._lastDocument = util.weakReference(val); },
35     get lastDocument() this._lastDocument && this._lastDocument.get(),
36
37     modifiable: true,
38
39     cleanup: function cleanup(reason) {
40         for (let hive in values(this.hives))
41             util.trapErrors("cleanup", hive);
42
43         this.hives = [];
44         for (let hive in keys(this.hiveMap))
45             delete this[hive];
46
47         if (reason != "shutdown")
48             this.children.splice(0).forEach(this.contexts.closure.removeGroup);
49     },
50     destroy: function destroy(reason) {
51         for (let hive in values(this.hives))
52             util.trapErrors("destroy", hive);
53
54         if (reason != "shutdown")
55             this.children.splice(0).forEach(this.contexts.closure.removeGroup);
56     },
57
58     argsExtra: function argsExtra() ({}),
59
60     makeArgs: function makeArgs(doc, context, args) {
61         let res = update({ doc: doc, context: context }, args);
62         return update(res, this.argsExtra(res), args);
63     },
64
65     get toStringParams() [this.name],
66
67     get builtin() this.modules.contexts.builtinGroups.indexOf(this) >= 0,
68
69 }, {
70     compileFilter: function (patterns, default_) {
71         if (arguments.length < 2)
72             default_ = false;
73
74         function siteFilter(uri)
75             let (match = array.nth(siteFilter.filters, function (f) f(uri), 0))
76                 match ? match.result : default_;
77
78         return update(siteFilter, {
79             toString: function () this.filters.join(","),
80
81             toJSONXML: function (modules) let (uri = modules && modules.buffer.uri)
82                 template.map(this.filters,
83                              function (f) ["span", { highlight: uri && f(uri) ? "Filter" : "" },
84                                                "toJSONXML" in f ? f.toJSONXML() : String(f)],
85                              ","),
86
87             filters: Option.parse.sitelist(patterns)
88         });
89     },
90
91     defaultFilter: Class.Memoize(function () this.compileFilter(["*"]))
92 });
93
94 var Contexts = Module("contexts", {
95     init: function () {
96         this.pluginModules = {};
97     },
98
99     cleanup: function () {
100         for each (let module in this.pluginModules)
101             util.trapErrors("unload", module);
102
103         this.pluginModules = {};
104     },
105
106     Local: function Local(dactyl, modules, window) ({
107         init: function () {
108             const contexts = this;
109             this.modules = modules;
110
111             Object.defineProperty(modules.plugins, "contexts", Const({}));
112
113             this.groupList = [];
114             this.groupMap = {};
115             this.groupsProto = {};
116             this.hives = {};
117             this.hiveProto = {};
118
119             this.builtin = this.addGroup("builtin", "Builtin items");
120             this.user = this.addGroup("user", "User-defined items", null, true);
121             this.builtinGroups = [this.builtin, this.user];
122             this.builtin.modifiable = false;
123
124             this.GroupFlag = Class("GroupFlag", CommandOption, {
125                 init: function (name) {
126                     this.name = name;
127
128                     this.type = ArgType("group", function (group) {
129                         return isString(group) ? contexts.getGroup(group, name)
130                                                : group[name];
131                     });
132                 },
133
134                 get toStringParams() [this.name],
135
136                 names: ["-group", "-g"],
137
138                 description: "Group to which to add",
139
140                 get default() (contexts.context && contexts.context.group || contexts.user)[this.name],
141
142                 completer: function (context) modules.completion.group(context)
143             });
144
145             memoize(modules, "userContext",  function () contexts.Context(modules.io.getRCFile("~", true), contexts.user, [modules, true]));
146             memoize(modules, "_userContext", function () contexts.Context(modules.io.getRCFile("~", true), contexts.user, [modules.userContext]));
147         },
148
149         cleanup: function () {
150             for each (let hive in this.groupList.slice())
151                 util.trapErrors("cleanup", hive, "shutdown");
152         },
153
154         destroy: function () {
155             for each (let hive in values(this.groupList.slice()))
156                 util.trapErrors("destroy", hive, "shutdown");
157
158             for (let [name, plugin] in iter(this.modules.plugins.contexts))
159                 if (plugin && "onUnload" in plugin && callable(plugin.onUnload))
160                     util.trapErrors("onUnload", plugin);
161         },
162
163         signals: {
164             "browser.locationChange": function (webProgress, request, uri) {
165                 this.flush();
166             }
167         },
168
169         Group: Class("Group", Group, { modules: modules, get hiveMap() modules.contexts.hives }),
170
171         Hives: Class("Hives", Class.Property, {
172             init: function init(name, constructor) {
173                 const { contexts } = modules;
174                 const self = this;
175
176                 if (this.Hive)
177                     return {
178                         enumerable: true,
179
180                         get: function () array(contexts.groups[self.name])
181                     };
182
183                 this.Hive = constructor;
184                 this.name = name;
185                 memoize(contexts.Group.prototype, name, function () {
186                     let group = constructor(this);
187                     this.hives.push(group);
188                     contexts.flush();
189                     return group;
190                 });
191
192                 memoize(contexts.hives, name,
193                         function () Object.create(Object.create(contexts.hiveProto,
194                                                                 { _hive: { value: name } })));
195
196                 memoize(contexts.groupsProto, name,
197                         function () [group[name] for (group in values(this.groups)) if (Set.has(group, name))]);
198             },
199
200             get toStringParams() [this.name, this.Hive]
201         })
202     }),
203
204     Context: function Context(file, group, args) {
205         const { contexts, io, newContext, plugins, userContext } = this.modules;
206
207         let isPlugin = array.nth(io.getRuntimeDirectories("plugins"),
208                                  function (dir) dir.contains(file, true),
209                                  0);
210         let isRuntime = array.nth(io.getRuntimeDirectories(""),
211                                   function (dir) dir.contains(file, true),
212                                   0);
213
214         let name = isPlugin ? file.getRelativeDescriptor(isPlugin).replace(File.PATH_SEP, "-")
215                             : file.leafName;
216         let id   = util.camelCase(name.replace(/\.[^.]*$/, ""));
217
218         let contextPath = file.path;
219         let self = Set.has(plugins, contextPath) && plugins.contexts[contextPath];
220
221         if (!self && isPlugin && false)
222             self = Set.has(plugins, id) && plugins[id];
223
224         if (self) {
225             if (Set.has(self, "onUnload"))
226                 util.trapErrors("onUnload", self);
227         }
228         else {
229             let params = Array.slice(args || [userContext]);
230             params[2] = params[2] || File(file).URI.spec;
231
232             self = args && !isArray(args) ? args : newContext.apply(null, params);
233             update(self, {
234                 NAME: Const(id),
235
236                 PATH: Const(file.path),
237
238                 CONTEXT: Const(self),
239
240                 set isGlobalModule(val) {
241                     // Hack.
242                     if (val)
243                         throw Contexts;
244                 },
245
246                 unload: Const(function unload() {
247                     if (plugins[this.NAME] === this || plugins[this.PATH] === this)
248                         if (this.onUnload)
249                             util.trapErrors("onUnload", this);
250
251                     if (plugins[this.NAME] === this)
252                         delete plugins[this.NAME];
253
254                     if (plugins[this.PATH] === this)
255                         delete plugins[this.PATH];
256
257                     if (plugins.contexts[contextPath] === this)
258                         delete plugins.contexts[contextPath];
259
260                     if (!this.GROUP.builtin)
261                         contexts.removeGroup(this.GROUP);
262                 })
263             });
264
265             if (group !== this.user)
266                 Class.replaceProperty(plugins, file.path, self);
267
268             // This belongs elsewhere
269             if (isPlugin)
270                 Object.defineProperty(plugins, self.NAME, {
271                     configurable: true,
272                     enumerable: true,
273                     get: function () self,
274                     set: function (val) {
275                         util.dactyl(val).reportError(FailedAssertion(_("plugin.notReplacingContext", self.NAME), 3, false), true);
276                     }
277                 });
278         }
279
280         let path = isRuntime ? file.getRelativeDescriptor(isRuntime) : file.path;
281         let name = isRuntime ? path.replace(/^(plugin|color)s([\\\/])/, "$1$2") : "script-" + path;
282
283         if (!group)
284             group = this.addGroup(commands.nameRegexp
285                                           .iterate(name.replace(/\.[^.]*$/, ""))
286                                           .join("-").replace(/--+/g, "-"),
287                                   _("context.scriptGroup", file.path),
288                                   null, false);
289
290         Class.replaceProperty(self, "GROUP", group);
291         Class.replaceProperty(self, "group", group);
292
293         return plugins.contexts[contextPath] = self;
294     },
295
296     Script: function Script(file, group) {
297         return this.Context(file, group, [this.modules.userContext, true]);
298     },
299
300     Module: function Module(uri, isPlugin) {
301         const { io, plugins } = this.modules;
302
303         let canonical = uri.spec;
304         if (uri.scheme == "resource")
305             canonical = services["resource:"].resolveURI(uri);
306
307         if (uri instanceof Ci.nsIFileURL)
308             var file = File(uri.file);
309
310         let isPlugin = array.nth(io.getRuntimeDirectories("plugins"),
311                                  function (dir) dir.contains(file, true),
312                                  0);
313
314         let name = isPlugin && file && file.getRelativeDescriptor(isPlugin)
315                                            .replace(File.PATH_SEP, "-");
316         let id   = util.camelCase(name.replace(/\.[^.]*$/, ""));
317
318         let self = Set.has(this.pluginModules, canonical) && this.pluginModules[canonical];
319
320         if (!self) {
321             self = Object.create(jsmodules);
322
323             update(self, {
324                 NAME: Const(id),
325
326                 PATH: Const(file && file.path),
327
328                 CONTEXT: Const(self),
329
330                 get isGlobalModule() true,
331                 set isGlobalModule(val) {
332                     util.assert(val, "Loading non-global module as global",
333                                 false);
334                 },
335
336                 unload: Const(function unload() {
337                     if (contexts.pluginModules[canonical] == this) {
338                         if (this.onUnload)
339                             util.trapErrors("onUnload", this);
340
341                         delete contexts.pluginModules[canonical];
342                     }
343
344                     for each (let { plugins } in overlay.modules)
345                         if (plugins[this.NAME] == this)
346                             delete plugins[this.name];
347                 })
348             });
349
350             JSMLoader.loadSubScript(uri.spec, self, File.defaultEncoding);
351             this.pluginModules[canonical] = self;
352         }
353
354         // This belongs elsewhere
355         if (isPlugin)
356             Object.defineProperty(plugins, self.NAME, {
357                 configurable: true,
358                 enumerable: true,
359                 get: function () self,
360                 set: function (val) {
361                     util.dactyl(val).reportError(FailedAssertion(_("plugin.notReplacingContext", self.NAME), 3, false), true);
362                 }
363             });
364
365         return self;
366     },
367
368     context: null,
369
370     /**
371      * Returns a frame object describing the currently executing
372      * command, if applicable, otherwise returns the passed frame.
373      *
374      * @param {nsIStackFrame} frame
375      */
376     getCaller: function getCaller(frame) {
377         if (this.context && this.context.file)
378            return {
379                 __proto__: frame,
380                 filename: this.context.file[0] == "[" ? this.context.file
381                                                       : File(this.context.file).URI.spec,
382                 lineNumber: this.context.line
383             };
384         return frame;
385     },
386
387     groups: Class.Memoize(function () this.matchingGroups()),
388
389     allGroups: Class.Memoize(function () Object.create(this.groupsProto, {
390         groups: { value: this.initializedGroups() }
391     })),
392
393     matchingGroups: function (uri) Object.create(this.groupsProto, {
394         groups: { value: this.activeGroups(uri) }
395     }),
396
397     activeGroups: function (uri) {
398         if (uri instanceof Ci.nsIDOMDocument)
399             var [doc, uri] = [uri, uri.documentURIObject || util.newURI(uri.documentURI)];
400
401         if (!uri)
402             var { uri, doc } = this.modules.buffer;
403
404         return this.initializedGroups().filter(function (g) {
405             let res = uri && g.filter(uri, doc);
406             if (doc)
407                 g.lastDocument = res && doc;
408             return res;
409         });
410     },
411
412     flush: function flush() {
413         delete this.groups;
414         delete this.allGroups;
415     },
416
417     initializedGroups: function (hive)
418         let (need = hive ? [hive] : Object.keys(this.hives))
419             this.groupList.filter(function (group) need.some(Set.has(group))),
420
421     addGroup: function addGroup(name, description, filter, persist, replace) {
422         let group = this.getGroup(name);
423         if (group)
424             name = group.name;
425
426         if (!group) {
427             group = this.Group(name, description, filter, persist);
428             this.groupList.unshift(group);
429             this.groupMap[name] = group;
430             this.hiveProto.__defineGetter__(name, function () group[this._hive]);
431         }
432
433         if (replace) {
434             util.trapErrors("cleanup", group);
435
436             if (description)
437                 group.description = description;
438             if (filter)
439                 group.filter = filter;
440             group.persist = persist;
441         }
442
443         this.flush();
444         return group;
445     },
446
447     removeGroup: function removeGroup(name) {
448         if (isObject(name)) {
449             if (this.groupList.indexOf(name) === -1)
450                 return;
451             name = name.name;
452         }
453
454         let group = this.getGroup(name);
455
456         util.assert(!group || !group.builtin, _("group.cantRemoveBuiltin"));
457
458         if (group) {
459             name = group.name;
460             this.groupList.splice(this.groupList.indexOf(group), 1);
461             util.trapErrors("destroy", group);
462         }
463
464         if (this.context && this.context.group === group)
465             this.context.group = null;
466
467         delete this.groupMap[name];
468         delete this.hiveProto[name];
469         this.flush();
470         return group;
471     },
472
473     getGroup: function getGroup(name, hive) {
474         if (name === "default")
475             var group = this.context && this.context.context && this.context.context.GROUP;
476         else if (Set.has(this.groupMap, name))
477             group = this.groupMap[name];
478
479         if (group && hive)
480             return group[hive];
481         return group;
482     },
483
484     getDocs: function getDocs(context) {
485         try {
486             if (isinstance(context, ["Sandbox"])) {
487                 let info = "INFO" in context && Cu.evalInSandbox("this.INFO instanceof XML ? INFO.toXMLString() : this.INFO", context);
488                 return /^</.test(info) ? XML(info) : info;
489             }
490             if (DOM.isJSONXML(context.INFO))
491                 return context.INFO;
492             if (typeof context.INFO == "xml" && config.haveGecko(null, "14.*"))
493                 return context.INFO;
494         }
495         catch (e) {}
496         return null;
497     },
498
499     bindMacro: function (args, default_, params) {
500         const { dactyl, events, modules } = this.modules;
501
502         function Proxy(obj, key) Class.Property({
503             configurable: true,
504             enumerable: true,
505             get: function Proxy_get() process(obj[key]),
506             set: function Proxy_set(val) obj[key] = val
507         })
508
509         let process = util.identity;
510
511         if (callable(params))
512             var makeParams = function makeParams(self, args)
513                 let (obj = params.apply(self, args))
514                     iter.toObject([k, Proxy(obj, k)] for (k in properties(obj)));
515         else if (params)
516             makeParams = function makeParams(self, args)
517                 iter.toObject([name, process(args[i])]
518                               for ([i, name] in Iterator(params)));
519
520         let rhs = args.literalArg;
521         let type = ["-builtin", "-ex", "-javascript", "-keys"].reduce(function (a, b) args[b] ? b : a, default_);
522
523         switch (type) {
524         case "-builtin":
525             let noremap = true;
526             /* fallthrough */
527         case "-keys":
528             let silent = args["-silent"];
529             rhs = DOM.Event.canonicalKeys(rhs, true);
530             var action = function action() {
531                 events.feedkeys(action.macro(makeParams(this, arguments)),
532                                 noremap, silent);
533             };
534             action.macro = util.compileMacro(rhs, true);
535             break;
536
537         case "-ex":
538             action = function action() modules.commands
539                                               .execute(action.macro, makeParams(this, arguments),
540                                                        false, null, action.context);
541             action.macro = util.compileMacro(rhs, true);
542             action.context = this.context && update({}, this.context);
543             break;
544
545         case "-javascript":
546             if (callable(params))
547                 action = dactyl.userEval("(function action() { with (action.makeParams(this, arguments)) {" + args.literalArg + "} })");
548             else
549                 action = dactyl.userFunc.apply(dactyl, params.concat(args.literalArg).array);
550             process = function (param) isObject(param) && param.valueOf ? param.valueOf() : param;
551             action.params = params;
552             action.makeParams = makeParams;
553             break;
554         }
555
556         action.toString = function toString() (type === default_ ? "" : type + " ") + rhs;
557         args = null;
558         return action;
559     },
560
561     withContext: function withContext(defaults, callback, self)
562         this.withSavedValues(["context"], function () {
563             this.context = defaults && update({}, defaults);
564             return callback.call(self, this.context);
565         })
566 }, {
567     Hive: Class("Hive", {
568         init: function init(group) {
569             this.group = group;
570         },
571
572         cleanup: function cleanup() {},
573         destroy: function destroy() {},
574
575         get modifiable() this.group.modifiable,
576
577         get argsExtra() this.group.argsExtra,
578         get makeArgs() this.group.makeArgs,
579         get builtin() this.group.builtin,
580
581         get name() this.group.name,
582         set name(val) this.group.name = val,
583
584         get description() this.group.description,
585         set description(val) this.group.description = val,
586
587         get filter() this.group.filter,
588         set filter(val) this.group.filter = val,
589
590         get persist() this.group.persist,
591         set persist(val) this.group.persist = val,
592
593         prefix: Class.Memoize(function () this.name === "builtin" ? "" : this.name + ":"),
594
595         get toStringParams() [this.name]
596     })
597 }, {
598     commands: function initCommands(dactyl, modules, window) {
599         const { commands, contexts } = modules;
600
601         commands.add(["gr[oup]"],
602             "Create or select a group",
603             function (args) {
604                 if (args.length > 0) {
605                     var name = Option.dequote(args[0]);
606                     util.assert(name !== "builtin", _("group.cantModifyBuiltin"));
607                     util.assert(commands.validName.test(name), _("group.invalidName", name));
608
609                     var group = contexts.getGroup(name);
610                 }
611                 else if (args.bang)
612                     var group = args.context && args.context.group;
613                 else
614                     return void modules.completion.listCompleter("group", "", null, null);
615
616                 util.assert(group || name, _("group.noCurrent"));
617
618                 let filter = Group.compileFilter(args["-locations"]);
619                 if (!group || args.bang)
620                     group = contexts.addGroup(name, args["-description"], filter, !args["-nopersist"], args.bang);
621                 else if (!group.builtin) {
622                     if (args.has("-locations"))
623                         group.filter = filter;
624                     if (args.has("-description"))
625                         group.description = args["-description"];
626                     if (args.has("-nopersist"))
627                         group.persist = !args["-nopersist"];
628                 }
629
630                 if (!group.builtin && args.has("-args")) {
631                     group.argsExtra = contexts.bindMacro({ literalArg: "return " + args["-args"] },
632                                                          "-javascript", util.identity);
633                     group.args = args["-args"];
634                 }
635
636                 if (args.context) {
637                     args.context.group = group;
638                     if (args.context.context) {
639                         args.context.context.group = group;
640
641                         let parent = args.context.context.GROUP;
642                         if (parent && parent != group) {
643                             group.parent = parent;
644                             if (!~parent.children.indexOf(group))
645                                 parent.children.push(group);
646                         }
647                     }
648                 }
649
650                 util.assert(!group.builtin ||
651                                 !["-description", "-locations", "-nopersist"]
652                                     .some(Set.has(args.explicitOpts)),
653                             _("group.cantModifyBuiltin"));
654             },
655             {
656                 argCount: "?",
657                 bang: true,
658                 completer: function (context, args) {
659                     if (args.length == 1)
660                         modules.completion.group(context);
661                 },
662                 keepQuotes: true,
663                 options: [
664                     {
665                         names: ["-args", "-a"],
666                         description: "JavaScript Object which augments the arguments passed to commands, mappings, and autocommands",
667                         type: CommandOption.STRING
668                     },
669                     {
670                         names: ["-description", "-desc", "-d"],
671                         description: "A description of this group",
672                         default: "User-defined group",
673                         type: CommandOption.STRING
674                     },
675                     {
676                         names: ["-locations", "-locs", "-loc", "-l"],
677                         description: "The URLs for which this group should be active",
678                         default: ["*"],
679                         type: CommandOption.LIST
680                     },
681                     {
682                         names: ["-nopersist", "-n"],
683                         description: "Do not save this group to an auto-generated RC file"
684                     }
685                 ],
686                 serialGroup: 20,
687                 serialize: function () [
688                     {
689                         command: this.name,
690                         bang: true,
691                         options: iter([v, typeof group[k] == "boolean" ? null : group[k]]
692                                       // FIXME: this map is expressed multiple times
693                                       for ([k, v] in Iterator({
694                                           args: "-args",
695                                           description: "-description",
696                                           filter: "-locations"
697                                       }))
698                                       if (group[k])).toObject(),
699                         arguments: [group.name],
700                         ignoreDefaults: true
701                     }
702                     for (group in values(contexts.initializedGroups()))
703                     if (!group.builtin && group.persist)
704                 ].concat([{ command: this.name, arguments: ["user"] }])
705             });
706
707         commands.add(["delg[roup]"],
708             "Delete a group",
709             function (args) {
710                 util.assert(args.bang ^ !!args[0], _("error.argumentOrBang"));
711
712                 if (args.bang)
713                     contexts.groupList = contexts.groupList.filter(function (g) g.builtin);
714                 else {
715                     util.assert(contexts.getGroup(args[0]), _("group.noSuch", args[0]));
716                     contexts.removeGroup(args[0]);
717                 }
718             },
719             {
720                 argCount: "?",
721                 bang: true,
722                 completer: function (context, args) {
723                     if (args.bang)
724                         return;
725                     context.filters.push(function ({ item }) !item.builtin);
726                     modules.completion.group(context);
727                 }
728             });
729
730         commands.add(["fini[sh]"],
731             "Stop sourcing a script file",
732             function (args) {
733                 util.assert(args.context, _("command.finish.illegal"));
734                 args.context.finished = true;
735             },
736             { argCount: "0" });
737
738         function checkStack(cmd) {
739             util.assert(contexts.context && contexts.context.stack &&
740                         contexts.context.stack[cmd] && contexts.context.stack[cmd].length,
741                         _("command.conditional.illegal"));
742         }
743         function pop(cmd) {
744             checkStack(cmd);
745             return contexts.context.stack[cmd].pop();
746         }
747         function push(cmd, value) {
748             util.assert(contexts.context, _("command.conditional.illegal"));
749             if (arguments.length < 2)
750                 value = contexts.context.noExecute;
751             contexts.context.stack = contexts.context.stack || {};
752             contexts.context.stack[cmd] = (contexts.context.stack[cmd] || []).concat([value]);
753         }
754
755         commands.add(["if"],
756             "Execute commands until the next :elseif, :else, or :endif only if the argument returns true",
757             function (args) { args.context.noExecute = !dactyl.userEval(args[0]); },
758             {
759                 always: function (args) { push("if"); },
760                 argCount: "1",
761                 literal: 0
762             });
763         commands.add(["elsei[f]", "elif"],
764             "Execute commands until the next :elseif, :else, or :endif only if the argument returns true",
765             function (args) {},
766             {
767                 always: function (args) {
768                     checkStack("if");
769                     args.context.noExecute = args.context.stack.if.slice(-1)[0] ||
770                         !args.context.noExecute || !dactyl.userEval(args[0]);
771                 },
772                 argCount: "1",
773                 literal: 0
774             });
775         commands.add(["el[se]"],
776             "Execute commands until the next :endif only if the previous conditionals were not executed",
777             function (args) {},
778             {
779                 always: function (args) {
780                     checkStack("if");
781                     args.context.noExecute = args.context.stack.if.slice(-1)[0] ||
782                         !args.context.noExecute;
783                 },
784                 argCount: "0"
785             });
786         commands.add(["en[dif]", "fi"],
787             "End a string of :if/:elseif/:else conditionals",
788             function (args) {},
789             {
790                 always: function (args) { args.context.noExecute = pop("if"); },
791                 argCount: "0"
792             });
793     },
794     completion: function initCompletion(dactyl, modules, window) {
795         const { completion, contexts } = modules;
796
797         completion.group = function group(context, active) {
798             context.title = ["Group"];
799             let uri = modules.buffer.uri;
800             context.keys = {
801                 active: function (group) group.filter(uri),
802                 text: "name",
803                 description: function (g) ["", g.filter.toJSONXML ? g.filter.toJSONXML(modules).concat("\u00a0") : "", g.description || ""]
804             };
805             context.completions = (active === undefined ? contexts.groupList : contexts.initializedGroups(active))
806                                     .slice(0, -1);
807
808             iter({ Active: true, Inactive: false }).forEach(function ([name, active]) {
809                 context.split(name, null, function (context) {
810                     context.title[0] = name + " Groups";
811                     context.filters.push(function ({ item }) !!item.filter(modules.buffer.uri) == active);
812                 });
813             });
814         };
815     }
816 });
817
818 endModule();
819
820 // catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
821
822 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: