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