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