1 // Copyright (c) 2010-2011 by Kris Maglione <maglione.k@gmail.com>
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
7 Components.utils.import("resource://dactyl/bootstrap.jsm");
8 defineModule("contexts", {
9 exports: ["Contexts", "Group", "contexts"],
10 require: ["services", "util"]
13 this.lazyRequire("overlay", ["overlay"]);
15 var Const = function Const(val) Class.Property({ enumerable: true, value: val });
17 var Group = Class("Group", {
18 init: function init(name, description, filter, persist) {
22 this.description = description;
23 this.filter = filter || this.constructor.defaultFilter;
24 this.persist = persist || false;
29 get contexts() this.modules.contexts,
31 set lastDocument(val) { this._lastDocument = util.weakReference(val); },
32 get lastDocument() this._lastDocument && this._lastDocument.get(),
36 cleanup: function cleanup(reason) {
37 for (let hive in values(this.hives))
38 util.trapErrors("cleanup", hive);
41 for (let hive in keys(this.hiveMap))
44 if (reason != "shutdown")
45 this.children.splice(0).forEach(this.contexts.closure.removeGroup);
47 destroy: function destroy(reason) {
48 for (let hive in values(this.hives))
49 util.trapErrors("destroy", hive);
51 if (reason != "shutdown")
52 this.children.splice(0).forEach(this.contexts.closure.removeGroup);
55 argsExtra: function argsExtra() ({}),
57 makeArgs: function makeArgs(doc, context, args) {
58 let res = update({ doc: doc, context: context }, args);
59 return update(res, this.argsExtra(res), args);
62 get toStringParams() [this.name],
64 get builtin() this.modules.contexts.builtinGroups.indexOf(this) >= 0,
67 compileFilter: function (patterns, default_) {
68 if (arguments.length < 2)
71 function siteFilter(uri)
72 let (match = array.nth(siteFilter.filters, function (f) f(uri), 0))
73 match ? match.result : default_;
75 return update(siteFilter, {
76 toString: function () this.filters.join(","),
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>,
83 filters: Option.parse.sitelist(patterns)
87 defaultFilter: Class.Memoize(function () this.compileFilter(["*"]))
90 var Contexts = Module("contexts", {
92 this.pluginModules = {};
95 cleanup: function () {
96 for each (let module in this.pluginModules)
97 util.trapErrors("unload", module);
99 this.pluginModules = {};
102 Local: function Local(dactyl, modules, window) ({
104 const contexts = this;
105 this.modules = modules;
107 Object.defineProperty(modules.plugins, "contexts", Const({}));
111 this.groupsProto = {};
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;
120 this.GroupFlag = Class("GroupFlag", CommandOption, {
121 init: function (name) {
124 this.type = ArgType("group", function (group) {
125 return isString(group) ? contexts.getGroup(group, name)
130 get toStringParams() [this.name],
132 names: ["-group", "-g"],
134 description: "Group to which to add",
136 get default() (contexts.context && contexts.context.group || contexts.user)[this.name],
138 completer: function (context) modules.completion.group(context)
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]));
145 cleanup: function () {
146 for each (let hive in this.groupList.slice())
147 util.trapErrors("cleanup", hive, "shutdown");
150 destroy: function () {
151 for each (let hive in values(this.groupList.slice()))
152 util.trapErrors("destroy", hive, "shutdown");
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);
160 "browser.locationChange": function (webProgress, request, uri) {
165 Group: Class("Group", Group, { modules: modules, get hiveMap() modules.contexts.hives }),
167 Hives: Class("Hives", Class.Property, {
168 init: function init(name, constructor) {
169 const { contexts } = modules;
176 get: function () array(contexts.groups[self.name])
179 this.Hive = constructor;
181 memoize(contexts.Group.prototype, name, function () {
182 let group = constructor(this);
183 this.hives.push(group);
188 memoize(contexts.hives, name,
189 function () Object.create(Object.create(contexts.hiveProto,
190 { _hive: { value: name } })));
192 memoize(contexts.groupsProto, name,
193 function () [group[name] for (group in values(this.groups)) if (Set.has(group, name))]);
196 get toStringParams() [this.name, this.Hive]
200 Context: function Context(file, group, args) {
201 const { contexts, io, newContext, plugins, userContext } = this.modules;
203 let isPlugin = array.nth(io.getRuntimeDirectories("plugins"),
204 function (dir) dir.contains(file, true),
206 let isRuntime = array.nth(io.getRuntimeDirectories(""),
207 function (dir) dir.contains(file, true),
210 let name = isPlugin ? file.getRelativeDescriptor(isPlugin).replace(File.PATH_SEP, "-")
212 let id = util.camelCase(name.replace(/\.[^.]*$/, ""));
214 let contextPath = file.path;
215 let self = Set.has(plugins, contextPath) && plugins.contexts[contextPath];
217 if (!self && isPlugin && false)
218 self = Set.has(plugins, id) && plugins[id];
221 if (Set.has(self, "onUnload"))
222 util.trapErrors("onUnload", self);
225 let params = Array.slice(args || [userContext]);
226 params[2] = params[2] || File(file).URI.spec;
228 self = args && !isArray(args) ? args : newContext.apply(null, params);
232 PATH: Const(file.path),
234 CONTEXT: Const(self),
236 set isGlobalModule(val) {
242 unload: Const(function unload() {
243 if (plugins[this.NAME] === this || plugins[this.PATH] === this)
245 util.trapErrors("onUnload", this);
247 if (plugins[this.NAME] === this)
248 delete plugins[this.NAME];
250 if (plugins[this.PATH] === this)
251 delete plugins[this.PATH];
253 if (plugins.contexts[contextPath] === this)
254 delete plugins.contexts[contextPath];
256 if (!this.GROUP.builtin)
257 contexts.removeGroup(this.GROUP);
261 if (group !== this.user)
262 Class.replaceProperty(plugins, file.path, self);
264 // This belongs elsewhere
266 Object.defineProperty(plugins, self.NAME, {
269 get: function () self,
270 set: function (val) {
271 util.dactyl(val).reportError(FailedAssertion(_("plugin.notReplacingContext", self.NAME), 3, false), true);
276 let path = isRuntime ? file.getRelativeDescriptor(isRuntime) : file.path;
277 let name = isRuntime ? path.replace(/^(plugin|color)s([\\\/])/, "$1$2") : "script-" + path;
280 group = this.addGroup(commands.nameRegexp
281 .iterate(name.replace(/\.[^.]*$/, ""))
282 .join("-").replace(/--+/g, "-"),
283 _("context.scriptGroup", file.path),
286 Class.replaceProperty(self, "GROUP", group);
287 Class.replaceProperty(self, "group", group);
289 return plugins.contexts[contextPath] = self;
292 Script: function Script(file, group) {
293 return this.Context(file, group, [this.modules.userContext, true]);
296 Module: function Module(uri, isPlugin) {
297 const { io, plugins } = this.modules;
299 let canonical = uri.spec;
300 if (uri.scheme == "resource")
301 canonical = services["resource:"].resolveURI(uri);
303 if (uri instanceof Ci.nsIFileURL)
304 var file = File(uri.file);
306 let isPlugin = array.nth(io.getRuntimeDirectories("plugins"),
307 function (dir) dir.contains(file, true),
310 let name = isPlugin && file && file.getRelativeDescriptor(isPlugin)
311 .replace(File.PATH_SEP, "-");
312 let id = util.camelCase(name.replace(/\.[^.]*$/, ""));
314 let self = Set.has(this.pluginModules, canonical) && this.pluginModules[canonical];
317 self = Object.create(jsmodules);
322 PATH: Const(file && file.path),
324 CONTEXT: Const(self),
326 get isGlobalModule() true,
327 set isGlobalModule(val) {
328 util.assert(val, "Loading non-global module as global",
332 unload: Const(function unload() {
333 if (contexts.pluginModules[canonical] == this) {
335 util.trapErrors("onUnload", this);
337 delete contexts.pluginModules[canonical];
340 for each (let { plugins } in overlay.modules)
341 if (plugins[this.NAME] == this)
342 delete plugins[this.name];
346 JSMLoader.loadSubScript(uri.spec, self, File.defaultEncoding);
347 this.pluginModules[canonical] = self;
350 // This belongs elsewhere
352 Object.defineProperty(plugins, self.NAME, {
355 get: function () self,
356 set: function (val) {
357 util.dactyl(val).reportError(FailedAssertion(_("plugin.notReplacingContext", self.NAME), 3, false), true);
367 * Returns a frame object describing the currently executing
368 * command, if applicable, otherwise returns the passed frame.
370 * @param {nsIStackFrame} frame
372 getCaller: function getCaller(frame) {
373 if (this.context && this.context.file)
376 filename: this.context.file[0] == "[" ? this.context.file
377 : services.io.newFileURI(File(this.context.file)).spec,
378 lineNumber: this.context.line
383 groups: Class.Memoize(function () this.matchingGroups()),
385 allGroups: Class.Memoize(function () Object.create(this.groupsProto, {
386 groups: { value: this.initializedGroups() }
389 matchingGroups: function (uri) Object.create(this.groupsProto, {
390 groups: { value: this.activeGroups(uri) }
393 activeGroups: function (uri) {
394 if (uri instanceof Ci.nsIDOMDocument)
395 var [doc, uri] = [uri, uri.documentURIObject || util.newURI(uri.documentURI)];
398 var { uri, doc } = this.modules.buffer;
400 return this.initializedGroups().filter(function (g) {
401 let res = uri && g.filter(uri, doc);
403 g.lastDocument = res && doc;
408 flush: function flush() {
410 delete this.allGroups;
413 initializedGroups: function (hive)
414 let (need = hive ? [hive] : Object.keys(this.hives))
415 this.groupList.filter(function (group) need.some(Set.has(group))),
417 addGroup: function addGroup(name, description, filter, persist, replace) {
418 let group = this.getGroup(name);
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]);
430 util.trapErrors("cleanup", group);
433 group.description = description;
435 group.filter = filter;
436 group.persist = persist;
443 removeGroup: function removeGroup(name) {
444 if (isObject(name)) {
445 if (this.groupList.indexOf(name) === -1)
450 let group = this.getGroup(name);
452 util.assert(!group || !group.builtin, _("group.cantRemoveBuiltin"));
456 this.groupList.splice(this.groupList.indexOf(group), 1);
457 util.trapErrors("destroy", group);
460 if (this.context && this.context.group === group)
461 this.context.group = null;
463 delete this.groupMap[name];
464 delete this.hiveProto[name];
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];
480 getDocs: function getDocs(context) {
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);
486 if (typeof context.INFO == "xml")
493 bindMacro: function (args, default_, params) {
494 const { dactyl, events, modules } = this.modules;
496 function Proxy(obj, key) Class.Property({
499 get: function Proxy_get() process(obj[key]),
500 set: function Proxy_set(val) obj[key] = val
503 let process = util.identity;
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)));
510 makeParams = function makeParams(self, args)
511 iter.toObject([name, process(args[i])]
512 for ([i, name] in Iterator(params)));
514 let rhs = args.literalArg;
515 let type = ["-builtin", "-ex", "-javascript", "-keys"].reduce(function (a, b) args[b] ? b : a, default_);
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)),
528 action.macro = util.compileMacro(rhs, true);
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);
540 if (callable(params))
541 action = dactyl.userEval("(function action() { with (action.makeParams(this, arguments)) {" + args.literalArg + "} })");
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;
550 action.toString = function toString() (type === default_ ? "" : type + " ") + rhs;
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);
561 Hive: Class("Hive", {
562 init: function init(group) {
566 cleanup: function cleanup() {},
567 destroy: function destroy() {},
569 get modifiable() this.group.modifiable,
571 get argsExtra() this.group.argsExtra,
572 get makeArgs() this.group.makeArgs,
573 get builtin() this.group.builtin,
575 get name() this.group.name,
576 set name(val) this.group.name = val,
578 get description() this.group.description,
579 set description(val) this.group.description = val,
581 get filter() this.group.filter,
582 set filter(val) this.group.filter = val,
584 get persist() this.group.persist,
585 set persist(val) this.group.persist = val,
587 prefix: Class.Memoize(function () this.name === "builtin" ? "" : this.name + ":"),
589 get toStringParams() [this.name]
592 commands: function initCommands(dactyl, modules, window) {
593 const { commands, contexts } = modules;
595 commands.add(["gr[oup]"],
596 "Create or select a group",
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));
603 var group = contexts.getGroup(name);
606 var group = args.context && args.context.group;
608 return void modules.completion.listCompleter("group", "", null, null);
610 util.assert(group || name, _("group.noCurrent"));
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"];
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"];
631 args.context.group = group;
632 if (args.context.context) {
633 args.context.context.group = group;
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);
644 util.assert(!group.builtin ||
645 !["-description", "-locations", "-nopersist"]
646 .some(Set.has(args.explicitOpts)),
647 _("group.cantModifyBuiltin"));
652 completer: function (context, args) {
653 if (args.length == 1)
654 modules.completion.group(context);
659 names: ["-args", "-a"],
660 description: "JavaScript Object which augments the arguments passed to commands, mappings, and autocommands",
661 type: CommandOption.STRING
664 names: ["-description", "-desc", "-d"],
665 description: "A description of this group",
666 default: ["User-defined group"],
667 type: CommandOption.STRING
670 names: ["-locations", "-locs", "-loc", "-l"],
671 description: ["The URLs for which this group should be active"],
673 type: CommandOption.LIST
676 names: ["-nopersist", "-n"],
677 description: "Do not save this group to an auto-generated RC file"
681 serialize: function () [
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({
689 description: "-description",
692 if (group[k])).toObject(),
693 arguments: [group.name],
696 for (group in values(contexts.initializedGroups()))
697 if (!group.builtin && group.persist)
698 ].concat([{ command: this.name, arguments: ["user"] }])
701 commands.add(["delg[roup]"],
704 util.assert(args.bang ^ !!args[0], _("error.argumentOrBang"));
707 contexts.groupList = contexts.groupList.filter(function (g) g.builtin);
709 util.assert(contexts.getGroup(args[0]), _("group.noSuch", args[0]));
710 contexts.removeGroup(args[0]);
716 completer: function (context, args) {
719 context.filters.push(function ({ item }) !item.builtin);
720 modules.completion.group(context);
724 commands.add(["fini[sh]"],
725 "Stop sourcing a script file",
727 util.assert(args.context, _("command.finish.illegal"));
728 args.context.finished = true;
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"));
739 return contexts.context.stack[cmd].pop();
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]);
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]); },
753 always: function (args) { push("if"); },
757 commands.add(["elsei[f]", "elif"],
758 "Execute commands until the next :elseif, :else, or :endif only if the argument returns true",
761 always: function (args) {
763 args.context.noExecute = args.context.stack.if.slice(-1)[0] ||
764 !args.context.noExecute || !dactyl.userEval(args[0]);
769 commands.add(["el[se]"],
770 "Execute commands until the next :endif only if the previous conditionals were not executed",
773 always: function (args) {
775 args.context.noExecute = args.context.stack.if.slice(-1)[0] ||
776 !args.context.noExecute;
780 commands.add(["en[dif]", "fi"],
781 "End a string of :if/:elseif/:else conditionals",
784 always: function (args) { args.context.noExecute = pop("if"); },
788 completion: function initCompletion(dactyl, modules, window) {
789 const { completion, contexts } = modules;
791 completion.group = function group(context, active) {
792 context.title = ["Group"];
793 let uri = modules.buffer.uri;
795 active: function (group) group.filter(uri),
797 description: function (g) <>{g.filter.toXML ? g.filter.toXML(modules) + <> </> : ""}{g.description || ""}</>
799 context.completions = (active === undefined ? contexts.groupList : contexts.initializedGroups(active))
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);
814 // catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
816 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: