1 // Copyright (c) 2010-2014 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 defineModule("contexts", {
8 exports: ["Contexts", "Group", "contexts"],
9 require: ["services", "util"]
12 lazyRequire("commands", ["ArgType", "CommandOption", "commands"]);
13 lazyRequire("options", ["Option"]);
14 lazyRequire("overlay", ["overlay"]);
15 lazyRequire("storage", ["File"]);
16 lazyRequire("template", ["template"]);
18 var Const = function Const(val) Class.Property({ enumerable: true, value: val });
20 var Group = Class("Group", {
21 init: function init(name, description, filter, persist) {
23 this.description = description;
24 this.filter = filter || this.constructor.defaultFilter;
25 this.persist = persist || false;
30 get contexts() this.modules.contexts,
32 set lastDocument(val) { this._lastDocument = util.weakReference(val); },
33 get lastDocument() this._lastDocument && this._lastDocument.get(),
37 cleanup: function cleanup(reason) {
38 for (let hive in values(this.hives))
39 util.trapErrors("cleanup", hive);
42 for (let hive in keys(this.hiveMap))
45 if (reason != "shutdown")
46 this.children.splice(0).forEach(this.contexts.bound.removeGroup);
48 destroy: function destroy(reason) {
49 for (let hive in values(this.hives))
50 util.trapErrors("destroy", hive);
52 if (reason != "shutdown")
53 this.children.splice(0).forEach(this.contexts.bound.removeGroup);
56 argsExtra: function argsExtra() ({}),
58 makeArgs: function makeArgs(doc, context, args) {
59 let res = update({ doc: doc, context: context }, args);
60 return update(res, this.argsExtra(res), args);
63 get toStringParams() [this.name],
65 get builtin() this.modules.contexts.builtinGroups.indexOf(this) >= 0,
68 compileFilter: function (patterns, default_=false) {
69 function siteFilter(uri)
70 let (match = siteFilter.filters.find(f => f(uri)))
74 return update(siteFilter, {
75 toString: function () this.filters.join(","),
77 toJSONXML: function (modules) let (uri = modules && modules.buffer.uri)
78 template.map(this.filters,
79 f => ["span", { highlight: uri && f(uri) ? "Filter" : "" },
80 ("toJSONXML" in f ? f.toJSONXML()
84 filters: Option.parse.sitelist(patterns)
88 defaultFilter: Class.Memoize(function () this.compileFilter(["*"]))
91 var Contexts = Module("contexts", {
93 this.pluginModules = {};
96 cleanup: function () {
97 for each (let module in this.pluginModules)
98 util.trapErrors("unload", module);
100 this.pluginModules = {};
103 Local: function Local(dactyl, modules, window) ({
105 const contexts = this;
106 this.modules = modules;
108 Object.defineProperty(modules.plugins, "contexts", Const({}));
112 this.groupsProto = {};
116 this.builtin = this.addGroup("builtin", "Builtin items");
117 this.user = this.addGroup("user", "User-defined items", null, true);
118 this.builtinGroups = [this.builtin, this.user];
119 this.builtin.modifiable = false;
121 this.GroupFlag = Class("GroupFlag", CommandOption, {
122 init: function (name) {
125 this.type = ArgType("group", function (group) {
126 return isString(group) ? contexts.getGroup(group, name)
131 get toStringParams() [this.name],
133 names: ["-group", "-g"],
135 description: "Group to which to add",
137 get default() (contexts.context && contexts.context.group || contexts.user)[this.name],
139 completer: function (context) modules.completion.group(context)
142 memoize(modules, "userContext", () => contexts.Context(modules.io.getRCFile("~", true), contexts.user, [modules, false]));
143 memoize(modules, "_userContext", () => modules.userContext);
146 cleanup: function () {
147 for (let hive of this.groupList.slice())
148 util.trapErrors("cleanup", hive, "shutdown");
151 destroy: function () {
152 for (let hive of values(this.groupList.slice()))
153 util.trapErrors("destroy", hive, "shutdown");
155 for each (let plugin in this.modules.plugins.contexts) {
156 if (plugin && "onUnload" in plugin && callable(plugin.onUnload))
157 util.trapErrors("onUnload", plugin);
159 if (isinstance(plugin, ["Sandbox"]))
160 util.trapErrors("nukeSandbox", Cu, plugin);
165 "browser.locationChange": function (webProgress, request, uri) {
170 Group: Class("Group", Group, { modules: modules, get hiveMap() modules.contexts.hives }),
172 Hives: Class("Hives", Class.Property, {
173 init: function init(name, constructor) {
174 const { contexts } = modules;
180 get: () => array(contexts.groups[this.name])
183 this.Hive = constructor;
185 memoize(contexts.Group.prototype, name, function () {
186 let group = constructor(this);
187 this.hives.push(group);
192 memoize(contexts.hives, name,
193 () => Object.create(Object.create(contexts.hiveProto,
194 { _hive: { value: name } })));
196 memoize(contexts.groupsProto, name,
197 function () [group[name]
198 for (group in values(this.groups))
199 if (hasOwnProperty(group, name))]);
202 get toStringParams() [this.name, this.Hive]
206 Context: function Context(file, group, args) {
207 const { contexts, io, newContext, plugins, userContext } = this.modules;
209 let isPlugin = io.getRuntimeDirectories("plugins")
210 .find(dir => dir.contains(file, true));
211 let isRuntime = io.getRuntimeDirectories("")
212 .find(dir => dir.contains(file, true));
214 let name = isPlugin ? file.getRelativeDescriptor(isPlugin).replace(File.PATH_SEP, "-")
216 let id = util.camelCase(name.replace(/\.[^.]*$/, ""));
218 let contextPath = file.path;
219 let self = hasOwnProperty(plugins, contextPath) && plugins.contexts[contextPath];
221 if (!self && isPlugin && false)
222 self = hasOwnProperty(plugins, id) && plugins[id];
225 if (hasOwnProperty(self, "onUnload"))
226 util.trapErrors("onUnload", self);
229 let params = Array.slice(args || [userContext]);
230 params[2] = params[2] || File(file).URI.spec;
232 self = args && !isArray(args) ? args : newContext.apply(null, params);
236 PATH: Const(file.path),
238 CONTEXT: Const(self),
240 set isGlobalModule(val) {
246 unload: Const(function unload() {
247 if (plugins[this.NAME] === this || plugins[this.PATH] === this)
249 util.trapErrors("onUnload", this);
251 if (plugins[this.NAME] === this)
252 delete plugins[this.NAME];
254 if (plugins[this.PATH] === this)
255 delete plugins[this.PATH];
257 if (plugins.contexts[contextPath] === this)
258 delete plugins.contexts[contextPath];
260 if (!this.GROUP.builtin)
261 contexts.removeGroup(this.GROUP);
265 if (group !== this.user)
266 Class.replaceProperty(plugins, file.path, self);
268 // This belongs elsewhere
270 Object.defineProperty(plugins, self.NAME, {
273 get: function () self,
274 set: function (val) {
275 util.dactyl(val).reportError(FailedAssertion(_("plugin.notReplacingContext", self.NAME), 3, false), true);
280 let path = isRuntime ? file.getRelativeDescriptor(isRuntime) : file.path;
281 let name = isRuntime ? path.replace(/^(plugin|color)s([\\\/])/, "$1$2") : "script-" + path;
284 group = this.addGroup(commands.nameRegexp
285 .iterate(name.replace(/\.[^.]*$/, ""))
286 .join("-").replace(/--+/g, "-"),
287 _("context.scriptGroup", file.path),
290 Class.replaceProperty(self, "GROUP", group);
291 Class.replaceProperty(self, "group", group);
293 return plugins.contexts[contextPath] = self;
296 Script: function Script(file, group) {
297 return this.Context(file, group, [this.modules.userContext, true]);
300 Module: function Module(uri, isPlugin) {
301 const { io, plugins } = this.modules;
303 let canonical = uri.spec;
304 if (uri.scheme == "resource")
305 canonical = services["resource:"].resolveURI(uri);
307 if (uri instanceof Ci.nsIFileURL)
308 var file = File(uri.file);
310 let isPlugin = io.getRuntimeDirectories("plugins")
311 .find(dir => dir.contains(file, true));
313 let name = isPlugin && file && file.getRelativeDescriptor(isPlugin)
314 .replace(File.PATH_SEP, "-");
315 let id = util.camelCase(name.replace(/\.[^.]*$/, ""));
317 let self = hasOwnProperty(this.pluginModules, canonical) && this.pluginModules[canonical];
320 self = Object.create(jsmodules);
325 PATH: Const(file && file.path),
327 CONTEXT: Const(self),
329 get isGlobalModule() true,
330 set isGlobalModule(val) {
331 util.assert(val, "Loading non-global module as global",
335 unload: Const(function unload() {
336 if (contexts.pluginModules[canonical] == this) {
338 util.trapErrors("onUnload", this);
340 delete contexts.pluginModules[canonical];
343 for (let { plugins } of overlay.modules)
344 if (plugins[this.NAME] == this)
345 delete plugins[this.name];
349 JSMLoader.loadSubScript(uri.spec, self, File.defaultEncoding);
350 this.pluginModules[canonical] = self;
353 // This belongs elsewhere
355 Object.defineProperty(plugins, self.NAME, {
358 get: function () self,
359 set: function (val) {
360 util.dactyl(val).reportError(FailedAssertion(_("plugin.notReplacingContext", self.NAME), 3, false), true);
370 * Returns a frame object describing the currently executing
371 * command, if applicable, otherwise returns the passed frame.
373 * @param {nsIStackFrame} frame
375 getCaller: function getCaller(frame) {
376 if (this.context && this.context.file)
379 filename: this.context.file[0] == "[" ? this.context.file
380 : File(this.context.file).URI.spec,
381 lineNumber: this.context.line
386 groups: Class.Memoize(function () this.matchingGroups()),
388 allGroups: Class.Memoize(function () Object.create(this.groupsProto, {
389 groups: { value: this.initializedGroups() }
392 matchingGroups: function (uri) Object.create(this.groupsProto, {
393 groups: { value: this.activeGroups(uri) }
396 activeGroups: function (uri) {
397 if (uri instanceof Ci.nsIDOMDocument)
398 var [doc, uri] = [uri, uri.documentURIObject || util.newURI(uri.documentURI)];
401 var { uri, doc } = this.modules.buffer;
403 return this.initializedGroups().filter(function (g) {
404 let res = uri && g.filter(uri, doc);
406 g.lastDocument = res && doc;
411 flush: function flush() {
413 delete this.allGroups;
416 initializedGroups: function (hive)
417 let (need = hive ? [hive] : Object.keys(this.hives))
418 this.groupList.filter(group => need.some(hasOwnProperty.bind(null, group))),
420 addGroup: function addGroup(name, description, filter, persist, replace) {
421 let group = this.getGroup(name);
426 group = this.Group(name, description, filter, persist);
427 this.groupList.unshift(group);
428 this.groupMap[name] = group;
429 this.hiveProto.__defineGetter__(name, function () group[this._hive]);
433 util.trapErrors("cleanup", group);
436 group.description = description;
438 group.filter = filter;
439 group.persist = persist;
446 removeGroup: function removeGroup(name) {
447 if (isObject(name)) {
448 if (this.groupList.indexOf(name) === -1)
453 let group = this.getGroup(name);
455 util.assert(!group || !group.builtin, _("group.cantRemoveBuiltin"));
459 this.groupList.splice(this.groupList.indexOf(group), 1);
460 util.trapErrors("destroy", group);
463 if (this.context && this.context.group === group)
464 this.context.group = null;
466 delete this.groupMap[name];
467 delete this.hiveProto[name];
472 getGroup: function getGroup(name, hive) {
473 if (name === "default")
474 var group = this.context && this.context.context && this.context.context.GROUP;
475 else if (hasOwnProperty(this.groupMap, name))
476 group = this.groupMap[name];
483 getDocs: function getDocs(context) {
485 if (DOM.isJSONXML(context.INFO))
492 bindMacro: function (args, default_, params) {
493 const { dactyl, events, modules } = this.modules;
495 function Proxy(obj, key) Class.Property({
498 get: function Proxy_get() process(obj[key]),
499 set: function Proxy_set(val) obj[key] = val
502 let process = util.identity;
504 if (callable(params))
505 var makeParams = function makeParams(self, args)
506 let (obj = params.apply(self, args))
507 iter.toObject([k, Proxy(obj, k)] for (k in properties(obj)));
509 makeParams = function makeParams(self, args)
510 iter.toObject([name, process(args[i])]
511 for ([i, name] in Iterator(params)));
513 let rhs = args.literalArg;
514 let type = ["-builtin", "-ex", "-javascript", "-keys"].reduce((a, b) => args[b] ? b : a, default_);
521 let silent = args["-silent"];
522 rhs = DOM.Event.canonicalKeys(rhs, true);
523 var action = function action() {
524 events.feedkeys(action.macro(makeParams(this, arguments)),
527 action.macro = util.compileMacro(rhs, true);
531 action = function action() modules.commands
532 .execute(action.macro, makeParams(this, arguments),
533 false, null, action.context);
534 action.macro = util.compileMacro(rhs, true);
535 action.context = this.context && update({}, this.context);
539 if (callable(params))
540 action = dactyl.userEval("(function action() { with (action.makeParams(this, arguments)) {" + args.literalArg + "} })");
542 action = dactyl.userFunc.apply(dactyl, params.concat(args.literalArg));
543 process = param => isObject(param) && param.valueOf ? param.valueOf() : param;
544 action.params = params;
545 action.makeParams = makeParams;
549 action.toString = function toString() (type === default_ ? "" : type + " ") + rhs;
554 withContext: function withContext(defaults, callback, self)
555 this.withSavedValues(["context"], function () {
556 this.context = defaults && update({}, defaults);
557 return callback.call(self, this.context);
560 Hive: Class("Hive", {
561 init: function init(group) {
565 cleanup: function cleanup() {},
566 destroy: function destroy() {},
568 get modifiable() this.group.modifiable,
570 get argsExtra() this.group.argsExtra,
571 get makeArgs() this.group.makeArgs,
572 get builtin() this.group.builtin,
574 get name() this.group.name,
575 set name(val) this.group.name = val,
577 get description() this.group.description,
578 set description(val) this.group.description = val,
580 get filter() this.group.filter,
581 set filter(val) this.group.filter = val,
583 get persist() this.group.persist,
584 set persist(val) this.group.persist = val,
586 prefix: Class.Memoize(function () this.name === "builtin" ? "" : this.name + ":"),
588 get toStringParams() [this.name]
591 commands: function initCommands(dactyl, modules, window) {
592 const { commands, contexts } = modules;
594 commands.add(["gr[oup]"],
595 "Create or select a group",
597 if (args.length > 0) {
598 var name = Option.dequote(args[0]);
599 util.assert(name !== "builtin", _("group.cantModifyBuiltin"));
600 util.assert(commands.validName.test(name), _("group.invalidName", name));
602 var group = contexts.getGroup(name);
605 var group = args.context && args.context.group;
607 return void modules.completion.listCompleter("group", "", null, null);
609 util.assert(group || name, _("group.noCurrent"));
611 let filter = Group.compileFilter(args["-locations"]);
612 if (!group || args.bang)
613 group = contexts.addGroup(name, args["-description"], filter, !args["-nopersist"], args.bang);
614 else if (!group.builtin) {
615 if (args.has("-locations"))
616 group.filter = filter;
617 if (args.has("-description"))
618 group.description = args["-description"];
619 if (args.has("-nopersist"))
620 group.persist = !args["-nopersist"];
623 if (!group.builtin && args.has("-args")) {
624 group.argsExtra = contexts.bindMacro({ literalArg: "return " + args["-args"] },
625 "-javascript", util.identity);
626 group.args = args["-args"];
630 args.context.group = group;
631 if (args.context.context) {
632 args.context.context.group = group;
634 let parent = args.context.context.GROUP;
635 if (parent && parent != group) {
636 group.parent = parent;
637 if (!~parent.children.indexOf(group))
638 parent.children.push(group);
643 util.assert(!group.builtin ||
644 !["-description", "-locations", "-nopersist"]
645 .some(hasOwnProperty.bind(null, args.explicitOpts)),
646 _("group.cantModifyBuiltin"));
651 completer: function (context, args) {
652 if (args.length == 1)
653 modules.completion.group(context);
658 names: ["-args", "-a"],
659 description: "JavaScript Object which augments the arguments passed to commands, mappings, and autocommands",
660 type: CommandOption.STRING
663 names: ["-description", "-desc", "-d"],
664 description: "A description of this group",
665 default: "User-defined group",
666 type: CommandOption.STRING
669 names: ["-locations", "-locs", "-loc", "-l"],
670 description: "The URLs for which this group should be active",
672 type: CommandOption.LIST
675 names: ["-nopersist", "-n"],
676 description: "Do not save this group to an auto-generated RC file"
680 serialize: function () [
684 options: iter([v, typeof group[k] == "boolean" ? null : group[k]]
685 // FIXME: this map is expressed multiple times
686 for ([k, v] in Iterator({
688 description: "-description",
691 if (group[k])).toObject(),
692 arguments: [group.name],
695 for (group in values(contexts.initializedGroups()))
696 if (!group.builtin && group.persist)
697 ].concat([{ command: this.name, arguments: ["user"] }])
700 commands.add(["delg[roup]"],
703 util.assert(args.bang ^ !!args[0], _("error.argumentOrBang"));
706 contexts.groupList = contexts.groupList.filter(g => g.builtin);
708 util.assert(contexts.getGroup(args[0]), _("group.noSuch", args[0]));
709 contexts.removeGroup(args[0]);
715 completer: function (context, args) {
718 context.filters.push(({ item }) => !item.builtin);
719 modules.completion.group(context);
723 commands.add(["fini[sh]"],
724 "Stop sourcing a script file",
726 util.assert(args.context, _("command.finish.illegal"));
727 args.context.finished = true;
731 function checkStack(cmd) {
732 util.assert(contexts.context && contexts.context.stack &&
733 contexts.context.stack[cmd] && contexts.context.stack[cmd].length,
734 _("command.conditional.illegal"));
738 return contexts.context.stack[cmd].pop();
740 function push(cmd, value) {
741 util.assert(contexts.context, _("command.conditional.illegal"));
742 if (arguments.length < 2)
743 value = contexts.context.noExecute;
744 contexts.context.stack = contexts.context.stack || {};
745 contexts.context.stack[cmd] = (contexts.context.stack[cmd] || []).concat([value]);
749 "Execute commands until the next :elseif, :else, or :endif only if the argument returns true",
750 function (args) { args.context.noExecute = !dactyl.userEval(args[0]); },
752 always: function (args) { push("if"); },
756 commands.add(["elsei[f]", "elif"],
757 "Execute commands until the next :elseif, :else, or :endif only if the argument returns true",
760 always: function (args) {
762 args.context.noExecute = args.context.stack.if.slice(-1)[0] ||
763 !args.context.noExecute || !dactyl.userEval(args[0]);
768 commands.add(["el[se]"],
769 "Execute commands until the next :endif only if the previous conditionals were not executed",
772 always: function (args) {
774 args.context.noExecute = args.context.stack.if.slice(-1)[0] ||
775 !args.context.noExecute;
779 commands.add(["en[dif]", "fi"],
780 "End a string of :if/:elseif/:else conditionals",
783 always: function (args) { args.context.noExecute = pop("if"); },
787 completion: function initCompletion(dactyl, modules, window) {
788 const { completion, contexts } = modules;
790 completion.group = function group(context, active) {
791 context.title = ["Group"];
792 let uri = modules.buffer.uri;
794 active: group => group.filter(uri),
796 description: function (g) ["", g.filter.toJSONXML ? g.filter.toJSONXML(modules).concat("\u00a0") : "", g.description || ""]
798 context.completions = (active === undefined ? contexts.groupList : contexts.initializedGroups(active))
801 iter({ Active: true, Inactive: false }).forEach(function ([name, active]) {
802 context.split(name, null, function (context) {
803 context.title[0] = name + " Groups";
804 context.filters.push(({ item }) => !!item.filter(modules.buffer.uri) == active);
813 // catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
815 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: