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