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