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