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.
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"]
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;
30 cleanup: function cleanup() {
31 for (let hive in values(this.hives))
32 util.trapErrors("cleanup", hive);
35 for (let hive in keys(this.hiveMap))
38 destroy: function destroy() {
39 for (let hive in values(this.hives))
40 util.trapErrors("destroy", hive);
43 argsExtra: function argsExtra() ({}),
45 get toStringParams() [this.name],
47 get builtin() this.modules.contexts.builtinGroups.indexOf(this) >= 0,
50 compileFilter: function (patterns, default_) {
51 if (arguments.length < 2)
54 function siteFilter(uri)
55 let (match = array.nth(siteFilter.filters, function (f) f(uri), 0))
56 match ? match.result : default_;
58 return update(siteFilter, {
59 toString: function () this.filters.join(","),
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>,
66 filters: Option.parse.sitelist(patterns)
70 defaultFilter: Class.memoize(function () this.compileFilter(["*"]))
73 var Contexts = Module("contexts", {
74 Local: function Local(dactyl, modules, window) ({
76 const contexts = this;
77 this.modules = modules;
79 Object.defineProperty(modules.plugins, "contexts", Const({}));
83 this.groupsProto = {};
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;
92 this.GroupFlag = Class("GroupFlag", CommandOption, {
93 init: function (name) {
96 this.type = ArgType("group", function (group) {
97 return isString(group) ? contexts.getGroup(group, name)
102 get toStringParams() [this.name],
104 names: ["-group", "-g"],
106 description: "Group to which to add",
108 get default() (contexts.context && contexts.context.group || contexts.user)[this.name],
110 completer: function (context) modules.completion.group(context)
114 cleanup: function () {
115 for (let hive in values(this.groupList))
116 util.trapErrors("cleanup", hive);
119 destroy: function () {
120 for (let hive in values(this.groupList))
121 util.trapErrors("destroy", hive);
123 for (let [name, plugin] in iter(this.modules.plugins.contexts))
124 if (plugin && "onUnload" in plugin)
125 util.trapErrors("onUnload", plugin);
129 "browser.locationChange": function (webProgress, request, uri) {
134 Group: Class("Group", Group, { modules: modules, get hiveMap() modules.contexts.hives }),
136 Hives: Class("Hives", Class.Property, {
137 init: function init(name, constructor) {
138 const { contexts } = modules;
145 get: function () array(contexts.groups[self.name])
148 this.Hive = constructor;
150 memoize(contexts.Group.prototype, name, function () {
151 let group = constructor(this);
152 this.hives.push(group);
157 memoize(contexts.hives, name,
158 function () Object.create(Object.create(contexts.hiveProto,
159 { _hive: { value: name } })));
161 memoize(contexts.groupsProto, name,
162 function () [group[name] for (group in values(this.groups)) if (set.has(group, name))]);
165 get toStringParams() [this.name, this.Hive]
169 Context: function Context(file, group, args) {
170 const { contexts, io, newContext, plugins, userContext } = this.modules;
172 let isPlugin = array.nth(io.getRuntimeDirectories("plugins"),
173 function (dir) dir.contains(file, true),
175 let isRuntime = array.nth(io.getRuntimeDirectories(""),
176 function (dir) dir.contains(file, true),
179 let contextPath = file.path;
180 let self = set.has(plugins, contextPath) && plugins.contexts[contextPath];
183 if (set.has(self, "onUnload"))
187 let name = isPlugin ? file.getRelativeDescriptor(isPlugin).replace(File.PATH_SEP, "-")
190 self = update(newContext.apply(null, args || [userContext]), {
191 NAME: Const(name.replace(/\.[^.]*$/, "").replace(/-([a-z])/g, function (m, n1) n1.toUpperCase())),
193 PATH: Const(file.path),
195 CONTEXT: Const(self),
197 unload: Const(function unload() {
198 if (plugins[this.NAME] === this || plugins[this.PATH] === this)
202 if (plugins[this.NAME] === this)
203 delete plugins[this.NAME];
205 if (plugins[this.PATH] === this)
206 delete plugins[this.PATH];
208 if (plugins.contexts[contextPath] === this)
209 delete plugins.contexts[contextPath];
211 if (!this.GROUP.builtin)
212 contexts.removeGroup(this.GROUP);
215 Class.replaceProperty(plugins, file.path, self);
217 // This belongs elsewhere
218 if (isPlugin && args)
219 Object.defineProperty(plugins, self.NAME, {
222 get: function () self,
223 set: function (val) {
224 util.dactyl(val).reportError(FailedAssertion(_("plugin.notReplacingContext", self.NAME), 3, false), true);
229 let path = isRuntime ? file.getRelativeDescriptor(isRuntime) : file.path;
230 let name = isRuntime ? path.replace(/^(plugin|color)s([\\\/])/, "$1$2") : "script-" + path;
233 group = this.addGroup(commands.nameRegexp
234 .iterate(name.replace(/\.[^.]*$/, ""))
235 .join("-").replace(/--+/g, "-"),
236 "Script group for " + file.path,
239 Class.replaceProperty(self, "GROUP", group);
240 Class.replaceProperty(self, "group", group);
242 return plugins.contexts[contextPath] = self;
245 Script: function Script(file, group) {
246 return this.Context(file, group, [this.modules.userContext, true]);
252 * Returns a frame object describing the currently executing
253 * command, if applicable, otherwise returns the passed frame.
255 * @param {nsIStackFrame} frame
257 getCaller: function getCaller(frame) {
258 if (this.context && this.context.file)
261 filename: this.context.file[0] == "[" ? this.context.file
262 : services.io.newFileURI(File(this.context.file)).spec,
263 lineNumber: this.context.line
268 groups: Class.memoize(function () this.matchingGroups(this.modules.buffer.uri)),
270 allGroups: Class.memoize(function () Object.create(this.groupsProto, {
271 groups: { value: this.initializedGroups() }
274 matchingGroups: function (uri) Object.create(this.groupsProto, {
275 groups: { value: this.activeGroups(uri) },
278 activeGroups: function (uri, doc) {
280 ({ uri, doc }) = this.modules.buffer;
281 return this.initializedGroups().filter(function (g) uri && g.filter(uri, doc));
284 flush: function flush() {
286 delete this.allGroups;
289 initializedGroups: function (hive)
290 let (need = hive ? [hive] : Object.keys(this.hives))
291 this.groupList.filter(function (group) need.some(set.has(group))),
293 addGroup: function addGroup(name, description, filter, persist, replace) {
294 let group = this.getGroup(name);
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]);
306 util.trapErrors("cleanup", group);
308 group.description = description;
310 group.filter = filter
311 group.persist = persist;
318 removeGroup: function removeGroup(name, filter) {
319 if (isObject(name)) {
320 if (this.groupList.indexOf(name) === -1)
325 let group = this.getGroup(name);
327 util.assert(!group || !group.builtin, _("group.cantRemoveBuiltin"));
331 this.groupList.splice(this.groupList.indexOf(group), 1);
332 util.trapErrors("destroy", group);
335 if (this.context && this.context.group === group)
336 this.context.group = null;
338 delete this.groupMap[name];
339 delete this.hiveProto[name];
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];
355 bindMacro: function (args, default_, params) {
356 const { dactyl, events, modules } = this.modules;
358 let process = util.identity;
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))));
365 makeParams = function makeParams(self, args)
366 iter.toObject([name, process(args[i])]
367 for ([i, name] in Iterator(params)));
369 let rhs = args.literalArg;
370 let type = ["-builtin", "-ex", "-javascript", "-keys"].reduce(function (a, b) args[b] ? b : a, default_);
376 let silent = args["-silent"];
377 rhs = events.canonicalKeys(rhs, true);
378 var action = function action() {
379 events.feedkeys(action.macro(makeParams(this, arguments)),
382 action.macro = util.compileMacro(rhs, true);
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);
392 if (callable(params))
393 action = dactyl.userEval("(function action() { with (action.makeParams(this, arguments)) {" + args.literalArg + "} })");
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;
401 action.toString = function toString() (type === default_ ? "" : type + " ") + rhs;
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);
412 Hive: Class("Hive", {
413 init: function init(group) {
417 cleanup: function cleanup() {},
418 destroy: function destroy() {},
420 get modifiable() this.group.modifiable,
422 get argsExtra() this.group.argsExtra,
423 get builtin() this.group.builtin,
425 get name() this.group.name,
426 set name(val) this.group.name = val,
428 get description() this.group.description,
429 set description(val) this.group.description = val,
431 get filter() this.group.filter,
432 set filter(val) this.group.filter = val,
434 get persist() this.group.persist,
435 set persist(val) this.group.persist = val,
437 prefix: Class.memoize(function () this.name === "builtin" ? "" : this.name + ":"),
439 get toStringParams() [this.name]
442 commands: function initCommands(dactyl, modules, window) {
443 const { commands, contexts } = modules;
445 commands.add(["gr[oup]"],
446 "Create or select a group",
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));
453 var group = contexts.getGroup(name);
456 var group = args.context && args.context.group;
458 return void modules.completion.listCompleter("group", "", null, null);
460 util.assert(group || name, _("group.noCurrent"));
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"]
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"];
481 args.context.group = group;
483 util.assert(!group.builtin ||
484 !["-description", "-locations", "-nopersist"]
485 .some(set.has(args.explicitOpts)),
486 _("group.cantModifyBuiltin"));
491 completer: function (context, args) {
492 if (args.length == 1)
493 modules.completion.group(context);
498 names: ["-args", "-a"],
499 description: "JavaScript Object which augments the arguments passed to commands, mappings, and autocommands",
500 type: CommandOption.STRING
503 names: ["-description", "-desc", "-d"],
504 description: "A description of this group",
505 default: ["User-defined group"],
506 type: CommandOption.STRING
509 names: ["-locations", "-locs", "-loc", "-l"],
510 description: ["The URLs for which this group should be active"],
512 type: CommandOption.LIST
515 names: ["-nopersist", "-n"],
516 description: "Do not save this group to an auto-generated RC file"
520 serialize: function () [
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({
528 description: "-description",
531 if (group[k])).toObject(),
532 arguments: [group.name],
535 for (group in values(contexts.initializedGroups()))
536 if (!group.builtin && group.persist)
537 ].concat([{ command: this.name, arguments: ["user"] }])
540 commands.add(["delg[roup]"],
543 util.assert(contexts.getGroup(args[0]), _("group.noSuch", args[0]));
544 contexts.removeGroup(args[0]);
548 completer: function (context, args) {
549 modules.completion.group(context);
550 context.filters.push(function ({ item }) !item.builtin);
554 commands.add(["fini[sh]"],
555 "Stop sourcing a script file",
557 util.assert(args.context, _("command.finish.illegal"));
558 args.context.finished = true;
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"));
569 return contexts.context.stack[cmd].pop();
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]);
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]); },
583 always: function (args) { push("if"); },
587 commands.add(["elsei[f]", "elif"],
588 "Execute commands until the next :elseif, :else, or :endif only if the argument returns true",
591 always: function (args) {
593 args.context.noExecute = args.context.stack.if.slice(-1)[0] ||
594 !args.context.noExecute || !dactyl.userEval(args[0]);
599 commands.add(["el[se]"],
600 "Execute commands until the next :endif only if the previous conditionals were not executed",
603 always: function (args) {
605 args.context.noExecute = args.context.stack.if.slice(-1)[0] ||
606 !args.context.noExecute;
610 commands.add(["en[dif]", "fi"],
611 "End a string of :if/:elseif/:else conditionals",
614 always: function (args) { args.context.noExecute = pop("if"); },
618 completion: function initCompletion(dactyl, modules, window) {
619 const { completion, contexts } = modules;
621 completion.group = function group(context, active) {
622 context.title = ["Group"];
623 let uri = modules.buffer.uri;
625 active: function (group) group.filter(uri),
627 description: function (g) <>{g.filter.toXML ? g.filter.toXML(modules) + <> </> : ""}{g.description || ""}</>
629 context.completions = (active === undefined ? contexts.groupList : contexts.initializedGroups(active))
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);
644 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
646 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: