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