]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/contexts.jsm
15549e37cede5a2d10de6f1bb818571bd77bed00
[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 try {
8
9 Components.utils.import("resource://dactyl/bootstrap.jsm");
10 defineModule("contexts", {
11     exports: ["Contexts", "Group", "contexts"],
12     use: ["commands", "messages", "options", "services", "storage", "styles", "template", "util"]
13 }, this);
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     },
27
28     modifiable: true,
29
30     cleanup: function cleanup() {
31         for (let hive in values(this.hives))
32             util.trapErrors("cleanup", hive);
33
34         this.hives = [];
35         for (let hive in keys(this.hiveMap))
36             delete this[hive];
37     },
38     destroy: function destroy() {
39         for (let hive in values(this.hives))
40             util.trapErrors("destroy", hive);
41     },
42
43     argsExtra: function argsExtra() ({}),
44
45     get toStringParams() [this.name],
46
47     get builtin() this.modules.contexts.builtinGroups.indexOf(this) >= 0,
48
49 }, {
50     compileFilter: function (patterns, default_) {
51         if (arguments.length < 2)
52             default_ = false;
53
54         function siteFilter(uri)
55             let (match = array.nth(siteFilter.filters, function (f) f(uri), 0))
56                 match ? match.result : default_;
57
58         return update(siteFilter, {
59             toString: function () this.filters.join(","),
60
61             toXML: function (modules) let (uri = modules && modules.buffer.uri)
62                 template.map(this.filters,
63                              function (f) <span highlight={uri && f(uri) ? "Filter" : ""}>{f}</span>,
64                              <>,</>),
65
66             filters: Option.parse.sitelist(patterns)
67         });
68     },
69
70     defaultFilter: Class.memoize(function () this.compileFilter(["*"]))
71 });
72
73 var Contexts = Module("contexts", {
74     Local: function Local(dactyl, modules, window) ({
75         init: function () {
76             const contexts = this;
77             this.modules = modules;
78
79             Object.defineProperty(modules.plugins, "contexts", Const({}));
80
81             this.groupList = [];
82             this.groupMap = {};
83             this.groupsProto = {};
84             this.hives = {};
85             this.hiveProto = {};
86
87             this.builtin = this.addGroup("builtin", "Builtin items");
88             this.user = this.addGroup("user", "User-defined items", null, true);
89             this.builtinGroups = [this.builtin, this.user];
90             this.builtin.modifiable = false;
91
92             this.GroupFlag = Class("GroupFlag", CommandOption, {
93                 init: function (name) {
94                     this.name = name;
95
96                     this.type = ArgType("group", function (group) {
97                         return isString(group) ? contexts.getGroup(group, name)
98                                                : group[name];
99                     });
100                 },
101
102                 get toStringParams() [this.name],
103
104                 names: ["-group", "-g"],
105
106                 description: "Group to which to add",
107
108                 get default() (contexts.context && contexts.context.group || contexts.user)[this.name],
109
110                 completer: function (context) modules.completion.group(context)
111             });
112         },
113
114         cleanup: function () {
115             for (let hive in values(this.groupList))
116                 util.trapErrors("cleanup", hive);
117         },
118
119         destroy: function () {
120             for (let hive in values(this.groupList))
121                 util.trapErrors("destroy", hive);
122
123             for (let [name, plugin] in iter(this.modules.plugins.contexts))
124                 if (plugin && "onUnload" in plugin)
125                     util.trapErrors("onUnload", plugin);
126         },
127
128         signals: {
129             "browser.locationChange": function (webProgress, request, uri) {
130                 this.flush();
131             }
132         },
133
134         Group: Class("Group", Group, { modules: modules, get hiveMap() modules.contexts.hives }),
135
136         Hives: Class("Hives", Class.Property, {
137             init: function init(name, constructor) {
138                 const { contexts } = modules;
139                 const self = this;
140
141                 if (this.Hive)
142                     return {
143                         enumerable: true,
144
145                         get: function () array(contexts.groups[self.name])
146                     };
147
148                 this.Hive = constructor;
149                 this.name = name;
150                 memoize(contexts.Group.prototype, name, function () {
151                     let group = constructor(this);
152                     this.hives.push(group);
153                     contexts.flush();
154                     return group;
155                 });
156
157                 memoize(contexts.hives, name,
158                         function () Object.create(Object.create(contexts.hiveProto,
159                                                                 { _hive: { value: name } })));
160
161                 memoize(contexts.groupsProto, name,
162                         function () [group[name] for (group in values(this.groups)) if (set.has(group, name))]);
163             },
164
165             get toStringParams() [this.name, this.Hive]
166         })
167     }),
168
169     Context: function Context(file, group, args) {
170         const { contexts, io, newContext, plugins, userContext } = this.modules;
171
172         let isPlugin = array.nth(io.getRuntimeDirectories("plugins"),
173                                  function (dir) dir.contains(file, true),
174                                  0);
175         let isRuntime = array.nth(io.getRuntimeDirectories(""),
176                                   function (dir) dir.contains(file, true),
177                                   0);
178
179         let contextPath = file.path;
180         let self = set.has(plugins, contextPath) && plugins.contexts[contextPath];
181
182         if (self) {
183             if (set.has(self, "onUnload"))
184                 self.onUnload();
185         }
186         else {
187             let name = isPlugin ? file.getRelativeDescriptor(isPlugin).replace(File.PATH_SEP, "-")
188                                 : file.leafName;
189
190             self = update(newContext.apply(null, args || [userContext]), {
191                 NAME: Const(name.replace(/\.[^.]*$/, "").replace(/-([a-z])/g, function (m, n1) n1.toUpperCase())),
192
193                 PATH: Const(file.path),
194
195                 CONTEXT: Const(self),
196
197                 unload: Const(function unload() {
198                     if (plugins[this.NAME] === this || plugins[this.PATH] === this)
199                         if (this.onUnload)
200                             this.onUnload();
201
202                     if (plugins[this.NAME] === this)
203                         delete plugins[this.NAME];
204
205                     if (plugins[this.PATH] === this)
206                         delete plugins[this.PATH];
207
208                     if (plugins.contexts[contextPath] === this)
209                         delete plugins.contexts[contextPath];
210
211                     if (!this.GROUP.builtin)
212                         contexts.removeGroup(this.GROUP);
213                 })
214             });
215             Class.replaceProperty(plugins, file.path, self);
216
217             // This belongs elsewhere
218             if (isPlugin && args)
219                 Object.defineProperty(plugins, self.NAME, {
220                     configurable: true,
221                     enumerable: true,
222                     get: function () self,
223                     set: function (val) {
224                         util.dactyl(val).reportError(FailedAssertion(_("plugin.notReplacingContext", self.NAME), 3, false), true);
225                     }
226                 });
227         }
228
229         let path = isRuntime ? file.getRelativeDescriptor(isRuntime) : file.path;
230         let name = isRuntime ? path.replace(/^(plugin|color)s([\\\/])/, "$1$2") : "script-" + path;
231
232         if (!group)
233             group = this.addGroup(commands.nameRegexp
234                                           .iterate(name.replace(/\.[^.]*$/, ""))
235                                           .join("-").replace(/--+/g, "-"),
236                                   "Script group for " + file.path,
237                                   null, false);
238
239         Class.replaceProperty(self, "GROUP", group);
240         Class.replaceProperty(self, "group", group);
241
242         return plugins.contexts[contextPath] = self;
243     },
244
245     Script: function Script(file, group) {
246         return this.Context(file, group, [this.modules.userContext, true]);
247     },
248
249     context: null,
250
251     /**
252      * Returns a frame object describing the currently executing
253      * command, if applicable, otherwise returns the passed frame.
254      *
255      * @param {nsIStackFrame} frame
256      */
257     getCaller: function getCaller(frame) {
258         if (this.context && this.context.file)
259            return {
260                 __proto__: frame,
261                 filename: this.context.file[0] == "[" ? this.context.file
262                                                       : services.io.newFileURI(File(this.context.file)).spec,
263                 lineNumber: this.context.line
264             };
265         return frame;
266     },
267
268     groups: Class.memoize(function () this.matchingGroups(this.modules.buffer.uri)),
269
270     allGroups: Class.memoize(function () Object.create(this.groupsProto, {
271         groups: { value: this.initializedGroups() }
272     })),
273
274     matchingGroups: function (uri) Object.create(this.groupsProto, {
275         groups: { value: this.activeGroups(uri) },
276     }),
277
278     activeGroups: function (uri, doc) {
279         if (!uri)
280             ({ uri, doc }) = this.modules.buffer;
281         return this.initializedGroups().filter(function (g) uri && g.filter(uri, doc));
282     },
283
284     flush: function flush() {
285         delete this.groups;
286         delete this.allGroups;
287     },
288
289     initializedGroups: function (hive)
290         let (need = hive ? [hive] : Object.keys(this.hives))
291             this.groupList.filter(function (group) need.some(set.has(group))),
292
293     addGroup: function addGroup(name, description, filter, persist, replace) {
294         let group = this.getGroup(name);
295         if (group)
296             name = group.name;
297
298         if (!group) {
299             group = this.Group(name, description, filter, persist);
300             this.groupList.unshift(group);
301             this.groupMap[name] = group;
302             this.hiveProto.__defineGetter__(name, function () group[this._hive]);
303         }
304
305         if (replace) {
306             util.trapErrors("cleanup", group);
307             if (description)
308                 group.description = description;
309             if (filter)
310                 group.filter = filter
311             group.persist = persist;
312         }
313
314         this.flush();
315         return group;
316     },
317
318     removeGroup: function removeGroup(name, filter) {
319         if (isObject(name)) {
320             if (this.groupList.indexOf(name) === -1)
321                 return;
322             name = name.name;
323         }
324
325         let group = this.getGroup(name);
326
327         util.assert(!group || !group.builtin, _("group.cantRemoveBuiltin"));
328
329         if (group) {
330             name = group.name;
331             this.groupList.splice(this.groupList.indexOf(group), 1);
332             util.trapErrors("destroy", group);
333         }
334
335         if (this.context && this.context.group === group)
336             this.context.group = null;
337
338         delete this.groupMap[name];
339         delete this.hiveProto[name];
340         this.flush();
341         return group;
342     },
343
344     getGroup: function getGroup(name, hive) {
345         if (name === "default")
346             var group = this.context && this.context.context && this.context.context.GROUP;
347         else if (set.has(this.groupMap, name))
348             group = this.groupMap[name];
349
350         if (group && hive)
351             return group[hive];
352         return group;
353     },
354
355     bindMacro: function (args, default_, params) {
356         const { dactyl, events, modules } = this.modules;
357
358         let process = util.identity;
359
360         if (callable(params))
361             var makeParams = function makeParams(self, args)
362                 iter.toObject([k, process(v)]
363                                for ([k, v] in iter(params.apply(self, args))));
364         else if (params)
365             makeParams = function makeParams(self, args)
366                 iter.toObject([name, process(args[i])]
367                               for ([i, name] in Iterator(params)));
368
369         let rhs = args.literalArg;
370         let type = ["-builtin", "-ex", "-javascript", "-keys"].reduce(function (a, b) args[b] ? b : a, default_);
371         switch (type) {
372         case "-builtin":
373             let noremap = true;
374             /* fallthrough */
375         case "-keys":
376             let silent = args["-silent"];
377             rhs = events.canonicalKeys(rhs, true);
378             var action = function action() {
379                 events.feedkeys(action.macro(makeParams(this, arguments)),
380                                 noremap, silent);
381             }
382             action.macro = util.compileMacro(rhs, true);
383             break;
384         case "-ex":
385             action = function action() modules.commands
386                                               .execute(action.macro, makeParams(this, arguments),
387                                                        false, null, action.context);
388             action.macro = util.compileMacro(rhs, true);
389             action.context = this.context && update({}, this.context);
390             break;
391         case "-javascript":
392             if (callable(params))
393                 action = dactyl.userEval("(function action() { with (action.makeParams(this, arguments)) {" + args.literalArg + "} })");
394             else
395                 action = dactyl.userFunc.apply(dactyl, params.concat(args.literalArg).array);
396             process = function (param) isObject(param) && param.valueOf ? param.valueOf() : param;
397             action.params = params;
398             action.makeParams = makeParams;
399             break;
400         }
401         action.toString = function toString() (type === default_ ? "" : type + " ") + rhs;
402         args = null;
403         return action;
404     },
405
406     withContext: function withContext(defaults, callback, self)
407         this.withSavedValues(["context"], function () {
408             this.context = defaults && update({}, defaults);
409             return callback.call(self, this.context);
410         })
411 }, {
412     Hive: Class("Hive", {
413         init: function init(group) {
414             this.group = group;
415         },
416
417         cleanup: function cleanup() {},
418         destroy: function destroy() {},
419
420         get modifiable() this.group.modifiable,
421
422         get argsExtra() this.group.argsExtra,
423         get builtin() this.group.builtin,
424
425         get name() this.group.name,
426         set name(val) this.group.name = val,
427
428         get description() this.group.description,
429         set description(val) this.group.description = val,
430
431         get filter() this.group.filter,
432         set filter(val) this.group.filter = val,
433
434         get persist() this.group.persist,
435         set persist(val) this.group.persist = val,
436
437         prefix: Class.memoize(function () this.name === "builtin" ? "" : this.name + ":"),
438
439         get toStringParams() [this.name]
440     })
441 }, {
442     commands: function initCommands(dactyl, modules, window) {
443         const { commands, contexts } = modules;
444
445         commands.add(["gr[oup]"],
446             "Create or select a group",
447             function (args) {
448                 if (args.length > 0) {
449                     var name = Option.dequote(args[0]);
450                     util.assert(name !== "builtin", _("group.cantModifyBuiltin"));
451                     util.assert(commands.validName.test(name), _("group.invalidName", name));
452
453                     var group = contexts.getGroup(name);
454                 }
455                 else if (args.bang)
456                     var group = args.context && args.context.group;
457                 else
458                     return void modules.completion.listCompleter("group", "", null, null);
459
460                 util.assert(group || name, _("group.noCurrent"));
461
462                 let filter = Group.compileFilter(args["-locations"]);
463                 if (!group || args.bang)
464                     group = contexts.addGroup(name, args["-description"], filter, !args["-nopersist"], args.bang);
465                 else if (!group.builtin) {
466                     if (args.has("-locations"))
467                         group.filter = filter;
468                     if (args.has("-description"))
469                         group.description = args["-description"]
470                     if (args.has("-nopersist"))
471                         group.persist = !args["-nopersist"]
472                 }
473
474                 if (!group.builtin && args.has("-args")) {
475                     group.argsExtra = contexts.bindMacro({ literalArg: "return " + args["-args"] },
476                                                          "-javascript", util.identity);
477                     group.args = args["-args"];
478                 }
479
480                 if (args.context)
481                     args.context.group = group;
482
483                 util.assert(!group.builtin ||
484                                 !["-description", "-locations", "-nopersist"]
485                                     .some(set.has(args.explicitOpts)),
486                             _("group.cantModifyBuiltin"));
487             },
488             {
489                 argCount: "?",
490                 bang: true,
491                 completer: function (context, args) {
492                     if (args.length == 1)
493                         modules.completion.group(context);
494                 },
495                 keepQuotes: true,
496                 options: [
497                     {
498                         names: ["-args", "-a"],
499                         description: "JavaScript Object which augments the arguments passed to commands, mappings, and autocommands",
500                         type: CommandOption.STRING
501                     },
502                     {
503                         names: ["-description", "-desc", "-d"],
504                         description: "A description of this group",
505                         default: ["User-defined group"],
506                         type: CommandOption.STRING
507                     },
508                     {
509                         names: ["-locations", "-locs", "-loc", "-l"],
510                         description: ["The URLs for which this group should be active"],
511                         default: ["*"],
512                         type: CommandOption.LIST
513                     },
514                     {
515                         names: ["-nopersist", "-n"],
516                         description: "Do not save this group to an auto-generated RC file"
517                     }
518                 ],
519                 serialGroup: 20,
520                 serialize: function () [
521                     {
522                         command: this.name,
523                         bang: true,
524                         options: iter([v, typeof group[k] == "boolean" ? null : group[k]]
525                                       // FIXME: this map is expressed multiple times
526                                       for ([k, v] in Iterator({
527                                           args: "-args",
528                                           description: "-description",
529                                           filter: "-locations"
530                                       }))
531                                       if (group[k])).toObject(),
532                         arguments: [group.name],
533                         ignoreDefaults: true
534                     }
535                     for (group in values(contexts.initializedGroups()))
536                     if (!group.builtin && group.persist)
537                 ].concat([{ command: this.name, arguments: ["user"] }])
538             });
539
540         commands.add(["delg[roup]"],
541             "Delete a group",
542             function (args) {
543                 util.assert(contexts.getGroup(args[0]), _("group.noSuch", args[0]));
544                 contexts.removeGroup(args[0]);
545             },
546             {
547                 argCount: "1",
548                 completer: function (context, args) {
549                     modules.completion.group(context);
550                     context.filters.push(function ({ item }) !item.builtin);
551                 }
552             });
553
554         commands.add(["fini[sh]"],
555             "Stop sourcing a script file",
556             function (args) {
557                 util.assert(args.context, _("command.finish.illegal"));
558                 args.context.finished = true;
559             },
560             { argCount: "0" });
561
562         function checkStack(cmd) {
563             util.assert(contexts.context && contexts.context.stack &&
564                         contexts.context.stack[cmd] && contexts.context.stack[cmd].length,
565                         _("command.conditional.illegal"));
566         }
567         function pop(cmd) {
568             checkStack(cmd);
569             return contexts.context.stack[cmd].pop();
570         }
571         function push(cmd, value) {
572             util.assert(contexts.context, _("command.conditional.illegal"));
573             if (arguments.length < 2)
574                 value = contexts.context.noExecute;
575             contexts.context.stack = contexts.context.stack || {};
576             contexts.context.stack[cmd] = (contexts.context.stack[cmd] || []).concat([value]);
577         }
578
579         commands.add(["if"],
580             "Execute commands until the next :elseif, :else, or :endif only if the argument returns true",
581             function (args) { args.context.noExecute = !dactyl.userEval(args[0]); },
582             {
583                 always: function (args) { push("if"); },
584                 argCount: "1",
585                 literal: 0
586             });
587         commands.add(["elsei[f]", "elif"],
588             "Execute commands until the next :elseif, :else, or :endif only if the argument returns true",
589             function (args) {},
590             {
591                 always: function (args) {
592                     checkStack("if");
593                     args.context.noExecute = args.context.stack.if.slice(-1)[0] ||
594                         !args.context.noExecute || !dactyl.userEval(args[0]);
595                 },
596                 argCount: "1",
597                 literal: 0
598             });
599         commands.add(["el[se]"],
600             "Execute commands until the next :endif only if the previous conditionals were not executed",
601             function (args) {},
602             {
603                 always: function (args) {
604                     checkStack("if");
605                     args.context.noExecute = args.context.stack.if.slice(-1)[0] ||
606                         !args.context.noExecute;
607                 },
608                 argCount: "0"
609             });
610         commands.add(["en[dif]", "fi"],
611             "End a string of :if/:elseif/:else conditionals",
612             function (args) {},
613             {
614                 always: function (args) { args.context.noExecute = pop("if"); },
615                 argCount: "0"
616             });
617     },
618     completion: function initCompletion(dactyl, modules, window) {
619         const { completion, contexts } = modules;
620
621         completion.group = function group(context, active) {
622             context.title = ["Group"];
623             let uri = modules.buffer.uri;
624             context.keys = {
625                 active: function (group) group.filter(uri),
626                 text: "name",
627                 description: function (g) <>{g.filter.toXML ? g.filter.toXML(modules) + <>&#xa0;</> : ""}{g.description || ""}</>
628             };
629             context.completions = (active === undefined ? contexts.groupList : contexts.initializedGroups(active))
630                                     .slice(0, -1);
631
632             iter({ Active: true, Inactive: false }).forEach(function ([name, active]) {
633                 context.split(name, null, function (context) {
634                     context.title[0] = name + " Groups";
635                     context.filters.push(function ({ item }) !!item.filter(modules.buffer.uri) == active);
636                 });
637             });
638         };
639     }
640 });
641
642 endModule();
643
644 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
645
646 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: