summaryrefslogtreecommitdiff
path: root/common/content
diff options
context:
space:
mode:
Diffstat (limited to 'common/content')
-rw-r--r--common/content/autocommands.js275
-rw-r--r--common/content/base.js293
-rw-r--r--common/content/bookmarks.js1805
-rw-r--r--common/content/browser.js427
-rw-r--r--common/content/buffer.js2955
-rw-r--r--common/content/commandline.js1867
-rw-r--r--common/content/commands.js1895
-rw-r--r--common/content/completion.js1665
-rw-r--r--common/content/editor.js2077
-rw-r--r--common/content/eval.js3
-rw-r--r--common/content/events.js2881
-rw-r--r--common/content/finder.js815
-rw-r--r--common/content/help.js3
-rw-r--r--common/content/hints.js1022
-rw-r--r--common/content/history.js233
-rw-r--r--common/content/io.js2040
-rw-r--r--common/content/liberator-overlay.js38
-rw-r--r--common/content/liberator.js2667
-rw-r--r--common/content/liberator.xul27
-rw-r--r--common/content/mappings.js782
-rw-r--r--common/content/marks.js343
-rw-r--r--common/content/modes.js436
-rw-r--r--common/content/modules.js73
-rw-r--r--common/content/options.js2010
-rw-r--r--common/content/quickmarks.js173
-rw-r--r--common/content/sanitizer.js455
-rw-r--r--common/content/services.js172
-rw-r--r--common/content/statusline.js245
-rw-r--r--common/content/style.js593
-rw-r--r--common/content/tabs.js2110
-rw-r--r--common/content/template.js141
-rw-r--r--common/content/ui.js2312
-rw-r--r--common/content/util.js524
33 files changed, 15711 insertions, 17646 deletions
diff --git a/common/content/autocommands.js b/common/content/autocommands.js
new file mode 100644
index 00000000..5ce1ab49
--- /dev/null
+++ b/common/content/autocommands.js
@@ -0,0 +1,275 @@
+// Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
+//
+// This work is licensed for reuse under an MIT license. Details are
+// given in the LICENSE.txt file included with this file.
+
+
+/** @scope modules */
+
+const AutoCommand = new Struct("event", "pattern", "command");
+
+/**
+ * @instance autocommands
+ */
+const AutoCommands = Module("autocommands", {
+ init: function () {
+ this._store = [];
+ },
+
+ __iterator__: function () util.Array.itervalues(this._store),
+
+ /**
+ * Adds a new autocommand. <b>cmd</b> will be executed when one of the
+ * specified <b>events</b> occurs and the URL of the applicable buffer
+ * matches <b>regex</b>.
+ *
+ * @param {Array} events The array of event names for which this
+ * autocommand should be executed.
+ * @param {string} regex The URL pattern to match against the buffer URL.
+ * @param {string} cmd The Ex command to run.
+ */
+ add: function (events, regex, cmd) {
+ if (typeof events == "string") {
+ events = events.split(",");
+ liberator.log("DEPRECATED: the events list arg to autocommands.add() should be an array of event names");
+ }
+ events.forEach(function (event) {
+ this._store.push(new AutoCommand(event, RegExp(regex), cmd));
+ });
+ },
+
+ /**
+ * Returns all autocommands with a matching <b>event</b> and
+ * <b>regex</b>.
+ *
+ * @param {string} event The event name filter.
+ * @param {string} regex The URL pattern filter.
+ * @returns {AutoCommand[]}
+ */
+ get: function (event, regex) {
+ return this._store.filter(function (autoCmd) matchAutoCmd(autoCmd, event, regex));
+ },
+
+ /**
+ * Deletes all autocommands with a matching <b>event</b> and
+ * <b>regex</b>.
+ *
+ * @param {string} event The event name filter.
+ * @param {string} regex The URL pattern filter.
+ */
+ remove: function (event, regex) {
+ this._store = this._store.filter(function (autoCmd) !matchAutoCmd(autoCmd, event, regex));
+ },
+
+ /**
+ * Lists all autocommands with a matching <b>event</b> and
+ * <b>regex</b>.
+ *
+ * @param {string} event The event name filter.
+ * @param {string} regex The URL pattern filter.
+ */
+ list: function (event, regex) {
+ let cmds = {};
+
+ // XXX
+ this._store.forEach(function (autoCmd) {
+ if (matchAutoCmd(autoCmd, event, regex)) {
+ cmds[autoCmd.event] = cmds[autoCmd.event] || [];
+ cmds[autoCmd.event].push(autoCmd);
+ }
+ });
+
+ let list = template.commandOutput(
+ <table>
+ <tr highlight="Title">
+ <td colspan="2">----- Auto Commands -----</td>
+ </tr>
+ {
+ template.map(cmds, function ([event, items])
+ <tr highlight="Title">
+ <td colspan="2">{event}</td>
+ </tr>
+ +
+ template.map(items, function (item)
+ <tr>
+ <td>&#160;{item.pattern.source}</td>
+ <td>{item.command}</td>
+ </tr>))
+ }
+ </table>);
+
+ commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
+ },
+
+ /**
+ * Triggers the execution of all autocommands registered for
+ * <b>event</b>. A map of <b>args</b> is passed to each autocommand
+ * when it is being executed.
+ *
+ * @param {string} event The event to fire.
+ * @param {Object} args The args to pass to each autocommand.
+ */
+ trigger: function (event, args) {
+ if (options.get("eventignore").has("all", event))
+ return;
+
+ let autoCmds = this._store.filter(function (autoCmd) autoCmd.event == event);
+
+ liberator.echomsg("Executing " + event + " Auto commands for \"*\"", 8);
+
+ let lastPattern = null;
+ let url = args.url || "";
+
+ for (let [, autoCmd] in Iterator(autoCmds)) {
+ if (autoCmd.pattern.test(url)) {
+ if (!lastPattern || lastPattern.source != autoCmd.pattern.source)
+ liberator.echomsg("Executing " + event + " Auto commands for \"" + autoCmd.pattern.source + "\"", 8);
+
+ lastPattern = autoCmd.pattern;
+ liberator.echomsg("autocommand " + autoCmd.command, 9);
+
+ if (typeof autoCmd.command == "function") {
+ try {
+ autoCmd.command.call(autoCmd, args);
+ }
+ catch (e) {
+ liberator.reportError(e);
+ liberator.echoerr(e);
+ }
+ }
+ else
+ liberator.execute(commands.replaceTokens(autoCmd.command, args), null, true);
+ }
+ }
+ }
+}, {
+ matchAutoCmd: function (autoCmd, event, regex) {
+ return (!event || autoCmd.event == event) && (!regex || autoCmd.pattern.source == regex);
+ },
+}, {
+ commands: function () {
+ commands.add(["au[tocmd]"],
+ "Execute commands automatically on events",
+ function (args) {
+ let [event, regex, cmd] = args;
+ let events = [];
+
+ try {
+ RegExp(regex);
+ }
+ catch (e) {
+ liberator.assert(false, "E475: Invalid argument: " + regex);
+ }
+
+ if (event) {
+ // NOTE: event can only be a comma separated list for |:au {event} {pat} {cmd}|
+ let validEvents = config.autocommands.map(function (event) event[0]);
+ validEvents.push("*");
+
+ events = event.split(",");
+ liberator.assert(events.every(function (event) validEvents.indexOf(event) >= 0),
+ "E216: No such group or event: " + event);
+ }
+
+ if (cmd) { // add new command, possibly removing all others with the same event/pattern
+ if (args.bang)
+ autocommands.remove(event, regex);
+ if (args["-javascript"])
+ cmd = eval("(function (args) { with(args) {" + cmd + "} })");
+ autocommands.add(events, regex, cmd);
+ }
+ else {
+ if (event == "*")
+ event = null;
+
+ if (args.bang) {
+ // TODO: "*" only appears to work in Vim when there is a {group} specified
+ if (args[0] != "*" || regex)
+ autocommands.remove(event, regex); // remove all
+ }
+ else
+ autocommands.list(event, regex); // list all
+ }
+ }, {
+ bang: true,
+ completer: function (context) completion.autocmdEvent(context),
+ literal: 2,
+ options: [[["-javascript", "-js"], commands.OPTION_NOARG]]
+ });
+
+ [
+ {
+ name: "do[autocmd]",
+ description: "Apply the autocommands matching the specified URL pattern to the current buffer"
+ }, {
+ name: "doautoa[ll]",
+ description: "Apply the autocommands matching the specified URL pattern to all buffers"
+ }
+ ].forEach(function (command) {
+ commands.add([command.name],
+ command.description,
+ // TODO: Perhaps this should take -args to pass to the command?
+ function (args) {
+ // Vim compatible
+ if (args.length == 0)
+ return void liberator.echomsg("No matching autocommands");
+
+ let [event, url] = args;
+ let defaultURL = url || buffer.URL;
+ let validEvents = config.autocommands.map(function (e) e[0]);
+
+ // TODO: add command validators
+ liberator.assert(event != "*",
+ "E217: Can't execute autocommands for ALL events");
+ liberator.assert(validEvents.indexOf(event) >= 0,
+ "E216: No such group or event: " + args);
+ liberator.assert(autocommands.get(event).some(function (c) c.pattern.test(defaultURL)),
+ "No matching autocommands");
+
+ if (this.name == "doautoall" && liberator.has("tabs")) {
+ let current = tabs.index();
+
+ for (let i = 0; i < tabs.count; i++) {
+ tabs.select(i);
+ // if no url arg is specified use the current buffer's URL
+ autocommands.trigger(event, { url: url || buffer.URL });
+ }
+
+ tabs.select(current);
+ }
+ else
+ autocommands.trigger(event, { url: defaultURL });
+ }, {
+ argCount: "*", // FIXME: kludged for proper error message should be "1".
+ completer: function (context) completion.autocmdEvent(context)
+ });
+ });
+ },
+ completion: function () {
+ completion.setFunctionCompleter(autocommands.get, [function () config.autocommands]);
+
+ completion.autocmdEvent = function autocmdEvent(context) {
+ context.completions = config.autocommands;
+ };
+
+ completion.macro = function macro(context) {
+ context.title = ["Macro", "Keys"];
+ context.completions = [item for (item in events.getMacros())];
+ };
+ },
+ options: function () {
+ options.add(["eventignore", "ei"],
+ "List of autocommand event names which should be ignored",
+ "stringlist", "",
+ {
+ completer: function () config.autocommands.concat([["all", "All events"]]),
+ validator: Option.validateCompleter
+ });
+
+ options.add(["focuscontent", "fc"],
+ "Try to stay in normal mode after loading a web page",
+ "boolean", false);
+ },
+});
+
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/base.js b/common/content/base.js
new file mode 100644
index 00000000..119810c6
--- /dev/null
+++ b/common/content/base.js
@@ -0,0 +1,293 @@
+function array(obj) {
+ if (isgenerator(obj))
+ obj = [k for (k in obj)];
+ else if (obj.length)
+ obj = Array.slice(obj);
+ return util.Array(obj);
+}
+
+function keys(obj) {
+ if ('__iterator__' in obj) {
+ var iter = obj.__iterator__;
+ yield '__iterator__';
+ // This is dangerous, but necessary.
+ delete obj.__iterator__;
+ }
+ for (var k in obj)
+ if (obj.hasOwnProperty(k))
+ yield k;
+ if (iter !== undefined)
+ obj.__iterator__ = iter;
+}
+function values(obj) {
+ for (var k in obj)
+ if (obj.hasOwnProperty(k))
+ yield obj[k];
+}
+function foreach(iter, fn, self) {
+ for (let val in iter)
+ fn.call(self, val);
+}
+
+function dict(ary) {
+ var obj = {};
+ for (var i=0; i < ary.length; i++) {
+ var val = ary[i];
+ obj[val[0]] = val[1];
+ }
+ return obj;
+}
+
+function set(ary) {
+ var obj = {}
+ if (ary)
+ for (var i=0; i < ary.length; i++)
+ obj[ary[i]] = true;
+ return obj;
+}
+set.add = function(set, key) { set[key] = true }
+set.remove = function(set, key) { delete set[key] }
+
+function iter(obj) {
+ if (obj instanceof Ci.nsISimpleEnumerator)
+ return (function () {
+ while (obj.hasMoreElements())
+ yield obj.getNext();
+ })()
+ if (isinstance(obj, [Ci.nsIStringEnumerator, Ci.nsIUTF8StringEnumerator]))
+ return (function () {
+ while (obj.hasMore())
+ yield obj.getNext();
+ })();
+ if (isinstance(obj, Ci.nsIDOMNodeIterator))
+ return (function () {
+ try {
+ while (true)
+ yield obj.nextNode()
+ }
+ catch (e) {}
+ })();
+ if (isinstance(obj, [HTMLCollection, NodeList]))
+ return util.Array.iteritems(obj);
+ if (obj instanceof NamedNodeMap)
+ return (function () {
+ for (let i=0; i < obj.length; i++)
+ yield [obj.name, obj]
+ })();
+ return Iterator(obj);
+}
+
+function issubclass(targ, src) {
+ return src === targ ||
+ targ && typeof targ === "function" && targ.prototype instanceof src;
+}
+
+function isinstance(targ, src) {
+ const types = {
+ boolean: Boolean,
+ string: String,
+ function: Function,
+ number: Number,
+ }
+ src = Array.concat(src);
+ for (var i=0; i < src.length; i++) {
+ if (targ instanceof src[i])
+ return true;
+ var type = types[typeof targ];
+ if (type && issubclass(src[i], type))
+ return true;
+ }
+ return false;
+}
+
+function isobject(obj) {
+ return typeof obj === "object" && obj != null;
+}
+
+function isarray(obj) {
+ return Object.prototype.toString(obj) == "[object Array]";
+}
+
+function isgenerator(val) {
+ return Object.prototype.toString(obj) == "[object Generator]";
+}
+
+function isstring(val) {
+ return typeof val === "string" || val instanceof String;
+}
+
+function callable(val) {
+ return typeof val === "function";
+}
+
+function call(fn) {
+ fn.apply(arguments[1], Array.slice(arguments, 2));
+ return fn;
+}
+
+function curry(fn, length, acc) {
+ if (length == null)
+ length = fn.length;
+ if (length == 0)
+ return fn;
+
+ /* Close over function with 'this' */
+ function close(self, fn) function () fn.apply(self, Array.slice(arguments));
+
+ let first = (arguments.length < 3);
+ if (acc == null)
+ acc = [];
+
+ return function() {
+ let args = acc.concat(Array.slice(arguments));
+
+ /* The curried result should preserve 'this' */
+ if (arguments.length == 0)
+ return close(this, arguments.callee);
+
+ if (args.length >= length)
+ return fn.apply(this, args);
+
+ if (first)
+ fn = close(this, fn);
+ return curry(fn, length, args);
+ }
+}
+
+function update(targ) {
+ for (let i=1; i < arguments.length; i++) {
+ let src = arguments[i];
+ foreach(keys(src || {}), function(k) {
+ var get = src.__lookupGetter__(k),
+ set = src.__lookupSetter__(k);
+ if (!get && !set) {
+ var v = src[k];
+ targ[k] = v;
+ if (targ.__proto__ && callable(v)) {
+ v.superapply = function(self, args) {
+ return targ.__proto__[k].apply(self, args);
+ }
+ v.supercall = function(self) {
+ return v.superapply(self, Array.slice(arguments, 1));
+ }
+ }
+ }
+ if (get)
+ targ.__defineGetter__(k, get);
+ if (set)
+ targ.__defineSetter__(k, set);
+ });
+ }
+ return targ;
+}
+
+function extend(subc, superc, overrides) {
+ subc.prototype = { __proto__: superc.prototype };
+ update(subc.prototype, overrides);
+
+ subc.superclass = superc.prototype;
+ subc.prototype.constructor = subc;
+ subc.prototype.__class__ = subc;
+
+ if (superc.prototype.constructor === Object.prototype.constructor)
+ superc.prototype.constructor = superc;
+}
+
+function Class() {
+ function constructor() {
+ let self = {
+ __proto__: Constructor.prototype,
+ constructor: Constructor,
+ get closure() {
+ delete this.closure;
+ const self = this;
+ return this.closure = dict([k for (k in this) if (!self.__lookupGetter__(k) && callable(self[k]))].map(
+ function (k) [k, function () self[k].apply(self, arguments)]));
+ }
+ };
+ var res = self.init.apply(self, arguments);
+ return res !== undefined ? res : self
+ }
+
+ var args = Array.slice(arguments);
+ if (isstring(args[0]))
+ var name = args.shift();
+ var superclass = Class;
+ if (callable(args[0]))
+ superclass = args.shift();
+
+ var Constructor = eval("(function " + (name || superclass.name) +
+ String.substr(constructor, 20) + ")");
+
+ if (!('init' in superclass.prototype)) {
+ var superc = superclass;
+ superclass = function Shim() {}
+ extend(superclass, superc, {
+ init: superc,
+ });
+ }
+
+ extend(Constructor, superclass, args[0]);
+ update(Constructor, args[1]);
+ args = args.slice(2);
+ Array.forEach(args, function(obj) {
+ if (callable(obj))
+ obj = obj.prototype;
+ update(Constructor.prototype, obj);
+ });
+ return Constructor;
+}
+Class.toString = function () "[class " + this.constructor.name + "]",
+Class.prototype = {
+ init: function() {},
+ toString: function () "[instance " + this.constructor.name + "]",
+};
+
+const Struct = Class("Struct", {
+ init: function () {
+ let args = Array.slice(arguments);
+ this.__defineGetter__("length", function () args.length);
+ this.__defineGetter__("members", function () args.slice());
+ for (let arg in Iterator(args)) {
+ let [i, name] = arg;
+ this.__defineGetter__(name, function () this[i]);
+ this.__defineSetter__(name, function (val) { this[i] = val; });
+ }
+ function Struct() {
+ let self = this instanceof arguments.callee ? this : new arguments.callee();
+ //for (let [k, v] in Iterator(Array.slice(arguments))) // That is makes using struct twice as slow as the following code:
+ for (let i = 0; i < arguments.length; i++) {
+ if (arguments[i] != undefined)
+ self[i] = arguments[i];
+ }
+ return self;
+ }
+ Struct.prototype = this;
+ Struct.defaultValue = function (key, val) {
+ let i = args.indexOf(key);
+ Struct.prototype.__defineGetter__(i, function () (this[i] = val.call(this), this[i])); // Kludge for FF 3.0
+ Struct.prototype.__defineSetter__(i, function (val) {
+ let value = val;
+ this.__defineGetter__(i, function () value);
+ this.__defineSetter__(i, function (val) { value = val });
+ });
+ };
+ return this.constructor = Struct;
+ },
+
+ clone: function clone() {
+ return this.constructor.apply(null, this.slice());
+ },
+ // Iterator over our named members
+ __iterator__: function () {
+ let self = this;
+ return ([v, self[v]] for ([k, v] in Iterator(self.members)))
+ }
+});
+// Add no-sideeffect array methods. Can't set new Array() as the prototype or
+// get length() won't work.
+for (let [, k] in Iterator(["concat", "every", "filter", "forEach", "indexOf", "join", "lastIndexOf",
+ "map", "reduce", "reduceRight", "reverse", "slice", "some", "sort"]))
+ Struct.prototype[k] = Array.prototype[k];
+
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/bookmarks.js b/common/content/bookmarks.js
index d2ccda20..14e6bc61 100644
--- a/common/content/bookmarks.js
+++ b/common/content/bookmarks.js
@@ -6,1263 +6,686 @@
const DEFAULT_FAVICON = "chrome://mozapps/skin/places/defaultFavicon.png";
-// Try to import older command line history, quick marks, etc.
-liberator.registerObserver("load", function () {
- let branch = "extensions." + config.name.toLowerCase();
- if (!options.getPref(branch + ".commandline_cmd_history"))
- return;
-
- let store = storage["history-command"];
- let pref = options.getPref(branch + ".commandline_cmd_history");
- for (let [k, v] in Iterator(pref.split("\n")))
- store.push(v);
-
- store = storage["quickmarks"];
- pref = options.getPref(branch + ".quickmarks")
- .split("\n");
- while (pref.length > 0)
- store.set(pref.shift(), pref.shift());
-
- options.resetPref(branch + ".commandline_cmd_history");
- options.resetPref(branch + ".commandline_search_history");
- options.resetPref(branch + ".quickmarks");
-});
-
// also includes methods for dealing with keywords and search engines
-function Bookmarks() //{{{
-{
- ////////////////////////////////////////////////////////////////////////////////
- ////////////////////// PRIVATE SECTION /////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- const historyService = PlacesUtils.history;
- const bookmarksService = PlacesUtils.bookmarks;
- const taggingService = PlacesUtils.tagging;
- const faviconService = services.get("favicon");
- const livemarkService = services.get("livemark");
-
- // XXX for strange Firefox bug :(
- // Error: [Exception... "Component returned failure code: 0x8000ffff (NS_ERROR_UNEXPECTED) [nsIObserverService.addObserver]"
- // nsresult: "0x8000ffff (NS_ERROR_UNEXPECTED)"
- // location: "JS frame :: file://~firefox/components/nsTaggingService.js :: anonymous :: line 89"
- // data: no]
- // Source file: file://~firefox/components/nsTaggingService.js
- taggingService.getTagsForURI(window.makeURI("http://mysterious.bug"), {});
-
- const Bookmark = new Struct("url", "title", "icon", "keyword", "tags", "id");
- const Keyword = new Struct("keyword", "title", "icon", "url");
- Bookmark.defaultValue("icon", function () getFavicon(this.url));
- Bookmark.prototype.__defineGetter__("extra", function () [
- ["keyword", this.keyword, "Keyword"],
- ["tags", this.tags.join(", "), "Tag"]
- ].filter(function (item) item[1]));
-
- const storage = modules.storage;
- function Cache(name, store)
- {
- const rootFolders = [bookmarksService.toolbarFolder, bookmarksService.bookmarksMenuFolder, bookmarksService.unfiledBookmarksFolder];
- const sleep = liberator.sleep; // Storage objects are global to all windows, 'liberator' isn't.
-
- let bookmarks = [];
- let self = this;
-
- this.__defineGetter__("name", function () name);
- this.__defineGetter__("store", function () store);
- this.__defineGetter__("bookmarks", function () this.load());
-
- this.__defineGetter__("keywords",
- function () [new Keyword(k.keyword, k.title, k.icon, k.url) for ([, k] in Iterator(self.bookmarks)) if (k.keyword)]);
-
- this.__iterator__ = function () (val for ([, val] in Iterator(self.bookmarks)));
-
- function loadBookmark(node)
- {
- try
- {
- let uri = util.newURI(node.uri);
- let keyword = bookmarksService.getKeywordForBookmark(node.itemId);
- let tags = taggingService.getTagsForURI(uri, {}) || [];
- let bmark = new Bookmark(node.uri, node.title, node.icon && node.icon.spec, keyword, tags, node.itemId);
-
- bookmarks.push(bmark);
- return bmark;
+const Bookmarks = Module("bookmarks", {
+ requires: ["autocommands", "liberator", "storage", "services"],
+
+ init: function () {
+ const faviconService = services.get("favicon");
+ const bookmarksService = services.get("bookmarks");
+ const historyService = services.get("history");
+ const tagging = PlacesUtils.tagging;
+
+ this.getFavicon = getFavicon;
+ function getFavicon(uri) {
+ try {
+ return faviconService.getFaviconImageForPage(util.newURI(uri)).spec;
}
- catch (e)
- {
- liberator.dump("Failed to create bookmark for URI: " + node.uri);
- liberator.reportError(e);
- return null;
+ catch (e) {
+ return "";
}
}
- function readBookmark(id)
- {
- return {
- itemId: id,
- uri: bookmarksService.getBookmarkURI(id).spec,
- title: bookmarksService.getItemTitle(id)
- };
- }
+ // Fix for strange Firefox bug:
+ // Error: [Exception... "Component returned failure code: 0x8000ffff (NS_ERROR_UNEXPECTED) [nsIObserverService.addObserver]"
+ // nsresult: "0x8000ffff (NS_ERROR_UNEXPECTED)"
+ // location: "JS frame :: file://~firefox/components/nsTaggingService.js :: anonymous :: line 89"
+ // data: no]
+ // Source file: file://~firefox/components/nsTaggingService.js
+ tagging.getTagsForURI(window.makeURI("http://mysterious.bug"), {});
+
+ const Bookmark = new Struct("url", "title", "icon", "keyword", "tags", "id");
+ const Keyword = new Struct("keyword", "title", "icon", "url");
+ Bookmark.defaultValue("icon", function () getFavicon(this.url));
+ Bookmark.prototype.__defineGetter__("extra", function () [
+ ["keyword", this.keyword, "Keyword"],
+ ["tags", this.tags.join(", "), "Tag"]
+ ].filter(function (item) item[1]));
+
+ const storage = modules.storage;
+ function Cache(name, store) {
+ const rootFolders = [bookmarksService.toolbarFolder, bookmarksService.bookmarksMenuFolder, bookmarksService.unfiledBookmarksFolder];
+ const sleep = liberator.sleep; // Storage objects are global to all windows, 'liberator' isn't.
+
+ let bookmarks = [];
+ let self = this;
+
+ this.__defineGetter__("name", function () name);
+ this.__defineGetter__("store", function () store);
+ this.__defineGetter__("bookmarks", function () this.load());
+
+ this.__defineGetter__("keywords",
+ function () [new Keyword(k.keyword, k.title, k.icon, k.url) for ([, k] in Iterator(self.bookmarks)) if (k.keyword)]);
+
+ this.__iterator__ = function () (val for ([, val] in Iterator(self.bookmarks)));
+
+ function loadBookmark(node) {
+ try {
+ let uri = util.newURI(node.uri);
+ let keyword = bookmarksService.getKeywordForBookmark(node.itemId);
+ let tags = tagging.getTagsForURI(uri, {}) || [];
+ let bmark = new Bookmark(node.uri, node.title, node.icon && node.icon.spec, keyword, tags, node.itemId);
+
+ bookmarks.push(bmark);
+ return bmark;
+ }
+ catch (e) {
+ liberator.dump("Failed to create bookmark for URI: " + node.uri);
+ liberator.reportError(e);
+ return null;
+ }
+ }
- function deleteBookmark(id)
- {
- let length = bookmarks.length;
- bookmarks = bookmarks.filter(function (item) item.id != id);
- return bookmarks.length < length;
- }
+ function readBookmark(id) {
+ return {
+ itemId: id,
+ uri: bookmarksService.getBookmarkURI(id).spec,
+ title: bookmarksService.getItemTitle(id)
+ };
+ }
- this.findRoot = function findRoot(id)
- {
- do
- {
- var root = id;
- id = bookmarksService.getFolderIdForItem(id);
- } while (id != bookmarksService.placesRoot && id != root);
- return root;
- }
+ function deleteBookmark(id) {
+ let length = bookmarks.length;
+ bookmarks = bookmarks.filter(function (item) item.id != id);
+ return bookmarks.length < length;
+ }
- this.isBookmark = function (id) rootFolders.indexOf(self.findRoot(id)) >= 0;
+ this.findRoot = function findRoot(id) {
+ do {
+ var root = id;
+ id = bookmarksService.getFolderIdForItem(id);
+ } while (id != bookmarksService.placesRoot && id != root);
+ return root;
+ }
- this.isRegularBookmark = function findRoot(id)
- {
- do
- {
- var root = id;
- if (livemarkService && livemarkService.isLivemark(id))
- return false;
- id = bookmarksService.getFolderIdForItem(id);
- } while (id != bookmarksService.placesRoot && id != root);
- return rootFolders.indexOf(root) >= 0;
- }
+ this.isBookmark = function (id) rootFolders.indexOf(self.findRoot(id)) >= 0;
- // since we don't use a threaded bookmark loading (by set preload)
- // anymore, is this loading synchronization still needed? --mst
- let loading = false;
- this.load = function load()
- {
- if (loading)
- {
- while (loading)
- sleep(10);
- return bookmarks;
+ this.isRegularBookmark = function findRoot(id) {
+ do {
+ var root = id;
+ if (services.get("livemark") && services.get("livemark").isLivemark(id))
+ return false;
+ id = bookmarksService.getFolderIdForItem(id);
+ } while (id != bookmarksService.placesRoot && id != root);
+ return rootFolders.indexOf(root) >= 0;
}
- // update our bookmark cache
- bookmarks = [];
- loading = true;
+ // since we don't use a threaded bookmark loading (by set preload)
+ // anymore, is this loading synchronization still needed? --mst
+ let loading = false;
+ this.load = function load() {
+ if (loading) {
+ while (loading)
+ sleep(10);
+ return bookmarks;
+ }
- let folders = rootFolders.slice();
- let query = historyService.getNewQuery();
- let options = historyService.getNewQueryOptions();
- while (folders.length > 0)
- {
- query.setFolders(folders, 1);
- folders.shift();
- let result = historyService.executeQuery(query, options);
- let folder = result.root;
- folder.containerOpen = true;
-
- // iterate over the immediate children of this folder
- for (let i = 0; i < folder.childCount; i++)
- {
- let node = folder.getChild(i);
- if (node.type == node.RESULT_TYPE_FOLDER) // folder
- folders.push(node.itemId);
- else if (node.type == node.RESULT_TYPE_URI) // bookmark
- loadBookmark(node);
+ // update our bookmark cache
+ bookmarks = [];
+ loading = true;
+
+ let folders = rootFolders.slice();
+ let query = historyService.getNewQuery();
+ let options = historyService.getNewQueryOptions();
+ while (folders.length > 0) {
+ query.setFolders(folders, 1);
+ folders.shift();
+ let result = historyService.executeQuery(query, options);
+ let folder = result.root;
+ folder.containerOpen = true;
+
+ // iterate over the immediate children of this folder
+ for (let i = 0; i < folder.childCount; i++) {
+ let node = folder.getChild(i);
+ if (node.type == node.RESULT_TYPE_FOLDER) // folder
+ folders.push(node.itemId);
+ else if (node.type == node.RESULT_TYPE_URI) // bookmark
+ loadBookmark(node);
+ }
+
+ // close a container after using it!
+ folder.containerOpen = false;
}
+ this.__defineGetter__("bookmarks", function () bookmarks);
+ loading = false;
+ return bookmarks;
+ };
- // close a container after using it!
- folder.containerOpen = false;
- }
- this.__defineGetter__("bookmarks", function () bookmarks);
- loading = false;
- return bookmarks;
+ var observer = {
+ onBeginUpdateBatch: function onBeginUpdateBatch() {},
+ onEndUpdateBatch: function onEndUpdateBatch() {},
+ onItemVisited: function onItemVisited() {},
+ onItemMoved: function onItemMoved() {},
+ onItemAdded: function onItemAdded(itemId, folder, index) {
+ // liberator.dump("onItemAdded(" + itemId + ", " + folder + ", " + index + ")\n");
+ if (bookmarksService.getItemType(itemId) == bookmarksService.TYPE_BOOKMARK) {
+ if (self.isBookmark(itemId)) {
+ let bmark = loadBookmark(readBookmark(itemId));
+ storage.fireEvent(name, "add", bmark);
+ }
+ }
+ },
+ onItemRemoved: function onItemRemoved(itemId, folder, index) {
+ // liberator.dump("onItemRemoved(" + itemId + ", " + folder + ", " + index + ")\n");
+ if (deleteBookmark(itemId))
+ storage.fireEvent(name, "remove", itemId);
+ },
+ onItemChanged: function onItemChanged(itemId, property, isAnnotation, value) {
+ if (isAnnotation)
+ return;
+ // liberator.dump("onItemChanged(" + itemId + ", " + property + ", " + value + ")\n");
+ let bookmark = bookmarks.filter(function (item) item.id == itemId)[0];
+ if (bookmark) {
+ if (property == "tags")
+ value = tagging.getTagsForURI(util.newURI(bookmark.url), {});
+ if (property in bookmark)
+ bookmark[property] = value;
+ storage.fireEvent(name, "change", itemId);
+ }
+ },
+ QueryInterface: function QueryInterface(iid) {
+ if (iid.equals(Ci.nsINavBookmarkObserver) || iid.equals(Ci.nsISupports))
+ return this;
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+ };
+
+ bookmarksService.addObserver(observer, false);
+ }
+
+ let bookmarkObserver = function (key, event, arg) {
+ if (event == "add")
+ autocommands.trigger("BookmarkAdd", arg);
+ statusline.updateUrl();
};
- var observer = {
- onBeginUpdateBatch: function onBeginUpdateBatch() {},
- onEndUpdateBatch: function onEndUpdateBatch() {},
- onItemVisited: function onItemVisited() {},
- onItemMoved: function onItemMoved() {},
- onItemAdded: function onItemAdded(itemId, folder, index)
- {
- // liberator.dump("onItemAdded(" + itemId + ", " + folder + ", " + index + ")\n");
- if (bookmarksService.getItemType(itemId) == bookmarksService.TYPE_BOOKMARK)
- {
- if (self.isBookmark(itemId))
- {
- let bmark = loadBookmark(readBookmark(itemId));
- storage.fireEvent(name, "add", bmark);
+ this._cache = storage.newObject("bookmark-cache", Cache, { store: false });
+ storage.addObserver("bookmark-cache", bookmarkObserver, window);
+ },
+
+
+ get format() ({
+ anchored: false,
+ title: ["URL", "Info"],
+ keys: { text: "url", description: "title", icon: "icon", extra: "extra", tags: "tags" },
+ process: [template.icon, template.bookmarkDescription]
+ }),
+
+ // TODO: why is this a filter? --djk
+ get: function get(filter, tags, maxItems, extra) {
+ return completion.runCompleter("bookmark", filter, maxItems, tags, extra);
+ },
+
+ // if starOnly = true it is saved in the unfiledBookmarksFolder, otherwise in the bookmarksMenuFolder
+ add: function add(starOnly, title, url, keyword, tags, force) {
+ try {
+ let uri = util.createURI(url);
+ if (!force) {
+ for (let bmark in this._cache) {
+ if (bmark[0] == uri.spec) {
+ var id = bmark[5];
+ if (title)
+ services.get("bookmarks").setItemTitle(id, title);
+ break;
}
}
- },
- onItemRemoved: function onItemRemoved(itemId, folder, index)
- {
- // liberator.dump("onItemRemoved(" + itemId + ", " + folder + ", " + index + ")\n");
- if (deleteBookmark(itemId))
- storage.fireEvent(name, "remove", itemId);
- },
- onItemChanged: function onItemChanged(itemId, property, isAnnotation, value)
- {
- if (isAnnotation)
- return;
- // liberator.dump("onItemChanged(" + itemId + ", " + property + ", " + value + ")\n");
- let bookmark = bookmarks.filter(function (item) item.id == itemId)[0];
- if (bookmark)
- {
- if (property == "tags")
- value = taggingService.getTagsForURI(util.newURI(bookmark.url), {});
- if (property in bookmark)
- bookmark[property] = value;
- storage.fireEvent(name, "change", itemId);
- }
- },
- QueryInterface: function QueryInterface(iid)
- {
- if (iid.equals(Ci.nsINavBookmarkObserver) || iid.equals(Ci.nsISupports))
- return this;
- throw Cr.NS_ERROR_NO_INTERFACE;
}
- };
-
- bookmarksService.addObserver(observer, false);
- }
+ if (id == undefined)
+ id = services.get("bookmarks").insertBookmark(
+ services.get("bookmarks")[starOnly ? "unfiledBookmarksFolder" : "bookmarksMenuFolder"],
+ uri, -1, title || url);
+ if (!id)
+ return false;
- function getFavicon(uri)
- {
- try
- {
- return faviconService.getFaviconImageForPage(util.newURI(uri)).spec;
+ if (keyword)
+ services.get("bookmarks").setKeywordForBookmark(id, keyword);
+ if (tags) {
+ PlacesUtils.tagging.untagURI(uri, null);
+ PlacesUtils.tagging.tagURI(uri, tags);
+ }
}
- catch (e)
- {
- return "";
+ catch (e) {
+ liberator.log(e, 0);
+ return false;
}
- }
-
- let bookmarkObserver = function (key, event, arg)
- {
- if (event == "add")
- autocommands.trigger("BookmarkAdd", arg);
- statusline.updateUrl();
- };
-
- var cache = storage.newObject("bookmark-cache", Cache, { store: false });
- storage.addObserver("bookmark-cache", bookmarkObserver, window);
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// OPTIONS /////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- options.add(["defsearch", "ds"],
- "Set the default search engine",
- "string", "google",
- {
- completer: function completer(context)
- {
- completion.search(context, true);
- context.completions = [["", "Don't perform searches by default"]].concat(context.completions);
- },
- validator: Option.validateCompleter
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// MAPPINGS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
- var myModes = config.browserModes;
+ return true;
+ },
- mappings.add(myModes, ["a"],
- "Open a prompt to bookmark the current URL",
- function ()
- {
- let options = {};
-
- let bmarks = bookmarks.get(buffer.URL).filter(function (bmark) bmark.url == buffer.URL);
+ toggle: function toggle(url) {
+ if (!url)
+ return;
- if (bmarks.length == 1)
- {
- let bmark = bmarks[0];
+ let count = this.remove(url);
+ if (count > 0)
+ commandline.echo("Removed bookmark: " + url, commandline.HL_NORMAL, commandline.FORCE_SINGLELINE);
+ else {
+ let title = buffer.title || url;
+ let extra = "";
+ if (title != url)
+ extra = " (" + title + ")";
+ this.add(true, title, url);
+ commandline.echo("Added bookmark: " + url + extra, commandline.HL_NORMAL, commandline.FORCE_SINGLELINE);
+ }
+ },
- options["-title"] = bmark.title;
- if (bmark.keyword)
- options["-keyword"] = bmark.keyword;
- if (bmark.tags.length > 0)
- options["-tags"] = bmark.tags.join(", ");
- }
- else
- {
- if (buffer.title != buffer.URL)
- options["-title"] = buffer.title;
+ isBookmarked: function isBookmarked(url) {
+ try {
+ return services.get("bookmarks").getBookmarkIdsForURI(makeURI(url), {})
+ .some(this._cache.isRegularBookmark);
+ }
+ catch (e) {
+ return false;
+ }
+ },
+
+ // returns number of deleted bookmarks
+ remove: function remove(url) {
+ try {
+ let uri = util.newURI(url);
+ let bmarks = services.get("bookmarks").getBookmarkIdsForURI(uri, {})
+ .filter(this._cache.isRegularBookmark);
+ bmarks.forEach(services.get("bookmarks").removeItem);
+ return bmarks.length;
+ }
+ catch (e) {
+ liberator.log(e, 0);
+ return 0;
+ }
+ },
+
+ // TODO: add filtering
+ // also ensures that each search engine has a Liberator-friendly alias
+ getSearchEngines: function getSearchEngines() {
+ let searchEngines = [];
+ for (let [, engine] in Iterator(services.get("browserSearch").getVisibleEngines({}))) {
+ let alias = engine.alias;
+ if (!alias || !/^[a-z0-9_-]+$/.test(alias))
+ alias = engine.name.replace(/^\W*([a-zA-Z_-]+).*/, "$1").toLowerCase();
+ if (!alias)
+ alias = "search"; // for search engines which we can't find a suitable alias
+
+ // make sure we can use search engines which would have the same alias (add numbers at the end)
+ let newAlias = alias;
+ for (let j = 1; j <= 10; j++) { // <=10 is intentional
+ if (!searchEngines.some(function (item) item[0] == newAlias))
+ break;
+
+ newAlias = alias + j;
}
+ // only write when it changed, writes are really slow
+ if (engine.alias != newAlias)
+ engine.alias = newAlias;
- commandline.open(":",
- commands.commandToString({ command: "bmark", options: options, arguments: [buffer.URL], bang: bmarks.length == 1 }),
- modes.EX);
- });
-
- mappings.add(myModes, ["A"],
- "Toggle bookmarked state of current URL",
- function () { bookmarks.toggle(buffer.URL); });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMMANDS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- commands.add(["ju[mps]"],
- "Show jumplist",
- function ()
- {
- let sh = history.session;
- let list = template.jumps(sh.index, sh);
- commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
- },
- { argCount: "0" });
-
- // TODO: Clean this up.
- function tags(context, args)
- {
- let filter = context.filter;
- let have = filter.split(",");
-
- args.completeFilter = have.pop();
-
- let prefix = filter.substr(0, filter.length - args.completeFilter.length);
- let tags = util.Array.uniq(util.Array.flatten([b.tags for ([k, b] in Iterator(cache.bookmarks))]));
-
- return [[prefix + tag, tag] for ([i, tag] in Iterator(tags)) if (have.indexOf(tag) < 0)];
- }
+ searchEngines.push([engine.alias, engine.description, engine.iconURI && engine.iconURI.spec]);
+ }
- function title(context, args)
- {
- if (!args.bang)
- return [[content.document.title, "Current Page Title"]];
- context.keys.text = "title";
- context.keys.description = "url";
- return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: args["-keyword"], title: context.filter });
- }
+ return searchEngines;
+ },
- function keyword(context, args)
- {
- if (!args.bang)
- return [];
- context.keys.text = "keyword";
- return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: context.filter, title: args["-title"] });
- }
+ getSuggestions: function getSuggestions(engineName, query, callback) {
+ const responseType = "application/x-suggestions+json";
- commands.add(["bma[rk]"],
- "Add a bookmark",
- function (args)
- {
- let url = args.length == 0 ? buffer.URL : args[0];
- let title = args["-title"] || (args.length == 0 ? buffer.title : null);
- let keyword = args["-keyword"] || null;
- let tags = args["-tags"] || [];
+ let engine = services.get("browserSearch").getEngineByAlias(engineName);
+ if (engine && engine.supportsResponseType(responseType))
+ var queryURI = engine.getSubmission(query, responseType).uri.spec;
+ if (!queryURI)
+ return [];
- if (bookmarks.add(false, title, url, keyword, tags, args.bang))
- {
- let extra = (title == url) ? "" : " (" + title + ")";
- liberator.echomsg("Added bookmark: " + url + extra, 1, commandline.FORCE_SINGLELINE);
+ function process(resp) {
+ let results = [];
+ try {
+ results = services.get("json").decode(resp.responseText)[1];
+ results = [[item, ""] for ([k, item] in Iterator(results)) if (typeof item == "string")];
}
- else
- liberator.echoerr("Exxx: Could not add bookmark `" + title + "'", commandline.FORCE_SINGLELINE);
- },
- {
- argCount: "?",
- bang: true,
- completer: function (context, args)
- {
- if (!args.bang)
- {
- context.completions = [[content.document.documentURI, "Current Location"]];
- return;
- }
- completion.bookmark(context, args["-tags"], { keyword: args["-keyword"], title: args["-title"] });
- },
- options: [[["-title", "-t"], commands.OPTION_STRING, null, title],
- [["-tags", "-T"], commands.OPTION_LIST, null, tags],
- [["-keyword", "-k"], commands.OPTION_STRING, function (arg) /\w/.test(arg)]]
- });
-
- commands.add(["bmarks"],
- "List or open multiple bookmarks",
- function (args)
- {
- bookmarks.list(args.join(" "), args["-tags"] || [], args.bang, args["-max"]);
- },
- {
- bang: true,
- completer: function completer(context, args)
- {
- context.quote = null;
- context.filter = args.join(" ");
- completion.bookmark(context, args["-tags"]);
- },
- options: [[["-tags", "-T"], commands.OPTION_LIST, null, tags],
- [["-max", "-m"], commands.OPTION_INT]]
- });
-
- commands.add(["delbm[arks]"],
- "Delete a bookmark",
- function (args)
- {
- if (args.bang)
- {
- commandline.input("This will delete all bookmarks. Would you like to continue? (yes/[no]) ",
- function (resp) {
- if (resp && resp.match(/^y(es)?$/i))
- {
- cache.bookmarks.forEach(function (bmark) { bookmarksService.removeItem(bmark.id); });
- liberator.echomsg("All bookmarks deleted", 1, commandline.FORCE_SINGLELINE);
- }
- });
+ catch (e) {}
+ if (!callback)
+ return results;
+ callback(results);
+ }
+
+ let resp = util.httpGet(queryURI, callback && process);
+ if (!callback)
+ return process(resp);
+ },
+
+ // TODO: add filtering
+ // format of returned array:
+ // [keyword, helptext, url]
+ getKeywords: function getKeywords() {
+ return this._cache.keywords;
+ },
+
+ // full search string including engine name as first word in @param text
+ // if @param useDefSearch is true, it uses the default search engine
+ // @returns the url for the search string
+ // if the search also requires a postData, [url, postData] is returned
+ getSearchURL: function getSearchURL(text, useDefsearch) {
+ let searchString = (useDefsearch ? options["defsearch"] + " " : "") + text;
+
+ // we need to make sure our custom alias have been set, even if the user
+ // did not :open <tab> once before
+ this.getSearchEngines();
+
+ // ripped from Firefox
+ function getShortcutOrURI(url) {
+ var shortcutURL = null;
+ var keyword = url;
+ var param = "";
+ var offset = url.indexOf(" ");
+ if (offset > 0) {
+ keyword = url.substr(0, offset);
+ param = url.substr(offset + 1);
}
- else
- {
- let url = args.string || buffer.URL;
- let deletedCount = bookmarks.remove(url);
- liberator.echomsg(deletedCount + " bookmark(s) with url " + url.quote() + " deleted", 1, commandline.FORCE_SINGLELINE);
+ var engine = services.get("browserSearch").getEngineByAlias(keyword);
+ if (engine) {
+ var submission = engine.getSubmission(param, null);
+ return [submission.uri.spec, submission.postData];
}
- },
- {
- argCount: "?",
- bang: true,
- completer: function completer(context) completion.bookmark(context),
- literal: 0
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMPLETIONS /////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- completion.bookmark = function bookmark(context, tags, extra) {
- context.title = ["Bookmark", "Title"];
- context.format = bookmarks.format;
- for (let val in Iterator(extra || []))
- {
- let [k, v] = val; // Need block scope here for the closure
- if (v)
- context.filters.push(function (item) this._match(v, item[k]));
+ [shortcutURL, postData] = PlacesUtils.getURLAndPostDataForKeyword(keyword);
+ if (!shortcutURL)
+ return [url, null];
+
+ let data = window.unescape(postData || "");
+ if (/%s/i.test(shortcutURL) || /%s/i.test(data)) {
+ var charset = "";
+ var matches = shortcutURL.match(/^(.*)\&mozcharset=([a-zA-Z][_\-a-zA-Z0-9]+)\s*$/);
+ if (matches)
+ [, shortcutURL, charset] = matches;
+ else {
+ try {
+ charset = services.get("history").getCharsetForURI(window.makeURI(shortcutURL));
+ }
+ catch (e) {}
+ }
+ var encodedParam;
+ if (charset)
+ encodedParam = escape(window.convertFromUnicode(charset, param));
+ else
+ encodedParam = encodeURIComponent(param);
+ shortcutURL = shortcutURL.replace(/%s/g, encodedParam).replace(/%S/g, param);
+ if (/%s/i.test(data))
+ postData = window.getPostDataStream(data, param, encodedParam, "application/x-www-form-urlencoded");
+ }
+ else if (param)
+ return [shortcutURL, null];
+ return [shortcutURL, postData];
}
- // Need to make a copy because set completions() checks instanceof Array,
- // and this may be an Array from another window.
- context.completions = Array.slice(storage["bookmark-cache"].bookmarks);
- completion.urls(context, tags);
- };
-
- completion.search = function search(context, noSuggest) {
- let [, keyword, space, args] = context.filter.match(/^\s*(\S*)(\s*)(.*)$/);
- let keywords = bookmarks.getKeywords();
- let engines = bookmarks.getSearchEngines();
-
- context.title = ["Search Keywords"];
- context.completions = keywords.concat(engines);
- context.keys = { text: 0, description: 1, icon: 2 };
-
- if (!space || noSuggest)
- return;
- context.fork("suggest", keyword.length + space.length, this, "searchEngineSuggest",
- keyword, true);
-
- let item = keywords.filter(function (k) k.keyword == keyword)[0];
- if (item && item.url.indexOf("%s") > -1)
- context.fork("keyword/" + keyword, keyword.length + space.length, null, function (context) {
- context.format = history.format;
- context.title = [keyword + " Quick Search"];
- // context.background = true;
- context.compare = CompletionContext.Sort.unsorted;
- context.generate = function () {
- let [begin, end] = item.url.split("%s");
-
- return history.get({ uri: window.makeURI(begin), uriIsPrefix: true }).map(function (item) {
- let rest = item.url.length - end.length;
- let query = item.url.substring(begin.length, rest);
- if (item.url.substr(rest) == end && query.indexOf("&") == -1)
- {
- item.url = decodeURIComponent(query.replace(/#.*/, ""));
- return item;
- }
- }).filter(util.identity);
- };
- });
- };
+ let [url, postData] = getShortcutOrURI(searchString);
+
+ if (url == searchString)
+ return null;
+ if (postData)
+ return [url, postData];
+ return url; // can be null
+ },
+
+ // if openItems is true, open the matching bookmarks items in tabs rather than display
+ list: function list(filter, tags, openItems, maxItems) {
+ // FIXME: returning here doesn't make sense
+ // Why the hell doesn't it make sense? --Kris
+ // Because it unconditionally bypasses the final error message
+ // block and does so only when listing items, not opening them. In
+ // short it breaks the :bmarks command which doesn't make much
+ // sense to me but I'm old-fashioned. --djk
+ if (!openItems)
+ return completion.listCompleter("bookmark", filter, maxItems, tags);
+ let items = completion.runCompleter("bookmark", filter, maxItems, tags);
+
+ if (items.length)
+ return liberator.open(items.map(function (i) i.url), liberator.NEW_TAB);
+
+ if (filter.length > 0 && tags.length > 0)
+ liberator.echoerr("E283: No bookmarks matching tags: \"" + tags + "\" and string: \"" + filter + "\"");
+ else if (filter.length > 0)
+ liberator.echoerr("E283: No bookmarks matching string: \"" + filter + "\"");
+ else if (tags.length > 0)
+ liberator.echoerr("E283: No bookmarks matching tags: \"" + tags + "\"");
+ else
+ liberator.echoerr("No bookmarks set");
+ }
+}, {
+}, {
+ commands: function () {
+ commands.add(["ju[mps]"],
+ "Show jumplist",
+ function () {
+ let sh = history.session;
+ let list = template.jumps(sh.index, sh);
+ commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
+ },
+ { argCount: "0" });
- completion.searchEngineSuggest = function searchEngineSuggest(context, engineAliases, kludge) {
- if (!context.filter)
- return;
+ // TODO: Clean this up.
+ function tags(context, args) {
+ let filter = context.filter;
+ let have = filter.split(",");
- let engineList = (engineAliases || options["suggestengines"] || "google").split(",");
+ args.completeFilter = have.pop();
- let completions = [];
- engineList.forEach(function (name) {
- let engine = services.get("browserSearch").getEngineByAlias(name);
- if (!engine)
- return;
- let [, word] = /^\s*(\S+)/.exec(context.filter) || [];
- if (!kludge && word == name) // FIXME: Check for matching keywords
- return;
- let ctxt = context.fork(name, 0);
-
- ctxt.title = [engine.description + " Suggestions"];
- ctxt.compare = CompletionContext.Sort.unsorted;
- ctxt.incomplete = true;
- bookmarks.getSuggestions(name, ctxt.filter, function (compl) {
- ctxt.incomplete = false;
- ctxt.completions = compl;
- });
- });
- };
-
- completion.addUrlCompleter("S", "Suggest engines", completion.searchEngineSuggest);
- completion.addUrlCompleter("b", "Bookmarks", completion.bookmark);
- completion.addUrlCompleter("s", "Search engines and keyword URLs", completion.search);
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// PUBLIC SECTION //////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- return {
-
- get format() ({
- anchored: false,
- title: ["URL", "Info"],
- keys: { text: "url", description: "title", icon: "icon", extra: "extra", tags: "tags" },
- process: [template.icon, template.bookmarkDescription]
- }),
-
- // TODO: why is this a filter? --djk
- get: function get(filter, tags, maxItems, extra)
- {
- return completion.runCompleter("bookmark", filter, maxItems, tags, extra);
- },
-
- // if starOnly = true it is saved in the unfiledBookmarksFolder, otherwise in the bookmarksMenuFolder
- add: function add(starOnly, title, url, keyword, tags, force)
- {
- try
- {
- let uri = util.createURI(url);
- if (!force)
- {
- for (let bmark in cache)
- {
- if (bmark[0] == uri.spec)
- {
- var id = bmark[5];
- if (title)
- bookmarksService.setItemTitle(id, title);
- break;
- }
- }
- }
- if (id == undefined)
- id = bookmarksService.insertBookmark(
- bookmarksService[starOnly ? "unfiledBookmarksFolder" : "bookmarksMenuFolder"],
- uri, -1, title || url);
- if (!id)
- return false;
-
- if (keyword)
- bookmarksService.setKeywordForBookmark(id, keyword);
- if (tags)
- {
- taggingService.untagURI(uri, null);
- taggingService.tagURI(uri, tags);
- }
- }
- catch (e)
- {
- liberator.log(e, 0);
- return false;
- }
+ let prefix = filter.substr(0, filter.length - args.completeFilter.length);
+ let tags = util.Array.uniq(util.Array.flatten([b.tags for ([k, b] in Iterator(this._cache.bookmarks))]));
- return true;
- },
+ return [[prefix + tag, tag] for ([i, tag] in Iterator(tags)) if (have.indexOf(tag) < 0)];
+ }
- toggle: function toggle(url)
- {
- if (!url)
- return;
+ function title(context, args) {
+ if (!args.bang)
+ return [[content.document.title, "Current Page Title"]];
+ context.keys.text = "title";
+ context.keys.description = "url";
+ return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: args["-keyword"], title: context.filter });
+ }
- let count = this.remove(url);
- if (count > 0)
- commandline.echo("Removed bookmark: " + url, commandline.HL_NORMAL, commandline.FORCE_SINGLELINE);
- else
- {
- let title = buffer.title || url;
- let extra = "";
- if (title != url)
- extra = " (" + title + ")";
- this.add(true, title, url);
- commandline.echo("Added bookmark: " + url + extra, commandline.HL_NORMAL, commandline.FORCE_SINGLELINE);
- }
- },
+ function keyword(context, args) {
+ if (!args.bang)
+ return [];
+ context.keys.text = "keyword";
+ return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: context.filter, title: args["-title"] });
+ }
- isBookmarked: function isBookmarked(url)
- {
- try
- {
- return bookmarksService.getBookmarkIdsForURI(makeURI(url), {})
- .some(cache.isRegularBookmark);
- }
- catch (e)
- {
- return false;
- }
- },
+ commands.add(["bma[rk]"],
+ "Add a bookmark",
+ function (args) {
+ let url = args.length == 0 ? buffer.URL : args[0];
+ let title = args["-title"] || (args.length == 0 ? buffer.title : null);
+ let keyword = args["-keyword"] || null;
+ let tags = args["-tags"] || [];
+
+ if (bookmarks.add(false, title, url, keyword, tags, args.bang)) {
+ let extra = (title == url) ? "" : " (" + title + ")";
+ liberator.echomsg("Added bookmark: " + url + extra, 1, commandline.FORCE_SINGLELINE);
+ }
+ else
+ liberator.echoerr("Exxx: Could not add bookmark `" + title + "'", commandline.FORCE_SINGLELINE);
+ }, {
+ argCount: "?",
+ bang: true,
+ completer: function (context, args) {
+ if (!args.bang) {
+ context.completions = [[content.document.documentURI, "Current Location"]];
+ return;
+ }
+ completion.bookmark(context, args["-tags"], { keyword: args["-keyword"], title: args["-title"] });
+ },
+ options: [[["-title", "-t"], commands.OPTION_STRING, null, title],
+ [["-tags", "-T"], commands.OPTION_LIST, null, tags],
+ [["-keyword", "-k"], commands.OPTION_STRING, function (arg) /\w/.test(arg)]]
+ });
- // returns number of deleted bookmarks
- remove: function remove(url)
- {
- try
- {
- let uri = util.newURI(url);
- let bmarks = bookmarksService.getBookmarkIdsForURI(uri, {})
- .filter(cache.isRegularBookmark);
- bmarks.forEach(bookmarksService.removeItem);
- return bmarks.length;
- }
- catch (e)
+ commands.add(["bmarks"],
+ "List or open multiple bookmarks",
+ function (args) {
+ bookmarks.list(args.join(" "), args["-tags"] || [], args.bang, args["-max"]);
+ },
{
- liberator.log(e, 0);
- return 0;
- }
- },
-
- getFavicon: function (url) getFavicon(url),
+ bang: true,
+ completer: function completer(context, args) {
+ context.quote = null;
+ context.filter = args.join(" ");
+ completion.bookmark(context, args["-tags"]);
+ },
+ options: [[["-tags", "-T"], commands.OPTION_LIST, null, tags],
+ [["-max", "-m"], commands.OPTION_INT]]
+ });
- // TODO: add filtering
- // also ensures that each search engine has a Liberator-friendly alias
- getSearchEngines: function getSearchEngines()
- {
- let searchEngines = [];
- for (let [, engine] in Iterator(services.get("browserSearch").getVisibleEngines({})))
- {
- let alias = engine.alias;
- if (!alias || !/^[a-z0-9_-]+$/.test(alias))
- alias = engine.name.replace(/^\W*([a-zA-Z_-]+).*/, "$1").toLowerCase();
- if (!alias)
- alias = "search"; // for search engines which we can't find a suitable alias
-
- // make sure we can use search engines which would have the same alias (add numbers at the end)
- let newAlias = alias;
- for (let j = 1; j <= 10; j++) // <=10 is intentional
- {
- if (!searchEngines.some(function (item) item[0] == newAlias))
- break;
+ commands.add(["delbm[arks]"],
+ "Delete a bookmark",
+ function (args) {
+ if (args.bang) {
+ commandline.input("This will delete all bookmarks. Would you like to continue? (yes/[no]) ",
+ function (resp) {
+ if (resp && resp.match(/^y(es)?$/i)) {
+ bookmarks._cache.bookmarks.forEach(function (bmark) { services.get("bookmarks").removeItem(bmark.id); });
+ liberator.echomsg("All bookmarks deleted", 1, commandline.FORCE_SINGLELINE);
+ }
+ });
+ }
+ else {
+ let url = args.string || buffer.URL;
+ let deletedCount = bookmarks.remove(url);
- newAlias = alias + j;
+ liberator.echomsg(deletedCount + " bookmark(s) with url " + url.quote() + " deleted", 1, commandline.FORCE_SINGLELINE);
}
- // only write when it changed, writes are really slow
- if (engine.alias != newAlias)
- engine.alias = newAlias;
- searchEngines.push([engine.alias, engine.description, engine.iconURI && engine.iconURI.spec]);
- }
+ },
+ {
+ argCount: "?",
+ bang: true,
+ completer: function completer(context) completion.bookmark(context),
+ literal: 0
+ });
+ },
+ mappings: function () {
+ var myModes = config.browserModes;
- return searchEngines;
- },
+ mappings.add(myModes, ["a"],
+ "Open a prompt to bookmark the current URL",
+ function () {
+ let options = {};
- getSuggestions: function getSuggestions(engineName, query, callback)
- {
- const responseType = "application/x-suggestions+json";
+ let bmarks = bookmarks.get(buffer.URL).filter(function (bmark) bmark.url == buffer.URL);
- let engine = services.get("browserSearch").getEngineByAlias(engineName);
- if (engine && engine.supportsResponseType(responseType))
- var queryURI = engine.getSubmission(query, responseType).uri.spec;
- if (!queryURI)
- return [];
+ if (bmarks.length == 1) {
+ let bmark = bmarks[0];
- function process(resp)
- {
- let results = [];
- try
- {
- results = services.get("json").decode(resp.responseText)[1];
- results = [[item, ""] for ([k, item] in Iterator(results)) if (typeof item == "string")];
+ options["-title"] = bmark.title;
+ if (bmark.keyword)
+ options["-keyword"] = bmark.keyword;
+ if (bmark.tags.length > 0)
+ options["-tags"] = bmark.tags.join(", ");
}
- catch (e) {}
- if (!callback)
- return results;
- callback(results);
- }
-
- let resp = util.httpGet(queryURI, callback && process);
- if (!callback)
- return process(resp);
- },
-
- // TODO: add filtering
- // format of returned array:
- // [keyword, helptext, url]
- getKeywords: function getKeywords()
- {
- return cache.keywords;
- },
-
- // full search string including engine name as first word in @param text
- // if @param useDefSearch is true, it uses the default search engine
- // @returns the url for the search string
- // if the search also requires a postData, [url, postData] is returned
- getSearchURL: function getSearchURL(text, useDefsearch)
- {
- let searchString = (useDefsearch ? options["defsearch"] + " " : "") + text;
-
- // we need to make sure our custom alias have been set, even if the user
- // did not :open <tab> once before
- this.getSearchEngines();
-
- // ripped from Firefox
- function getShortcutOrURI(url) {
- var shortcutURL = null;
- var keyword = url;
- var param = "";
- var offset = url.indexOf(" ");
- if (offset > 0)
- {
- keyword = url.substr(0, offset);
- param = url.substr(offset + 1);
+ else {
+ if (buffer.title != buffer.URL)
+ options["-title"] = buffer.title;
}
- var engine = services.get("browserSearch").getEngineByAlias(keyword);
- if (engine)
- {
- var submission = engine.getSubmission(param, null);
- return [submission.uri.spec, submission.postData];
- }
+ commandline.open(":",
+ commands.commandToString({ command: "bmark", options: options, arguments: [buffer.URL], bang: bmarks.length == 1 }),
+ modes.EX);
+ });
- [shortcutURL, postData] = PlacesUtils.getURLAndPostDataForKeyword(keyword);
- if (!shortcutURL)
- return [url, null];
-
- let data = window.unescape(postData || "");
- if (/%s/i.test(shortcutURL) || /%s/i.test(data))
- {
- var charset = "";
- var matches = shortcutURL.match(/^(.*)\&mozcharset=([a-zA-Z][_\-a-zA-Z0-9]+)\s*$/);
- if (matches)
- [, shortcutURL, charset] = matches;
- else
- {
- try
- {
- charset = PlacesUtils.history.getCharsetForURI(window.makeURI(shortcutURL));
- }
- catch (e) {}
- }
- var encodedParam;
- if (charset)
- encodedParam = escape(window.convertFromUnicode(charset, param));
- else
- encodedParam = encodeURIComponent(param);
- shortcutURL = shortcutURL.replace(/%s/g, encodedParam).replace(/%S/g, param);
- if (/%s/i.test(data))
- postData = window.getPostDataStream(data, param, encodedParam, "application/x-www-form-urlencoded");
- }
- else if (param)
- return [shortcutURL, null];
- return [shortcutURL, postData];
+ mappings.add(myModes, ["A"],
+ "Toggle bookmarked state of current URL",
+ function () { bookmarks.toggle(buffer.URL); });
+ },
+ options: function () {
+ options.add(["defsearch", "ds"],
+ "Set the default search engine",
+ "string", "google",
+ {
+ completer: function completer(context) {
+ completion.search(context, true);
+ context.completions = [["", "Don't perform searches by default"]].concat(context.completions);
+ },
+ validator: Option.validateCompleter
+ });
+ },
+ completion: function () {
+ completion.bookmark = function bookmark(context, tags, extra) {
+ context.title = ["Bookmark", "Title"];
+ context.format = bookmarks.format;
+ for (let val in Iterator(extra || [])) {
+ let [k, v] = val; // Need block scope here for the closure
+ if (v)
+ context.filters.push(function (item) this._match(v, item[k]));
}
+ // Need to make a copy because set completions() checks instanceof Array,
+ // and this may be an Array from another window.
+ context.completions = Array.slice(storage["bookmark-cache"].bookmarks);
+ completion.urls(context, tags);
+ };
- let [url, postData] = getShortcutOrURI(searchString);
-
- if (url == searchString)
- return null;
- if (postData)
- return [url, postData];
- return url; // can be null
- },
-
- // if openItems is true, open the matching bookmarks items in tabs rather than display
- list: function list(filter, tags, openItems, maxItems)
- {
- // FIXME: returning here doesn't make sense
- // Why the hell doesn't it make sense? --Kris
- // Because it unconditionally bypasses the final error message
- // block and does so only when listing items, not opening them. In
- // short it breaks the :bmarks command which doesn't make much
- // sense to me but I'm old-fashioned. --djk
- if (!openItems)
- return completion.listCompleter("bookmark", filter, maxItems, tags);
- let items = completion.runCompleter("bookmark", filter, maxItems, tags);
-
- if (items.length)
- return liberator.open(items.map(function (i) i.url), liberator.NEW_TAB);
-
- if (filter.length > 0 && tags.length > 0)
- liberator.echoerr("E283: No bookmarks matching tags: \"" + tags + "\" and string: \"" + filter + "\"");
- else if (filter.length > 0)
- liberator.echoerr("E283: No bookmarks matching string: \"" + filter + "\"");
- else if (tags.length > 0)
- liberator.echoerr("E283: No bookmarks matching tags: \"" + tags + "\"");
- else
- liberator.echoerr("No bookmarks set");
- }
- };
- //}}}
-} //}}}
-
-function History() //{{{
-{
- ////////////////////////////////////////////////////////////////////////////////
- ////////////////////// PRIVATE SECTION /////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- const historyService = PlacesUtils.history;
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// MAPPINGS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- var myModes = config.browserModes;
-
- mappings.add(myModes,
- ["<C-o>"], "Go to an older position in the jump list",
- function (count) { history.stepTo(-Math.max(count, 1)); },
- { count: true });
-
- mappings.add(myModes,
- ["<C-i>"], "Go to a newer position in the jump list",
- function (count) { history.stepTo(Math.max(count, 1)); },
- { count: true });
-
- mappings.add(myModes,
- ["H", "<A-Left>", "<M-Left>"], "Go back in the browser history",
- function (count) { history.stepTo(-Math.max(count, 1)); },
- { count: true });
-
- mappings.add(myModes,
- ["L", "<A-Right>", "<M-Right>"], "Go forward in the browser history",
- function (count) { history.stepTo(Math.max(count, 1)); },
- { count: true });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMMANDS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- commands.add(["ba[ck]"],
- "Go back in the browser history",
- function (args)
- {
- let url = args.literalArg;
-
- if (args.bang)
- history.goToStart();
- else
- {
- if (url)
- {
- let sh = history.session;
- if (/^\d+(:|$)/.test(url) && sh.index - parseInt(url) in sh)
- return void window.getWebNavigation().gotoIndex(sh.index - parseInt(url));
-
- for (let [i, ent] in Iterator(sh.slice(0, sh.index).reverse()))
- if (ent.URI.spec == url)
- return void window.getWebNavigation().gotoIndex(i);
- liberator.echoerr("Exxx: URL not found in history");
- }
- else
- history.stepTo(-Math.max(args.count, 1));
- }
- },
- {
- argCount: "?",
- bang: true,
- completer: function completer(context)
- {
- let sh = history.session;
+ completion.search = function search(context, noSuggest) {
+ let [, keyword, space, args] = context.filter.match(/^\s*(\S*)(\s*)(.*)$/);
+ let keywords = bookmarks.getKeywords();
+ let engines = bookmarks.getSearchEngines();
- context.anchored = false;
- context.compare = CompletionContext.Sort.unsorted;
- context.filters = [CompletionContext.Filter.textDescription];
- context.completions = sh.slice(0, sh.index).reverse();
- context.keys = { text: function (item) (sh.index - item.index) + ": " + item.URI.spec, description: "title", icon: "icon" };
- },
- count: true,
- literal: 0
- });
-
- commands.add(["fo[rward]", "fw"],
- "Go forward in the browser history",
- function (args)
- {
- let url = args.literalArg;
-
- if (args.bang)
- history.goToEnd();
- else
- {
- if (url)
- {
- let sh = history.session;
- if (/^\d+(:|$)/.test(url) && sh.index + parseInt(url) in sh)
- return void window.getWebNavigation().gotoIndex(sh.index + parseInt(url));
-
- for (let [i, ent] in Iterator(sh.slice(sh.index + 1)))
- if (ent.URI.spec == url)
- return void window.getWebNavigation().gotoIndex(i);
- liberator.echoerr("Exxx: URL not found in history");
- }
- else
- history.stepTo(Math.max(args.count, 1));
- }
- },
- {
- argCount: "?",
- bang: true,
- completer: function completer(context)
- {
- let sh = history.session;
+ context.title = ["Search Keywords"];
+ context.completions = keywords.concat(engines);
+ context.keys = { text: 0, description: 1, icon: 2 };
- context.anchored = false;
- context.compare = CompletionContext.Sort.unsorted;
- context.filters = [CompletionContext.Filter.textDescription];
- context.completions = sh.slice(sh.index + 1);
- context.keys = { text: function (item) (item.index - sh.index) + ": " + item.URI.spec, description: "title", icon: "icon" };
- },
- count: true,
- literal: 0
- });
-
- commands.add(["hist[ory]", "hs"],
- "Show recently visited URLs",
- function (args) { history.list(args.join(" "), args.bang, args["-max"] || 1000); },
- {
- bang: true,
- completer: function (context) { context.quote = null; completion.history(context); },
- // completer: function (filter) completion.history(filter)
- options: [[["-max", "-m"], commands.OPTION_INT]]
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMPLETIONS /////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- completion.history = function _history(context, maxItems) {
- context.format = history.format;
- context.title = ["History"];
- context.compare = CompletionContext.Sort.unsorted;
- //context.background = true;
- if (context.maxItems == null)
- context.maxItems = 100;
- context.regenerate = true;
- context.generate = function () history.get(context.filter, this.maxItems);
- };
-
- completion.addUrlCompleter("h", "History", completion.history);
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// PUBLIC SECTION //////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- return {
-
- get format() bookmarks.format,
-
- get service() historyService,
-
- get: function get(filter, maxItems)
- {
- // no query parameters will get all history
- let query = historyService.getNewQuery();
- let options = historyService.getNewQueryOptions();
-
- if (typeof filter == "string")
- filter = { searchTerms: filter };
- for (let [k, v] in Iterator(filter))
- query[k] = v;
- options.sortingMode = options.SORT_BY_DATE_DESCENDING;
- options.resultType = options.RESULTS_AS_URI;
- if (maxItems > 0)
- options.maxResults = maxItems;
-
- // execute the query
- let root = historyService.executeQuery(query, options).root;
- root.containerOpen = true;
- let items = util.map(util.range(0, root.childCount), function (i) {
- let node = root.getChild(i);
- return {
- url: node.uri,
- title: node.title,
- icon: node.icon ? node.icon.spec : DEFAULT_FAVICON
- };
+ if (!space || noSuggest)
+ return;
+
+ context.fork("suggest", keyword.length + space.length, this, "searchEngineSuggest",
+ keyword, true);
+
+ let item = keywords.filter(function (k) k.keyword == keyword)[0];
+ if (item && item.url.indexOf("%s") > -1)
+ context.fork("keyword/" + keyword, keyword.length + space.length, null, function (context) {
+ context.format = history.format;
+ context.title = [keyword + " Quick Search"];
+ // context.background = true;
+ context.compare = CompletionContext.Sort.unsorted;
+ context.generate = function () {
+ let [begin, end] = item.url.split("%s");
+
+ return history.get({ uri: window.makeURI(begin), uriIsPrefix: true }).map(function (item) {
+ let rest = item.url.length - end.length;
+ let query = item.url.substring(begin.length, rest);
+ if (item.url.substr(rest) == end && query.indexOf("&") == -1) {
+ item.url = decodeURIComponent(query.replace(/#.*/, ""));
+ return item;
+ }
+ }).filter(util.identity);
+ };
+ });
+ };
+
+ completion.searchEngineSuggest = function searchEngineSuggest(context, engineAliases, kludge) {
+ if (!context.filter)
+ return;
+
+ let engineList = (engineAliases || options["suggestengines"] || "google").split(",");
+
+ let completions = [];
+ engineList.forEach(function (name) {
+ let engine = services.get("browserSearch").getEngineByAlias(name);
+ if (!engine)
+ return;
+ let [, word] = /^\s*(\S+)/.exec(context.filter) || [];
+ if (!kludge && word == name) // FIXME: Check for matching keywords
+ return;
+ let ctxt = context.fork(name, 0);
+
+ ctxt.title = [engine.description + " Suggestions"];
+ ctxt.compare = CompletionContext.Sort.unsorted;
+ ctxt.incomplete = true;
+ bookmarks.getSuggestions(name, ctxt.filter, function (compl) {
+ ctxt.incomplete = false;
+ ctxt.completions = compl;
+ });
});
- root.containerOpen = false; // close a container after using it!
-
- return items;
- },
-
- get session()
- {
- let sh = window.getWebNavigation().sessionHistory;
- let obj = [];
- obj.index = sh.index;
- obj.__iterator__ = function () util.Array.iteritems(this);
- for (let i in util.range(0, sh.count))
- {
- obj[i] = { index: i, __proto__: sh.getEntryAtIndex(i, false) };
- util.memoize(obj[i], "icon",
- function (obj) services.get("favicon").getFaviconImageForPage(obj.URI).spec);
- }
- return obj;
- },
-
- // TODO: better names
- stepTo: function stepTo(steps)
- {
- let start = 0;
- let end = window.getWebNavigation().sessionHistory.count - 1;
- let current = window.getWebNavigation().sessionHistory.index;
-
- if (current == start && steps < 0 || current == end && steps > 0)
- liberator.beep();
- else
- {
- let index = util.Math.constrain(current + steps, start, end);
- window.getWebNavigation().gotoIndex(index);
- }
- },
-
- goToStart: function goToStart()
- {
- let index = window.getWebNavigation().sessionHistory.index;
-
- if (index > 0)
- window.getWebNavigation().gotoIndex(0);
- else
- liberator.beep();
-
- },
-
- goToEnd: function goToEnd()
- {
- let sh = window.getWebNavigation().sessionHistory;
- let max = sh.count - 1;
-
- if (sh.index < max)
- window.getWebNavigation().gotoIndex(max);
- else
- liberator.beep();
-
- },
-
- // if openItems is true, open the matching history items in tabs rather than display
- list: function list(filter, openItems, maxItems)
- {
- // FIXME: returning here doesn't make sense
- // Why the hell doesn't it make sense? --Kris
- // See comment at bookmarks.list --djk
- if (!openItems)
- return completion.listCompleter("history", filter, maxItems);
- let items = completion.runCompleter("history", filter, maxItems);
-
- if (items.length)
- return liberator.open(items.map(function (i) i.url), liberator.NEW_TAB);
-
- if (filter.length > 0)
- liberator.echoerr("E283: No history matching \"" + filter + "\"");
- else
- liberator.echoerr("No history set");
- }
- };
- //}}}
-} //}}}
-
-/** @scope modules */
-
-/**
- * @instance quickmarks
- */
-function QuickMarks() //{{{
-{
- ////////////////////////////////////////////////////////////////////////////////
- ////////////////////// PRIVATE SECTION /////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- var qmarks = storage.newMap("quickmarks", { store: true });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// MAPPINGS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- var myModes = config.browserModes;
-
- mappings.add(myModes,
- ["go"], "Jump to a QuickMark",
- function (arg) { quickmarks.jumpTo(arg, liberator.CURRENT_TAB); },
- { arg: true });
-
- mappings.add(myModes,
- ["gn"], "Jump to a QuickMark in a new tab",
- function (arg)
- {
- quickmarks.jumpTo(arg,
- /\bquickmark\b/.test(options["activate"]) ?
- liberator.NEW_TAB : liberator.NEW_BACKGROUND_TAB);
- },
- { arg: true });
-
- mappings.add(myModes,
- ["M"], "Add new QuickMark for current URL",
- function (arg)
- {
- if (/[^a-zA-Z0-9]/.test(arg))
- return void liberator.beep();
-
- quickmarks.add(arg, buffer.URL);
- },
- { arg: true });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMMANDS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- commands.add(["delqm[arks]"],
- "Delete the specified QuickMarks",
- function (args)
- {
- // TODO: finish arg parsing - we really need a proper way to do this. :)
- // assert(args.bang ^ args.string)
- liberator.assert( args.bang || args.string, "E471: Argument required");
- liberator.assert(!args.bang || !args.string, "E474: Invalid argument");
-
- if (args.bang)
- quickmarks.removeAll();
- else
- quickmarks.remove(args.string);
- },
- {
- bang: true,
- completer: function (context)
- {
- context.title = ["QuickMark", "URL"];
- context.completions = qmarks;
- }
- });
-
- commands.add(["qma[rk]"],
- "Mark a URL with a letter for quick access",
- function (args)
- {
- let matches = args.string.match(/^([a-zA-Z0-9])(?:\s+(.+))?$/);
- if (!matches)
- liberator.echoerr("E488: Trailing characters");
- else if (!matches[2])
- quickmarks.add(matches[1], buffer.URL);
- else
- quickmarks.add(matches[1], matches[2]);
- },
- { argCount: "+" });
-
- commands.add(["qmarks"],
- "Show all QuickMarks",
- function (args)
- {
- args = args.string;
-
- // ignore invalid qmark characters unless there are no valid qmark chars
- liberator.assert(!args || /[a-zA-Z0-9]/.test(args), "E283: No QuickMarks matching \"" + args + "\"");
-
- let filter = args.replace(/[^a-zA-Z0-9]/g, "");
- quickmarks.list(filter);
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// PUBLIC SECTION //////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- return {
-
- /**
- * Adds a new quickmark with name <b>qmark</b> referencing
- * the URL <b>location</b>. Any existing quickmark with the same name
- * will be replaced.
- *
- * @param {string} qmark The name of the quickmark {A-Z}.
- * @param {string} location The URL accessed by this quickmark.
- */
- add: function add(qmark, location)
- {
- qmarks.set(qmark, location);
- liberator.echomsg("Added Quick Mark '" + qmark + "': " + location, 1);
- },
-
- /**
- * Deletes the specified quickmarks. The <b>filter</b> is a list of
- * quickmarks and ranges are supported. Eg. "ab c d e-k".
- *
- * @param {string} filter The list of quickmarks to delete.
- *
- */
- remove: function remove(filter)
- {
- let pattern = RegExp("[" + filter.replace(/\s+/g, "") + "]");
-
- for (let [qmark, ] in qmarks)
- {
- if (pattern.test(qmark))
- qmarks.remove(qmark);
- }
- },
-
- /**
- * Removes all quickmarks.
- */
- removeAll: function removeAll()
- {
- qmarks.clear();
- },
-
- /**
- * Opens the URL referenced by the specified <b>qmark</b>.
- *
- * @param {string} qmark The quickmark to open.
- * @param {number} where A constant describing where to open the page.
- * See {@link Liberator#open}.
- */
- jumpTo: function jumpTo(qmark, where)
- {
- let url = qmarks.get(qmark);
-
- if (url)
- liberator.open(url, where);
- else
- liberator.echoerr("E20: QuickMark not set");
- },
-
- /**
- * Lists all quickmarks matching <b>filter</b> in the message window.
- *
- * @param {string} filter The list of quickmarks to display. Eg. "abc"
- * Ranges are not supported.
- */
- // FIXME: filter should match that of quickmarks.remove or vice versa
- list: function list(filter)
- {
- let marks = [k for ([k, v] in qmarks)];
- let lowercaseMarks = marks.filter(function (x) /[a-z]/.test(x)).sort();
- let uppercaseMarks = marks.filter(function (x) /[A-Z]/.test(x)).sort();
- let numberMarks = marks.filter(function (x) /[0-9]/.test(x)).sort();
-
- marks = Array.concat(lowercaseMarks, uppercaseMarks, numberMarks);
-
- liberator.assert(marks.length > 0, "No QuickMarks set");
-
- if (filter.length > 0)
- {
- marks = marks.filter(function (qmark) filter.indexOf(qmark) >= 0);
- liberator.assert(marks.length >= 0, "E283: No QuickMarks matching \"" + filter + "\"");
- }
+ };
- let items = [[mark, qmarks.get(mark)] for ([k, mark] in Iterator(marks))];
- template.genericTable(items, { title: ["QuickMark", "URL"] });
- }
- };
- //}}}
-} //}}}
+ completion.addUrlCompleter("S", "Suggest engines", completion.searchEngineSuggest);
+ completion.addUrlCompleter("b", "Bookmarks", completion.bookmark);
+ completion.addUrlCompleter("s", "Search engines and keyword URLs", completion.search);
+ },
+});
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/browser.js b/common/content/browser.js
index 842aa185..00d580db 100644
--- a/common/content/browser.js
+++ b/common/content/browser.js
@@ -9,15 +9,10 @@
/**
* @instance browser
*/
-function Browser() //{{{
-{
- ////////////////////////////////////////////////////////////////////////////////
- ////////////////////// PRIVATE SECTION /////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
+const Browser = Module("browser", {
+}, {
// TODO: support 'nrformats'? -> probably not worth it --mst
- function incrementURL(count)
- {
+ incrementURL: function (count) {
let matches = buffer.URL.match(/(.*?)(\d+)(\D*)$/);
if (!matches)
return void liberator.beep();
@@ -25,253 +20,219 @@ function Browser() //{{{
let [, pre, number, post] = matches;
let newNumber = parseInt(number, 10) + count;
let newNumberStr = String(newNumber > 0 ? newNumber : 0);
- if (number.match(/^0/)) // add 0009<C-a> should become 0010
- {
+ if (number.match(/^0/)) { // add 0009<C-a> should become 0010
while (newNumberStr.length < number.length)
newNumberStr = "0" + newNumberStr;
}
liberator.open(pre + newNumberStr + post);
}
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// OPTIONS /////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- options.add(["encoding", "enc"],
- "Sets the current buffer's character encoding",
- "string", "UTF-8",
- {
- scope: options.OPTION_SCOPE_LOCAL,
- getter: function () getBrowser().docShell.QueryInterface(Ci.nsIDocCharset).charset,
- setter: function (val)
+}, {
+ options: function () {
+ options.add(["encoding", "enc"],
+ "Sets the current buffer's character encoding",
+ "string", "UTF-8",
{
- if (options["encoding"] == val)
- return val;
+ scope: options.OPTION_SCOPE_LOCAL,
+ getter: function () getBrowser().docShell.QueryInterface(Ci.nsIDocCharset).charset,
+ setter: function (val) {
+ if (options["encoding"] == val)
+ return val;
+
+ // Stolen from browser.jar/content/browser/browser.js, more or less.
+ try {
+ getBrowser().docShell.QueryInterface(Ci.nsIDocCharset).charset = val;
+ PlacesUtils.history.setCharsetForURI(getWebNavigation().currentURI, val);
+ getWebNavigation().reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
+ }
+ catch (e) { liberator.reportError(e); }
+ },
+ completer: function (context) completion.charset(context),
+ validator: Option.validateCompleter
+ });
- // Stolen from browser.jar/content/browser/browser.js, more or less.
- try
+ // only available in FF 3.5
+ services.add("privateBrowsing", "@mozilla.org/privatebrowsing;1", Ci.nsIPrivateBrowsingService);
+ if (services.get("privateBrowsing")) {
+ options.add(["private", "pornmode"],
+ "Set the 'private browsing' option",
+ "boolean", false,
{
- getBrowser().docShell.QueryInterface(Ci.nsIDocCharset).charset = val;
- PlacesUtils.history.setCharsetForURI(getWebNavigation().currentURI, val);
- getWebNavigation().reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
- }
- catch (e) { liberator.reportError(e); }
- },
- completer: function (context) completion.charset(context),
- validator: Option.validateCompleter
- });
+ setter: function (value) services.get("privateBrowsing").privateBrowsingEnabled = value,
+ getter: function () services.get("privateBrowsing").privateBrowsingEnabled
+ });
+ let services = modules.services; // Storage objects are global to all windows, 'modules' isn't.
+ storage.newObject("private-mode", function () {
+ ({
+ init: function () {
+ services.get("observer").addObserver(this, "private-browsing", false);
+ services.get("observer").addObserver(this, "quit-application", false);
+ this.private = services.get("privateBrowsing").privateBrowsingEnabled;
+ },
+ observe: function (subject, topic, data) {
+ if (topic == "private-browsing") {
+ if (data == "enter")
+ storage.privateMode = true;
+ else if (data == "exit")
+ storage.privateMode = false;
+ storage.fireEvent("private-mode", "change", storage.privateMode);
+ }
+ else if (topic == "quit-application") {
+ services.get("observer").removeObserver(this, "quit-application");
+ services.get("observer").removeObserver(this, "private-browsing");
+ }
+ }
+ }).init();
+ }, { store: false });
+ storage.addObserver("private-mode",
+ function (key, event, value) {
+ autocommands.trigger("PrivateMode", { state: value });
+ }, window);
+ }
- // only available in FF 3.5
- services.add("privateBrowsing", "@mozilla.org/privatebrowsing;1", Ci.nsIPrivateBrowsingService);
- if (services.get("privateBrowsing"))
- {
- options.add(["private", "pornmode"],
- "Set the 'private browsing' option",
- "boolean", false,
- {
- setter: function (value) services.get("privateBrowsing").privateBrowsingEnabled = value,
- getter: function () services.get("privateBrowsing").privateBrowsingEnabled
+ options.add(["urlseparator"],
+ "Set the separator regex used to separate multiple URL args",
+ "string", ",\\s");
+ },
+
+ mappings: function () {
+ mappings.add([modes.NORMAL],
+ ["y"], "Yank current location to the clipboard",
+ function () { util.copyToClipboard(buffer.URL, true); });
+
+ // opening websites
+ mappings.add([modes.NORMAL],
+ ["o"], "Open one or more URLs",
+ function () { commandline.open(":", "open ", modes.EX); });
+
+ mappings.add([modes.NORMAL], ["O"],
+ "Open one or more URLs, based on current location",
+ function () { commandline.open(":", "open " + buffer.URL, modes.EX); });
+
+ mappings.add([modes.NORMAL], ["t"],
+ "Open one or more URLs in a new tab",
+ function () { commandline.open(":", "tabopen ", modes.EX); });
+
+ mappings.add([modes.NORMAL], ["T"],
+ "Open one or more URLs in a new tab, based on current location",
+ function () { commandline.open(":", "tabopen " + buffer.URL, modes.EX); });
+
+ mappings.add([modes.NORMAL], ["w"],
+ "Open one or more URLs in a new window",
+ function () { commandline.open(":", "winopen ", modes.EX); });
+
+ mappings.add([modes.NORMAL], ["W"],
+ "Open one or more URLs in a new window, based on current location",
+ function () { commandline.open(":", "winopen " + buffer.URL, modes.EX); });
+
+ mappings.add([modes.NORMAL],
+ ["<C-a>"], "Increment last number in URL",
+ function (count) { Browser.incrementURL(Math.max(count, 1)); },
+ { count: true });
+
+ mappings.add([modes.NORMAL],
+ ["<C-x>"], "Decrement last number in URL",
+ function (count) { Browser.incrementURL(-Math.max(count, 1)); },
+ { count: true });
+
+ mappings.add([modes.NORMAL], ["~"],
+ "Open home directory",
+ function () { liberator.open("~"); });
+
+ mappings.add([modes.NORMAL], ["gh"],
+ "Open homepage",
+ function () { BrowserHome(); });
+
+ mappings.add([modes.NORMAL], ["gH"],
+ "Open homepage in a new tab",
+ function () {
+ let homepages = gHomeButton.getHomePage();
+ liberator.open(homepages, { from: "homepage", where: liberator.NEW_TAB });
});
- let services = modules.services; // Storage objects are global to all windows, 'modules' isn't.
- storage.newObject("private-mode", function () {
- ({
- init: function ()
- {
- services.get("observer").addObserver(this, "private-browsing", false);
- services.get("observer").addObserver(this, "quit-application", false);
- this.private = services.get("privateBrowsing").privateBrowsingEnabled;
- },
- observe: function (subject, topic, data)
- {
- if (topic == "private-browsing")
- {
- if (data == "enter")
- storage.privateMode = true;
- else if (data == "exit")
- storage.privateMode = false;
- storage.fireEvent("private-mode", "change", storage.privateMode);
+
+ mappings.add([modes.NORMAL], ["gu"],
+ "Go to parent directory",
+ function (count) {
+ function isDirectory(url) {
+ if (/^file:\/|^\//.test(url)) {
+ let file = io.File(url);
+ return file.exists() && file.isDirectory();
}
- else if (topic == "quit-application")
- {
- services.get("observer").removeObserver(this, "quit-application");
- services.get("observer").removeObserver(this, "private-browsing");
+ else {
+ // for all other locations just check if the URL ends with /
+ return /\/$/.test(url);
}
}
- }).init();
- }, { store: false });
- storage.addObserver("private-mode",
- function (key, event, value) {
- autocommands.trigger("PrivateMode", { state: value });
- }, window);
- }
-
- options.add(["urlseparator"],
- "Set the separator regex used to separate multiple URL args",
- "string", ",\\s");
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// MAPPINGS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- mappings.add([modes.NORMAL],
- ["y"], "Yank current location to the clipboard",
- function () { util.copyToClipboard(buffer.URL, true); });
-
- // opening websites
- mappings.add([modes.NORMAL],
- ["o"], "Open one or more URLs",
- function () { commandline.open(":", "open ", modes.EX); });
-
- mappings.add([modes.NORMAL], ["O"],
- "Open one or more URLs, based on current location",
- function () { commandline.open(":", "open " + buffer.URL, modes.EX); });
-
- mappings.add([modes.NORMAL], ["t"],
- "Open one or more URLs in a new tab",
- function () { commandline.open(":", "tabopen ", modes.EX); });
-
- mappings.add([modes.NORMAL], ["T"],
- "Open one or more URLs in a new tab, based on current location",
- function () { commandline.open(":", "tabopen " + buffer.URL, modes.EX); });
-
- mappings.add([modes.NORMAL], ["w"],
- "Open one or more URLs in a new window",
- function () { commandline.open(":", "winopen ", modes.EX); });
-
- mappings.add([modes.NORMAL], ["W"],
- "Open one or more URLs in a new window, based on current location",
- function () { commandline.open(":", "winopen " + buffer.URL, modes.EX); });
- mappings.add([modes.NORMAL],
- ["<C-a>"], "Increment last number in URL",
- function (count) { incrementURL(Math.max(count, 1)); },
- { count: true });
+ if (count < 1)
+ count = 1;
- mappings.add([modes.NORMAL],
- ["<C-x>"], "Decrement last number in URL",
- function (count) { incrementURL(-Math.max(count, 1)); },
- { count: true });
-
- mappings.add([modes.NORMAL], ["~"],
- "Open home directory",
- function () { liberator.open("~"); });
-
- mappings.add([modes.NORMAL], ["gh"],
- "Open homepage",
- function () { BrowserHome(); });
-
- mappings.add([modes.NORMAL], ["gH"],
- "Open homepage in a new tab",
- function ()
- {
- let homepages = gHomeButton.getHomePage();
- liberator.open(homepages, { from: "homepage", where: liberator.NEW_TAB });
- });
-
- mappings.add([modes.NORMAL], ["gu"],
- "Go to parent directory",
- function (count)
- {
- function isDirectory(url)
- {
- if (/^file:\/|^\//.test(url))
- {
- let file = io.File(url);
- return file.exists() && file.isDirectory();
+ // XXX
+ let url = buffer.URL;
+ for (let i = 0; i < count; i++) {
+ if (isDirectory(url))
+ url = url.replace(/^(.*?:)(.*?)([^\/]+\/*)$/, "$1$2/");
+ else
+ url = url.replace(/^(.*?:)(.*?)(\/+[^\/]+)$/, "$1$2/");
}
- else
- {
- // for all other locations just check if the URL ends with /
- return /\/$/.test(url);
- }
- }
+ url = url.replace(/^(.*:\/+.*?)\/+$/, "$1/"); // get rid of more than 1 / at the end
- if (count < 1)
- count = 1;
-
- // XXX
- let url = buffer.URL;
- for (let i = 0; i < count; i++)
- {
- if (isDirectory(url))
- url = url.replace(/^(.*?:)(.*?)([^\/]+\/*)$/, "$1$2/");
+ if (url == buffer.URL)
+ liberator.beep();
else
- url = url.replace(/^(.*?:)(.*?)(\/+[^\/]+)$/, "$1$2/");
- }
- url = url.replace(/^(.*:\/+.*?)\/+$/, "$1/"); // get rid of more than 1 / at the end
-
- if (url == buffer.URL)
- liberator.beep();
- else
- liberator.open(url);
- },
- { count: true });
-
- mappings.add([modes.NORMAL], ["gU"],
- "Go to the root of the website",
- function ()
- {
- let uri = content.document.location;
- if (/(about|mailto):/.test(uri.protocol)) // exclude these special protocols for now
- return void liberator.beep();
- liberator.open(uri.protocol + "//" + (uri.host || "") + "/");
- });
-
- mappings.add([modes.NORMAL], ["<C-l>"],
- "Redraw the screen",
- function () { commands.get("redraw").execute("", false); });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMMANDS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- commands.add(["downl[oads]", "dl"],
- "Show progress of current downloads",
- function ()
- {
- liberator.open("chrome://mozapps/content/downloads/downloads.xul",
- options.get("newtab").has("all", "downloads")
- ? liberator.NEW_TAB : liberator.CURRENT_TAB);
- },
- { argCount: "0" });
-
- commands.add(["o[pen]", "e[dit]"],
- "Open one or more URLs in the current tab",
- function (args)
- {
- args = args.string;
+ liberator.open(url);
+ },
+ { count: true });
+
+ mappings.add([modes.NORMAL], ["gU"],
+ "Go to the root of the website",
+ function () {
+ let uri = content.document.location;
+ if (/(about|mailto):/.test(uri.protocol)) // exclude these special protocols for now
+ return void liberator.beep();
+ liberator.open(uri.protocol + "//" + (uri.host || "") + "/");
+ });
- if (args)
- liberator.open(args);
- else
- liberator.open("about:blank");
- },
- {
- completer: function (context) completion.url(context),
- literal: 0,
- privateData: true,
- });
+ mappings.add([modes.NORMAL], ["<C-l>"],
+ "Redraw the screen",
+ function () { commands.get("redraw").execute("", false); });
+ },
+
+ commands: function () {
+ commands.add(["downl[oads]", "dl"],
+ "Show progress of current downloads",
+ function () {
+ liberator.open("chrome://mozapps/content/downloads/downloads.xul",
+ options.get("newtab").has("all", "downloads")
+ ? liberator.NEW_TAB : liberator.CURRENT_TAB);
+ },
+ { argCount: "0" });
- commands.add(["redr[aw]"],
- "Redraw the screen",
- function ()
- {
- let wu = window.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDOMWindowUtils);
- wu.redraw();
- modes.show();
- },
- { argCount: "0" });
+ commands.add(["o[pen]", "e[dit]"],
+ "Open one or more URLs in the current tab",
+ function (args) {
+ args = args.string;
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// PUBLIC SECTION //////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
+ if (args)
+ liberator.open(args);
+ else
+ liberator.open("about:blank");
+ }, {
+ completer: function (context) completion.url(context),
+ literal: 0,
+ privateData: true,
+ });
- return {
- // TODO: extract browser-specific functionality from liberator
- };
- //}}}
-} //}}}
+ commands.add(["redr[aw]"],
+ "Redraw the screen",
+ function () {
+ let wu = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ wu.redraw();
+ modes.show();
+ },
+ { argCount: "0" });
+ }
+});
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/buffer.js b/common/content/buffer.js
index 51623b24..30dbdfcf 100644
--- a/common/content/buffer.js
+++ b/common/content/buffer.js
@@ -14,32 +14,779 @@ const Point = new Struct("x", "y");
* files.
* @instance buffer
*/
-function Buffer() //{{{
-{
- ////////////////////////////////////////////////////////////////////////////////
- ////////////////////// PRIVATE SECTION /////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- if ("ZoomManager" in window)
- {
- const ZOOM_MIN = Math.round(ZoomManager.MIN * 100);
- const ZOOM_MAX = Math.round(ZoomManager.MAX * 100);
+const Buffer = Module("buffer", {
+ init: function () {
+
+ this.pageInfo = {};
+
+ this.addPageInfoSection("f", "Feeds", function (verbose) {
+ let doc = window.content.document;
+
+ const feedTypes = {
+ "application/rss+xml": "RSS",
+ "application/atom+xml": "Atom",
+ "text/xml": "XML",
+ "application/xml": "XML",
+ "application/rdf+xml": "XML"
+ };
+
+ function isValidFeed(data, principal, isFeed) {
+ if (!data || !principal)
+ return false;
+
+ if (!isFeed) {
+ var type = data.type && data.type.toLowerCase();
+ type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
+
+ isFeed = ["application/rss+xml", "application/atom+xml"].indexOf(type) >= 0 ||
+ // really slimy: general XML types with magic letters in the title
+ type in feedTypes && /\brss\b/i.test(data.title);
+ }
+
+ if (isFeed) {
+ try {
+ window.urlSecurityCheck(data.href, principal,
+ Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
+ }
+ catch (e) {
+ isFeed = false;
+ }
+ }
+
+ if (type)
+ data.type = type;
+
+ return isFeed;
+ }
+
+ let nFeed = 0;
+ for (let link in util.evaluateXPath(["link[@href and (@rel='feed' or (@rel='alternate' and @type))]"], doc)) {
+ let rel = link.rel.toLowerCase();
+ let feed = { title: link.title, href: link.href, type: link.type || "" };
+ if (isValidFeed(feed, doc.nodePrincipal, rel == "feed")) {
+ nFeed++;
+ let type = feedTypes[feed.type] || "RSS";
+ if (verbose)
+ yield [feed.title, template.highlightURL(feed.href, true) + <span class="extra-info">&#xa0;({type})</span>];
+ }
+ }
+
+ if (!verbose && nFeed)
+ yield nFeed + " feed" + (nFeed > 1 ? "s" : "");
+ });
+
+ this.addPageInfoSection("g", "General Info", function (verbose) {
+ let doc = window.content.document;
+
+ // get file size
+ const ACCESS_READ = Ci.nsICache.ACCESS_READ;
+ let cacheKey = doc.location.toString().replace(/#.*$/, "");
+
+ for (let proto in util.Array.itervalues(["HTTP", "FTP"])) {
+ try {
+ var cacheEntryDescriptor = services.get("cache").createSession(proto, 0, true)
+ .openCacheEntry(cacheKey, ACCESS_READ, false);
+ break;
+ }
+ catch (e) {}
+ }
+
+ let pageSize = []; // [0] bytes; [1] kbytes
+ if (cacheEntryDescriptor) {
+ pageSize[0] = util.formatBytes(cacheEntryDescriptor.dataSize, 0, false);
+ pageSize[1] = util.formatBytes(cacheEntryDescriptor.dataSize, 2, true);
+ if (pageSize[1] == pageSize[0])
+ pageSize.length = 1; // don't output "xx Bytes" twice
+ }
+
+ let lastModVerbose = new Date(doc.lastModified).toLocaleString();
+ let lastMod = new Date(doc.lastModified).toLocaleFormat("%x %X");
+
+ if (lastModVerbose == "Invalid Date" || new Date(doc.lastModified).getFullYear() == 1970)
+ lastModVerbose = lastMod = null;
+
+ if (!verbose) {
+ if (pageSize[0])
+ yield (pageSize[1] || pageSize[0]) + " bytes";
+ yield lastMod;
+ return;
+ }
+
+ yield ["Title", doc.title];
+ yield ["URL", template.highlightURL(doc.location.toString(), true)];
+
+ let ref = "referrer" in doc && doc.referrer;
+ if (ref)
+ yield ["Referrer", template.highlightURL(ref, true)];
+
+ if (pageSize[0])
+ yield ["File Size", pageSize[1] ? pageSize[1] + " (" + pageSize[0] + ")"
+ : pageSize[0]];
+
+ yield ["Mime-Type", doc.contentType];
+ yield ["Encoding", doc.characterSet];
+ yield ["Compatibility", doc.compatMode == "BackCompat" ? "Quirks Mode" : "Full/Almost Standards Mode"];
+ if (lastModVerbose)
+ yield ["Last Modified", lastModVerbose];
+ });
+
+ this.addPageInfoSection("m", "Meta Tags", function (verbose) {
+ // get meta tag data, sort and put into pageMeta[]
+ let metaNodes = window.content.document.getElementsByTagName("meta");
+
+ return Array.map(metaNodes, function (node) [(node.name || node.httpEquiv), template.highlightURL(node.content)])
+ .sort(function (a, b) util.compareIgnoreCase(a[0], b[0]));
+ });
+ },
+
+ /**
+ * @property {Array} The alternative style sheets for the current
+ * buffer. Only returns style sheets for the 'screen' media type.
+ */
+ get alternateStyleSheets() {
+ let stylesheets = window.getAllStyleSheets(window.content);
+
+ return stylesheets.filter(
+ function (stylesheet) /^(screen|all|)$/i.test(stylesheet.media.mediaText) && !/^\s*$/.test(stylesheet.title)
+ );
+ },
+
+ /**
+ * @property {Object} A map of page info sections to their
+ * content generating functions.
+ */
+ pageInfo: null,
+
+ /**
+ * @property {number} A value indicating whether the buffer is loaded.
+ * Values may be:
+ * 0 - Loading.
+ * 1 - Fully loaded.
+ * 2 - Load failed.
+ */
+ get loaded() {
+ if (window.content.document.pageIsFullyLoaded !== undefined)
+ return window.content.document.pageIsFullyLoaded;
+ return 0; // in doubt return "loading"
+ },
+ set loaded(value) {
+ window.content.document.pageIsFullyLoaded = value;
+ },
+
+ /**
+ * @property {Node} The last focused input field in the buffer. Used
+ * by the "gi" key binding.
+ */
+ get lastInputField() window.content.document.lastInputField || null,
+ set lastInputField(value) { window.content.document.lastInputField = value; },
+
+ /**
+ * @property {string} The current top-level document's URL.
+ */
+ get URL() window.content.location.href,
+
+ /**
+ * @property {string} The current top-level document's URL, sans any
+ * fragment identifier.
+ */
+ get URI() {
+ let loc = window.content.location;
+ return loc.href.substr(0, loc.href.length - loc.hash.length);
+ },
+
+ /**
+ * @property {number} The buffer's height in pixels.
+ */
+ get pageHeight() window.content.innerHeight,
+
+ /**
+ * @property {number} The current browser's text zoom level, as a
+ * percentage with 100 as 'normal'. Only affects text size.
+ */
+ get textZoom() getBrowser().markupDocumentViewer.textZoom * 100,
+ set textZoom(value) { Buffer.setZoom(value, false); },
+
+ /**
+ * @property {number} The current browser's text zoom level, as a
+ * percentage with 100 as 'normal'. Affects text size, as well as
+ * image size and block size.
+ */
+ get fullZoom() getBrowser().markupDocumentViewer.fullZoom * 100,
+ set fullZoom(value) { Buffer.setZoom(value, true); },
+
+ /**
+ * @property {string} The current document's title.
+ */
+ get title() window.content.document.title,
+
+ /**
+ * @property {number} The buffer's horizontal scroll percentile.
+ */
+ get scrollXPercent() {
+ let win = Buffer.findScrollableWindow();
+ if (win.scrollMaxX > 0)
+ return Math.round(win.scrollX / win.scrollMaxX * 100);
+ else
+ return 0;
+ },
+
+ /**
+ * @property {number} The buffer's vertical scroll percentile.
+ */
+ get scrollYPercent() {
+ let win = Buffer.findScrollableWindow();
+ if (win.scrollMaxY > 0)
+ return Math.round(win.scrollY / win.scrollMaxY * 100);
+ else
+ return 0;
+ },
+
+ /**
+ * Adds a new section to the page information output.
+ *
+ * @param {string} option The section's value in 'pageinfo'.
+ * @param {string} title The heading for this section's
+ * output.
+ * @param {function} func The function to generate this
+ * section's output.
+ */
+ addPageInfoSection: function addPageInfoSection(option, title, func) {
+ this.pageInfo[option] = [func, title];
+ },
+
+ /**
+ * Returns the currently selected word. If the selection is
+ * null, it tries to guess the word that the caret is
+ * positioned in.
+ *
+ * NOTE: might change the selection
+ *
+ * @returns {string}
+ */
+ // FIXME: getSelection() doesn't always preserve line endings, see:
+ // https://www.mozdev.org/bugs/show_bug.cgi?id=19303
+ getCurrentWord: function () {
+ let selection = window.content.getSelection();
+ let range = selection.getRangeAt(0);
+ if (selection.isCollapsed) {
+ let selController = this.selectionController;
+ let caretmode = selController.getCaretEnabled();
+ selController.setCaretEnabled(true);
+ // Only move backwards if the previous character is not a space.
+ if (range.startOffset > 0 && !/\s/.test(range.startContainer.textContent[range.startOffset - 1]))
+ selController.wordMove(false, false);
+
+ selController.wordMove(true, true);
+ selController.setCaretEnabled(caretmode);
+ return String.match(selection, /\w*/)[0];
+ }
+ if (util.computedStyle(range.startContainer).whiteSpace == "pre"
+ && util.computedStyle(range.endContainer).whiteSpace == "pre")
+ return String(range);
+ return String(selection);
+ },
+
+ /**
+ * Focuses the given element. In contrast to a simple
+ * elem.focus() call, this function works for iframes and
+ * image maps.
+ *
+ * @param {Node} elem The element to focus.
+ */
+ focusElement: function (elem) {
+ let doc = window.content.document;
+ if (elem instanceof HTMLFrameElement || elem instanceof HTMLIFrameElement)
+ return void elem.contentWindow.focus();
+ else if (elem instanceof HTMLInputElement && elem.type == "file") {
+ Buffer.openUploadPrompt(elem);
+ buffer.lastInputField = elem;
+ return;
+ }
+
+ elem.focus();
+
+ // for imagemap
+ if (elem instanceof HTMLAreaElement) {
+ try {
+ let [x, y] = elem.getAttribute("coords").split(",").map(parseFloat);
+
+ elem.dispatchEvent(events.create(doc, "mouseover", { screenX: x, screenY: y }));
+ }
+ catch (e) {}
+ }
+ },
+
+ /**
+ * Tries to guess links the like of "next" and "prev". Though it has a
+ * singularly horrendous name, it turns out to be quite useful.
+ *
+ * @param {string} rel The relationship to look for. Looks for
+ * links with matching @rel or @rev attributes, and,
+ * failing that, looks for an option named rel +
+ * "pattern", and finds the last link matching that
+ * RegExp.
+ */
+ followDocumentRelationship: function (rel) {
+ let regexes = options.get(rel + "pattern").values
+ .map(function (re) RegExp(re, "i"));
+
+ function followFrame(frame) {
+ function iter(elems) {
+ for (let i = 0; i < elems.length; i++)
+ if (elems[i].rel.toLowerCase() == rel || elems[i].rev.toLowerCase() == rel)
+ yield elems[i];
+ }
+
+ // <link>s have higher priority than normal <a> hrefs
+ let elems = frame.document.getElementsByTagName("link");
+ for (let elem in iter(elems)) {
+ liberator.open(elem.href);
+ return true;
+ }
+
+ // no links? ok, look for hrefs
+ elems = frame.document.getElementsByTagName("a");
+ for (let elem in iter(elems)) {
+ buffer.followLink(elem, liberator.CURRENT_TAB);
+ return true;
+ }
+
+ let res = util.evaluateXPath(options.get("hinttags").defaultValue, frame.document);
+ for (let [, regex] in Iterator(regexes)) {
+ for (let i in util.range(res.snapshotLength, 0, -1)) {
+ let elem = res.snapshotItem(i);
+ if (regex.test(elem.textContent) ||
+ regex.test(elem.title) ||
+ Array.some(elem.childNodes, function (child) regex.test(child.alt)))
+ {
+ buffer.followLink(elem, liberator.CURRENT_TAB);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ let ret = followFrame(window.content);
+ if (!ret)
+ // only loop through frames if the main content didn't match
+ ret = Array.some(window.content.frames, followFrame);
+
+ if (!ret)
+ liberator.beep();
+ },
+
+ /**
+ * Fakes a click on a link.
+ *
+ * @param {Node} elem The element to click.
+ * @param {number} where Where to open the link. See
+ * {@link liberator.open}.
+ */
+ followLink: function (elem, where) {
+ let doc = elem.ownerDocument;
+ let view = doc.defaultView;
+ let offsetX = 1;
+ let offsetY = 1;
+
+ if (elem instanceof HTMLFrameElement || elem instanceof HTMLIFrameElement) {
+ elem.contentWindow.focus();
+ return;
+ }
+ else if (elem instanceof HTMLAreaElement) { // for imagemap
+ let coords = elem.getAttribute("coords").split(",");
+ offsetX = Number(coords[0]) + 1;
+ offsetY = Number(coords[1]) + 1;
+ }
+ else if (elem instanceof HTMLInputElement && elem.type == "file") {
+ Buffer.openUploadPrompt(elem);
+ return;
+ }
+
+ let ctrlKey = false, shiftKey = false;
+ switch (where) {
+ case liberator.NEW_TAB:
+ case liberator.NEW_BACKGROUND_TAB:
+ ctrlKey = true;
+ shiftKey = (where != liberator.NEW_BACKGROUND_TAB);
+ break;
+ case liberator.NEW_WINDOW:
+ shiftKey = true;
+ break;
+ case liberator.CURRENT_TAB:
+ break;
+ default:
+ liberator.log("Invalid where argument for followLink()", 0);
+ }
+
+ elem.focus();
+
+ options.withContext(function () {
+ options.setPref("browser.tabs.loadInBackground", true);
+ ["mousedown", "mouseup", "click"].forEach(function (event) {
+ elem.dispatchEvent(events.create(doc, event, {
+ screenX: offsetX, screenY: offsetY,
+ ctrlKey: ctrlKey, shiftKey: shiftKey, metaKey: ctrlKey
+ }));
+ });
+ });
+ },
+
+ /**
+ * @property {nsISelectionController} The current document's selection
+ * controller.
+ */
+ get selectionController() getBrowser().docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsISelectionDisplay)
+ .QueryInterface(Ci.nsISelectionController),
+
+ /**
+ * Opens the appropriate context menu for <b>elem</b>.
+ *
+ * @param {Node} elem The context element.
+ */
+ openContextMenu: function (elem) {
+ document.popupNode = elem;
+ let menu = document.getElementById("contentAreaContextMenu");
+ menu.showPopup(elem, -1, -1, "context", "bottomleft", "topleft");
+ },
+
+ /**
+ * Saves a page link to disk.
+ *
+ * @param {HTMLAnchorElement} elem The page link to save.
+ * @param {boolean} skipPrompt Whether to open the "Save Link As..."
+ * dialog.
+ */
+ saveLink: function (elem, skipPrompt) {
+ let doc = elem.ownerDocument;
+ let url = window.makeURLAbsolute(elem.baseURI, elem.href);
+ let text = elem.textContent;
+
+ try {
+ window.urlSecurityCheck(url, doc.nodePrincipal);
+ // we always want to save that link relative to the current working directory
+ options.setPref("browser.download.lastDir", io.getCurrentDirectory().path);
+ window.saveURL(url, text, null, true, skipPrompt, makeURI(url, doc.characterSet));
+ }
+ catch (e) {
+ liberator.echoerr(e);
+ }
+ },
+
+ /**
+ * Scrolls to the bottom of the current buffer.
+ */
+ scrollBottom: function () {
+ Buffer.scrollToPercent(null, 100);
+ },
+
+ /**
+ * Scrolls the buffer laterally <b>cols</b> columns.
+ *
+ * @param {number} cols The number of columns to scroll. A positive
+ * value scrolls right and a negative value left.
+ */
+ scrollColumns: function (cols) {
+ Buffer.scrollHorizontal(null, "columns", cols);
+ },
+
+ /**
+ * Scrolls to the top of the current buffer.
+ */
+ scrollEnd: function () {
+ Buffer.scrollToPercent(100, null);
+ },
+
+ /**
+ * Scrolls the buffer vertically <b>lines</b> rows.
+ *
+ * @param {number} lines The number of lines to scroll. A positive
+ * value scrolls down and a negative value up.
+ */
+ scrollLines: function (lines) {
+ Buffer.scrollVertical(null, "lines", lines);
+ },
+
+ /**
+ * Scrolls the buffer vertically <b>pages</b> pages.
+ *
+ * @param {number} pages The number of pages to scroll. A positive
+ * value scrolls down and a negative value up.
+ */
+ scrollPages: function (pages) {
+ Buffer.scrollVertical(null, "pages", pages);
+ },
+
+ /**
+ * Scrolls the buffer vertically 'scroll' lines.
+ *
+ * @param {boolean} direction The direction to scroll. If true then
+ * scroll up and if false scroll down.
+ * @param {number} count The multiple of 'scroll' lines to scroll.
+ * @optional
+ */
+ scrollByScrollSize: function (direction, count) {
+ direction = direction ? 1 : -1;
+ count = count || 1;
+ let win = Buffer.findScrollableWindow();
+
+ Buffer.checkScrollYBounds(win, direction);
+
+ if (options["scroll"] > 0)
+ this.scrollLines(options["scroll"] * direction);
+ else // scroll half a page down in pixels
+ win.scrollBy(0, win.innerHeight / 2 * direction);
+ },
+
+ _scrollByScrollSize: function _scrollByScrollSize(count, direction) {
+ if (count > 0)
+ options["scroll"] = count;
+ buffer.scrollByScrollSize(direction);
+ },
+
+ /**
+ * Scrolls the buffer to the specified screen percentiles.
+ *
+ * @param {number} x The horizontal page percentile.
+ * @param {number} y The vertical page percentile.
+ */
+ scrollToPercent: function (x, y) {
+ Buffer.scrollToPercent(x, y);
+ },
+
+ /**
+ * Scrolls the buffer to the specified screen pixels.
+ *
+ * @param {number} x The horizontal pixel.
+ * @param {number} y The vertical pixel.
+ */
+ scrollTo: function (x, y) {
+ marks.add("'", true);
+ content.scrollTo(x, y);
+ },
+
+ /**
+ * Scrolls the current buffer laterally to its leftmost.
+ */
+ scrollStart: function () {
+ Buffer.scrollToPercent(0, null);
+ },
+
+ /**
+ * Scrolls the current buffer vertically to the top.
+ */
+ scrollTop: function () {
+ Buffer.scrollToPercent(null, 0);
+ },
+
+ // TODO: allow callback for filtering out unwanted frames? User defined?
+ /**
+ * Shifts the focus to another frame within the buffer. Each buffer
+ * contains at least one frame.
+ *
+ * @param {number} count The number of frames to skip through.
+ * @param {boolean} forward The direction of motion.
+ */
+ shiftFrameFocus: function (count, forward) {
+ if (!(window.content.document instanceof HTMLDocument))
+ return;
+
+ count = Math.max(count, 1);
+ let frames = [];
+
+ // find all frames - depth-first search
+ (function (frame) {
+ if (frame.document.body instanceof HTMLBodyElement)
+ frames.push(frame);
+ Array.forEach(frame.frames, arguments.callee);
+ })(window.content);
+
+ if (frames.length == 0) // currently top is always included
+ return;
+
+ // remove all unfocusable frames
+ // TODO: find a better way to do this - walking the tree is too slow
+ let start = document.commandDispatcher.focusedWindow;
+ frames = frames.filter(function (frame) {
+ frame.focus();
+ return document.commandDispatcher.focusedWindow == frame;
+ });
+ start.focus();
+
+ // find the currently focused frame index
+ // TODO: If the window is a frameset then the first _frame_ should be
+ // focused. Since this is not the current FF behaviour,
+ // we initalize current to -1 so the first call takes us to the
+ // first frame.
+ let current = frames.indexOf(document.commandDispatcher.focusedWindow);
+
+ // calculate the next frame to focus
+ let next = current;
+ if (forward) {
+ next = current + count;
+
+ if (next > frames.length - 1) {
+ if (current == frames.length - 1)
+ liberator.beep();
+ next = frames.length - 1; // still allow the frame indicator to be activated
+ }
+ }
+ else {
+ next = current - count;
+
+ if (next < 0) {
+ if (current == 0)
+ liberator.beep();
+ next = 0; // still allow the frame indicator to be activated
+ }
+ }
+
+ // focus next frame and scroll into view
+ frames[next].focus();
+ if (frames[next] != window.content)
+ frames[next].frameElement.scrollIntoView(false);
+
+ // add the frame indicator
+ let doc = frames[next].document;
+ let indicator = util.xmlToDom(<div highlight="FrameIndicator"/>, doc);
+ doc.body.appendChild(indicator);
+
+ setTimeout(function () { doc.body.removeChild(indicator); }, 500);
+
+ // Doesn't unattach
+ //doc.body.setAttributeNS(NS.uri, "activeframe", "true");
+ //setTimeout(function () { doc.body.removeAttributeNS(NS.uri, "activeframe"); }, 500);
+ },
+
+ // similar to pageInfo
+ // TODO: print more useful information, just like the DOM inspector
+ /**
+ * Displays information about the specified element.
+ *
+ * @param {Node} elem The element to query.
+ */
+ showElementInfo: function (elem) {
+ liberator.echo(<>Element:<br/>{util.objectToString(elem, true)}</>, commandline.FORCE_MULTILINE);
+ },
+
+ /**
+ * Displays information about the current buffer.
+ *
+ * @param {boolean} verbose Display more verbose information.
+ * @param {string} sections A string limiting the displayed sections.
+ * @default The value of 'pageinfo'.
+ */
+ showPageInfo: function (verbose, sections) {
+ // Ctrl-g single line output
+ if (!verbose) {
+ let file = content.document.location.pathname.split("/").pop() || "[No Name]";
+ let title = content.document.title || "[No Title]";
+
+ let info = template.map("gf", function (opt)
+ template.map(this.pageInfo[opt][0](), util.identity, ", "),
+ ", ");
+
+ if (bookmarks.isBookmarked(this.URL))
+ info += ", bookmarked";
+
+ let pageInfoText = <>{file.quote()} [{info}] {title}</>;
+ liberator.echo(pageInfoText, commandline.FORCE_SINGLELINE);
+ return;
+ }
+
+ let option = sections || options["pageinfo"];
+ let list = template.map(option, function (option) {
+ let opt = this.pageInfo[option];
+ if (opt)
+ return template.table(opt[1], opt[0](true));
+ }, <br/>);
+ liberator.echo(list, commandline.FORCE_MULTILINE);
+ },
+
+ /**
+ * Opens a viewer to inspect the source of the currently selected
+ * range.
+ */
+ viewSelectionSource: function () {
+ // copied (and tuned somebit) from browser.jar -> nsContextMenu.js
+ let focusedWindow = document.commandDispatcher.focusedWindow;
+ if (focusedWindow == window)
+ focusedWindow = content;
+
+ let docCharset = null;
+ if (focusedWindow)
+ docCharset = "charset=" + focusedWindow.document.characterSet;
+
+ let reference = null;
+ reference = focusedWindow.getSelection();
+
+ let docUrl = null;
+ window.openDialog("chrome://global/content/viewPartialSource.xul",
+ "_blank", "scrollbars,resizable,chrome,dialog=no",
+ docUrl, docCharset, reference, "selection");
+ },
+
+ /**
+ * Opens a viewer to inspect the source of the current buffer or the
+ * specified <b>url</b>. Either the default viewer or the configured
+ * external editor is used.
+ *
+ * @param {string} url The URL of the source.
+ * @default The current buffer.
+ * @param {boolean} useExternalEditor View the source in the external editor.
+ */
+ viewSource: function (url, useExternalEditor) {
+ url = url || buffer.URI;
+
+ if (useExternalEditor)
+ editor.editFileExternally(url);
+ else {
+ const PREFIX = "view-source:";
+ if (url.indexOf(PREFIX) == 0)
+ url = url.substr(PREFIX.length);
+ else
+ url = PREFIX + url;
+ liberator.open(url, { hide: true });
+ }
+ },
+
+ /**
+ * Increases the zoom level of the current buffer.
+ *
+ * @param {number} steps The number of zoom levels to jump.
+ * @param {boolean} fullZoom Whether to use full zoom or text zoom.
+ */
+ zoomIn: function (steps, fullZoom) {
+ Buffer.bumpZoomLevel(steps, fullZoom);
+ },
+
+ /**
+ * Decreases the zoom level of the current buffer.
+ *
+ * @param {number} steps The number of zoom levels to jump.
+ * @param {boolean} fullZoom Whether to use full zoom or text zoom.
+ */
+ zoomOut: function (steps, fullZoom) {
+ Buffer.bumpZoomLevel(-steps, fullZoom);
}
+}, {
+ ZOOM_MIN: "ZoomManager" in window && Math.round(ZoomManager.MIN * 100),
+ ZOOM_MAX: "ZoomManager" in window && Math.round(ZoomManager.MAX * 100),
- function setZoom(value, fullZoom)
- {
- liberator.assert(value >= ZOOM_MIN && value <= ZOOM_MAX,
- "Zoom value out of range (" + ZOOM_MIN + " - " + ZOOM_MAX + "%)");
+ setZoom: function setZoom(value, fullZoom) {
+ liberator.assert(value >= Buffer.ZOOM_MIN || value <= Buffer.ZOOM_MAX,
+ "Zoom value out of range (" + Buffer.ZOOM_MIN + " - " + Buffer.ZOOM_MAX + "%)");
ZoomManager.useFullZoom = fullZoom;
ZoomManager.zoom = value / 100;
if ("FullZoom" in window)
FullZoom._applySettingToPref();
liberator.echomsg((fullZoom ? "Full" : "Text") + " zoom: " + value + "%");
- }
+ },
- function bumpZoomLevel(steps, fullZoom)
- {
+ bumpZoomLevel: function bumpZoomLevel(steps, fullZoom) {
let values = ZoomManager.zoomValues;
let cur = values.indexOf(ZoomManager.snap(ZoomManager.zoom));
let i = util.Math.constrain(cur + steps, 0, values.length - 1);
@@ -47,18 +794,16 @@ function Buffer() //{{{
if (i == cur && fullZoom == ZoomManager.useFullZoom)
liberator.beep();
- setZoom(Math.round(values[i] * 100), fullZoom);
- }
+ Buffer.setZoom(Math.round(values[i] * 100), fullZoom);
+ },
- function checkScrollYBounds(win, direction)
- {
+ checkScrollYBounds: function checkScrollYBounds(win, direction) {
// NOTE: it's possible to have scrollY > scrollMaxY - FF bug?
if (direction > 0 && win.scrollY >= win.scrollMaxY || direction < 0 && win.scrollY == 0)
liberator.beep();
- }
+ },
- function findScrollableWindow()
- {
+ findScrollableWindow: function findScrollableWindow() {
let win = window.document.commandDispatcher.focusedWindow;
if (win && (win.scrollMaxX > 0 || win.scrollMaxY > 0))
return win;
@@ -72,20 +817,17 @@ function Buffer() //{{{
return frame;
return win;
- }
+ },
- function findScrollable(dir, horizontal)
- {
+ findScrollable: function findScrollable(dir, horizontal) {
let pos = "scrollTop", size = "clientHeight", max = "scrollHeight", layoutSize = "offsetHeight",
overflow = "overflowX", border1 = "borderTopWidth", border2 = "borderBottomWidth";
if (horizontal)
pos = "scrollLeft", size = "clientWidth", max = "scrollWidth", layoutSize = "offsetWidth",
overflow = "overflowX", border1 = "borderLeftWidth", border2 = "borderRightWidth";
- function find(elem)
- {
- for (; elem && elem.parentNode instanceof Element; elem = elem.parentNode)
- {
+ function find(elem) {
+ for (; elem && elem.parentNode instanceof Element; elem = elem.parentNode) {
let style = util.computedStyle(elem);
let borderSize = parseInt(style[border1]) + parseInt(style[border2]);
let realSize = elem[size];
@@ -102,18 +844,16 @@ function Buffer() //{{{
if (content.getSelection().rangeCount)
var elem = find(content.getSelection().getRangeAt(0).startContainer);
- if (!(elem instanceof Element))
- {
- let doc = findScrollableWindow().document;
+ if (!(elem instanceof Element)) {
+ let doc = Buffer.findScrollableWindow().document;
elem = find(doc.body || doc.getElementsByTagName("body")[0] ||
doc.documentElement);
}
return elem;
- }
+ },
- function scrollVertical(elem, increment, number)
- {
- elem = elem || findScrollable(number, false);
+ scrollVertical: function scrollVertical(elem, increment, number) {
+ elem = elem || Buffer.findScrollable(number, false);
let fontSize = parseInt(util.computedStyle(elem).fontSize);
let increment;
if (increment == "lines")
@@ -124,10 +864,9 @@ function Buffer() //{{{
throw Error()
elem.scrollTop += number * increment;
- }
- function scrollHorizontal(elem, increment, number)
- {
- elem = elem || findScrollable(number, true);
+ },
+ scrollHorizontal: function scrollHorizontal(elem, increment, number) {
+ elem = elem || Buffer.findScrollable(number, true);
let fontSize = parseInt(util.computedStyle(elem).fontSize);
let increment;
if (increment == "columns")
@@ -138,11 +877,10 @@ function Buffer() //{{{
throw Error()
elem.scrollLeft += number * increment;
- }
+ },
- function scrollElemToPercent(elem, horizontal, vertical)
- {
- elem = elem || findScrollable();
+ scrollElemToPercent: function scrollElemToPercent(elem, horizontal, vertical) {
+ elem = elem || Buffer.findScrollable();
marks.add("'", true);
if (horizontal != null)
@@ -150,11 +888,10 @@ function Buffer() //{{{
if (vertical != null)
elem.scrollTop = (elem.scrollHeight - elem.clientHeight) * (vertical / 100);
- }
+ },
- function scrollToPercent(horizontal, vertical)
- {
- let win = findScrollableWindow();
+ scrollToPercent: function scrollToPercent(horizontal, vertical) {
+ let win = Buffer.findScrollableWindow();
let h, v;
if (horizontal == null)
@@ -169,539 +906,191 @@ function Buffer() //{{{
marks.add("'", true);
win.scrollTo(h, v);
- }
+ },
- // Holds option: [function, title] to generate :pageinfo sections
- var pageInfo = {};
- function addPageInfoSection(option, title, func)
- {
- pageInfo[option] = [func, title];
- }
-
- function openUploadPrompt(elem)
- {
+ openUploadPrompt: function openUploadPrompt(elem) {
commandline.input("Upload file: ", function (path) {
let file = io.File(path);
if (!file.exists())
return void liberator.beep();
elem.value = file.path;
- },
- {
+ }, {
completer: completion.file,
default: elem.value
});
- }
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// OPTIONS /////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- options.add(["nextpattern"], // \u00BB is » (>> in a single char)
- "Patterns to use when guessing the 'next' page in a document sequence",
- "stringlist", "\\bnext\\b,^>$,^(>>|\u00BB)$,^(>|\u00BB),(>|\u00BB)$,\\bmore\\b");
-
- options.add(["previouspattern"], // \u00AB is « (<< in a single char)
- "Patterns to use when guessing the 'previous' page in a document sequence",
- "stringlist", "\\bprev|previous\\b,^<$,^(<<|\u00AB)$,^(<|\u00AB),(<|\u00AB)$");
-
- options.add(["pageinfo", "pa"],
- "Desired info in the :pageinfo output",
- "charlist", "gfm",
- {
- completer: function (context) [[k, v[1]] for ([k, v] in Iterator(pageInfo))],
- validator: Option.validateCompleter
- });
-
- options.add(["scroll", "scr"],
- "Number of lines to scroll with <C-u> and <C-d> commands",
- "number", 0,
- { validator: function (value) value >= 0 });
-
- options.add(["showstatuslinks", "ssli"],
- "Show the destination of the link under the cursor in the status bar",
- "number", 1,
- {
- completer: function (context) [
- ["0", "Don't show link destination"],
- ["1", "Show the link in the status line"],
- ["2", "Show the link in the command line"]
- ],
- validator: Option.validateCompleter
- });
-
- options.add(["usermode", "um"],
- "Show current website with a minimal style sheet to make it easily accessible",
- "boolean", false,
- {
- setter: function (value) getBrowser().markupDocumentViewer.authorStyleDisabled = value,
- getter: function () getBrowser().markupDocumentViewer.authorStyleDisabled
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// MAPPINGS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- var myModes = config.browserModes;
-
- mappings.add(myModes, ["."],
- "Repeat the last key event",
- function (count)
- {
- if (mappings.repeat)
- {
- for (let i in util.interruptibleRange(0, Math.max(count, 1), 100))
- mappings.repeat();
- }
- },
- { count: true });
-
- mappings.add(myModes, ["i", "<Insert>"],
- "Start caret mode",
- function ()
- {
- // setting this option notifies an observer which takes care of the
- // mode setting
- options.setPref("accessibility.browsewithcaret", true);
- });
-
- mappings.add(myModes, ["<C-c>"],
- "Stop loading the current web page",
- function () { tabs.stop(getBrowser().mCurrentTab); });
-
- // scrolling
- mappings.add(myModes, ["j", "<Down>", "<C-e>"],
- "Scroll document down",
- function (count) { buffer.scrollLines(Math.max(count, 1)); },
- { count: true });
-
- mappings.add(myModes, ["k", "<Up>", "<C-y>"],
- "Scroll document up",
- function (count) { buffer.scrollLines(-Math.max(count, 1)); },
- { count: true });
-
- mappings.add(myModes, liberator.has("mail") ? ["h"] : ["h", "<Left>"],
- "Scroll document to the left",
- function (count) { buffer.scrollColumns(-Math.max(count, 1)); },
- { count: true });
-
- mappings.add(myModes, liberator.has("mail") ? ["l"] : ["l", "<Right>"],
- "Scroll document to the right",
- function (count) { buffer.scrollColumns(Math.max(count, 1)); },
- { count: true });
-
- mappings.add(myModes, ["0", "^"],
- "Scroll to the absolute left of the document",
- function () { buffer.scrollStart(); });
-
- mappings.add(myModes, ["$"],
- "Scroll to the absolute right of the document",
- function () { buffer.scrollEnd(); });
-
- mappings.add(myModes, ["gg", "<Home>"],
- "Go to the top of the document",
- function (count) { buffer.scrollToPercent(buffer.scrollXPercent, Math.max(count, 0)); },
- { count: true });
-
- mappings.add(myModes, ["G", "<End>"],
- "Go to the end of the document",
- function (count) { buffer.scrollToPercent(buffer.scrollXPercent, count >= 0 ? count : 100); },
- { count: true });
-
- mappings.add(myModes, ["%"],
- "Scroll to {count} percent of the document",
- function (count)
- {
- if (count > 0 && count <= 100)
- buffer.scrollToPercent(buffer.scrollXPercent, count);
- else
- liberator.beep();
- },
- { count: true });
-
- function scrollByScrollSize(count, direction)
- {
- if (count > 0)
- options["scroll"] = count;
- buffer.scrollByScrollSize(direction);
- }
+ },
+}, {
+ commands: function () {
+ commands.add(["frameo[nly]"],
+ "Show only the current frame's page",
+ function (args) {
+ liberator.open(tabs.localStore.focusedFrame.document.documentURI);
+ },
+ { argCount: "0" });
+
+ commands.add(["ha[rdcopy]"],
+ "Print current document",
+ function (args) {
+ let arg = args[0];
+
+ // FIXME: arg handling is a bit of a mess, check for filename
+ liberator.assert(!arg || arg[0] == ">" && !liberator.has("Win32"),
+ "E488: Trailing characters");
+
+ options.withContext(function () {
+ if (arg) {
+ options.setPref("print.print_to_file", "true");
+ options.setPref("print.print_to_filename", io.File(arg.substr(1)).path);
+ liberator.echomsg("Printing to file: " + arg.substr(1));
+ }
+ else
+ liberator.echomsg("Sending to printer...");
- mappings.add(myModes, ["<C-d>"],
- "Scroll window downwards in the buffer",
- function (count) { scrollByScrollSize(count, true); },
- { count: true });
-
- mappings.add(myModes, ["<C-u>"],
- "Scroll window upwards in the buffer",
- function (count) { scrollByScrollSize(count, false); },
- { count: true });
-
- mappings.add(myModes, ["<C-b>", "<PageUp>", "<S-Space>"],
- "Scroll up a full page",
- function (count) { buffer.scrollPages(-Math.max(count, 1)); },
- { count: true });
-
- mappings.add(myModes, ["<C-f>", "<PageDown>", "<Space>"],
- "Scroll down a full page",
- function (count) { buffer.scrollPages(Math.max(count, 1)); },
- { count: true });
-
- mappings.add(myModes, ["]f"],
- "Focus next frame",
- function (count) { buffer.shiftFrameFocus(Math.max(count, 1), true); },
- { count: true });
-
- mappings.add(myModes, ["[f"],
- "Focus previous frame",
- function (count) { buffer.shiftFrameFocus(Math.max(count, 1), false); },
- { count: true });
-
- mappings.add(myModes, ["]]"],
- "Follow the link labeled 'next' or '>' if it exists",
- function (count) { buffer.followDocumentRelationship("next"); },
- { count: true });
-
- mappings.add(myModes, ["[["],
- "Follow the link labeled 'prev', 'previous' or '<' if it exists",
- function (count) { buffer.followDocumentRelationship("previous"); },
- { count: true });
-
- mappings.add(myModes, ["gf"],
- "View source",
- function () { buffer.viewSource(null, false); });
-
- mappings.add(myModes, ["gF"],
- "View source with an external editor",
- function () { buffer.viewSource(null, true); });
-
- mappings.add(myModes, ["|"],
- "Toggle between rendered and source view",
- function () { buffer.viewSource(null, false); });
-
- mappings.add(myModes, ["gi"],
- "Focus last used input field",
- function (count)
- {
- if (count < 1 && buffer.lastInputField)
- buffer.focusElement(buffer.lastInputField);
- else
- {
- let xpath = ["input[not(@type) or @type='text' or @type='password' or @type='file']",
- "textarea[not(@disabled) and not(@readonly)]"];
+ options.setPref("print.always_print_silent", args.bang);
+ options.setPref("print.show_print_progress", !args.bang);
- let elements = [m for (m in util.evaluateXPath(xpath))].filter(function (match) {
- let computedStyle = util.computedStyle(match);
- return computedStyle.visibility != "hidden" && computedStyle.display != "none";
+ getBrowser().contentWindow.print();
});
- if (elements.length > 0)
- buffer.focusElement(elements[util.Math.constrain(count, 1, elements.length) - 1]);
+ if (arg)
+ liberator.echomsg("Printed: " + arg.substr(1));
else
- liberator.beep();
- }
- },
- { count: true });
-
- mappings.add(myModes, ["gP"],
- "Open (put) a URL based on the current clipboard contents in a new buffer",
- function ()
- {
- liberator.open(util.readFromClipboard(),
- liberator[options.get("activate").has("paste") ? "NEW_BACKGROUND_TAB" : "NEW_TAB"]);
- });
-
- mappings.add(myModes, ["p", "<MiddleMouse>"],
- "Open (put) a URL based on the current clipboard contents in the current buffer",
- function ()
- {
- let url = util.readFromClipboard();
- if (url)
- liberator.open(url);
- else
- liberator.beep();
- });
-
- mappings.add(myModes, ["P"],
- "Open (put) a URL based on the current clipboard contents in a new buffer",
- function ()
- {
- let url = util.readFromClipboard();
- if (url)
- liberator.open(url, { from: "activate", where: liberator.NEW_TAB });
- else
- liberator.beep();
- });
-
- // reloading
- mappings.add(myModes, ["r"],
- "Reload the current web page",
- function () { tabs.reload(getBrowser().mCurrentTab, false); });
+ liberator.echomsg("Print job sent.");
+ },
+ {
+ argCount: "?",
+ literal: 0,
+ bang: true
+ });
- mappings.add(myModes, ["R"],
- "Reload while skipping the cache",
- function () { tabs.reload(getBrowser().mCurrentTab, true); });
+ commands.add(["pa[geinfo]"],
+ "Show various page information",
+ function (args) { buffer.showPageInfo(true, args[0]); },
+ {
+ argCount: "?",
+ completer: function (context) {
+ completion.optionValue(context, "pageinfo", "+", "");
+ context.title = ["Page Info"];
+ }
+ });
- // yanking
- mappings.add(myModes, ["Y"],
- "Copy selected text or current word",
- function ()
- {
- let sel = buffer.getCurrentWord();
+ commands.add(["pagest[yle]", "pas"],
+ "Select the author style sheet to apply",
+ function (args) {
+ let arg = args.literalArg;
- if (sel)
- util.copyToClipboard(sel, true);
- else
- liberator.beep();
- });
+ let titles = buffer.alternateStyleSheets.map(function (stylesheet) stylesheet.title);
- // zooming
- mappings.add(myModes, ["zi", "+"],
- "Enlarge text zoom of current web page",
- function (count) { buffer.zoomIn(Math.max(count, 1), false); },
- { count: true });
-
- mappings.add(myModes, ["zm"],
- "Enlarge text zoom of current web page by a larger amount",
- function (count) { buffer.zoomIn(Math.max(count, 1) * 3, false); },
- { count: true });
-
- mappings.add(myModes, ["zo", "-"],
- "Reduce text zoom of current web page",
- function (count) { buffer.zoomOut(Math.max(count, 1), false); },
- { count: true });
-
- mappings.add(myModes, ["zr"],
- "Reduce text zoom of current web page by a larger amount",
- function (count) { buffer.zoomOut(Math.max(count, 1) * 3, false); },
- { count: true });
-
- mappings.add(myModes, ["zz"],
- "Set text zoom value of current web page",
- function (count) { buffer.textZoom = count > 1 ? count : 100; },
- { count: true });
-
- mappings.add(myModes, ["zI"],
- "Enlarge full zoom of current web page",
- function (count) { buffer.zoomIn(Math.max(count, 1), true); },
- { count: true });
-
- mappings.add(myModes, ["zM"],
- "Enlarge full zoom of current web page by a larger amount",
- function (count) { buffer.zoomIn(Math.max(count, 1) * 3, true); },
- { count: true });
-
- mappings.add(myModes, ["zO"],
- "Reduce full zoom of current web page",
- function (count) { buffer.zoomOut(Math.max(count, 1), true); },
- { count: true });
-
- mappings.add(myModes, ["zR"],
- "Reduce full zoom of current web page by a larger amount",
- function (count) { buffer.zoomOut(Math.max(count, 1) * 3, true); },
- { count: true });
-
- mappings.add(myModes, ["zZ"],
- "Set full zoom value of current web page",
- function (count) { buffer.fullZoom = count > 1 ? count : 100; },
- { count: true });
-
- // page info
- mappings.add(myModes, ["<C-g>"],
- "Print the current file name",
- function (count) { buffer.showPageInfo(false); },
- { count: true });
-
- mappings.add(myModes, ["g<C-g>"],
- "Print file information",
- function () { buffer.showPageInfo(true); });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMMANDS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- commands.add(["frameo[nly]"],
- "Show only the current frame's page",
- function (args)
- {
- liberator.open(tabs.localStore.focusedFrame.document.documentURI);
- },
- { argCount: "0" });
-
- commands.add(["ha[rdcopy]"],
- "Print current document",
- function (args)
- {
- let arg = args[0];
-
- // FIXME: arg handling is a bit of a mess, check for filename
- liberator.assert(!arg || arg[0] == ">" && !liberator.has("Win32"),
- "E488: Trailing characters");
-
- options.withContext(function () {
- if (arg)
- {
- options.setPref("print.print_to_file", "true");
- options.setPref("print.print_to_filename", io.File(arg.substr(1)).path);
- liberator.echomsg("Printing to file: " + arg.substr(1));
- }
- else
- liberator.echomsg("Sending to printer...");
+ liberator.assert(!arg || titles.indexOf(arg) >= 0,
+ "E475: Invalid argument: " + arg);
- options.setPref("print.always_print_silent", args.bang);
- options.setPref("print.show_print_progress", !args.bang);
+ if (options["usermode"])
+ options["usermode"] = false;
- getBrowser().contentWindow.print();
+ window.stylesheetSwitchAll(window.content, arg);
+ },
+ {
+ argCount: "?",
+ completer: function (context) completion.alternateStyleSheet(context),
+ literal: 0
});
- if (arg)
- liberator.echomsg("Printed: " + arg.substr(1));
- else
- liberator.echomsg("Print job sent.");
- },
- {
- argCount: "?",
- literal: 0,
- bang: true
- });
-
- commands.add(["pa[geinfo]"],
- "Show various page information",
- function (args) { buffer.showPageInfo(true, args[0]); },
- {
- argCount: "?",
- completer: function (context)
+ commands.add(["re[load]"],
+ "Reload the current web page",
+ function (args) { tabs.reload(getBrowser().mCurrentTab, args.bang); },
{
- completion.optionValue(context, "pageinfo", "+", "");
- context.title = ["Page Info"];
- }
- });
-
- commands.add(["pagest[yle]", "pas"],
- "Select the author style sheet to apply",
- function (args)
- {
- let arg = args.literalArg;
+ bang: true,
+ argCount: "0"
+ });
- let titles = buffer.alternateStyleSheets.map(function (stylesheet) stylesheet.title);
+ // TODO: we're prompted if download.useDownloadDir isn't set and no arg specified - intentional?
+ commands.add(["sav[eas]", "w[rite]"],
+ "Save current document to disk",
+ function (args) {
+ let doc = window.content.document;
+ let chosenData = null;
+ let filename = args[0];
- liberator.assert(!arg || titles.indexOf(arg) >= 0,
- "E475: Invalid argument: " + arg);
+ if (filename) {
+ let file = io.File(filename);
- if (options["usermode"])
- options["usermode"] = false;
+ liberator.assert(!file.exists() || args.bang,
+ "E13: File exists (add ! to override)");
- window.stylesheetSwitchAll(window.content, arg);
- },
- {
- argCount: "?",
- completer: function (context) completion.alternateStyleSheet(context),
- literal: 0
- });
+ chosenData = { file: file, uri: window.makeURI(doc.location.href, doc.characterSet) };
+ }
- commands.add(["re[load]"],
- "Reload the current web page",
- function (args) { tabs.reload(getBrowser().mCurrentTab, args.bang); },
- {
- bang: true,
- argCount: "0"
- });
+ // if browser.download.useDownloadDir = false then the "Save As"
+ // dialog is used with this as the default directory
+ // TODO: if we're going to do this shouldn't it be done in setCWD or the value restored?
+ options.setPref("browser.download.lastDir", io.getCurrentDirectory().path);
- // TODO: we're prompted if download.useDownloadDir isn't set and no arg specified - intentional?
- commands.add(["sav[eas]", "w[rite]"],
- "Save current document to disk",
- function (args)
- {
- let doc = window.content.document;
- let chosenData = null;
- let filename = args[0];
+ try {
+ var contentDisposition = window.content
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .getDocumentMetadata("content-disposition");
+ }
+ catch (e) {}
- if (filename)
+ window.internalSave(doc.location.href, doc, null, contentDisposition,
+ doc.contentType, false, null, chosenData, doc.referrer ?
+ window.makeURI(doc.referrer) : null, true);
+ },
{
- let file = io.File(filename);
-
- liberator.assert(!file.exists() || args.bang,
- "E13: File exists (add ! to override)");
+ argCount: "?",
+ bang: true,
+ completer: function (context) completion.file(context)
+ });
- chosenData = { file: file, uri: window.makeURI(doc.location.href, doc.characterSet) };
- }
+ commands.add(["st[op]"],
+ "Stop loading the current web page",
+ function () { tabs.stop(getBrowser().mCurrentTab); },
+ { argCount: "0" });
- // if browser.download.useDownloadDir = false then the "Save As"
- // dialog is used with this as the default directory
- // TODO: if we're going to do this shouldn't it be done in setCWD or the value restored?
- options.setPref("browser.download.lastDir", io.getCurrentDirectory().path);
-
- try
+ commands.add(["vie[wsource]"],
+ "View source code of current document",
+ function (args) { buffer.viewSource(args[0], args.bang); },
{
- var contentDisposition = window.content
- .QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDOMWindowUtils)
- .getDocumentMetadata("content-disposition");
- }
- catch (e) {}
-
- window.internalSave(doc.location.href, doc, null, contentDisposition,
- doc.contentType, false, null, chosenData, doc.referrer ?
- window.makeURI(doc.referrer) : null, true);
- },
- {
- argCount: "?",
- bang: true,
- completer: function (context) completion.file(context)
- });
+ argCount: "?",
+ bang: true,
+ completer: function (context) completion.url(context, "bhf")
+ });
- commands.add(["st[op]"],
- "Stop loading the current web page",
- function () { tabs.stop(getBrowser().mCurrentTab); },
- { argCount: "0" });
-
- commands.add(["vie[wsource]"],
- "View source code of current document",
- function (args) { buffer.viewSource(args[0], args.bang); },
- {
- argCount: "?",
- bang: true,
- completer: function (context) completion.url(context, "bhf")
- });
+ commands.add(["zo[om]"],
+ "Set zoom value of current web page",
+ function (args) {
+ let arg = args[0];
+ let level;
+
+ if (!arg)
+ level = 100;
+ else if (/^\d+$/.test(arg))
+ level = parseInt(arg, 10);
+ else if (/^[+-]\d+$/.test(arg)) {
+ if (args.bang)
+ level = buffer.fullZoom + parseInt(arg, 10);
+ else
+ level = buffer.textZoom + parseInt(arg, 10);
+
+ // relative args shouldn't take us out of range
+ level = util.Math.constrain(level, Buffer.ZOOM_MIN, Buffer.ZOOM_MAX);
+ }
+ else
+ liberator.assert(false, "E488: Trailing characters");
- commands.add(["zo[om]"],
- "Set zoom value of current web page",
- function (args)
- {
- let arg = args[0];
- let level;
-
- if (!arg)
- level = 100;
- else if (/^\d+$/.test(arg))
- level = parseInt(arg, 10);
- else if (/^[+-]\d+$/.test(arg))
- {
if (args.bang)
- level = buffer.fullZoom + parseInt(arg, 10);
+ buffer.fullZoom = level;
else
- level = buffer.textZoom + parseInt(arg, 10);
-
- // relative args shouldn't take us out of range
- level = util.Math.constrain(level, ZOOM_MIN, ZOOM_MAX);
- }
- else
- liberator.assert(false, "E488: Trailing characters");
-
- if (args.bang)
- buffer.fullZoom = level;
- else
- buffer.textZoom = level;
- },
- {
- argCount: "?",
- bang: true
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMPLETIONS /////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- liberator.registerObserver("load_completion", function () {
+ buffer.textZoom = level;
+ },
+ {
+ argCount: "?",
+ bang: true
+ });
+ },
+ completion: function () {
completion.alternateStyleSheet = function alternateStylesheet(context) {
context.title = ["Stylesheet", "Location"];
@@ -750,1220 +1139,306 @@ function Buffer() //{{{
};
});
};
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// PAGE INFO ///////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- addPageInfoSection("f", "Feeds", function (verbose) {
- let doc = window.content.document;
-
- const feedTypes = {
- "application/rss+xml": "RSS",
- "application/atom+xml": "Atom",
- "text/xml": "XML",
- "application/xml": "XML",
- "application/rdf+xml": "XML"
- };
-
- function isValidFeed(data, principal, isFeed)
- {
- if (!data || !principal)
- return false;
-
- if (!isFeed)
- {
- var type = data.type && data.type.toLowerCase();
- type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
-
- isFeed = ["application/rss+xml", "application/atom+xml"].indexOf(type) >= 0 ||
- // really slimy: general XML types with magic letters in the title
- type in feedTypes && /\brss\b/i.test(data.title);
- }
-
- if (isFeed)
- {
- try
- {
- window.urlSecurityCheck(data.href, principal,
- Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
- }
- catch (e)
- {
- isFeed = false;
- }
- }
-
- if (type)
- data.type = type;
-
- return isFeed;
- }
-
- let nFeed = 0;
- for (let link in util.evaluateXPath(["link[@href and (@rel='feed' or (@rel='alternate' and @type))]"], doc))
- {
- let rel = link.rel.toLowerCase();
- let feed = { title: link.title, href: link.href, type: link.type || "" };
- if (isValidFeed(feed, doc.nodePrincipal, rel == "feed"))
- {
- nFeed++;
- let type = feedTypes[feed.type] || "RSS";
- if (verbose)
- yield [feed.title, template.highlightURL(feed.href, true) + <span class="extra-info">&#xa0;({type})</span>];
- }
- }
-
- if (!verbose && nFeed)
- yield nFeed + " feed" + (nFeed > 1 ? "s" : "");
- });
-
- addPageInfoSection("g", "General Info", function (verbose) {
- let doc = window.content.document;
-
- // get file size
- const ACCESS_READ = Ci.nsICache.ACCESS_READ;
- let cacheKey = doc.location.toString().replace(/#.*$/, "");
-
- for (let proto in util.Array.itervalues(["HTTP", "FTP"]))
- {
- try
- {
- var cacheEntryDescriptor = services.get("cache").createSession(proto, 0, true)
- .openCacheEntry(cacheKey, ACCESS_READ, false);
- break;
- }
- catch (e) {}
- }
-
- let pageSize = []; // [0] bytes; [1] kbytes
- if (cacheEntryDescriptor)
- {
- pageSize[0] = util.formatBytes(cacheEntryDescriptor.dataSize, 0, false);
- pageSize[1] = util.formatBytes(cacheEntryDescriptor.dataSize, 2, true);
- if (pageSize[1] == pageSize[0])
- pageSize.length = 1; // don't output "xx Bytes" twice
- }
-
- let lastModVerbose = new Date(doc.lastModified).toLocaleString();
- let lastMod = new Date(doc.lastModified).toLocaleFormat("%x %X");
-
- if (lastModVerbose == "Invalid Date" || new Date(doc.lastModified).getFullYear() == 1970)
- lastModVerbose = lastMod = null;
-
- if (!verbose)
- {
- if (pageSize[0])
- yield (pageSize[1] || pageSize[0]) + " bytes";
- yield lastMod;
- return;
- }
-
- yield ["Title", doc.title];
- yield ["URL", template.highlightURL(doc.location.toString(), true)];
-
- let ref = "referrer" in doc && doc.referrer;
- if (ref)
- yield ["Referrer", template.highlightURL(ref, true)];
-
- if (pageSize[0])
- yield ["File Size", pageSize[1] ? pageSize[1] + " (" + pageSize[0] + ")"
- : pageSize[0]];
-
- yield ["Mime-Type", doc.contentType];
- yield ["Encoding", doc.characterSet];
- yield ["Compatibility", doc.compatMode == "BackCompat" ? "Quirks Mode" : "Full/Almost Standards Mode"];
- if (lastModVerbose)
- yield ["Last Modified", lastModVerbose];
- });
-
- addPageInfoSection("m", "Meta Tags", function (verbose) {
- // get meta tag data, sort and put into pageMeta[]
- let metaNodes = window.content.document.getElementsByTagName("meta");
-
- return Array.map(metaNodes, function (node) [(node.name || node.httpEquiv), template.highlightURL(node.content)])
- .sort(function (a, b) util.compareIgnoreCase(a[0], b[0]));
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// PUBLIC SECTION //////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- return {
-
- get evaluateXPath() util.evaluateXPath,
-
- /**
- * @property {Array} The alternative style sheets for the current
- * buffer. Only returns style sheets for the 'screen' media type.
- */
- get alternateStyleSheets()
- {
- let stylesheets = window.getAllStyleSheets(window.content);
-
- return stylesheets.filter(
- function (stylesheet) /^(screen|all|)$/i.test(stylesheet.media.mediaText) && !/^\s*$/.test(stylesheet.title)
- );
- },
-
- /**
- * @property {Object} A map of page info sections to their
- * content generating functions.
- */
- get pageInfo() pageInfo,
-
- /**
- * @property {number} A value indicating whether the buffer is loaded.
- * Values may be:
- * 0 - Loading.
- * 1 - Fully loaded.
- * 2 - Load failed.
- */
- get loaded()
- {
- if (window.content.document.pageIsFullyLoaded !== undefined)
- return window.content.document.pageIsFullyLoaded;
- else
- return 0; // in doubt return "loading"
- },
- set loaded(value)
- {
- window.content.document.pageIsFullyLoaded = value;
- },
-
- /**
- * @property {Node} The last focused input field in the buffer. Used
- * by the "gi" key binding.
- */
- get lastInputField() window.content.document.lastInputField || null,
- set lastInputField(value) { window.content.document.lastInputField = value; },
-
- /**
- * @property {string} The current top-level document's URL.
- */
- get URL() window.content.location.href,
-
- /**
- * @property {string} The current top-level document's URL, sans any
- * fragment identifier.
- */
- get URI()
- {
- let loc = window.content.location;
- return loc.href.substr(0, loc.href.length - loc.hash.length);
- },
-
- /**
- * @property {number} The buffer's height in pixels.
- */
- get pageHeight() window.content.innerHeight,
-
- /**
- * @property {number} The current browser's text zoom level, as a
- * percentage with 100 as 'normal'. Only affects text size.
- */
- get textZoom() getBrowser().markupDocumentViewer.textZoom * 100,
- set textZoom(value) { setZoom(value, false); },
-
- /**
- * @property {number} The current browser's text zoom level, as a
- * percentage with 100 as 'normal'. Affects text size, as well as
- * image size and block size.
- */
- get fullZoom() getBrowser().markupDocumentViewer.fullZoom * 100,
- set fullZoom(value) { setZoom(value, true); },
-
- /**
- * @property {string} The current document's title.
- */
- get title() window.content.document.title,
-
- /**
- * @property {number} The buffer's horizontal scroll percentile.
- */
- get scrollXPercent()
- {
- let win = findScrollableWindow();
- if (win.scrollMaxX > 0)
- return Math.round(win.scrollX / win.scrollMaxX * 100);
- else
- return 0;
- },
-
- /**
- * @property {number} The buffer's vertical scroll percentile.
- */
- get scrollYPercent()
- {
- let win = findScrollableWindow();
- if (win.scrollMaxY > 0)
- return Math.round(win.scrollY / win.scrollMaxY * 100);
- else
- return 0;
- },
-
- /**
- * Adds a new section to the page information output.
- *
- * @param {string} option The section's value in 'pageinfo'.
- * @param {string} title The heading for this section's
- * output.
- * @param {function} func The function to generate this
- * section's output.
- */
- addPageInfoSection: addPageInfoSection,
-
- /**
- * Returns the currently selected word. If the selection is
- * null, it tries to guess the word that the caret is
- * positioned in.
- *
- * NOTE: might change the selection
- *
- * @returns {string}
- */
- // FIXME: getSelection() doesn't always preserve line endings, see:
- // https://www.mozdev.org/bugs/show_bug.cgi?id=19303
- getCurrentWord: function ()
- {
- let selection = window.content.getSelection();
- let range = selection.getRangeAt(0);
- if (selection.isCollapsed)
- {
- let selController = this.selectionController;
- let caretmode = selController.getCaretEnabled();
- selController.setCaretEnabled(true);
- // Only move backwards if the previous character is not a space.
- if (range.startOffset > 0 && !/\s/.test(range.startContainer.textContent[range.startOffset - 1]))
- selController.wordMove(false, false);
-
- selController.wordMove(true, true);
- selController.setCaretEnabled(caretmode);
- return String.match(selection, /\w*/)[0];
- }
- if (util.computedStyle(range.startContainer).whiteSpace == "pre"
- && util.computedStyle(range.endContainer).whiteSpace == "pre")
- return String(range);
- return String(selection);
- },
-
- /**
- * Focuses the given element. In contrast to a simple
- * elem.focus() call, this function works for iframes and
- * image maps.
- *
- * @param {Node} elem The element to focus.
- */
- focusElement: function (elem)
- {
- let doc = window.content.document;
- if (elem instanceof HTMLFrameElement || elem instanceof HTMLIFrameElement)
- return void elem.contentWindow.focus();
- else if (elem instanceof HTMLInputElement && elem.type == "file")
- {
- openUploadPrompt(elem);
- buffer.lastInputField = elem;
- return;
- }
-
- elem.focus();
-
- // for imagemap
- if (elem instanceof HTMLAreaElement)
- {
- try
- {
- let [x, y] = elem.getAttribute("coords").split(",").map(parseFloat);
-
- elem.dispatchEvent(events.create(doc, "mouseover", { screenX: x, screenY: y }));
- }
- catch (e) {}
- }
- },
-
- /**
- * Tries to guess links the like of "next" and "prev". Though it has a
- * singularly horrendous name, it turns out to be quite useful.
- *
- * @param {string} rel The relationship to look for. Looks for
- * links with matching @rel or @rev attributes, and,
- * failing that, looks for an option named rel +
- * "pattern", and finds the last link matching that
- * RegExp.
- */
- followDocumentRelationship: function (rel)
- {
- let regexes = options.get(rel + "pattern").values
- .map(function (re) RegExp(re, "i"));
-
- function followFrame(frame)
- {
- function iter(elems)
- {
- for (let i = 0; i < elems.length; i++)
- if (elems[i].rel.toLowerCase() == rel || elems[i].rev.toLowerCase() == rel)
- yield elems[i];
+ },
+ mappings: function () {
+ var myModes = config.browserModes;
+
+ mappings.add(myModes, ["."],
+ "Repeat the last key event",
+ function (count) {
+ if (mappings.repeat) {
+ for (let i in util.interruptibleRange(0, Math.max(count, 1), 100))
+ mappings.repeat();
}
-
- // <link>s have higher priority than normal <a> hrefs
- let elems = frame.document.getElementsByTagName("link");
- for (let elem in iter(elems))
- {
- liberator.open(elem.href);
- return true;
- }
-
- // no links? ok, look for hrefs
- elems = frame.document.getElementsByTagName("a");
- for (let elem in iter(elems))
- {
- buffer.followLink(elem, liberator.CURRENT_TAB);
- return true;
- }
-
- let res = util.evaluateXPath(options.get("hinttags").defaultValue, frame.document);
- for (let [, regex] in Iterator(regexes))
- {
- for (let i in util.range(res.snapshotLength, 0, -1))
- {
- let elem = res.snapshotItem(i);
- if (regex.test(elem.textContent) ||
- regex.test(elem.title) ||
- Array.some(elem.childNodes, function (child) regex.test(child.alt)))
- {
- buffer.followLink(elem, liberator.CURRENT_TAB);
- return true;
- }
- }
- }
- return false;
- }
-
- let ret = followFrame(window.content);
- if (!ret)
- // only loop through frames if the main content didn't match
- ret = Array.some(window.content.frames, followFrame);
-
- if (!ret)
- liberator.beep();
- },
-
- /**
- * Fakes a click on a link.
- *
- * @param {Node} elem The element to click.
- * @param {number} where Where to open the link. See
- * {@link liberator.open}.
- */
- followLink: function (elem, where)
- {
- let doc = elem.ownerDocument;
- let view = doc.defaultView;
- let offsetX = 1;
- let offsetY = 1;
-
- if (elem instanceof HTMLFrameElement || elem instanceof HTMLIFrameElement)
- {
- elem.contentWindow.focus();
- return;
- }
- else if (elem instanceof HTMLAreaElement) // for imagemap
- {
- let coords = elem.getAttribute("coords").split(",");
- offsetX = Number(coords[0]) + 1;
- offsetY = Number(coords[1]) + 1;
- }
- else if (elem instanceof HTMLInputElement && elem.type == "file")
- {
- openUploadPrompt(elem);
- return;
- }
-
- let ctrlKey = false, shiftKey = false;
- switch (where)
- {
- case liberator.NEW_TAB:
- case liberator.NEW_BACKGROUND_TAB:
- ctrlKey = true;
- shiftKey = (where != liberator.NEW_BACKGROUND_TAB);
- break;
- case liberator.NEW_WINDOW:
- shiftKey = true;
- break;
- case liberator.CURRENT_TAB:
- break;
- default:
- liberator.log("Invalid where argument for followLink()", 0);
- }
-
- elem.focus();
-
- options.withContext(function () {
- options.setPref("browser.tabs.loadInBackground", true);
- ["mousedown", "mouseup", "click"].forEach(function (event) {
- elem.dispatchEvent(events.create(doc, event, {
- screenX: offsetX, screenY: offsetY,
- ctrlKey: ctrlKey, shiftKey: shiftKey, metaKey: ctrlKey
- }));
- });
+ },
+ { count: true });
+
+ mappings.add(myModes, ["i", "<Insert>"],
+ "Start caret mode",
+ function () {
+ // setting this option notifies an observer which takes care of the
+ // mode setting
+ options.setPref("accessibility.browsewithcaret", true);
});
- },
-
- /**
- * @property {nsISelectionController} The current document's selection
- * controller.
- */
- get selectionController() getBrowser().docShell
- .QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsISelectionDisplay)
- .QueryInterface(Ci.nsISelectionController),
-
- /**
- * Opens the appropriate context menu for <b>elem</b>.
- *
- * @param {Node} elem The context element.
- */
- openContextMenu: function (elem)
- {
- document.popupNode = elem;
- let menu = document.getElementById("contentAreaContextMenu");
- menu.showPopup(elem, -1, -1, "context", "bottomleft", "topleft");
- },
-
- /**
- * Saves a page link to disk.
- *
- * @param {HTMLAnchorElement} elem The page link to save.
- * @param {boolean} skipPrompt Whether to open the "Save Link As..."
- * dialog.
- */
- saveLink: function (elem, skipPrompt)
- {
- let doc = elem.ownerDocument;
- let url = window.makeURLAbsolute(elem.baseURI, elem.href);
- let text = elem.textContent;
-
- try
- {
- window.urlSecurityCheck(url, doc.nodePrincipal);
- // we always want to save that link relative to the current working directory
- options.setPref("browser.download.lastDir", io.getCurrentDirectory().path);
- window.saveURL(url, text, null, true, skipPrompt, makeURI(url, doc.characterSet));
- }
- catch (e)
- {
- liberator.echoerr(e);
- }
- },
-
- /**
- * Scrolls to the bottom of the current buffer.
- */
- scrollBottom: function ()
- {
- scrollToPercent(null, 100);
- },
-
- /**
- * Scrolls the buffer laterally <b>cols</b> columns.
- *
- * @param {number} cols The number of columns to scroll. A positive
- * value scrolls right and a negative value left.
- */
- scrollColumns: function (cols)
- {
- scrollHorizontal(null, "columns", cols);
- },
-
- /**
- * Scrolls to the top of the current buffer.
- */
- scrollEnd: function ()
- {
- scrollToPercent(100, null);
- },
-
- /**
- * Scrolls the buffer vertically <b>lines</b> rows.
- *
- * @param {number} lines The number of lines to scroll. A positive
- * value scrolls down and a negative value up.
- */
- scrollLines: function (lines)
- {
- scrollVertical(null, "lines", lines);
- },
-
- /**
- * Scrolls the buffer vertically <b>pages</b> pages.
- *
- * @param {number} pages The number of pages to scroll. A positive
- * value scrolls down and a negative value up.
- */
- scrollPages: function (pages)
- {
- scrollVertical(null, "pages", pages);
- },
-
- /**
- * Scrolls the buffer vertically 'scroll' lines.
- *
- * @param {boolean} direction The direction to scroll. If true then
- * scroll up and if false scroll down.
- * @param {number} count The multiple of 'scroll' lines to scroll.
- * @optional
- */
- scrollByScrollSize: function (direction, count)
- {
- direction = direction ? 1 : -1;
- count = count || 1;
- let win = findScrollableWindow();
-
- checkScrollYBounds(win, direction);
-
- if (options["scroll"] > 0)
- this.scrollLines(options["scroll"] * direction);
- else // scroll half a page down in pixels
- win.scrollBy(0, win.innerHeight / 2 * direction);
- },
-
- /**
- * Scrolls the buffer to the specified screen percentiles.
- *
- * @param {number} x The horizontal page percentile.
- * @param {number} y The vertical page percentile.
- */
- scrollToPercent: function (x, y)
- {
- scrollToPercent(x, y);
- },
-
- /**
- * Scrolls the buffer to the specified screen pixels.
- *
- * @param {number} x The horizontal pixel.
- * @param {number} y The vertical pixel.
- */
- scrollTo: function (x, y)
- {
- marks.add("'", true);
- content.scrollTo(x, y);
- },
-
- /**
- * Scrolls the current buffer laterally to its leftmost.
- */
- scrollStart: function ()
- {
- scrollToPercent(0, null);
- },
-
- /**
- * Scrolls the current buffer vertically to the top.
- */
- scrollTop: function ()
- {
- scrollToPercent(null, 0);
- },
-
- // TODO: allow callback for filtering out unwanted frames? User defined?
- /**
- * Shifts the focus to another frame within the buffer. Each buffer
- * contains at least one frame.
- *
- * @param {number} count The number of frames to skip through.
- * @param {boolean} forward The direction of motion.
- */
- shiftFrameFocus: function (count, forward)
- {
- if (!(window.content.document instanceof HTMLDocument))
- return;
- count = Math.max(count, 1);
- let frames = [];
-
- // find all frames - depth-first search
- (function (frame) {
- if (frame.document.body instanceof HTMLBodyElement)
- frames.push(frame);
- Array.forEach(frame.frames, arguments.callee);
- })(window.content);
-
- if (frames.length == 0) // currently top is always included
- return;
-
- // remove all unfocusable frames
- // TODO: find a better way to do this - walking the tree is too slow
- let start = document.commandDispatcher.focusedWindow;
- frames = frames.filter(function (frame) {
- frame.focus();
- return document.commandDispatcher.focusedWindow == frame;
- });
- start.focus();
-
- // find the currently focused frame index
- // TODO: If the window is a frameset then the first _frame_ should be
- // focused. Since this is not the current FF behaviour,
- // we initalize current to -1 so the first call takes us to the
- // first frame.
- let current = frames.indexOf(document.commandDispatcher.focusedWindow);
-
- // calculate the next frame to focus
- let next = current;
- if (forward)
- {
- next = current + count;
-
- if (next > frames.length - 1)
- {
- if (current == frames.length - 1)
- liberator.beep();
- next = frames.length - 1; // still allow the frame indicator to be activated
- }
- }
- else
- {
- next = current - count;
-
- if (next < 0)
- {
- if (current == 0)
+ mappings.add(myModes, ["<C-c>"],
+ "Stop loading the current web page",
+ function () { tabs.stop(getBrowser().mCurrentTab); });
+
+ // scrolling
+ mappings.add(myModes, ["j", "<Down>", "<C-e>"],
+ "Scroll document down",
+ function (count) { buffer.scrollLines(Math.max(count, 1)); },
+ { count: true });
+
+ mappings.add(myModes, ["k", "<Up>", "<C-y>"],
+ "Scroll document up",
+ function (count) { buffer.scrollLines(-Math.max(count, 1)); },
+ { count: true });
+
+ mappings.add(myModes, liberator.has("mail") ? ["h"] : ["h", "<Left>"],
+ "Scroll document to the left",
+ function (count) { buffer.scrollColumns(-Math.max(count, 1)); },
+ { count: true });
+
+ mappings.add(myModes, liberator.has("mail") ? ["l"] : ["l", "<Right>"],
+ "Scroll document to the right",
+ function (count) { buffer.scrollColumns(Math.max(count, 1)); },
+ { count: true });
+
+ mappings.add(myModes, ["0", "^"],
+ "Scroll to the absolute left of the document",
+ function () { buffer.scrollStart(); });
+
+ mappings.add(myModes, ["$"],
+ "Scroll to the absolute right of the document",
+ function () { buffer.scrollEnd(); });
+
+ mappings.add(myModes, ["gg", "<Home>"],
+ "Go to the top of the document",
+ function (count) { buffer.scrollToPercent(buffer.scrollXPercent, Math.max(count, 0)); },
+ { count: true });
+
+ mappings.add(myModes, ["G", "<End>"],
+ "Go to the end of the document",
+ function (count) { buffer.scrollToPercent(buffer.scrollXPercent, count >= 0 ? count : 100); },
+ { count: true });
+
+ mappings.add(myModes, ["%"],
+ "Scroll to {count} percent of the document",
+ function (count) {
+ if (count > 0 && count <= 100)
+ buffer.scrollToPercent(buffer.scrollXPercent, count);
+ else
+ liberator.beep();
+ },
+ { count: true });
+
+ mappings.add(myModes, ["<C-d>"],
+ "Scroll window downwards in the buffer",
+ function (count) { buffer._scrollByScrollSize(count, true); },
+ { count: true });
+
+ mappings.add(myModes, ["<C-u>"],
+ "Scroll window upwards in the buffer",
+ function (count) { buffer._scrollByScrollSize(count, false); },
+ { count: true });
+
+ mappings.add(myModes, ["<C-b>", "<PageUp>", "<S-Space>"],
+ "Scroll up a full page",
+ function (count) { buffer.scrollPages(-Math.max(count, 1)); },
+ { count: true });
+
+ mappings.add(myModes, ["<C-f>", "<PageDown>", "<Space>"],
+ "Scroll down a full page",
+ function (count) { buffer.scrollPages(Math.max(count, 1)); },
+ { count: true });
+
+ mappings.add(myModes, ["]f"],
+ "Focus next frame",
+ function (count) { buffer.shiftFrameFocus(Math.max(count, 1), true); },
+ { count: true });
+
+ mappings.add(myModes, ["[f"],
+ "Focus previous frame",
+ function (count) { buffer.shiftFrameFocus(Math.max(count, 1), false); },
+ { count: true });
+
+ mappings.add(myModes, ["]]"],
+ "Follow the link labeled 'next' or '>' if it exists",
+ function (count) { buffer.followDocumentRelationship("next"); },
+ { count: true });
+
+ mappings.add(myModes, ["[["],
+ "Follow the link labeled 'prev', 'previous' or '<' if it exists",
+ function (count) { buffer.followDocumentRelationship("previous"); },
+ { count: true });
+
+ mappings.add(myModes, ["gf"],
+ "View source",
+ function () { buffer.viewSource(null, false); });
+
+ mappings.add(myModes, ["gF"],
+ "View source with an external editor",
+ function () { buffer.viewSource(null, true); });
+
+ mappings.add(myModes, ["|"],
+ "Toggle between rendered and source view",
+ function () { buffer.viewSource(null, false); });
+
+ mappings.add(myModes, ["gi"],
+ "Focus last used input field",
+ function (count) {
+ if (count < 1 && buffer.lastInputField)
+ buffer.focusElement(buffer.lastInputField);
+ else {
+ let xpath = ["input[not(@type) or @type='text' or @type='password' or @type='file']",
+ "textarea[not(@disabled) and not(@readonly)]"];
+
+ let elements = [m for (m in util.evaluateXPath(xpath))].filter(function (match) {
+ let computedStyle = util.computedStyle(match);
+ return computedStyle.visibility != "hidden" && computedStyle.display != "none";
+ });
+
+ if (elements.length > 0)
+ buffer.focusElement(elements[util.Math.constrain(count, 1, elements.length) - 1]);
+ else
liberator.beep();
- next = 0; // still allow the frame indicator to be activated
}
- }
-
- // focus next frame and scroll into view
- frames[next].focus();
- if (frames[next] != window.content)
- frames[next].frameElement.scrollIntoView(false);
-
- // add the frame indicator
- let doc = frames[next].document;
- let indicator = util.xmlToDom(<div highlight="FrameIndicator"/>, doc);
- doc.body.appendChild(indicator);
-
- setTimeout(function () { doc.body.removeChild(indicator); }, 500);
-
- // Doesn't unattach
- //doc.body.setAttributeNS(NS.uri, "activeframe", "true");
- //setTimeout(function () { doc.body.removeAttributeNS(NS.uri, "activeframe"); }, 500);
- },
-
- // similar to pageInfo
- // TODO: print more useful information, just like the DOM inspector
- /**
- * Displays information about the specified element.
- *
- * @param {Node} elem The element to query.
- */
- showElementInfo: function (elem)
- {
- liberator.echo(<>Element:<br/>{util.objectToString(elem, true)}</>, commandline.FORCE_MULTILINE);
- },
-
- /**
- * Displays information about the current buffer.
- *
- * @param {boolean} verbose Display more verbose information.
- * @param {string} sections A string limiting the displayed sections.
- * @default The value of 'pageinfo'.
- */
- showPageInfo: function (verbose, sections)
- {
- // Ctrl-g single line output
- if (!verbose)
- {
- let file = content.document.location.pathname.split("/").pop() || "[No Name]";
- let title = content.document.title || "[No Title]";
-
- let info = template.map("gf", function (opt)
- template.map(pageInfo[opt][0](), util.identity, ", "),
- ", ");
-
- if (bookmarks.isBookmarked(this.URL))
- info += ", bookmarked";
-
- let pageInfoText = <>{file.quote()} [{info}] {title}</>;
- liberator.echo(pageInfoText, commandline.FORCE_SINGLELINE);
- return;
- }
+ },
+ { count: true });
+
+ mappings.add(myModes, ["gP"],
+ "Open (put) a URL based on the current clipboard contents in a new buffer",
+ function () {
+ liberator.open(util.readFromClipboard(),
+ liberator[options.get("activate").has("paste") ? "NEW_BACKGROUND_TAB" : "NEW_TAB"]);
+ });
- let option = sections || options["pageinfo"];
- let list = template.map(option, function (option) {
- let opt = pageInfo[option];
- if (opt)
- return template.table(opt[1], opt[0](true));
- }, <br/>);
- liberator.echo(list, commandline.FORCE_MULTILINE);
- },
-
- /**
- * Opens a viewer to inspect the source of the currently selected
- * range.
- */
- viewSelectionSource: function ()
- {
- // copied (and tuned somebit) from browser.jar -> nsContextMenu.js
- let focusedWindow = document.commandDispatcher.focusedWindow;
- if (focusedWindow == window)
- focusedWindow = content;
-
- let docCharset = null;
- if (focusedWindow)
- docCharset = "charset=" + focusedWindow.document.characterSet;
-
- let reference = null;
- reference = focusedWindow.getSelection();
-
- let docUrl = null;
- window.openDialog("chrome://global/content/viewPartialSource.xul",
- "_blank", "scrollbars,resizable,chrome,dialog=no",
- docUrl, docCharset, reference, "selection");
- },
-
- /**
- * Opens a viewer to inspect the source of the current buffer or the
- * specified <b>url</b>. Either the default viewer or the configured
- * external editor is used.
- *
- * @param {string} url The URL of the source.
- * @default The current buffer.
- * @param {boolean} useExternalEditor View the source in the external editor.
- */
- viewSource: function (url, useExternalEditor)
- {
- url = url || buffer.URI;
-
- if (useExternalEditor)
- editor.editFileExternally(url);
- else
- {
- const PREFIX = "view-source:";
- if (url.indexOf(PREFIX) == 0)
- url = url.substr(PREFIX.length);
+ mappings.add(myModes, ["p", "<MiddleMouse>"],
+ "Open (put) a URL based on the current clipboard contents in the current buffer",
+ function () {
+ let url = util.readFromClipboard();
+ if (url)
+ liberator.open(url);
else
- url = PREFIX + url;
- liberator.open(url, { hide: true });
- }
- },
-
- /**
- * Increases the zoom level of the current buffer.
- *
- * @param {number} steps The number of zoom levels to jump.
- * @param {boolean} fullZoom Whether to use full zoom or text zoom.
- */
- zoomIn: function (steps, fullZoom)
- {
- bumpZoomLevel(steps, fullZoom);
- },
-
- /**
- * Decreases the zoom level of the current buffer.
- *
- * @param {number} steps The number of zoom levels to jump.
- * @param {boolean} fullZoom Whether to use full zoom or text zoom.
- */
- zoomOut: function (steps, fullZoom)
- {
- bumpZoomLevel(-steps, fullZoom);
- }
- };
- //}}}
-} //}}}
-
-/**
- * @instance marks
- */
-function Marks() //{{{
-{
- ////////////////////////////////////////////////////////////////////////////////
- ////////////////////// PRIVATE SECTION /////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- var localMarks = storage.newMap("local-marks", { store: true, privateData: true });
- var urlMarks = storage.newMap("url-marks", { store: true, privateData: true });
-
- var pendingJumps = [];
- var appContent = document.getElementById("appcontent");
-
- if (appContent)
- appContent.addEventListener("load", onPageLoad, true);
-
- function onPageLoad(event)
- {
- let win = event.originalTarget.defaultView;
- for (let [i, mark] in Iterator(pendingJumps))
- {
- if (win && win.location.href == mark.location)
- {
- buffer.scrollToPercent(mark.position.x * 100, mark.position.y * 100);
- pendingJumps.splice(i, 1);
- return;
- }
- }
- }
-
- function markToString(name, mark)
- {
- return name + ", " + mark.location +
- ", (" + Math.round(mark.position.x * 100) +
- "%, " + Math.round(mark.position.y * 100) + "%)" +
- (("tab" in mark) ? ", tab: " + tabs.index(mark.tab) : "");
- }
-
- function removeLocalMark(mark)
- {
- let localmark = localMarks.get(mark);
- if (localmark)
- {
- let win = window.content;
- for (let [i, ] in Iterator(localmark))
- {
- if (localmark[i].location == win.location.href)
- {
- liberator.log("Deleting local mark: " + markToString(mark, localmark[i]), 5);
- localmark.splice(i, 1);
- if (localmark.length == 0)
- localMarks.remove(mark);
- break;
- }
- }
- }
- }
-
- function removeURLMark(mark)
- {
- let urlmark = urlMarks.get(mark);
- if (urlmark)
- {
- liberator.log("Deleting URL mark: " + markToString(mark, urlmark), 5);
- urlMarks.remove(mark);
- }
- }
-
- function isLocalMark(mark) /^['`a-z]$/.test(mark);
- function isURLMark(mark) /^[A-Z0-9]$/.test(mark);
-
- function localMarkIter()
- {
- for (let [mark, value] in localMarks)
- for (let [, val] in Iterator(value))
- yield [mark, val];
- }
-
- function getSortedMarks()
- {
- // local marks
- let location = window.content.location.href;
- let lmarks = [i for (i in localMarkIter()) if (i[1].location == location)];
- lmarks.sort();
-
- // URL marks
- // FIXME: why does umarks.sort() cause a "Component is not available =
- // NS_ERROR_NOT_AVAILABLE" exception when used here?
- let umarks = [i for (i in urlMarks)];
- umarks.sort(function (a, b) a[0].localeCompare(b[0]));
-
- return lmarks.concat(umarks);
- }
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// MAPPINGS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- var myModes = config.browserModes;
-
- mappings.add(myModes,
- ["m"], "Set mark at the cursor position",
- function (arg)
- {
- if (/[^a-zA-Z]/.test(arg))
- return void liberator.beep();
-
- marks.add(arg);
- },
- { arg: true });
-
- mappings.add(myModes,
- ["'", "`"], "Jump to the mark in the current buffer",
- function (arg) { marks.jumpTo(arg); },
- { arg: true });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMMANDS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- commands.add(["delm[arks]"],
- "Delete the specified marks",
- function (args)
- {
- let special = args.bang;
- args = args.string;
-
- // assert(special ^ args)
- liberator.assert( special || args, "E471: Argument required");
- liberator.assert(!special || !args, "E474: Invalid argument");
-
- let matches;
- if (matches = args.match(/(?:(?:^|[^a-zA-Z0-9])-|-(?:$|[^a-zA-Z0-9])|[^a-zA-Z0-9 -]).*/))
- {
- // NOTE: this currently differs from Vim's behavior which
- // deletes any valid marks in the arg list, up to the first
- // invalid arg, as well as giving the error message.
- liberator.echoerr("E475: Invalid argument: " + matches[0]);
- return;
- }
- // check for illegal ranges - only allow a-z A-Z 0-9
- if (matches = args.match(/[a-zA-Z0-9]-[a-zA-Z0-9]/g))
- {
- for (let i = 0; i < matches.length; i++)
- {
- let start = matches[i][0];
- let end = matches[i][2];
- if (/[a-z]/.test(start) != /[a-z]/.test(end) ||
- /[A-Z]/.test(start) != /[A-Z]/.test(end) ||
- /[0-9]/.test(start) != /[0-9]/.test(end) ||
- start > end)
- {
- liberator.echoerr("E475: Invalid argument: " + args.match(matches[i] + ".*")[0]);
- return;
- }
- }
- }
-
- marks.remove(args, special);
- },
- {
- bang: true,
- completer: function (context) completion.mark(context)
- });
-
- commands.add(["ma[rk]"],
- "Mark current location within the web page",
- function (args)
- {
- let mark = args[0];
+ liberator.beep();
+ });
- liberator.assert(mark.length <= 1, "E488: Trailing characters");
- liberator.assert(/[a-zA-Z]/.test(mark),
- "E191: Argument must be a letter or forward/backward quote");
+ mappings.add(myModes, ["P"],
+ "Open (put) a URL based on the current clipboard contents in a new buffer",
+ function () {
+ let url = util.readFromClipboard();
+ if (url)
+ liberator.open(url, { from: "activate", where: liberator.NEW_TAB });
+ else
+ liberator.beep();
+ });
- marks.add(mark);
- },
- { argCount: "1" });
+ // reloading
+ mappings.add(myModes, ["r"],
+ "Reload the current web page",
+ function () { tabs.reload(getBrowser().mCurrentTab, false); });
- commands.add(["marks"],
- "Show all location marks of current web page",
- function (args)
- {
- args = args.string;
+ mappings.add(myModes, ["R"],
+ "Reload while skipping the cache",
+ function () { tabs.reload(getBrowser().mCurrentTab, true); });
- // ignore invalid mark characters unless there are no valid mark chars
- liberator.assert(!args || /[a-zA-Z]/.test(args),
- "E283: No marks matching " + args.quote());
+ // yanking
+ mappings.add(myModes, ["Y"],
+ "Copy selected text or current word",
+ function () {
+ let sel = buffer.getCurrentWord();
- let filter = args.replace(/[^a-zA-Z]/g, "");
- marks.list(filter);
- });
+ if (sel)
+ util.copyToClipboard(sel, true);
+ else
+ liberator.beep();
+ });
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMPLETIONS /////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- completion.mark = function mark(context) {
- function percent(i) Math.round(i * 100);
-
- // FIXME: Line/Column doesn't make sense with %
- context.title = ["Mark", "Line Column File"];
- context.keys.description = function ([, m]) percent(m.position.y) + "% " + percent(m.position.x) + "% " + m.location;
- context.completions = marks.all;
- };
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// PUBLIC SECTION //////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- return {
-
- /**
- * @property {Array} Returns all marks, both local and URL, in a sorted
- * array.
- */
- get all() getSortedMarks(),
-
- /**
- * Add a named mark for the current buffer, at its current position.
- * If mark matches [A-Z], it's considered a URL mark, and will jump to
- * the same position at the same URL no matter what buffer it's
- * selected from. If it matches [a-z'"], it's a local mark, and can
- * only be recalled from a buffer with a matching URL.
- *
- * @param {string} mark The mark name.
- * @param {boolean} silent Whether to output error messages.
- */
- // TODO: add support for frameset pages
- add: function (mark, silent)
- {
- let win = window.content;
- let doc = win.document;
-
- if (!doc.body)
- return;
- if (doc.body instanceof HTMLFrameSetElement)
+ // zooming
+ mappings.add(myModes, ["zi", "+"],
+ "Enlarge text zoom of current web page",
+ function (count) { buffer.zoomIn(Math.max(count, 1), false); },
+ { count: true });
+
+ mappings.add(myModes, ["zm"],
+ "Enlarge text zoom of current web page by a larger amount",
+ function (count) { buffer.zoomIn(Math.max(count, 1) * 3, false); },
+ { count: true });
+
+ mappings.add(myModes, ["zo", "-"],
+ "Reduce text zoom of current web page",
+ function (count) { buffer.zoomOut(Math.max(count, 1), false); },
+ { count: true });
+
+ mappings.add(myModes, ["zr"],
+ "Reduce text zoom of current web page by a larger amount",
+ function (count) { buffer.zoomOut(Math.max(count, 1) * 3, false); },
+ { count: true });
+
+ mappings.add(myModes, ["zz"],
+ "Set text zoom value of current web page",
+ function (count) { buffer.textZoom = count > 1 ? count : 100; },
+ { count: true });
+
+ mappings.add(myModes, ["zI"],
+ "Enlarge full zoom of current web page",
+ function (count) { buffer.zoomIn(Math.max(count, 1), true); },
+ { count: true });
+
+ mappings.add(myModes, ["zM"],
+ "Enlarge full zoom of current web page by a larger amount",
+ function (count) { buffer.zoomIn(Math.max(count, 1) * 3, true); },
+ { count: true });
+
+ mappings.add(myModes, ["zO"],
+ "Reduce full zoom of current web page",
+ function (count) { buffer.zoomOut(Math.max(count, 1), true); },
+ { count: true });
+
+ mappings.add(myModes, ["zR"],
+ "Reduce full zoom of current web page by a larger amount",
+ function (count) { buffer.zoomOut(Math.max(count, 1) * 3, true); },
+ { count: true });
+
+ mappings.add(myModes, ["zZ"],
+ "Set full zoom value of current web page",
+ function (count) { buffer.fullZoom = count > 1 ? count : 100; },
+ { count: true });
+
+ // page info
+ mappings.add(myModes, ["<C-g>"],
+ "Print the current file name",
+ function (count) { buffer.showPageInfo(false); },
+ { count: true });
+
+ mappings.add(myModes, ["g<C-g>"],
+ "Print file information",
+ function () { buffer.showPageInfo(true); });
+ },
+ options: function () {
+ options.add(["nextpattern"], // \u00BB is » (>> in a single char)
+ "Patterns to use when guessing the 'next' page in a document sequence",
+ "stringlist", "\\bnext\\b,^>$,^(>>|\u00BB)$,^(>|\u00BB),(>|\u00BB)$,\\bmore\\b");
+
+ options.add(["previouspattern"], // \u00AB is « (<< in a single char)
+ "Patterns to use when guessing the 'previous' page in a document sequence",
+ "stringlist", "\\bprev|previous\\b,^<$,^(<<|\u00AB)$,^(<|\u00AB),(<|\u00AB)$");
+
+ options.add(["pageinfo", "pa"],
+ "Desired info in the :pageinfo output",
+ "charlist", "gfm",
{
- if (!silent)
- liberator.echoerr("Marks support for frameset pages not implemented yet");
- return;
- }
+ completer: function (context) [[k, v[1]] for ([k, v] in Iterator(this.pageInfo))],
+ validator: Option.validateCompleter
+ });
- let x = win.scrollMaxX ? win.pageXOffset / win.scrollMaxX : 0;
- let y = win.scrollMaxY ? win.pageYOffset / win.scrollMaxY : 0;
- let position = { x: x, y: y };
+ options.add(["scroll", "scr"],
+ "Number of lines to scroll with <C-u> and <C-d> commands",
+ "number", 0,
+ { validator: function (value) value >= 0 });
- if (isURLMark(mark))
- {
- urlMarks.set(mark, { location: win.location.href, position: position, tab: tabs.getTab() });
- if (!silent)
- liberator.log("Adding URL mark: " + markToString(mark, urlMarks.get(mark)), 5);
- }
- else if (isLocalMark(mark))
- {
- // remove any previous mark of the same name for this location
- removeLocalMark(mark);
- if (!localMarks.get(mark))
- localMarks.set(mark, []);
- let vals = { location: win.location.href, position: position };
- localMarks.get(mark).push(vals);
- if (!silent)
- liberator.log("Adding local mark: " + markToString(mark, vals), 5);
- }
- },
-
- /**
- * Remove all marks matching <b>filter</b>. If <b>special</b> is
- * given, removes all local marks.
- *
- * @param {string} filter A string containing one character for each
- * mark to be removed.
- * @param {boolean} special Whether to delete all local marks.
- */
- // FIXME: Shouldn't special be replaced with a null filter?
- remove: function (filter, special)
- {
- if (special)
- {
- // :delmarks! only deletes a-z marks
- for (let [mark, ] in localMarks)
- removeLocalMark(mark);
- }
- else
+ options.add(["showstatuslinks", "ssli"],
+ "Show the destination of the link under the cursor in the status bar",
+ "number", 1,
{
- for (let [mark, ] in urlMarks)
- {
- if (filter.indexOf(mark) >= 0)
- removeURLMark(mark);
- }
- for (let [mark, ] in localMarks)
- {
- if (filter.indexOf(mark) >= 0)
- removeLocalMark(mark);
- }
- }
- },
-
- /**
- * Jumps to the named mark. See {@link #add}
- *
- * @param {string} mark The mark to jump to.
- */
- jumpTo: function (mark)
- {
- let ok = false;
-
- if (isURLMark(mark))
- {
- let slice = urlMarks.get(mark);
- if (slice && slice.tab && slice.tab.linkedBrowser)
- {
- if (slice.tab.parentNode != getBrowser().tabContainer)
- {
- pendingJumps.push(slice);
- // NOTE: this obviously won't work on generated pages using
- // non-unique URLs :(
- liberator.open(slice.location, liberator.NEW_TAB);
- return;
- }
- let index = tabs.index(slice.tab);
- if (index != -1)
- {
- tabs.select(index);
- let win = slice.tab.linkedBrowser.contentWindow;
- if (win.location.href != slice.location)
- {
- pendingJumps.push(slice);
- win.location.href = slice.location;
- return;
- }
- liberator.log("Jumping to URL mark: " + markToString(mark, slice), 5);
- buffer.scrollToPercent(slice.position.x * 100, slice.position.y * 100);
- ok = true;
- }
- }
- }
- else if (isLocalMark(mark))
- {
- let win = window.content;
- let slice = localMarks.get(mark) || [];
-
- for (let [, lmark] in Iterator(slice))
- {
- if (win.location.href == lmark.location)
- {
- liberator.log("Jumping to local mark: " + markToString(mark, lmark), 5);
- buffer.scrollToPercent(lmark.position.x * 100, lmark.position.y * 100);
- ok = true;
- break;
- }
- }
- }
-
- if (!ok)
- liberator.echoerr("E20: Mark not set");
- },
-
- /**
- * List all marks matching <b>filter</b>.
- *
- * @param {string} filter
- */
- list: function (filter)
- {
- let marks = getSortedMarks();
-
- liberator.assert(marks.length > 0, "No marks set");
+ completer: function (context) [
+ ["0", "Don't show link destination"],
+ ["1", "Show the link in the status line"],
+ ["2", "Show the link in the command line"]
+ ],
+ validator: Option.validateCompleter
+ });
- if (filter.length > 0)
+ options.add(["usermode", "um"],
+ "Show current website with a minimal style sheet to make it easily accessible",
+ "boolean", false,
{
- marks = marks.filter(function (mark) filter.indexOf(mark[0]) >= 0);
- liberator.assert(marks.length > 0, "E283: No marks matching " + filter.quote());
- }
-
- let list = template.tabular(
- ["Mark", "Line", "Column", "File"],
- ["", "text-align: right", "text-align: right", "color: green"],
- ([mark[0],
- Math.round(mark[1].position.x * 100) + "%",
- Math.round(mark[1].position.y * 100) + "%",
- mark[1].location]
- for ([, mark] in Iterator(marks))));
- commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
- }
-
- };
- //}}}
-} //}}}
+ setter: function (value) getBrowser().markupDocumentViewer.authorStyleDisabled = value,
+ getter: function () getBrowser().markupDocumentViewer.authorStyleDisabled
+ });
+ },
+});
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/commandline.js b/common/content/commandline.js
new file mode 100644
index 00000000..6a66f63d
--- /dev/null
+++ b/common/content/commandline.js
@@ -0,0 +1,1867 @@
+// Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
+//
+// This work is licensed for reuse under an MIT license. Details are
+// given in the LICENSE.txt file included with this file.
+
+/** @scope modules */
+
+/**
+ * This class is used for prompting of user input and echoing of messages.
+ *
+ * It consists of a prompt and command field be sure to only create objects of
+ * this class when the chrome is ready.
+ */
+const CommandLine = Module("commandline", {
+ requires: ["liberator", "modes", "services", "storage", "template", "util"],
+
+ init: function () {
+ const self = this;
+
+ this._callbacks = {};
+
+ storage.newArray("history-search", { store: true, privateData: true });
+ storage.newArray("history-command", { store: true, privateData: true });
+
+ // Really inideal.
+ let services = modules.services; // Storage objects are global to all windows, 'modules' isn't.
+ storage.newObject("sanitize", function () {
+ ({
+ CLEAR: "browser:purge-session-history",
+ QUIT: "quit-application",
+ init: function () {
+ services.get("observer").addObserver(this, this.CLEAR, false);
+ services.get("observer").addObserver(this, this.QUIT, false);
+ },
+ observe: function (subject, topic, data) {
+ switch (topic) {
+ case this.CLEAR:
+ ["search", "command"].forEach(function (mode) {
+ CommandLine.History(null, mode).sanitize();
+ });
+ break;
+ case this.QUIT:
+ services.get("observer").removeObserver(this, this.CLEAR);
+ services.get("observer").removeObserver(this, this.QUIT);
+ break;
+ }
+ }
+ }).init();
+ }, { store: false });
+ storage.addObserver("sanitize",
+ function (key, event, value) {
+ autocommands.trigger("Sanitize", {});
+ }, window);
+
+ this._messageHistory = { //{{{
+ _messages: [],
+ get messages() {
+ let max = options["messages"];
+
+ // resize if 'messages' has changed
+ if (this._messages.length > max)
+ this._messages = this._messages.splice(this._messages.length - max);
+
+ return this._messages;
+ },
+
+ get length() this._messages.length,
+
+ clear: function clear() {
+ this._messages = [];
+ },
+
+ add: function add(message) {
+ if (!message)
+ return;
+
+ if (this._messages.length >= options["messages"])
+ this._messages.shift();
+
+ this._messages.push(message);
+ }
+ }; //}}}
+
+ this._lastMowOutput = null;
+
+ this._silent = false;
+ this._quiet = false;
+ this._keepCommand = false;
+ this._lastEcho = null;
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// TIMERS //////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ this._statusTimer = new Timer(5, 100, function statusTell() {
+ if (self._completions == null)
+ return;
+ if (self._completions.selected == null)
+ statusline.updateProgress("");
+ else
+ statusline.updateProgress("match " + (self._completions.selected + 1) + " of " + self._completions.items.length);
+ });
+
+ this._autocompleteTimer = new Timer(200, 500, function autocompleteTell(tabPressed) {
+ if (!events.feedingKeys && self._completions && options.get("wildoptions").has("auto")) {
+ self._completions.complete(true, false);
+ self._completions.itemList.show();
+ }
+ });
+
+ // This timer just prevents <Tab>s from queueing up when the
+ // system is under load (and, thus, giving us several minutes of
+ // the completion list scrolling). Multiple <Tab> presses are
+ // still processed normally, as the time is flushed on "keyup".
+ this._tabTimer = new Timer(0, 0, function tabTell(event) {
+ if (self._completions)
+ self._completions.tab(event.shiftKey);
+ });
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// VARIABLES ///////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ this._completionList = new ItemList("liberator-completions");
+ this._completions = null;
+ this._history = null;
+
+ this._startHints = false; // whether we're waiting to start hints mode
+ this._lastSubstring = "";
+
+ // the containing box for the this._promptWidget and this._commandWidget
+ this._commandlineWidget = document.getElementById("liberator-commandline");
+ // the prompt for the current command, for example : or /. Can be blank
+ this._promptWidget = document.getElementById("liberator-commandline-prompt");
+ // the command bar which contains the current command
+ this._commandWidget = document.getElementById("liberator-commandline-command");
+
+ this._messageBox = document.getElementById("liberator-message");
+
+ this._commandWidget.inputField.QueryInterface(Ci.nsIDOMNSEditableElement);
+ this._messageBox.inputField.QueryInterface(Ci.nsIDOMNSEditableElement);
+
+ // the widget used for multiline output
+ this._multilineOutputWidget = document.getElementById("liberator-multiline-output");
+ this._outputContainer = this._multilineOutputWidget.parentNode;
+
+ this._multilineOutputWidget.contentDocument.body.id = "liberator-multiline-output-content";
+
+ // the widget used for multiline intput
+ this._multilineInputWidget = document.getElementById("liberator-multiline-input");
+
+ // we need to save the mode which were in before opening the command line
+ // this is then used if we focus the command line again without the "official"
+ // way of calling "open"
+ this._currentExtendedMode = null; // the extended mode which we last openend the command line for
+ this._currentPrompt = null;
+ this._currentCommand = null;
+
+ // save the arguments for the inputMultiline method which are needed in the event handler
+ this._multilineRegexp = null;
+ this._multilineCallback = null;
+
+
+ this._input = {};
+
+ this.registerCallback("submit", modes.EX, function (command) {
+ commands.repeat = command;
+ liberator.execute(command);
+ });
+ this.registerCallback("complete", modes.EX, function (context) {
+ context.fork("ex", 0, completion, "ex");
+ });
+ this.registerCallback("change", modes.EX, function (command) {
+ self._autocompleteTimer.tell(false);
+ });
+
+ this.registerCallback("cancel", modes.PROMPT, cancelPrompt);
+ this.registerCallback("submit", modes.PROMPT, closePrompt);
+ this.registerCallback("change", modes.PROMPT, function (str) {
+ if (self._input.complete)
+ self._autocompleteTimer.tell(false);
+ if (self._input.change)
+ return self._input.change.call(commandline, str);
+ });
+ this.registerCallback("complete", modes.PROMPT, function (context) {
+ if (self._input.complete)
+ context.fork("input", 0, commandline, input.complete);
+ });
+
+ function cancelPrompt(value) {
+ let callback = self._input.cancel;
+ self._input = {};
+ if (callback)
+ callback.call(self, value != null ? value : commandline.command);
+ }
+
+ function closePrompt(value) {
+ let callback = self._input.submit;
+ self._input = {};
+ if (callback)
+ callback.call(self, value != null ? value : commandline.command);
+ }
+ },
+
+ /**
+ * Highlight the messageBox according to <b>group</b>.
+ */
+ _setHighlightGroup: function (group) {
+ this._messageBox.setAttributeNS(NS.uri, "highlight", group);
+ },
+
+ /**
+ * Determines whether the command line should be visible.
+ *
+ * @returns {boolean}
+ */
+ _commandShown: function () modes.main == modes.COMMAND_LINE &&
+ !(modes.extended & (modes.INPUT_MULTILINE | modes.OUTPUT_MULTILINE)),
+
+ /**
+ * Set the command-line prompt.
+ *
+ * @param {string} val
+ * @param {string} highlightGroup
+ */
+ _setPrompt: function (val, highlightGroup) {
+ this._promptWidget.value = val;
+ this._promptWidget.size = val.length;
+ this._promptWidget.collapsed = (val == "");
+ this._promptWidget.setAttributeNS(NS.uri, "highlight", highlightGroup || commandline.HL_NORMAL);
+ },
+
+ /**
+ * Set the command-line input value. The caret is reset to the
+ * end of the line.
+ *
+ * @param {string} cmd
+ */
+ _setCommand: function (cmd) {
+ this._commandWidget.value = cmd;
+ this._commandWidget.selectionStart = cmd.length;
+ this._commandWidget.selectionEnd = cmd.length;
+ },
+
+ /**
+ * Display a message in the command-line area.
+ *
+ * @param {string} str
+ * @param {string} highlightGroup
+ * @param {boolean} forceSingle If provided, don't let over-long
+ * messages move to the MOW.
+ */
+ _echoLine: function (str, highlightGroup, forceSingle) {
+ this._setHighlightGroup(highlightGroup);
+ this._messageBox.value = str;
+
+ liberator.triggerObserver("echoLine", str, highlightGroup, forceSingle);
+
+ if (!this._commandShown())
+ commandline.hide();
+
+ let field = this._messageBox.inputField;
+ if (!forceSingle && field.editor.rootElement.scrollWidth > field.scrollWidth)
+ this._echoMultiline(<span highlight="Message">{str}</span>, highlightGroup);
+ },
+
+ /**
+ * Display a multiline message.
+ *
+ * @param {string} str
+ * @param {string} highlightGroup
+ */
+ // TODO: resize upon a window resize
+ _echoMultiline: function (str, highlightGroup) {
+ let doc = this._multilineOutputWidget.contentDocument;
+ let win = this._multilineOutputWidget.contentWindow;
+
+ liberator.triggerObserver("echoMultiline", str, highlightGroup);
+
+ // If it's already XML, assume it knows what it's doing.
+ // Otherwise, white space is significant.
+ // The problem elsewhere is that E4X tends to insert new lines
+ // after interpolated data.
+ XML.ignoreWhitespace = typeof str != "xml";
+ this._lastMowOutput = <div class="ex-command-output" style="white-space: nowrap" highlight={highlightGroup}>{template.maybeXML(str)}</div>;
+ let output = util.xmlToDom(this._lastMowOutput, doc);
+ XML.ignoreWhitespace = true;
+
+ // FIXME: need to make sure an open MOW is closed when commands
+ // that don't generate output are executed
+ if (this._outputContainer.collapsed)
+ doc.body.innerHTML = "";
+
+ doc.body.appendChild(output);
+
+ commandline.updateOutputHeight(true);
+
+ if (options["more"] && win.scrollMaxY > 0) {
+ // start the last executed command's output at the top of the screen
+ let elements = doc.getElementsByClassName("ex-command-output");
+ elements[elements.length - 1].scrollIntoView(true);
+ }
+ else
+ win.scrollTo(0, doc.height);
+
+ win.focus();
+
+ this._startHints = false;
+ modes.set(modes.COMMAND_LINE, modes.OUTPUT_MULTILINE);
+ commandline.updateMorePrompt();
+ },
+
+ /**
+ * Ensure that the multiline input widget is the correct size.
+ */
+ _autosizeMultilineInputWidget: function () {
+ let lines = this._multilineInputWidget.value.split("\n").length - 1;
+
+ this._multilineInputWidget.setAttribute("rows", Math.max(lines, 1));
+ },
+
+
+
+ HL_NORMAL: "Normal",
+ HL_ERRORMSG: "ErrorMsg",
+ HL_MODEMSG: "ModeMsg",
+ HL_MOREMSG: "MoreMsg",
+ HL_QUESTION: "Question",
+ HL_INFOMSG: "InfoMsg",
+ HL_WARNINGMSG: "WarningMsg",
+ HL_LINENR: "LineNr",
+
+ FORCE_MULTILINE : 1 << 0,
+ FORCE_SINGLELINE : 1 << 1,
+ DISALLOW_MULTILINE : 1 << 2, // if an echo() should try to use the single line
+ // but output nothing when the MOW is open; when also
+ // FORCE_MULTILINE is given, FORCE_MULTILINE takes precedence
+ APPEND_TO_MESSAGES : 1 << 3, // add the string to the message this._history
+
+ get completionContext() this._completions.context,
+
+ get mode() (modes.extended == modes.EX) ? "cmd" : "search",
+
+ get silent() this._silent,
+ set silent(val) {
+ this._silent = val;
+ this._quiet = this._quiet;
+ },
+ get quiet() this._quiet,
+ set quiet(val) {
+ this._quiet = val;
+ Array.forEach(document.getElementById("liberator-commandline").childNodes, function (node) {
+ node.style.opacity = this._quiet || this._silent ? "0" : "";
+ });
+ },
+
+ // @param type can be:
+ // "submit": when the user pressed enter in the command line
+ // "change"
+ // "cancel"
+ // "complete"
+ registerCallback: function (type, mode, func) {
+ if (!(type in this._callbacks))
+ this._callbacks[type] = {};
+ this._callbacks[type][mode] = func;
+ },
+
+ triggerCallback: function (type, mode, data) {
+ if (this._callbacks[type] && this._callbacks[type][mode])
+ this._callbacks[type][mode].call(this, data);
+ },
+
+ runSilently: function (func, self) {
+ let wasSilent = this._silent;
+ this._silent = true;
+ try {
+ func.call(self);
+ }
+ finally {
+ this._silent = wasSilent;
+ }
+ },
+
+ get command() {
+ try {
+ // The long path is because of complications with the
+ // completion preview.
+ return this._commandWidget.inputField.editor.rootElement.firstChild.textContent;
+ }
+ catch (e) {
+ return this._commandWidget.value;
+ }
+ },
+ set command(cmd) this._commandWidget.value = cmd,
+
+ get message() this._messageBox.value,
+
+ /**
+ * Open the command line. The main mode is set to
+ * COMMAND_LINE, the extended mode to <b>extendedMode</b>.
+ * Further, callbacks defined for <b>extendedMode</b> are
+ * triggered as appropriate (see {@link #registerCallback}).
+ *
+ * @param {string} prompt
+ * @param {string} cmd
+ * @param {number} extendedMode
+ */
+ open: function open(prompt, cmd, extendedMode) {
+ // save the current prompts, we need it later if the command widget
+ // receives focus without calling the this.open() method
+ this._currentPrompt = prompt || "";
+ this._currentCommand = cmd || "";
+ this._currentExtendedMode = extendedMode || null;
+ this._keepCommand = false;
+
+ this._setPrompt(this._currentPrompt);
+ this._setCommand(this._currentCommand);
+ this._commandlineWidget.collapsed = false;
+
+ modes.set(modes.COMMAND_LINE, this._currentExtendedMode);
+
+ this._commandWidget.focus();
+
+ this._history = CommandLine.History(this._commandWidget.inputField, (modes.extended == modes.EX) ? "command" : "search");
+ this._completions = CommandLine.Completions(this._commandWidget.inputField);
+
+ // open the completion list automatically if wanted
+ if (cmd.length)
+ commandline.triggerCallback("change", this._currentExtendedMode, cmd);
+ },
+
+ /**
+ * Closes the command line. This is ordinarily triggered automatically
+ * by a mode change. Will not hide the command line immediately if
+ * called directly after a successful command, otherwise it will.
+ */
+ close: function close() {
+ let mode = this._currentExtendedMode;
+ this._currentExtendedMode = null;
+ commandline.triggerCallback("cancel", mode);
+
+ if (this._history)
+ this._history.save();
+
+ this.resetCompletions(); // cancels any asynchronous completion still going on, must be before we set completions = null
+ this._completions = null;
+ this._history = null;
+
+ statusline.updateProgress(""); // we may have a "match x of y" visible
+ liberator.focusContent(false);
+
+ this._multilineInputWidget.collapsed = true;
+ this._completionList.hide();
+
+ if (!this._keepCommand || this._silent || this._quiet) {
+ this._outputContainer.collapsed = true;
+ commandline.updateMorePrompt();
+ this.hide();
+ }
+ if (!this._outputContainer.collapsed) {
+ modes.set(modes.COMMAND_LINE, modes.OUTPUT_MULTILINE);
+ commandline.updateMorePrompt();
+ }
+ this._keepCommand = false;
+ },
+
+ /**
+ * Hides the command line, and shows any status messages that
+ * are under it.
+ */
+ hide: function hide() {
+ this._commandlineWidget.collapsed = true;
+ },
+
+ /**
+ * Output the given string onto the command line. With no flags, the
+ * message will be shown in the status line if it's short enough to
+ * fit, and contains no new lines, and isn't XML. Otherwise, it will be
+ * shown in the MOW.
+ *
+ * @param {string} str
+ * @param {string} highlightGroup The Highlight group for the
+ * message.
+ * @default "Normal"
+ * @param {number} flags Changes the behavior as follows:
+ * commandline.APPEND_TO_MESSAGES - Causes message to be added to the
+ * messages this._history, and shown by :messages.
+ * commandline.FORCE_SINGLELINE - Forbids the command from being
+ * pushed to the MOW if it's too long or of there are already
+ * status messages being shown.
+ * commandline.DISALLOW_MULTILINE - Cancels the operation if the MOW
+ * is already visible.
+ * commandline.FORCE_MULTILINE - Forces the message to appear in
+ * the MOW.
+ */
+ echo: function echo(str, highlightGroup, flags) {
+ // liberator.echo uses different order of flags as it omits the highlight group, change commandline.echo argument order? --mst
+ if (this._silent)
+ return;
+
+ highlightGroup = highlightGroup || this.HL_NORMAL;
+
+ if (flags & this.APPEND_TO_MESSAGES)
+ this._messageHistory.add({ str: str, highlight: highlightGroup });
+
+ // The DOM isn't threadsafe. It must only be accessed from the main thread.
+ liberator.callInMainThread(function () {
+ if ((flags & this.DISALLOW_MULTILINE) && !this._outputContainer.collapsed)
+ return;
+
+ let single = flags & (this.FORCE_SINGLELINE | this.DISALLOW_MULTILINE);
+ let action = this._echoLine;
+
+ // TODO: this is all a bit convoluted - clean up.
+ // assume that FORCE_MULTILINE output is fully styled
+ if (!(flags & this.FORCE_MULTILINE) && !single && (!this._outputContainer.collapsed || this._messageBox.value == this._lastEcho)) {
+ highlightGroup += " Message";
+ action = this._echoMultiline;
+ }
+
+ if ((flags & this.FORCE_MULTILINE) || (/\n/.test(str) || typeof str == "xml") && !(flags & this.FORCE_SINGLELINE))
+ action = this._echoMultiline;
+
+ if (single)
+ this._lastEcho = null;
+ else {
+ if (this._messageBox.value == this._lastEcho)
+ this._echoMultiline(<span highlight="Message">{this._lastEcho}</span>,
+ this._messageBox.getAttributeNS(NS.uri, "highlight"));
+ this._lastEcho = (action == this._echoLine) && str;
+ }
+
+ if (action)
+ action.call(this, str, highlightGroup, single);
+ }, this);
+ },
+
+ /**
+ * Prompt the user. Sets modes.main to COMMAND_LINE, which the user may
+ * pop at any time to close the prompt.
+ *
+ * @param {string} prompt The input prompt to use.
+ * @param {function(string)} callback
+ * @param {Object} extra
+ * @... {function} onChange - A function to be called with the current
+ * input every time it changes.
+ * @... {function(CompletionContext)} completer - A completion function
+ * for the user's input.
+ * @... {string} promptHighlight - The HighlightGroup used for the
+ * prompt. @default "Question"
+ * @... {string} default - The initial value that will be returned
+ * if the user presses <CR> straightaway. @default ""
+ */
+ input: function _input(prompt, callback, extra) {
+ extra = extra || {};
+
+ this._input = {
+ submit: callback,
+ change: extra.onChange,
+ complete: extra.completer,
+ cancel: extra.onCancel
+ };
+
+ modes.push(modes.COMMAND_LINE, modes.PROMPT);
+ this._currentExtendedMode = modes.PROMPT;
+
+ this._setPrompt(prompt, extra.promptHighlight || this.HL_QUESTION);
+ this._setCommand(extra.default || "");
+ this._commandlineWidget.collapsed = false;
+ this._commandWidget.focus();
+
+ this._completions = CommandLine.Completions(this._commandWidget.inputField);
+ },
+
+ /**
+ * Get a multiline input from a user, up to but not including the line
+ * which matches the given regular expression. Then execute the
+ * callback with that string as a parameter.
+ *
+ * @param {RegExp} untilRegexp
+ * @param {function(string)} callbackFunc
+ */
+ // FIXME: Buggy, especially when pasting. Shouldn't use a RegExp.
+ inputMultiline: function inputMultiline(untilRegexp, callbackFunc) {
+ // Kludge.
+ let cmd = !this._commandWidget.collapsed && this.command;
+ modes.push(modes.COMMAND_LINE, modes.INPUT_MULTILINE);
+ if (cmd != false)
+ this._echoLine(cmd, this.HL_NORMAL);
+
+ // save the arguments, they are needed in the event handler onEvent
+ this._multilineRegexp = untilRegexp;
+ this._multilineCallback = callbackFunc;
+
+ this._multilineInputWidget.collapsed = false;
+ this._multilineInputWidget.value = "";
+ this._autosizeMultilineInputWidget();
+
+ setTimeout(function () { this._multilineInputWidget.focus(); }, 10);
+ },
+
+ /**
+ * Handles all command-line events. All key events are passed here when
+ * COMMAND_LINE mode is active, as well as all input, keyup, focus, and
+ * blur events sent to the command-line XUL element.
+ *
+ * @param {Event} event
+ * @private
+ */
+ onEvent: function onEvent(event) {
+ const self = this;
+ let command = this.command;
+
+ if (event.type == "blur") {
+ // prevent losing focus, there should be a better way, but it just didn't work otherwise
+ setTimeout(function () {
+ if (self._commandShown() && event.originalTarget == self._commandWidget.inputField)
+ self._commandWidget.inputField.focus();
+ }, 0);
+ }
+ else if (event.type == "focus") {
+ if (!self._commandShown() && event.target == self._commandWidget.inputField) {
+ event.target.blur();
+ liberator.beep();
+ }
+ }
+ else if (event.type == "input") {
+ this.resetCompletions();
+ commandline.triggerCallback("change", this._currentExtendedMode, command);
+ }
+ else if (event.type == "keypress") {
+ let key = events.toString(event);
+ if (this._completions)
+ this._completions.previewClear();
+ if (!this._currentExtendedMode)
+ return;
+
+ // user pressed <Enter> to carry out a command
+ // user pressing <Esc> is handled in the global onEscape
+ // FIXME: <Esc> should trigger "cancel" event
+ if (events.isAcceptKey(key)) {
+ let mode = this._currentExtendedMode; // save it here, as modes.pop() resets it
+ this._keepCommand = !userContext.hidden_option_no_command_afterimage;
+ this._currentExtendedMode = null; // Don't let modes.pop trigger "cancel"
+ modes.pop(!this._silent);
+ commandline.triggerCallback("submit", mode, command);
+ }
+ // user pressed <Up> or <Down> arrow to cycle this._history completion
+ else if (/^(<Up>|<Down>|<S-Up>|<S-Down>|<PageUp>|<PageDown>)$/.test(key)) {
+ // prevent tab from moving to the next field
+ event.preventDefault();
+ event.stopPropagation();
+
+ if (this._history)
+ this._history.select(/Up/.test(key), !/(Page|S-)/.test(key));
+ else
+ liberator.beep();
+ }
+ // user pressed <Tab> to get completions of a command
+ else if (key == "<Tab>" || key == "<S-Tab>") {
+ // prevent tab from moving to the next field
+ event.preventDefault();
+ event.stopPropagation();
+
+ this._tabTimer.tell(event);
+ }
+ else if (key == "<BS>") {
+ // reset the tab completion
+ //this.resetCompletions();
+
+ // and blur the command line if there is no text left
+ if (command.length == 0) {
+ commandline.triggerCallback("cancel", this._currentExtendedMode);
+ modes.pop();
+ }
+ }
+ else { // any other key
+ //this.resetCompletions();
+ }
+ // allow this event to be handled by the host app
+ }
+ else if (event.type == "keyup") {
+ let key = events.toString(event);
+ if (key == "<Tab>" || key == "<S-Tab>")
+ this._tabTimer.flush();
+ }
+ },
+
+ /**
+ * Multiline input events, they will come straight from
+ * #liberator-multiline-input in the XUL.
+ *
+ * @param {Event} event
+ */
+ onMultilineInputEvent: function onMultilineInputEvent(event) {
+ if (event.type == "keypress") {
+ let key = events.toString(event);
+ if (events.isAcceptKey(key)) {
+ let text = this._multilineInputWidget.value.substr(0, this._multilineInputWidget.selectionStart);
+ if (text.match(this._multilineRegexp)) {
+ text = text.replace(this._multilineRegexp, "");
+ modes.pop();
+ this._multilineInputWidget.collapsed = true;
+ this._multilineCallback.call(this, text);
+ }
+ }
+ else if (events.isCancelKey(key)) {
+ modes.pop();
+ this._multilineInputWidget.collapsed = true;
+ }
+ }
+ else if (event.type == "blur") {
+ if (modes.extended & modes.INPUT_MULTILINE)
+ setTimeout(function () { this._multilineInputWidget.inputField.focus(); }, 0);
+ }
+ else if (event.type == "input")
+ this._autosizeMultilineInputWidget();
+ return true;
+ },
+
+ /**
+ * Handle events when we are in multiline output mode, these come from
+ * liberator when modes.extended & modes.MULTILINE_OUTPUT and also from
+ * #liberator-multiline-output in the XUL.
+ *
+ * @param {Event} event
+ */
+ // FIXME: if 'more' is set and the MOW is not scrollable we should still
+ // allow a down motion after an up rather than closing
+ onMultilineOutputEvent: function onMultilineOutputEvent(event) {
+ let win = this._multilineOutputWidget.contentWindow;
+
+ let showMoreHelpPrompt = false;
+ let showMorePrompt = false;
+ let closeWindow = false;
+ let passEvent = false;
+
+ let key = events.toString(event);
+
+ // TODO: Wouldn't multiple handlers be cleaner? --djk
+ if (event.type == "click" && event.target instanceof HTMLAnchorElement) {
+ function openLink(where) {
+ event.preventDefault();
+ // FIXME: Why is this needed? --djk
+ if (event.target.getAttribute("href") == "#")
+ liberator.open(event.target.textContent, where);
+ else
+ liberator.open(event.target.href, where);
+ }
+
+ switch (key) {
+ case "<LeftMouse>":
+ if (event.originalTarget.getAttributeNS(NS.uri, "highlight") == "URL buffer-list") {
+ event.preventDefault();
+ tabs.select(parseInt(event.originalTarget.parentNode.parentNode.firstChild.textContent, 10) - 1);
+ }
+ else
+ openLink(liberator.CURRENT_TAB);
+ break;
+ case "<MiddleMouse>":
+ case "<C-LeftMouse>":
+ case "<C-M-LeftMouse>":
+ openLink(liberator.NEW_BACKGROUND_TAB);
+ break;
+ case "<S-MiddleMouse>":
+ case "<C-S-LeftMouse>":
+ case "<C-M-S-LeftMouse>":
+ openLink(liberator.NEW_TAB);
+ break;
+ case "<S-LeftMouse>":
+ openLink(liberator.NEW_WINDOW);
+ break;
+ }
+
+ return;
+ }
+
+ if (this._startHints) {
+ statusline.updateInputBuffer("");
+ this._startHints = false;
+ hints.show(key, undefined, win);
+ return;
+ }
+
+ function isScrollable() !win.scrollMaxY == 0;
+ function atEnd() win.scrollY / win.scrollMaxY >= 1;
+
+ switch (key) {
+ case "<Esc>":
+ closeWindow = true;
+ break; // handled globally in events.js:onEscape()
+
+ case ":":
+ commandline.open(":", "", modes.EX);
+ return;
+
+ // down a line
+ case "j":
+ case "<Down>":
+ if (options["more"] && isScrollable())
+ win.scrollByLines(1);
+ else
+ passEvent = true;
+ break;
+
+ case "<C-j>":
+ case "<C-m>":
+ case "<Return>":
+ if (options["more"] && isScrollable() && !atEnd())
+ win.scrollByLines(1);
+ else
+ closeWindow = true; // don't propagate the event for accept keys
+ break;
+
+ // up a line
+ case "k":
+ case "<Up>":
+ case "<BS>":
+ if (options["more"] && isScrollable())
+ win.scrollByLines(-1);
+ else if (options["more"] && !isScrollable())
+ showMorePrompt = true;
+ else
+ passEvent = true;
+ break;
+
+ // half page down
+ case "d":
+ if (options["more"] && isScrollable())
+ win.scrollBy(0, win.innerHeight / 2);
+ else
+ passEvent = true;
+ break;
+
+ // TODO: <LeftMouse> on the prompt line should scroll one page
+ // page down
+ case "f":
+ if (options["more"] && isScrollable())
+ win.scrollByPages(1);
+ else
+ passEvent = true;
+ break;
+
+ case "<Space>":
+ case "<PageDown>":
+ if (options["more"] && isScrollable() && !atEnd())
+ win.scrollByPages(1);
+ else
+ passEvent = true;
+ break;
+
+ // half page up
+ case "u":
+ // if (more and scrollable)
+ if (options["more"] && isScrollable())
+ win.scrollBy(0, -(win.innerHeight / 2));
+ else
+ passEvent = true;
+ break;
+
+ // page up
+ case "b":
+ if (options["more"] && isScrollable())
+ win.scrollByPages(-1);
+ else if (options["more"] && !isScrollable())
+ showMorePrompt = true;
+ else
+ passEvent = true;
+ break;
+
+ case "<PageUp>":
+ if (options["more"] && isScrollable())
+ win.scrollByPages(-1);
+ else
+ passEvent = true;
+ break;
+
+ // top of page
+ case "g":
+ if (options["more"] && isScrollable())
+ win.scrollTo(0, 0);
+ else if (options["more"] && !isScrollable())
+ showMorePrompt = true;
+ else
+ passEvent = true;
+ break;
+
+ // bottom of page
+ case "G":
+ if (options["more"] && isScrollable() && !atEnd())
+ win.scrollTo(0, win.scrollMaxY);
+ else
+ passEvent = true;
+ break;
+
+ // copy text to clipboard
+ case "<C-y>":
+ util.copyToClipboard(win.getSelection());
+ break;
+
+ // close the window
+ case "q":
+ closeWindow = true;
+ break;
+
+ case ";":
+ statusline.updateInputBuffer(";");
+ this._startHints = true;
+ break;
+
+ // unmapped key
+ default:
+ if (!options["more"] || !isScrollable() || atEnd() || events.isCancelKey(key))
+ passEvent = true;
+ else
+ showMoreHelpPrompt = true;
+ }
+
+ if (passEvent || closeWindow) {
+ modes.pop();
+
+ if (passEvent)
+ events.onKeyPress(event);
+ }
+ else
+ commandline.updateMorePrompt(showMorePrompt, showMoreHelpPrompt);
+ },
+
+ getSpaceNeeded: function getSpaceNeeded() {
+ let rect = this._commandlineWidget.getBoundingClientRect();
+ let offset = rect.bottom - window.innerHeight;
+ return Math.max(0, offset);
+ },
+
+ /**
+ * Update or remove the multiline output widget's "MORE" prompt.
+ *
+ * @param {boolean} force If true, "-- More --" is shown even if we're
+ * at the end of the output.
+ * @param {boolean} showHelp When true, show the valid key sequences
+ * and what they do.
+ */
+ updateMorePrompt: function updateMorePrompt(force, showHelp) {
+ if (this._outputContainer.collapsed)
+ return this._echoLine("", this.HL_NORMAL);
+
+ let win = this._multilineOutputWidget.contentWindow;
+ function isScrollable() !win.scrollMaxY == 0;
+ function atEnd() win.scrollY / win.scrollMaxY >= 1;
+
+ if (showHelp)
+ this._echoLine("-- More -- SPACE/d/j: screen/page/line down, b/u/k: up, q: quit", this.HL_MOREMSG, true);
+ else if (force || (options["more"] && isScrollable() && !atEnd()))
+ this._echoLine("-- More --", this.HL_MOREMSG, true);
+ else
+ this._echoLine("Press ENTER or type command to continue", this.HL_QUESTION, true);
+ },
+
+ /**
+ * Changes the height of the this._multilineOutputWidget to fit in the
+ * available space.
+ *
+ * @param {boolean} open If true, the widget will be opened if it's not
+ * already so.
+ */
+ updateOutputHeight: function updateOutputHeight(open) {
+ if (!open && this._outputContainer.collapsed)
+ return;
+
+ let doc = this._multilineOutputWidget.contentDocument;
+
+ availableHeight = config.outputHeight;
+ if (!this._outputContainer.collapsed)
+ availableHeight += parseFloat(this._outputContainer.height);
+ doc.body.style.minWidth = this._commandlineWidget.scrollWidth + "px";
+ this._outputContainer.height = Math.min(doc.height, availableHeight) + "px";
+ doc.body.style.minWidth = "";
+ this._outputContainer.collapsed = false;
+ },
+
+ resetCompletions: function resetCompletions() {
+ if (this._completions) {
+ this._completions.context.cancelAll();
+ this._completions.wildIndex = -1;
+ this._completions.previewClear();
+ }
+ if (this._history)
+ this._history.reset();
+ }
+}, {
+ /**
+ * A class for managing the this._history of an inputField.
+ *
+ * @param {HTMLInputElement} inputField
+ * @param {string} mode The mode for which we need this._history.
+ */
+ History: Class("History", {
+ init: function (inputField, mode) {
+ this.mode = mode;
+ this.input = inputField;
+ this.store = storage["history-" + mode];
+ this.reset();
+ },
+ /**
+ * Reset the this._history index to the first entry.
+ */
+ reset: function () {
+ this.index = null;
+ },
+ /**
+ * Save the last entry to the permanent store. All duplicate entries
+ * are removed and the list is truncated, if necessary.
+ */
+ save: function () {
+ if (events.feedingKeys)
+ return;
+ let str = this.input.value;
+ if (/^\s*$/.test(str))
+ return;
+ this.store.mutate("filter", function (line) (line.value || line) != str);
+ this.store.push({ value: str, timestamp: Date.now(), privateData: this.checkPrivate(str) });
+ this.store.truncate(options["history"], true);
+ },
+ /**
+ * @property {function} Returns whether a data item should be
+ * considered private.
+ */
+ checkPrivate: function (str) {
+ // Not really the ideal place for this check.
+ if (this.mode == "command")
+ return (commands.get(commands.parseCommand(str)[1]) || {}).privateData;
+ return false;
+ },
+ /**
+ * Removes any private data from this this._history.
+ */
+ sanitize: function (timespan) {
+ let range = [0, Number.MAX_VALUE];
+ if (liberator.has("sanitizer") && (timespan || options["sanitizetimespan"]))
+ range = sanitizer.getClearRange(timespan || options["sanitizetimespan"]);
+
+ this.store.mutate("filter", function (item) {
+ let timestamp = (item.timestamp || Date.now()/1000) * 1000;
+ return !line.privateData || timestamp < self.range[0] || timestamp > self.range[1];
+ });
+ },
+ /**
+ * Replace the current input field value.
+ *
+ * @param {string} val The new value.
+ */
+ replace: function (val) {
+ this.input.value = val;
+ commandline.triggerCallback("change", this._currentExtendedMode, val);
+ },
+
+ /**
+ * Move forward or backward in this._history.
+ *
+ * @param {boolean} backward Direction to move.
+ * @param {boolean} matchCurrent Search for matches starting
+ * with the current input value.
+ */
+ select: function (backward, matchCurrent) {
+ // always reset the tab completion if we use up/down keys
+ commandline._completions.reset();
+
+ let diff = backward ? -1 : 1;
+
+ if (this.index == null) {
+ this.original = this.input.value;
+ this.index = this.store.length;
+ }
+
+ // search the this._history for the first item matching the current
+ // commandline string
+ while (true) {
+ this.index += diff;
+ if (this.index < 0 || this.index > this.store.length) {
+ this.index = util.Math.constrain(this.index, 0, this.store.length);
+ liberator.beep();
+ // I don't know why this kludge is needed. It
+ // prevents the caret from moving to the end of
+ // the input field.
+ if (this.input.value == "") {
+ this.input.value = " ";
+ this.input.value = "";
+ }
+ break;
+ }
+
+ let hist = this.store.get(this.index);
+ // user pressed DOWN when there is no newer this._history item
+ if (!hist)
+ hist = this.original;
+ else
+ hist = (hist.value || hist);
+
+ if (!matchCurrent || hist.substr(0, this.original.length) == this.original) {
+ this.replace(hist);
+ break;
+ }
+ }
+ }
+ }),
+
+ /**
+ * A class for tab this._completions on an input field.
+ *
+ * @param {Object} input
+ */
+ Completions: Class("Completions", {
+ init: function (input) {
+ this.context = CompletionContext(input.editor);
+ this.context.onUpdate = this.closure._reset;
+ this.editor = input.editor;
+ this.selected = null;
+ this.wildmode = options.get("wildmode");
+ this.itemList = commandline._completionList;
+ this.itemList.setItems(this.context);
+ this.reset();
+ },
+
+ UP: {},
+ DOWN: {},
+ PAGE_UP: {},
+ PAGE_DOWN: {},
+ RESET: null,
+
+ get completion() {
+ let str = commandline.command;
+ return str.substring(this.prefix.length, str.length - this.suffix.length);
+ },
+ set completion set_completion(completion) {
+ this.previewClear();
+
+ // Change the completion text.
+ // The second line is a hack to deal with some substring
+ // preview corner cases.
+ commandline._commandWidget.value = this.prefix + completion + this.suffix;
+ this.editor.selection.focusNode.textContent = commandline._commandWidget.value;
+
+ // Reset the caret to one position after the completion.
+ this.caret = this.prefix.length + completion.length;
+ },
+
+ get caret() this.editor.selection.focusOffset,
+ set caret(offset) {
+ commandline._commandWidget.selectionStart = offset;
+ commandline._commandWidget.selectionEnd = offset;
+ },
+
+ get start() this.context.allItems.start,
+
+ get items() this.context.allItems.items,
+
+ get substring() this.context.longestAllSubstring,
+
+ get wildtype() this.wildtypes[this.wildIndex] || "",
+
+ get type() ({
+ list: this.wildmode.checkHas(this.wildtype, "list"),
+ longest: this.wildmode.checkHas(this.wildtype, "longest"),
+ first: this.wildmode.checkHas(this.wildtype, ""),
+ full: this.wildmode.checkHas(this.wildtype, "full")
+ }),
+
+ complete: function complete(show, tabPressed) {
+ this.context.reset();
+ this.context.tabPressed = tabPressed;
+ commandline.triggerCallback("complete", commandline._currentExtendedMode, this.context);
+ this.context.updateAsync = true;
+ this.reset(show, tabPressed);
+ this.wildIndex = 0;
+ },
+
+ preview: function preview() {
+ this.previewClear();
+ if (this.wildIndex < 0 || this.suffix || !this.items.length)
+ return;
+
+ let substring = "";
+ switch (this.wildtype.replace(/.*:/, "")) {
+ case "":
+ substring = this.items[0].text;
+ break;
+ case "longest":
+ if (this.items.length > 1) {
+ substring = this.substring;
+ break;
+ }
+ // Fallthrough
+ case "full":
+ let item = this.items[this.selected != null ? this.selected + 1 : 0];
+ if (item)
+ substring = item.text;
+ break;
+ }
+
+ // Don't show 1-character substrings unless we've just hit backspace
+ if (substring.length < 2 && (!this._lastSubstring || this._lastSubstring.indexOf(substring) != 0))
+ return;
+ this._lastSubstring = substring;
+
+ let value = this.completion;
+ if (util.compareIgnoreCase(value, substring.substr(0, value.length)))
+ return;
+ substring = substring.substr(value.length);
+ this.removeSubstring = substring;
+
+ let node = util.xmlToDom(<span highlight="Preview">{substring}</span>,
+ document);
+ let start = this.caret;
+ this.editor.insertNode(node, this.editor.rootElement, 1);
+ this.caret = start;
+ },
+
+ previewClear: function previewClear() {
+ let node = this.editor.rootElement.firstChild;
+ if (node && node.nextSibling) {
+ try {
+ this.editor.deleteNode(node.nextSibling);
+ }
+ catch (e) {
+ node.nextSibling.textContent = "";
+ }
+ }
+ else if (this.removeSubstring) {
+ let str = this.removeSubstring;
+ let cmd = commandline._commandWidget.value;
+ if (cmd.substr(cmd.length - str.length) == str)
+ commandline._commandWidget.value = cmd.substr(0, cmd.length - str.length);
+ }
+ delete this.removeSubstring;
+ },
+
+ reset: function reset(show) {
+ this.wildIndex = -1;
+
+ this.prefix = this.context.value.substring(0, this.start);
+ this.value = this.context.value.substring(this.start, this.caret);
+ this.suffix = this.context.value.substring(this.caret);
+
+ if (show) {
+ this.itemList.reset();
+ this.selected = null;
+ this.wildIndex = 0;
+ }
+
+ this.wildtypes = this.wildmode.values;
+ this.preview();
+ },
+
+ _reset: function _reset() {
+ this.prefix = this.context.value.substring(0, this.start);
+ this.value = this.context.value.substring(this.start, this.caret);
+ this.suffix = this.context.value.substring(this.caret);
+
+ this.itemList.reset();
+ this.itemList.selectItem(this.selected);
+
+ this.preview();
+ },
+
+ select: function select(idx) {
+ switch (idx) {
+ case this.UP:
+ if (this.selected == null)
+ idx = -2;
+ else
+ idx = this.selected - 1;
+ break;
+ case this.DOWN:
+ if (this.selected == null)
+ idx = 0;
+ else
+ idx = this.selected + 1;
+ break;
+ case this.RESET:
+ idx = null;
+ break;
+ default:
+ idx = util.Math.constrain(idx, 0, this.items.length - 1);
+ break;
+ }
+
+ if (idx == -1 || this.items.length && idx >= this.items.length || idx == null) {
+ // Wrapped. Start again.
+ this.selected = null;
+ this.completion = this.value;
+ }
+ else {
+ // Wait for contexts to complete if necessary.
+ // FIXME: Need to make idx relative to individual contexts.
+ let list = this.context.contextList;
+ if (idx == -2)
+ list = list.slice().reverse();
+ let n = 0;
+ try {
+ this.waiting = true;
+ for (let [, context] in Iterator(list)) {
+ function done() !(idx >= n + context.items.length || idx == -2 && !context.items.length);
+ while (context.incomplete && !done())
+ liberator.threadYield(false, true);
+
+ if (done())
+ break;
+
+ n += context.items.length;
+ }
+ }
+ finally {
+ this.waiting = false;
+ }
+
+ // See previous FIXME. This will break if new items in
+ // a previous context come in.
+ if (idx < 0)
+ idx = this.items.length - 1;
+ if (this.items.length == 0)
+ return;
+
+ this.selected = idx;
+ this.completion = this.items[idx].text;
+ }
+
+ this.itemList.selectItem(idx);
+ },
+
+ tabs: [],
+
+ tab: function tab(reverse) {
+ commandline._autocompleteTimer.flush();
+ // Check if we need to run the completer.
+ if (this.context.waitingForTab || this.wildIndex == -1)
+ this.complete(true, true);
+
+ this.tabs.push(reverse);
+ if (this.waiting)
+ return;
+
+ while (this.tabs.length) {
+ reverse = this.tabs.shift();
+ switch (this.wildtype.replace(/.*:/, "")) {
+ case "":
+ this.select(0);
+ break;
+ case "longest":
+ if (this.items.length > 1) {
+ if (this.substring && this.substring != this.completion)
+ this.completion = this.substring;
+ break;
+ }
+ // Fallthrough
+ case "full":
+ this.select(reverse ? this.UP : this.DOWN);
+ break;
+ }
+
+ if (this.type.list)
+ this.itemList.show();
+
+ this.wildIndex = util.Math.constrain(this.wildIndex + 1, 0, this.wildtypes.length - 1);
+ this.preview();
+
+ this._statusTimer.tell();
+ }
+
+ if (this.items.length == 0)
+ liberator.beep();
+ }
+ }),
+
+ /**
+ * eval() a JavaScript expression and return a string suitable
+ * to be echoed.
+ *
+ * @param {string} arg
+ * @param {boolean} useColor When true, the result is a
+ * highlighted XML object.
+ */
+ echoArgumentToString: function (arg, useColor) {
+ if (!arg)
+ return "";
+
+ try {
+ arg = liberator.eval(arg);
+ }
+ catch (e) {
+ liberator.echoerr(e);
+ return null;
+ }
+
+ if (typeof arg === "object")
+ arg = util.objectToString(arg, useColor);
+ else if (typeof arg == "string" && /\n/.test(arg))
+ arg = <span highlight="CmdOutput">{arg}</span>;
+ else
+ arg = String(arg);
+
+ return arg;
+ },
+}, {
+ commands: function () {
+ [
+ {
+ name: "ec[ho]",
+ description: "Echo the expression",
+ action: liberator.echo
+ },
+ {
+ name: "echoe[rr]",
+ description: "Echo the expression as an error message",
+ action: liberator.echoerr
+ },
+ {
+ name: "echom[sg]",
+ description: "Echo the expression as an informational message",
+ action: liberator.echomsg
+ }
+ ].forEach(function (command) {
+ commands.add([command.name],
+ command.description,
+ function (args) {
+ let str = CommandLine.echoArgumentToString(args.string, true);
+ if (str != null)
+ command.action(str);
+ }, {
+ completer: function (context) completion.javascript(context),
+ literal: 0
+ });
+ });
+
+ commands.add(["mes[sages]"],
+ "Display previously given messages",
+ function () {
+ // TODO: are all messages single line? Some display an aggregation
+ // of single line messages at least. E.g. :source
+ if (this._messageHistory.length == 1) {
+ let message = this._messageHistory.messages[0];
+ commandline.echo(message.str, message.highlight, commandline.FORCE_SINGLELINE);
+ }
+ else if (this._messageHistory.length > 1) {
+ XML.ignoreWhitespace = false;
+ let list = template.map(this._messageHistory.messages, function (message)
+ <div highlight={message.highlight + " Message"}>{message.str}</div>);
+ liberator.echo(list, commandline.FORCE_MULTILINE);
+ }
+ },
+ { argCount: "0" });
+
+ commands.add(["messc[lear]"],
+ "Clear the message this._history",
+ function () { this._messageHistory.clear(); },
+ { argCount: "0" });
+
+ commands.add(["sil[ent]"],
+ "Run a command silently",
+ function (args) {
+ commandline.runSilently(function () liberator.execute(args[0], null, true));
+ }, {
+ completer: function (context) completion.ex(context),
+ literal: 0
+ });
+ },
+ mappings: function () {
+ var myModes = [modes.COMMAND_LINE];
+
+ // TODO: move "<Esc>", "<C-[>" here from mappings
+ mappings.add(myModes,
+ ["<C-c>"], "Focus content",
+ function () { events.onEscape(); });
+
+ // Any "non-keyword" character triggers abbreviation expansion
+ // TODO: Add "<CR>" and "<Tab>" to this list
+ // At the moment, adding "<Tab>" breaks tab completion. Adding
+ // "<CR>" has no effect.
+ // TODO: Make non-keyword recognition smarter so that there need not
+ // be two lists of the same characters (one here and a regex in
+ // mappings.js)
+ mappings.add(myModes,
+ ["<Space>", '"', "'"], "Expand command line abbreviation",
+ function () {
+ commandline.resetCompletions();
+ return editor.expandAbbreviation("c");
+ },
+ { route: true });
+
+ mappings.add(myModes,
+ ["<C-]>", "<C-5>"], "Expand command line abbreviation",
+ function () { editor.expandAbbreviation("c"); });
+
+ mappings.add([modes.NORMAL],
+ ["g<"], "Redisplay the last command output",
+ function () {
+ if (this._lastMowOutput)
+ this._echoMultiline(this._lastMowOutput, commandline.HL_NORMAL);
+ else
+ liberator.beep();
+ });
+ },
+ options: function () {
+ options.add(["history", "hi"],
+ "Number of Ex commands and search patterns to store in the command-line this._history",
+ "number", 500,
+ { validator: function (value) value >= 0 });
+
+ options.add(["maxitems"],
+ "Maximum number of items to display at once",
+ "number", 20,
+ { validator: function (value) value >= 1 });
+
+ options.add(["messages", "msgs"],
+ "Number of messages to store in the message this._history",
+ "number", 100,
+ { validator: function (value) value >= 0 });
+
+ options.add(["more"],
+ "Pause the message list window when more than one screen of listings is displayed",
+ "boolean", true);
+
+ options.add(["showmode", "smd"],
+ "Show the current mode in the command line",
+ "boolean", true);
+
+ options.add(["suggestengines"],
+ "Engine Alias which has a feature of suggest",
+ "stringlist", "google",
+ {
+ completer: function completer(value) {
+ let engines = services.get("browserSearch").getEngines({})
+ .filter(function (engine) engine.supportsResponseType("application/x-suggestions+json"));
+
+ return engines.map(function (engine) [engine.alias, engine.description]);
+ },
+ validator: Option.validateCompleter
+ });
+
+ options.add(["complete", "cpt"],
+ "Items which are completed at the :open prompts",
+ "charlist", typeof(config.defaults["complete"]) == "string" ? config.defaults["complete"] : "slf",
+ {
+ completer: function (context) array(keys(completion.urlCompleters)),
+ validator: Option.validateCompleter
+ });
+
+ options.add(["wildcase", "wic"],
+ "Completion case matching mode",
+ "string", "smart",
+ {
+ completer: function () [
+ ["smart", "Case is significant when capital letters are typed"],
+ ["match", "Case is always significant"],
+ ["ignore", "Case is never significant"]
+ ],
+ validator: Option.validateCompleter
+ });
+
+ options.add(["wildignore", "wig"],
+ "List of file patterns to ignore when completing files",
+ "stringlist", "",
+ {
+ validator: function validator(values) {
+ // TODO: allow for escaping the ","
+ try {
+ RegExp("^(" + values.join("|") + ")$");
+ return true;
+ }
+ catch (e) {
+ return false;
+ }
+ }
+ });
+
+ options.add(["wildmode", "wim"],
+ "Define how command line completion works",
+ "stringlist", "list:full",
+ {
+ completer: function (context) [
+ // Why do we need ""?
+ ["", "Complete only the first match"],
+ ["full", "Complete the next full match"],
+ ["longest", "Complete to longest common string"],
+ ["list", "If more than one match, list all matches"],
+ ["list:full", "List all and complete first match"],
+ ["list:longest", "List all and complete common string"]
+ ],
+ validator: Option.validateCompleter,
+ checkHas: function (value, val) {
+ let [first, second] = value.split(":", 2);
+ return first == val || second == val;
+ }
+ });
+
+ options.add(["wildoptions", "wop"],
+ "Change how command line completion is done",
+ "stringlist", "",
+ {
+ completer: function completer(value) {
+ return [
+ ["", "Default completion that won't show or sort the results"],
+ ["auto", "Automatically show this._completions while you are typing"],
+ ["sort", "Always sort the completion list"]
+ ];
+ },
+ validator: Option.validateCompleter
+ });
+ },
+ styles: function () {
+ let fontSize = util.computedStyle(document.getElementById(config.mainWindowId)).fontSize;
+ styles.registerSheet("chrome://liberator/skin/liberator.css");
+ let error = styles.addSheet(true, "font-size", "chrome://liberator/content/buffer.xhtml",
+ "body { font-size: " + fontSize + "; }");
+ },
+});
+
+
+/**
+ * The list which is used for the completion box (and QuickFix window in
+ * future).
+ *
+ * @param {string} id The id of the <iframe> which will display the list. It
+ * must be in its own container element, whose height it will update as
+ * necessary.
+ */
+const ItemList = Class("ItemList", {
+ init: function (id) {
+ this._completionElements = [];
+
+ var iframe = document.getElementById(id);
+ if (!iframe) {
+ liberator.log("No iframe with id: " + id + " found, strange things may happen!"); // "The truth is out there..." -- djk
+ return; // XXX
+ }
+
+ this._doc = iframe.contentDocument;
+ this._container = iframe.parentNode;
+
+ this._doc.body.id = id + "-content";
+ this._doc.body.appendChild(this._doc.createTextNode(""));
+ this._doc.body.style.borderTop = "1px solid black"; // FIXME: For cases where this._completions/MOW are shown at once, or ls=0. Should use :highlight.
+
+ this._gradient = template.gradient("GradientLeft", "GradientRight");
+
+ this._items = null;
+ this._startIndex = -1; // The index of the first displayed item
+ this._endIndex = -1; // The index one *after* the last displayed item
+ this._selIndex = -1; // The index of the currently selected element
+ this._div = null;
+ this._divNodes = {};
+ this._minHeight = 0;
+ },
+
+ _dom: function (xml, map) util.xmlToDom(xml, this._doc, map),
+
+ _autoSize: function () {
+ const self = this;
+ if (this._container.collapsed)
+ this._div.style.minWidth = document.getElementById("liberator-commandline").scrollWidth + "px";
+ this._minHeight = Math.max(this._minHeight, this._divNodes.completions.getBoundingClientRect().bottom);
+ this._container.height = this._minHeight;
+ if (this._container.collapsed)
+ this._div.style.minWidth = "";
+ // FIXME: Belongs elsewhere.
+ commandline.updateOutputHeight(false);
+ setTimeout(function () { self._container.height -= commandline.getSpaceNeeded() }, 0);
+ },
+
+ _getCompletion: function (index) this._completionElements.snapshotItem(index - this._startIndex),
+
+ _init: function () {
+ this._div = this._dom(
+ <div class="ex-command-output" highlight="Normal" style="white-space: nowrap">
+ <div highlight="Completions" key="noCompletions"><span highlight="Title">No Completions</span></div>
+ <div key="completions"/>
+ <div highlight="Completions">
+ {
+ template.map(util.range(0, options["maxitems"] * 2), function (i)
+ <span highlight="CompItem">
+ <li highlight="NonText">~</li>
+ </span>)
+ }
+ </div>
+ </div>, this._divNodes);
+ this._doc.body.replaceChild(this._div, this._doc.body.firstChild);
+ //div.scrollIntoView(true);
+
+ this._items.contextList.forEach(function init_eachContext(context) {
+ delete context.cache.nodes;
+ if (!context.items.length && !context.message && !context.incomplete)
+ return;
+ context.cache.nodes = [];
+ this._dom(<div key="root" highlight="CompGroup">
+ <div highlight="Completions">
+ { context.createRow(context.title || [], "CompTitle") }
+ </div>
+ { this._gradient }
+ <div key="message" highlight="CompMsg"/>
+ <div key="up" highlight="CompLess"/>
+ <div key="items" highlight="Completions"/>
+ <div key="waiting" highlight="CompMsg">{ItemList.WAITING_MESSAGE}</div>
+ <div key="down" highlight="CompMore"/>
+ </div>, context.cache.nodes);
+ this._divNodes.completions.appendChild(context.cache.nodes.root);
+ }, this);
+
+ setTimeout(this.closure._autoSize, 0);
+ },
+
+ /**
+ * Uses the entries in "items" to this._fill the listbox and does incremental
+ * filling to speed up things.
+ *
+ * @param {number} offset Start at this index and show options["maxitems"].
+ */
+ _fill: function (offset) {
+ XML.ignoreWhiteSpace = false;
+ let diff = offset - this._startIndex;
+ if (this._items == null || offset == null || diff == 0 || offset < 0)
+ return false;
+
+ this._startIndex = offset;
+ this._endIndex = Math.min(this._startIndex + options["maxitems"], this._items.allItems.items.length);
+
+ let haveCompletions = false;
+ let off = 0;
+ let end = this._startIndex + options["maxitems"];
+ function getRows(context) {
+ function fix(n) util.Math.constrain(n, 0, len);
+ let len = context.items.length;
+ let start = off;
+ end -= !!context.message + context.incomplete;
+ off += len;
+
+ let s = fix(offset - start), e = fix(end - start);
+ return [s, e, context.incomplete && e >= offset && off - 1 < end];
+ }
+
+ this._items.contextList.forEach(function fill_eachContext(context) {
+ let nodes = context.cache.nodes;
+ if (!nodes)
+ return;
+ haveCompletions = true;
+
+ let root = nodes.root;
+ let items = nodes.items;
+ let [start, end, waiting] = getRows(context);
+
+ if (context.message)
+ nodes.message.textContent = context.message;
+ nodes.message.style.display = context.message ? "block" : "none";
+ nodes.waiting.style.display = waiting ? "block" : "none";
+ nodes.up.style.opacity = "0";
+ nodes.down.style.display = "none";
+
+ for (let [i, row] in Iterator(context.getRows(start, end, this._doc)))
+ nodes[i] = row;
+ for (let [i, row] in util.Array.iteritems(nodes)) {
+ if (!row)
+ continue;
+ let display = (i >= start && i < end);
+ if (display && row.parentNode != items) {
+ do {
+ var next = nodes[++i];
+ if (next && next.parentNode != items)
+ next = null;
+ }
+ while (!next && i < end)
+ items.insertBefore(row, next);
+ }
+ else if (!display && row.parentNode == items)
+ items.removeChild(row);
+ }
+ if (context.items.length == 0)
+ return;
+ nodes.up.style.opacity = (start == 0) ? "0" : "1";
+ if (end != context.items.length)
+ nodes.down.style.display = "block";
+ else
+ nodes.up.style.display = "block";
+ }, this);
+
+ this._divNodes.noCompletions.style.display = haveCompletions ? "none" : "block";
+
+ this._completionElements = util.evaluateXPath("//xhtml:div[@liberator:highlight='CompItem']", this._doc);
+
+ return true;
+ },
+
+ clear: function clear() { this.setItems(); this._doc.body.innerHTML = ""; },
+ hide: function hide() { this._container.collapsed = true; },
+ show: function show() { this._container.collapsed = false; },
+ visible: function visible() !this._container.collapsed,
+
+ reset: function () {
+ this._startIndex = this._endIndex = this._selIndex = -1;
+ this._div = null;
+ this.selectItem(-1);
+ },
+
+ // if @param selectedItem is given, show the list and select that item
+ setItems: function setItems(newItems, selectedItem) {
+ if (this._container.collapsed)
+ this._minHeight = 0;
+ this._startIndex = this._endIndex = this._selIndex = -1;
+ this._items = newItems;
+ this.reset();
+ if (typeof selectedItem == "number") {
+ this.selectItem(selectedItem);
+ this.show();
+ }
+ },
+
+ // select index, refill list if necessary
+ selectItem: function selectItem(index) {
+ //if (this._container.collapsed) // FIXME
+ // return;
+
+ //let now = Date.now();
+
+ if (this._div == null)
+ this._init();
+
+ let sel = this._selIndex;
+ let len = this._items.allItems.items.length;
+ let newOffset = this._startIndex;
+ let maxItems = options["maxitems"];
+ let contextLines = Math.min(3, parseInt((maxItems - 1) / 2));
+
+ if (index == -1 || index == null || index == len) { // wrapped around
+ if (this._selIndex < 0)
+ newOffset = 0;
+ this._selIndex = -1;
+ index = -1;
+ }
+ else {
+ if (index <= this._startIndex + contextLines)
+ newOffset = index - contextLines;
+ if (index >= this._endIndex - contextLines)
+ newOffset = index + contextLines - maxItems + 1;
+
+ newOffset = Math.min(newOffset, len - maxItems);
+ newOffset = Math.max(newOffset, 0);
+
+ this._selIndex = index;
+ }
+
+ if (sel > -1)
+ this._getCompletion(sel).removeAttribute("selected");
+ this._fill(newOffset);
+ if (index >= 0) {
+ this._getCompletion(index).setAttribute("selected", "true");
+ //this._getCompletion(index).scrollIntoView(false);
+ }
+
+ //if (index == 0)
+ // this.start = now;
+ //if (index == Math.min(len - 1, 100))
+ // liberator.dump({ time: Date.now() - this.start });
+ },
+
+ onEvent: function onEvent(event) false
+}, {
+ WAITING_MESSAGE: "Generating results...",
+});
+
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/commands.js b/common/content/commands.js
index 402e8275..9b4cdd34 100644
--- a/common/content/commands.js
+++ b/common/content/commands.js
@@ -8,6 +8,7 @@
// Do NOT create instances of this class yourself, use the helper method
// commands.add() instead
+
/**
* A class representing Ex commands. Instances are created by
* the {@link Commands} class.
@@ -31,135 +32,98 @@
* @optional
* @private
*/
-function Command(specs, description, action, extraInfo) //{{{
-{
- specs = Array.concat(specs);
-
- if (!extraInfo)
- extraInfo = {};
-
- // convert command name abbreviation specs of the form
- // 'shortname[optional-tail]' to short and long versions Eg. 'abc[def]' ->
- // 'abc', 'abcdef'
- function parseSpecs(specs)
- {
- // Whoever wrote the following should be ashamed. :(
- // Good grief! I have no words... -- djk ;-)
- // let shortNames = longNames = names = [];
- let names = [];
- let longNames = [];
- let shortNames = [];
+const Command = Class("Command", {
+ init: function (specs, description, action, extraInfo) {
+ specs = Array.concat(specs);
- for (let [, spec] in Iterator(specs))
- {
- let matches = spec.match(/(\w+)\[(\w+)\](\w*)/);
+ if (!extraInfo)
+ extraInfo = {};
- if (matches)
- {
- shortNames.push(matches[1] + matches[3]);
- longNames.push(matches[1] + matches[2] + matches[3]);
- // order as long1, short1, long2, short2
- names.push(matches[1] + matches[2]);
- names.push(matches[1]);
- }
- else
- {
- longNames.push(spec);
- names.push(spec);
- }
- }
-
- return { names: names, longNames: longNames, shortNames: shortNames };
- };
-
- let expandedSpecs = parseSpecs(specs);
- /**
- * @property {string[]} All of this command's name specs. e.g., "com[mand]"
- */
- this.specs = specs;
- /** @property {string[]} All of this command's short names, e.g., "com" */
- this.shortNames = expandedSpecs.shortNames;
- /**
- * @property {string[]} All of this command's long names, e.g., "command"
- */
- this.longNames = expandedSpecs.longNames;
-
- /** @property {string} The command's canonical name. */
- this.name = this.longNames[0];
- /** @property {string[]} All of this command's long and short names. */
- this.names = expandedSpecs.names; // return all command name aliases
-
- /** @property {string} This command's description, as shown in :exusage */
- this.description = description || "";
- /**
- * @property {function (Args)} The function called to execute this command.
- */
- this.action = action;
- /**
- * @property {string} This command's argument count spec.
- * @see Commands#parseArguments
- */
- this.argCount = extraInfo.argCount || 0;
- /**
- * @property {function (CompletionContext, Args)} This command's completer.
- * @see CompletionContext
- */
- this.completer = extraInfo.completer || null;
- /** @property {boolean} Whether this command accepts a here document. */
- this.hereDoc = extraInfo.hereDoc || false;
- /**
- * @property {Array} The options this command takes.
- * @see Commands@parseArguments
- */
- this.options = extraInfo.options || [];
- /**
- * @property {boolean} Whether this command may be called with a bang,
- * e.g., :com!
- */
- this.bang = extraInfo.bang || false;
- /**
- * @property {boolean} Whether this command may be called with a count,
- * e.g., :12bdel
- */
- this.count = extraInfo.count || false;
- /**
- * @property {boolean} At what index this command's literal arguments
- * begin. For instance, with a value of 2, all arguments starting with
- * the third are parsed as a single string, with all quoting characters
- * passed literally. This is especially useful for commands which take
- * key mappings or Ex command lines as arguments.
- */
- this.literal = extraInfo.literal == null ? null : extraInfo.literal;
- /**
- * @property {function} Should return an array of <b>Object</b>s suitable
- * to be passed to {@link Commands#commandToString}, one for each past
- * invocation which should be restored on subsequent @liberator
- * startups.
- */
- this.serial = extraInfo.serial;
- /**
- * @property {boolean} When true, invocations of this command
- * may contain private data which should be purged from
- * saved histories when clearing private data.
- */
- this.privateData = Boolean(extraInfo.privateData);
+ let expandedSpecs = Command.parseSpecs(specs);
+ /**
+ * @property {string[]} All of this command's name specs. e.g., "com[mand]"
+ */
+ this.specs = specs;
+ /** @property {string[]} All of this command's short names, e.g., "com" */
+ this.shortNames = expandedSpecs.shortNames;
+ /**
+ * @property {string[]} All of this command's long names, e.g., "command"
+ */
+ this.longNames = expandedSpecs.longNames;
- /**
- * @property {boolean} Specifies whether this is a user command. User
- * commands may be created by plugins, or directly by users, and,
- * unlike basic commands, may be overwritten. Users and plugin authors
- * should create only user commands.
- */
- this.user = extraInfo.user || false;
- /**
- * @property {string} For commands defined via :command, contains the Ex
- * command line to be executed upon invocation.
- */
- this.replacementText = extraInfo.replacementText || null;
-}
+ /** @property {string} The command's canonical name. */
+ this.name = this.longNames[0];
+ /** @property {string[]} All of this command's long and short names. */
+ this.names = expandedSpecs.names; // return all command name aliases
-Command.prototype = {
+ /** @property {string} This command's description, as shown in :exusage */
+ this.description = description || "";
+ /**
+ * @property {function (Args)} The function called to execute this command.
+ */
+ this.action = action;
+ /**
+ * @property {string} This command's argument count spec.
+ * @see Commands#parseArguments
+ */
+ this.argCount = extraInfo.argCount || 0;
+ /**
+ * @property {function (CompletionContext, Args)} This command's completer.
+ * @see CompletionContext
+ */
+ this.completer = extraInfo.completer || null;
+ /** @property {boolean} Whether this command accepts a here document. */
+ this.hereDoc = extraInfo.hereDoc || false;
+ /**
+ * @property {Array} The options this command takes.
+ * @see Commands@parseArguments
+ */
+ this.options = extraInfo.options || [];
+ /**
+ * @property {boolean} Whether this command may be called with a bang,
+ * e.g., :com!
+ */
+ this.bang = extraInfo.bang || false;
+ /**
+ * @property {boolean} Whether this command may be called with a count,
+ * e.g., :12bdel
+ */
+ this.count = extraInfo.count || false;
+ /**
+ * @property {boolean} At what index this command's literal arguments
+ * begin. For instance, with a value of 2, all arguments starting with
+ * the third are parsed as a single string, with all quoting characters
+ * passed literally. This is especially useful for commands which take
+ * key mappings or Ex command lines as arguments.
+ */
+ this.literal = extraInfo.literal == null ? null : extraInfo.literal;
+ /**
+ * @property {function} Should return an array of <b>Object</b>s suitable
+ * to be passed to {@link Commands#commandToString}, one for each past
+ * invocation which should be restored on subsequent @liberator
+ * startups.
+ */
+ this.serial = extraInfo.serial;
+ /**
+ * @property {boolean} When true, invocations of this command
+ * may contain private data which should be purged from
+ * saved histories when clearing private data.
+ */
+ this.privateData = Boolean(extraInfo.privateData);
+ /**
+ * @property {boolean} Specifies whether this is a user command. User
+ * commands may be created by plugins, or directly by users, and,
+ * unlike basic commands, may be overwritten. Users and plugin authors
+ * should create only user commands.
+ */
+ this.user = extraInfo.user || false;
+ /**
+ * @property {string} For commands defined via :command, contains the Ex
+ * command line to be executed upon invocation.
+ */
+ this.replacementText = extraInfo.replacementText || null;
+ },
/**
* Execute this command.
*
@@ -173,16 +137,14 @@ Command.prototype = {
* @deprecated
* @param {Object} modifiers Any modifiers to be passed to {@link #action}.
*/
- execute: function (args, bang, count, modifiers)
- {
+ execute: function (args, bang, count, modifiers) {
// XXX
bang = !!bang;
count = (count === undefined) ? -1 : count;
modifiers = modifiers || {};
let self = this;
- function exec(args)
- {
+ function exec(args) {
// FIXME: Move to parseCommand?
args = self.parseArgs(args);
if (!args)
@@ -192,11 +154,9 @@ Command.prototype = {
liberator.trapErrors(self.action, self, args, modifiers)
}
- if (this.hereDoc)
- {
+ if (this.hereDoc) {
let matches = args.match(/(.*)<<\s*(\S+)$/);
- if (matches && matches[2])
- {
+ if (matches && matches[2]) {
commandline.inputMultiline(RegExp("^" + matches[2] + "$", "m"),
function (args) { exec(matches[1] + "\n" + args); });
return;
@@ -212,10 +172,8 @@ Command.prototype = {
* @param {string} name The candidate name.
* @returns {boolean}
*/
- hasName: function (name)
- {
- for (let [, spec] in Iterator(this.specs))
- {
+ hasName: function (name) {
+ for (let [, spec] in Iterator(this.specs)) {
let fullName = spec.replace(/\[(\w+)]$/, "$1");
let index = spec.indexOf("[");
let min = index == -1 ? fullName.length : index;
@@ -241,735 +199,667 @@ Command.prototype = {
*/
parseArgs: function (args, complete, extra) commands.parseArgs(args, this.options, this.argCount, false, this.literal, complete, extra)
-}; //}}}
+}, {
-/**
- * @instance commands
- */
-function Commands() //{{{
-{
- ////////////////////////////////////////////////////////////////////////////////
- ////////////////////// PRIVATE SECTION /////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
+ /**
+ * convert command name abbreviation specs of the form
+ * 'shortname[optional-tail]' to short and long versions Eg. 'abc[def]' ->
+ * 'abc', 'abcdef'
+ */
+ parseSpecs: function (specs) {
+ // Whoever wrote the following should be ashamed. :(
+ // Good grief! I have no words... -- djk ;-)
+ // let shortNames = longNames = names = [];
+ let names = [];
+ let longNames = [];
+ let shortNames = [];
- var exCommands = [];
+ for (let [, spec] in Iterator(specs)) {
+ let matches = spec.match(/(\w+)\[(\w+)\](\w*)/);
- const QUOTE_STYLE = "vimperator";
+ if (matches) {
+ shortNames.push(matches[1] + matches[3]);
+ longNames.push(matches[1] + matches[2] + matches[3]);
+ // order as long1, short1, long2, short2
+ names.push(matches[1] + matches[2]);
+ names.push(matches[1]);
+ }
+ else {
+ longNames.push(spec);
+ names.push(spec);
+ }
+ }
- const quoteMap = {
- "\n": "n",
- "\t": "t"
- };
- function quote(q, list)
- {
- let re = RegExp("[" + list + "]", "g");
- return function (str) q + String.replace(str, re, function ($0) $0 in quoteMap ? quoteMap[$0] : ("\\" + $0)) + q;
- }
- const complQuote = { // FIXME
- '"': ['"', quote("", '\n\t"\\\\'), '"'],
- "'": ["'", quote("", "\\\\'"), "'"],
- "": ["", quote("", "\\\\ "), ""]
- };
- const quoteArg = {
- '"': quote('"', '\n\t"\\\\'),
- "'": quote("'", "\\\\'"),
- "": quote("", "\\\\ ")
- };
+ return { names: names, longNames: longNames, shortNames: shortNames };
+ },
+});
- function parseBool(arg)
- {
- if (/^(true|1|on)$/i.test(arg))
- return true;
- if (/^(false|0|off)$/i.test(arg))
- return false;
- return NaN;
- }
- const ArgType = new Struct("description", "parse");
- const argTypes = [
- null,
- ArgType("no arg", function (arg) !arg || null),
- ArgType("boolean", parseBool),
- ArgType("string", function (val) val),
- ArgType("int", parseInt),
- ArgType("float", parseFloat),
- ArgType("list", function (arg) arg && arg.split(/\s*,\s*/))
- ];
+/**
+ * @instance commands
+ */
+const ArgType = new Struct("description", "parse");
+const Commands = Module("commands", {
+ init: function () {
+ this._exCommands = [];
+ },
- // returns [count, parsed_argument]
- function parseArg(str)
- {
- let arg = "";
- let quote = null;
- let len = str.length;
+ // FIXME: remove later, when our option handler is better
+ /**
+ * @property {number} The option argument is unspecified. Any argument
+ * is accepted and caller is responsible for parsing the return
+ * value.
+ * @final
+ */
+ OPTION_ANY: 0,
- while (str.length && !/^\s/.test(str))
- {
- let res;
+ /**
+ * @property {number} The option doesn't accept an argument.
+ * @final
+ */
+ OPTION_NOARG: 1,
+ /**
+ * @property {number} The option accepts a boolean argument.
+ * @final
+ */
+ OPTION_BOOL: 2,
+ /**
+ * @property {number} The option accepts a string argument.
+ * @final
+ */
+ OPTION_STRING: 3,
+ /**
+ * @property {number} The option accepts an integer argument.
+ * @final
+ */
+ OPTION_INT: 4,
+ /**
+ * @property {number} The option accepts a float argument.
+ * @final
+ */
+ OPTION_FLOAT: 5,
+ /**
+ * @property {number} The option accepts a string list argument.
+ * E.g. "foo,bar"
+ * @final
+ */
+ OPTION_LIST: 6,
- switch (QUOTE_STYLE)
- {
- case "vim-sucks":
- if (res = str.match = str.match(/^()((?:[^\\\s]|\\.)+)((?:\\$)?)/))
- arg += res[2].replace(/\\(.)/g, "$1");
- break;
-
- case "vimperator":
- if (res = str.match(/^()((?:[^\\\s"']|\\.)+)((?:\\$)?)/))
- arg += res[2].replace(/\\(.)/g, "$1");
- else if (res = str.match(/^(")((?:[^\\"]|\\.)*)("?)/))
- arg += eval(res[0] + (res[3] ? "" : '"'));
- else if (res = str.match(/^(')((?:[^\\']|\\.)*)('?)/))
- arg += res[2].replace(/\\(.)/g, function (n0, n1) /[\\']/.test(n1) ? n1 : n0);
- break;
-
- case "rc-ish":
- if (res = str.match = str.match(/^()((?:[^\\\s"']|\\.)+)((?:\\$)?)/))
- arg += res[2].replace(/\\(.)/g, "$1");
- else if (res = str.match(/^(")((?:[^\\"]|\\.)*)("?)/))
- arg += eval(res[0] + (res[3] ? "" : '"'));
- else if (res = str.match(/^(')((?:[^']|'')*)('?)/))
- arg += res[2].replace("''", "'", "g");
- break;
-
- case "pythonesque":
- if (res = str.match = str.match(/^()((?:[^\\\s"']|\\.)+)((?:\\$)?)/))
- arg += res[2].replace(/\\(.)/g, "$1");
- else if (res = str.match(/^(""")((?:.?.?[^"])*)((?:""")?)/))
- arg += res[2];
- else if (res = str.match(/^(")((?:[^\\"]|\\.)*)("?)/))
- arg += eval(res[0] + (res[3] ? "" : '"'));
- else if (res = str.match(/^(')((?:[^\\']|\\.)*)('?)/))
- arg += res[2].replace(/\\(.)/g, function (n0, n1) /[\\']/.test(n1) ? n1 : n0);
- break;
- }
+ /**
+ * @property {number} Indicates that no count was specified for this
+ * command invocation.
+ * @final
+ */
+ COUNT_NONE: -1,
+ /**
+ * @property {number} Indicates that the full buffer range (1,$) was
+ * specified for this command invocation.
+ * @final
+ */
+ // FIXME: this isn't a count at all
+ COUNT_ALL: -2, // :%...
- if (!res)
- break;
- if (!res[3])
- quote = res[1];
- if (!res[1])
- quote = res[3];
- str = str.substr(res[0].length);
- }
+ /** @property {Iterator(Command)} @private */
+ __iterator__: function () {
+ let sorted = this._exCommands.sort(function (a, b) a.name > b.name);
+ return util.Array.itervalues(sorted);
+ },
- return [len - str.length, arg, quote];
- }
+ /** @property {string} The last executed Ex command line. */
+ repeat: null,
- function addCommand(command, replace)
- {
- if (exCommands.some(function (c) c.hasName(command.name)))
- {
+ _addCommand: function (command, replace) {
+ if (this._exCommands.some(function (c) c.hasName(command.name))) {
if (command.user && replace)
commands.removeUserCommand(command.name);
- else
- {
+ else {
liberator.log("Warning: :" + command.name + " already exists, NOT replacing existing command.", 1);
return false;
}
}
- exCommands.push(command);
+ this._exCommands.push(command);
return true;
- }
+ },
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// PUBLIC SECTION //////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
+ /**
+ * Adds a new default command.
+ *
+ * @param {string[]} names The names by which this command can be
+ * invoked. The first name specified is the command's canonical
+ * name.
+ * @param {string} description A description of the command.
+ * @param {function} action The action invoked by this command.
+ * @param {Object} extra An optional extra configuration hash.
+ * @optional
+ */
+ add: function (names, description, action, extra) {
+ return this._addCommand(new Command(names, description, action, extra), false);
+ },
- const self = {
+ /**
+ * Adds a new user-defined command.
+ *
+ * @param {string[]} names The names by which this command can be
+ * invoked. The first name specified is the command's canonical
+ * name.
+ * @param {string} description A description of the command.
+ * @param {function} action The action invoked by this command.
+ * @param {Object} extra An optional extra configuration hash.
+ * @param {boolean} replace Overwrite an existing command with the same
+ * canonical name.
+ */
+ addUserCommand: function (names, description, action, extra, replace) {
+ extra = extra || {};
+ extra.user = true;
+ description = description || "User defined command";
- // FIXME: remove later, when our option handler is better
- /**
- * @property {number} The option argument is unspecified. Any argument
- * is accepted and caller is responsible for parsing the return
- * value.
- * @final
- */
- OPTION_ANY: 0,
+ return this._addCommand(new Command(names, description, action, extra), replace);
+ },
- /**
- * @property {number} The option doesn't accept an argument.
- * @final
- */
- OPTION_NOARG: 1,
- /**
- * @property {number} The option accepts a boolean argument.
- * @final
- */
- OPTION_BOOL: 2,
- /**
- * @property {number} The option accepts a string argument.
- * @final
- */
- OPTION_STRING: 3,
- /**
- * @property {number} The option accepts an integer argument.
- * @final
- */
- OPTION_INT: 4,
- /**
- * @property {number} The option accepts a float argument.
- * @final
- */
- OPTION_FLOAT: 5,
- /**
- * @property {number} The option accepts a string list argument.
- * E.g. "foo,bar"
- * @final
- */
- OPTION_LIST: 6,
+ /**
+ * Returns the specified command invocation object serialized to
+ * an executable Ex command string.
+ *
+ * @param {Object} args The command invocation object.
+ * @returns {string}
+ */
+ commandToString: function (args) {
+ let res = [args.command + (args.bang ? "!" : "")];
+ function quote(str) Commands.quoteArg[/[\s"'\\]|^$/.test(str) ? '"' : ""](str);
+
+ for (let [opt, val] in Iterator(args.options || {})) {
+ let char = /^-.$/.test(opt) ? " " : "=";
+ if (val != null)
+ opt += char + quote(val)
+ res.push(opt);
+ }
+ for (let [, arg] in Iterator(args.arguments || []))
+ res.push(quote(arg));
- /**
- * @property {number} Indicates that no count was specified for this
- * command invocation.
- * @final
- */
- COUNT_NONE: -1,
- /**
- * @property {number} Indicates that the full buffer range (1,$) was
- * specified for this command invocation.
- * @final
- */
- // FIXME: this isn't a count at all
- COUNT_ALL: -2, // :%...
+ let str = args.literalArg;
+ if (str)
+ res.push(/\n/.test(str) ? "<<EOF\n" + str.replace(/\n$/, "") + "\nEOF" : str);
+ return res.join(" ");
+ },
- /** @property {Iterator(Command)} @private */
- __iterator__: function ()
- {
- let sorted = exCommands.sort(function (a, b) a.name > b.name);
- return util.Array.itervalues(sorted);
- },
+ /**
+ * Returns the command with matching <b>name</b>.
+ *
+ * @param {string} name The name of the command to return. This can be
+ * any of the command's names.
+ * @returns {Command}
+ */
+ get: function (name) {
+ return this._exCommands.filter(function (cmd) cmd.hasName(name))[0] || null;
+ },
- /** @property {string} The last executed Ex command line. */
- repeat: null,
+ /**
+ * Returns the user-defined command with matching <b>name</b>.
+ *
+ * @param {string} name The name of the command to return. This can be
+ * any of the command's names.
+ * @returns {Command}
+ */
+ getUserCommand: function (name) {
+ return this._exCommands.filter(function (cmd) cmd.user && cmd.hasName(name))[0] || null;
+ },
- /**
- * Adds a new default command.
- *
- * @param {string[]} names The names by which this command can be
- * invoked. The first name specified is the command's canonical
- * name.
- * @param {string} description A description of the command.
- * @param {function} action The action invoked by this command.
- * @param {Object} extra An optional extra configuration hash.
- * @optional
- */
- add: function (names, description, action, extra)
- {
- return addCommand(new Command(names, description, action, extra), false);
- },
+ /**
+ * Returns all user-defined commands.
+ *
+ * @returns {Command[]}
+ */
+ getUserCommands: function () {
+ return this._exCommands.filter(function (cmd) cmd.user);
+ },
- /**
- * Adds a new user-defined command.
- *
- * @param {string[]} names The names by which this command can be
- * invoked. The first name specified is the command's canonical
- * name.
- * @param {string} description A description of the command.
- * @param {function} action The action invoked by this command.
- * @param {Object} extra An optional extra configuration hash.
- * @param {boolean} replace Overwrite an existing command with the same
- * canonical name.
- */
- addUserCommand: function (names, description, action, extra, replace)
- {
- extra = extra || {};
- extra.user = true;
- description = description || "User defined command";
+ // TODO: should it handle comments?
+ // : it might be nice to be able to specify that certain quoting
+ // should be disabled E.g. backslash without having to resort to
+ // using literal etc.
+ // : error messages should be configurable or else we can ditch
+ // Vim compatibility but it actually gives useful messages
+ // sometimes rather than just "Invalid arg"
+ // : I'm not sure documenting the returned object here, and
+ // elsewhere, as type Args rather than simply Object makes sense,
+ // especially since it is further augmented for use in
+ // Command#action etc.
+ /**
+ * Parses <b>str</b> for options and plain arguments.
+ *
+ * The returned <b>Args</b> object is an augmented array of arguments.
+ * Any key/value pairs of <b>extra</b> will be available and the
+ * following additional properties:
+ * -opt - the value of the option -opt if specified
+ * string - the original argument string <b>str</b>
+ * literalArg - any trailing literal argument
+ *
+ * Quoting rules:
+ * '-quoted strings - only ' and \ itself are escaped
+ * "-quoted strings - also ", \n and \t are translated
+ * non-quoted strings - everything is taken literally apart from "\
+ * " and "\\"
+ *
+ * @param {string} str The Ex command-line string to parse. E.g.
+ * "-x=foo -opt=bar arg1 arg2"
+ * @param {Array} options The options accepted. These are specified as
+ * an array [names, type, validator, completions, multiple].
+ * names - an array of option names. The first name is the
+ * canonical option name.
+ * type - the option's value type. This is one of:
+ * (@link Commands#OPTION_NOARG),
+ * (@link Commands#OPTION_STRING),
+ * (@link Commands#OPTION_BOOL),
+ * (@link Commands#OPTION_INT),
+ * (@link Commands#OPTION_FLOAT),
+ * (@link Commands#OPTION_LIST),
+ * (@link Commands#OPTION_ANY)
+ * validator - a validator function
+ * completer - a list of completions, or a completion function
+ * multiple - whether this option can be specified multiple times
+ * E.g.
+ * options = [[["-force"], OPTION_NOARG],
+ * [["-fullscreen", "-f"], OPTION_BOOL],
+ * [["-language"], OPTION_STRING, validateFunc, ["perl", "ruby"]],
+ * [["-speed"], OPTION_INT],
+ * [["-acceleration"], OPTION_FLOAT],
+ * [["-accessories"], OPTION_LIST, null, ["foo", "bar"], true],
+ * [["-other"], OPTION_ANY]];
+ * @param {string} argCount The number of arguments accepted.
+ * "0": no arguments
+ * "1": exactly one argument
+ * "+": one or more arguments
+ * "*": zero or more arguments (default if unspecified)
+ * "?": zero or one arguments
+ * @param {boolean} allowUnknownOptions Whether unspecified options
+ * should cause an error.
+ * @param {number} literal The index at which any literal arg begins.
+ * See {@link Command#literal}.
+ * @param {CompletionContext} complete The relevant completion context
+ * when the args are being parsed for completion.
+ * @param {Object} extra Extra keys to be spliced into the returned
+ * Args object.
+ * @returns {Args}
+ */
+ parseArgs: function (str, options, argCount, allowUnknownOptions, literal, complete, extra) {
+ function getNextArg(str) {
+ let [count, arg, quote] = Commands.parseArg(str);
+ if (quote == "\\" && !complete)
+ return [,,,"Trailing \\"];
+ if (quote && !complete)
+ return [,,,"E114: Missing quote: " + quote];
+ return [count, arg, quote];
+ }
- return addCommand(new Command(names, description, action, extra), replace);
- },
+ if (!options)
+ options = [];
- /**
- * Returns the specified command invocation object serialized to
- * an executable Ex command string.
- *
- * @param {Object} args The command invocation object.
- * @returns {string}
- */
- commandToString: function (args)
- {
- let res = [args.command + (args.bang ? "!" : "")];
- function quote(str) quoteArg[/[\s"'\\]|^$/.test(str) ? '"' : ""](str);
-
- for (let [opt, val] in Iterator(args.options || {}))
- {
- let char = /^-.$/.test(opt) ? " " : "=";
- if (val != null)
- opt += char + quote(val)
- res.push(opt);
- }
- for (let [, arg] in Iterator(args.arguments || []))
- res.push(quote(arg));
+ if (!argCount)
+ argCount = "*";
- let str = args.literalArg;
- if (str)
- res.push(/\n/.test(str) ? "<<EOF\n" + str.replace(/\n$/, "") + "\nEOF" : str);
- return res.join(" ");
- },
+ var args = []; // parsed options
+ args.__iterator__ = function () util.Array.iteritems(this);
+ args.string = str; // for access to the unparsed string
+ args.literalArg = "";
- /**
- * Returns the command with matching <b>name</b>.
- *
- * @param {string} name The name of the command to return. This can be
- * any of the command's names.
- * @returns {Command}
- */
- get: function (name)
- {
- return exCommands.filter(function (cmd) cmd.hasName(name))[0] || null;
- },
+ // FIXME!
+ for (let [k, v] in Iterator(extra || []))
+ args[k] = v;
- /**
- * Returns the user-defined command with matching <b>name</b>.
- *
- * @param {string} name The name of the command to return. This can be
- * any of the command's names.
- * @returns {Command}
- */
- getUserCommand: function (name)
- {
- return exCommands.filter(function (cmd) cmd.user && cmd.hasName(name))[0] || null;
- },
+ var invalid = false;
+ // FIXME: best way to specify these requirements?
+ var onlyArgumentsRemaining = allowUnknownOptions || options.length == 0 || false; // after a -- has been found
+ var arg = null;
+ var count = 0; // the length of the argument
+ var i = 0;
+ var completeOpts;
- /**
- * Returns all user-defined commands.
- *
- * @returns {Command[]}
- */
- getUserCommands: function ()
- {
- return exCommands.filter(function (cmd) cmd.user);
- },
-
- // TODO: should it handle comments?
- // : it might be nice to be able to specify that certain quoting
- // should be disabled E.g. backslash without having to resort to
- // using literal etc.
- // : error messages should be configurable or else we can ditch
- // Vim compatibility but it actually gives useful messages
- // sometimes rather than just "Invalid arg"
- // : I'm not sure documenting the returned object here, and
- // elsewhere, as type Args rather than simply Object makes sense,
- // especially since it is further augmented for use in
- // Command#action etc.
- /**
- * Parses <b>str</b> for options and plain arguments.
- *
- * The returned <b>Args</b> object is an augmented array of arguments.
- * Any key/value pairs of <b>extra</b> will be available and the
- * following additional properties:
- * -opt - the value of the option -opt if specified
- * string - the original argument string <b>str</b>
- * literalArg - any trailing literal argument
- *
- * Quoting rules:
- * '-quoted strings - only ' and \ itself are escaped
- * "-quoted strings - also ", \n and \t are translated
- * non-quoted strings - everything is taken literally apart from "\
- * " and "\\"
- *
- * @param {string} str The Ex command-line string to parse. E.g.
- * "-x=foo -opt=bar arg1 arg2"
- * @param {Array} options The options accepted. These are specified as
- * an array [names, type, validator, completions, multiple].
- * names - an array of option names. The first name is the
- * canonical option name.
- * type - the option's value type. This is one of:
- * (@link Commands#OPTION_NOARG),
- * (@link Commands#OPTION_STRING),
- * (@link Commands#OPTION_BOOL),
- * (@link Commands#OPTION_INT),
- * (@link Commands#OPTION_FLOAT),
- * (@link Commands#OPTION_LIST),
- * (@link Commands#OPTION_ANY)
- * validator - a validator function
- * completer - a list of completions, or a completion function
- * multiple - whether this option can be specified multiple times
- * E.g.
- * options = [[["-force"], OPTION_NOARG],
- * [["-fullscreen", "-f"], OPTION_BOOL],
- * [["-language"], OPTION_STRING, validateFunc, ["perl", "ruby"]],
- * [["-speed"], OPTION_INT],
- * [["-acceleration"], OPTION_FLOAT],
- * [["-accessories"], OPTION_LIST, null, ["foo", "bar"], true],
- * [["-other"], OPTION_ANY]];
- * @param {string} argCount The number of arguments accepted.
- * "0": no arguments
- * "1": exactly one argument
- * "+": one or more arguments
- * "*": zero or more arguments (default if unspecified)
- * "?": zero or one arguments
- * @param {boolean} allowUnknownOptions Whether unspecified options
- * should cause an error.
- * @param {number} literal The index at which any literal arg begins.
- * See {@link Command#literal}.
- * @param {CompletionContext} complete The relevant completion context
- * when the args are being parsed for completion.
- * @param {Object} extra Extra keys to be spliced into the returned
- * Args object.
- * @returns {Args}
- */
- parseArgs: function (str, options, argCount, allowUnknownOptions, literal, complete, extra)
- {
- function getNextArg(str)
- {
- let [count, arg, quote] = parseArg(str);
- if (quote == "\\" && !complete)
- return [,,,"Trailing \\"];
- if (quote && !complete)
- return [,,,"E114: Missing quote: " + quote];
- return [count, arg, quote];
- }
+ // XXX
+ function matchOpts(arg) {
+ // Push possible option matches into completions
+ if (complete && !onlyArgumentsRemaining)
+ completeOpts = [[opt[0], opt[0][0]] for ([i, opt] in Iterator(options)) if (!(opt[0][0] in args))];
+ }
+ function resetCompletions() {
+ completeOpts = null;
+ args.completeArg = null;
+ args.completeOpt = null;
+ args.completeFilter = null;
+ args.completeStart = i;
+ args.quote = Commands.complQuote[""];
+ }
+ if (complete) {
+ resetCompletions();
+ matchOpts("");
+ args.completeArg = 0;
+ }
+
+ function echoerr(error) {
+ if (complete)
+ complete.message = error;
+ else
+ liberator.echoerr(error);
+ }
+
+ outer:
+ while (i < str.length || complete) {
+ // skip whitespace
+ while (/\s/.test(str[i]) && i < str.length)
+ i++;
+ if (i == str.length && !complete)
+ break;
- if (!options)
- options = [];
-
- if (!argCount)
- argCount = "*";
-
- var args = []; // parsed options
- args.__iterator__ = function () util.Array.iteritems(this);
- args.string = str; // for access to the unparsed string
- args.literalArg = "";
-
- // FIXME!
- for (let [k, v] in Iterator(extra || []))
- args[k] = v;
-
- var invalid = false;
- // FIXME: best way to specify these requirements?
- var onlyArgumentsRemaining = allowUnknownOptions || options.length == 0 || false; // after a -- has been found
- var arg = null;
- var count = 0; // the length of the argument
- var i = 0;
- var completeOpts;
-
- // XXX
- function matchOpts(arg)
- {
- // Push possible option matches into completions
- if (complete && !onlyArgumentsRemaining)
- completeOpts = [[opt[0], opt[0][0]] for ([i, opt] in Iterator(options)) if (!(opt[0][0] in args))];
- }
- function resetCompletions()
- {
- completeOpts = null;
- args.completeArg = null;
- args.completeOpt = null;
- args.completeFilter = null;
- args.completeStart = i;
- args.quote = complQuote[""];
- }
if (complete)
- {
resetCompletions();
- matchOpts("");
- args.completeArg = 0;
- }
- function echoerr(error)
- {
- if (complete)
- complete.message = error;
- else
- liberator.echoerr(error);
+ var sub = str.substr(i);
+ if ((!onlyArgumentsRemaining) && /^--(\s|$)/.test(sub)) {
+ onlyArgumentsRemaining = true;
+ i += 2;
+ continue;
}
- outer:
- while (i < str.length || complete)
- {
- // skip whitespace
- while (/\s/.test(str[i]) && i < str.length)
- i++;
- if (i == str.length && !complete)
- break;
+ var optname = "";
+ if (!onlyArgumentsRemaining) {
+ for (let [, opt] in Iterator(options)) {
+ for (let [, optname] in Iterator(opt[0])) {
+ if (sub.indexOf(optname) == 0) {
+ invalid = false;
+ arg = null;
+ quote = null;
+ count = 0;
+ let sep = sub[optname.length];
+ if (sep == "=" || /\s/.test(sep) && opt[1] != this.OPTION_NOARG) {
+ [count, arg, quote, error] = getNextArg(sub.substr(optname.length + 1));
+ liberator.assert(!error, error);
+
+ // if we add the argument to an option after a space, it MUST not be empty
+ if (sep != "=" && !quote && arg.length == 0)
+ arg = null;
+
+ count++; // to compensate the "=" character
+ }
+ else if (!/\s/.test(sep) && sep != undefined) // this isn't really an option as it has trailing characters, parse it as an argument
+ invalid = true;
- if (complete)
- resetCompletions();
-
- var sub = str.substr(i);
- if ((!onlyArgumentsRemaining) && /^--(\s|$)/.test(sub))
- {
- onlyArgumentsRemaining = true;
- i += 2;
- continue;
- }
+ let context = null;
+ if (!complete && quote) {
+ liberator.echoerr("Invalid argument for option " + optname);
+ return null;
+ }
- var optname = "";
- if (!onlyArgumentsRemaining)
- {
- for (let [, opt] in Iterator(options))
- {
- for (let [, optname] in Iterator(opt[0]))
- {
- if (sub.indexOf(optname) == 0)
- {
- invalid = false;
- arg = null;
- quote = null;
- count = 0;
- let sep = sub[optname.length];
- if (sep == "=" || /\s/.test(sep) && opt[1] != this.OPTION_NOARG)
- {
- [count, arg, quote, error] = getNextArg(sub.substr(optname.length + 1));
- liberator.assert(!error, error);
-
- // if we add the argument to an option after a space, it MUST not be empty
- if (sep != "=" && !quote && arg.length == 0)
- arg = null;
-
- count++; // to compensate the "=" character
- }
- else if (!/\s/.test(sep) && sep != undefined) // this isn't really an option as it has trailing characters, parse it as an argument
- invalid = true;
-
- let context = null;
- if (!complete && quote)
- {
- liberator.echoerr("Invalid argument for option " + optname);
- return null;
+ if (!invalid) {
+ if (complete && count > 0) {
+ args.completeStart += optname.length + 1;
+ args.completeOpt = opt;
+ args.completeFilter = arg;
+ args.quote = Commands.complQuote[quote] || Commands.complQuote[""];
}
-
- if (!invalid)
- {
- if (complete && count > 0)
- {
- args.completeStart += optname.length + 1;
- args.completeOpt = opt;
- args.completeFilter = arg;
- args.quote = complQuote[quote] || complQuote[""];
- }
- let type = argTypes[opt[1]];
- if (type && (!complete || arg != null))
- {
- let orig = arg;
- arg = type.parse(arg);
- if (arg == null || (typeof arg == "number" && isNaN(arg)))
- {
- if (!complete || orig != "" || args.completeStart != str.length)
- echoerr("Invalid argument for " + type.description + " option: " + optname);
- if (complete)
- complete.highlight(args.completeStart, count - 1, "SPELLCHECK");
- else
- return null;
- }
+ let type = Commands.argTypes[opt[1]];
+ if (type && (!complete || arg != null)) {
+ let orig = arg;
+ arg = type.parse(arg);
+ if (arg == null || (typeof arg == "number" && isNaN(arg))) {
+ if (!complete || orig != "" || args.completeStart != str.length)
+ echoerr("Invalid argument for " + type.description + " option: " + optname);
+ if (complete)
+ complete.highlight(args.completeStart, count - 1, "SPELLCHECK");
+ else
+ return null;
}
+ }
- // we have a validator function
- if (typeof opt[2] == "function")
- {
- if (opt[2].call(this, arg) == false)
- {
- echoerr("Invalid argument for option: " + optname);
- if (complete)
- complete.highlight(args.completeStart, count - 1, "SPELLCHECK");
- else
- return null;
- }
+ // we have a validator function
+ if (typeof opt[2] == "function") {
+ if (opt[2].call(this, arg) == false) {
+ echoerr("Invalid argument for option: " + optname);
+ if (complete)
+ complete.highlight(args.completeStart, count - 1, "SPELLCHECK");
+ else
+ return null;
}
+ }
- // option allowed multiple times
- if (!!opt[4])
- args[opt[0][0]] = (args[opt[0][0]] || []).concat(arg);
- else
- args[opt[0][0]] = opt[1] == this.OPTION_NOARG || arg;
+ // option allowed multiple times
+ if (!!opt[4])
+ args[opt[0][0]] = (args[opt[0][0]] || []).concat(arg);
+ else
+ args[opt[0][0]] = opt[1] == this.OPTION_NOARG || arg;
- i += optname.length + count;
- if (i == str.length)
- break outer;
- continue outer;
- }
- // if it is invalid, just fall through and try the next argument
+ i += optname.length + count;
+ if (i == str.length)
+ break outer;
+ continue outer;
}
+ // if it is invalid, just fall through and try the next argument
}
}
}
+ }
- matchOpts(sub);
-
- if (complete)
- {
- if (argCount == "0" || args.length > 0 && (/[1?]/.test(argCount)))
- complete.highlight(i, sub.length, "SPELLCHECK");
- }
-
- if (args.length == literal)
- {
- if (complete)
- args.completeArg = args.length;
- args.literalArg = sub;
- args.push(sub);
- args.quote = null;
- break;
- }
+ matchOpts(sub);
- // if not an option, treat this token as an argument
- let [count, arg, quote, error] = getNextArg(sub);
- liberator.assert(!error, error);
+ if (complete) {
+ if (argCount == "0" || args.length > 0 && (/[1?]/.test(argCount)))
+ complete.highlight(i, sub.length, "SPELLCHECK");
+ }
+ if (args.length == literal) {
if (complete)
- {
- args.quote = complQuote[quote] || complQuote[""];
- args.completeFilter = arg || "";
- }
- else if (count == -1)
- {
- liberator.echoerr("Error parsing arguments: " + arg);
- return null;
- }
- else if (!onlyArgumentsRemaining && /^-/.test(arg))
- {
- liberator.echoerr("Invalid option: " + arg);
- return null;
- }
+ args.completeArg = args.length;
+ args.literalArg = sub;
+ args.push(sub);
+ args.quote = null;
+ break;
+ }
- if (arg != null)
- args.push(arg);
- if (complete)
- args.completeArg = args.length - 1;
+ // if not an option, treat this token as an argument
+ let [count, arg, quote, error] = getNextArg(sub);
+ liberator.assert(!error, error);
- i += count;
- if (count <= 0 || i == str.length)
- break;
+ if (complete) {
+ args.quote = Commands.complQuote[quote] || Commands.complQuote[""];
+ args.completeFilter = arg || "";
+ }
+ else if (count == -1) {
+ liberator.echoerr("Error parsing arguments: " + arg);
+ return null;
+ }
+ else if (!onlyArgumentsRemaining && /^-/.test(arg)) {
+ liberator.echoerr("Invalid option: " + arg);
+ return null;
}
+ if (arg != null)
+ args.push(arg);
if (complete)
- {
- if (args.completeOpt)
- {
- let opt = args.completeOpt;
- let context = complete.fork(opt[0][0], args.completeStart);
- context.filter = args.completeFilter;
- if (typeof opt[3] == "function")
- var compl = opt[3](context, args);
- else
- compl = opt[3] || [];
- context.title = [opt[0][0]];
- context.quote = args.quote;
- context.completions = compl;
- }
- complete.advance(args.completeStart);
- complete.title = ["Options"];
- if (completeOpts)
- complete.completions = completeOpts;
- }
+ args.completeArg = args.length - 1;
- // check for correct number of arguments
- if (args.length == 0 && /^[1+]$/.test(argCount) ||
- literal != null && /[1+]/.test(argCount) && !/\S/.test(args.literalArg || ""))
- {
- if (!complete)
- {
- liberator.echoerr("E471: Argument required");
- return null;
- }
+ i += count;
+ if (count <= 0 || i == str.length)
+ break;
+ }
+
+ if (complete) {
+ if (args.completeOpt) {
+ let opt = args.completeOpt;
+ let context = complete.fork(opt[0][0], args.completeStart);
+ context.filter = args.completeFilter;
+ if (typeof opt[3] == "function")
+ var compl = opt[3](context, args);
+ else
+ compl = opt[3] || [];
+ context.title = [opt[0][0]];
+ context.quote = args.quote;
+ context.completions = compl;
}
- else if (args.length == 1 && (argCount == "0") ||
- args.length > 1 && /^[01?]$/.test(argCount))
- {
- echoerr("E488: Trailing characters");
+ complete.advance(args.completeStart);
+ complete.title = ["Options"];
+ if (completeOpts)
+ complete.completions = completeOpts;
+ }
+
+ // check for correct number of arguments
+ if (args.length == 0 && /^[1+]$/.test(argCount) ||
+ literal != null && /[1+]/.test(argCount) && !/\S/.test(args.literalArg || "")) {
+ if (!complete) {
+ liberator.echoerr("E471: Argument required");
return null;
}
+ }
+ else if (args.length == 1 && (argCount == "0") ||
+ args.length > 1 && /^[01?]$/.test(argCount)) {
+ echoerr("E488: Trailing characters");
+ return null;
+ }
- return args;
- },
+ return args;
+ },
- /**
- * Parses a complete Ex command.
- *
- * The parsed string is returned as an Array like
- * [count, command, bang, args]:
- * count - any count specified
- * command - the Ex command name
- * bang - whether the special "bang" version was called
- * args - the commands full argument string
- * E.g. ":2foo! bar" -> [2, "foo", true, "bar"]
- *
- * @param {string} str The Ex command line string.
- * @returns {Array}
- */
- // FIXME: why does this return an Array rather than Object?
- parseCommand: function (str)
- {
- // remove comments
- str.replace(/\s*".*$/, "");
-
- // 0 - count, 1 - cmd, 2 - special, 3 - args
- let matches = str.match(/^[:\s]*(\d+|%)?([a-zA-Z]+|!)(!)?(?:\s*(.*?))?$/);
- //var matches = str.match(/^:*(\d+|%)?([a-zA-Z]+|!)(!)?(?:\s*(.*?)\s*)?$/);
- if (!matches)
- return [null, null, null, null];
-
- let [, count, cmd, special, args] = matches;
-
- // parse count
- if (count)
- count = count == "%" ? this.COUNT_ALL: parseInt(count, 10);
- else
- count = this.COUNT_NONE;
+ /**
+ * Parses a complete Ex command.
+ *
+ * The parsed string is returned as an Array like
+ * [count, command, bang, args]:
+ * count - any count specified
+ * command - the Ex command name
+ * bang - whether the special "bang" version was called
+ * args - the commands full argument string
+ * E.g. ":2foo! bar" -> [2, "foo", true, "bar"]
+ *
+ * @param {string} str The Ex command line string.
+ * @returns {Array}
+ */
+ // FIXME: why does this return an Array rather than Object?
+ parseCommand: function (str) {
+ // remove comments
+ str.replace(/\s*".*$/, "");
+
+ // 0 - count, 1 - cmd, 2 - special, 3 - args
+ let matches = str.match(/^[:\s]*(\d+|%)?([a-zA-Z]+|!)(!)?(?:\s*(.*?))?$/);
+ //var matches = str.match(/^:*(\d+|%)?([a-zA-Z]+|!)(!)?(?:\s*(.*?)\s*)?$/);
+ if (!matches)
+ return [null, null, null, null];
+
+ let [, count, cmd, special, args] = matches;
+
+ // parse count
+ if (count)
+ count = count == "%" ? this.COUNT_ALL: parseInt(count, 10);
+ else
+ count = this.COUNT_NONE;
+
+ return [count, cmd, !!special, args || ""];
+ },
- return [count, cmd, !!special, args || ""];
- },
+ /** @property */
+ get complQuote() Commands.complQuote,
- /** @property */
- get complQuote() complQuote,
+ /** @property */
+ get quoteArg() Commands.quoteArg, // XXX: better somewhere else?
- /** @property */
- get quoteArg() quoteArg, // XXX: better somewhere else?
+ /**
+ * Remove the user-defined command with matching <b>name</b>.
+ *
+ * @param {string} name The name of the command to remove. This can be
+ * any of the command's names.
+ */
+ removeUserCommand: function (name) {
+ this._exCommands = this._exCommands.filter(function (cmd) !(cmd.user && cmd.hasName(name)));
+ },
- /**
- * Remove the user-defined command with matching <b>name</b>.
- *
- * @param {string} name The name of the command to remove. This can be
- * any of the command's names.
- */
- removeUserCommand: function (name)
- {
- exCommands = exCommands.filter(function (cmd) !(cmd.user && cmd.hasName(name)));
- },
+ // FIXME: still belong here? Also used for autocommand parameters.
+ /**
+ * Returns a string with all tokens in <b>string</b> matching "<key>"
+ * replaced with "value". Where "key" is a property of the specified
+ * <b>tokens</b> object and "value" is the corresponding value. The
+ * <lt> token can be used to include a literal "<" in the returned
+ * string. Any tokens prefixed with "q-" will be quoted except for
+ * <q-lt> which is treated like <lt>.
+ *
+ * @param {string} str The string with tokens to replace.
+ * @param {Object} tokens A map object whose keys are replaced with its
+ * values.
+ * @returns {string}
+ */
+ replaceTokens: function replaceTokens(str, tokens) {
+ return str.replace(/<((?:q-)?)([a-zA-Z]+)?>/g, function (match, quote, token) {
+ if (token == "lt") // Don't quote, as in Vim (but, why so in Vim? You'd think people wouldn't say <q-lt> if they didn't want it)
+ return "<";
+ let res = tokens[token];
+ if (res == undefined) // Ignore anything undefined
+ res = "<" + token + ">";
+ if (quote && typeof res != "number")
+ return Commands.quoteArg['"'](res);
+ return res;
+ });
+ }
+}, {
+ QUOTE_STYLE: "rc-ish",
- // FIXME: still belong here? Also used for autocommand parameters.
- /**
- * Returns a string with all tokens in <b>string</b> matching "<key>"
- * replaced with "value". Where "key" is a property of the specified
- * <b>tokens</b> object and "value" is the corresponding value. The
- * <lt> token can be used to include a literal "<" in the returned
- * string. Any tokens prefixed with "q-" will be quoted except for
- * <q-lt> which is treated like <lt>.
- *
- * @param {string} str The string with tokens to replace.
- * @param {Object} tokens A map object whose keys are replaced with its
- * values.
- * @returns {string}
- */
- replaceTokens: function replaceTokens(str, tokens)
- {
- return str.replace(/<((?:q-)?)([a-zA-Z]+)?>/g, function (match, quote, token) {
- if (token == "lt") // Don't quote, as in Vim (but, why so in Vim? You'd think people wouldn't say <q-lt> if they didn't want it)
- return "<";
- let res = tokens[token];
- if (res == undefined) // Ignore anything undefined
- res = "<" + token + ">";
- if (quote && typeof res != "number")
- return quoteArg['"'](res);
- return res;
- });
- }
- };
+ // returns [count, parsed_argument]
+ parseArg: function (str) {
+ let arg = "";
+ let quote = null;
+ let len = str.length;
+
+ while (str.length && !/^\s/.test(str)) {
+ let res;
+
+ switch (Commands.QUOTE_STYLE) {
+ case "vim-sucks":
+ if (res = str.match = str.match(/^()((?:[^\\\s]|\\.)+)((?:\\$)?)/))
+ arg += res[2].replace(/\\(.)/g, "$1");
+ break;
+
+ case "vimperator":
+ if (res = str.match(/^()((?:[^\\\s"']|\\.)+)((?:\\$)?)/))
+ arg += res[2].replace(/\\(.)/g, "$1");
+ else if (res = str.match(/^(")((?:[^\\"]|\\.)*)("?)/))
+ arg += eval(res[0] + (res[3] ? "" : '"'));
+ else if (res = str.match(/^(')((?:[^\\']|\\.)*)('?)/))
+ arg += res[2].replace(/\\(.)/g, function (n0, n1) /[\\']/.test(n1) ? n1 : n0);
+ break;
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// MAPPINGS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
+ case "rc-ish":
+ if (res = str.match = str.match(/^()((?:[^\\\s"']|\\.)+)((?:\\$)?)/))
+ arg += res[2].replace(/\\(.)/g, "$1");
+ else if (res = str.match(/^(")((?:[^\\"]|\\.)*)("?)/))
+ arg += eval(res[0] + (res[3] ? "" : '"'));
+ else if (res = str.match(/^(')((?:[^']|'')*)('?)/))
+ arg += res[2].replace("''", "'", "g");
+ break;
+
+ case "pythonesque":
+ if (res = str.match = str.match(/^()((?:[^\\\s"']|\\.)+)((?:\\$)?)/))
+ arg += res[2].replace(/\\(.)/g, "$1");
+ else if (res = str.match(/^(""")((?:.?.?[^"])*)((?:""")?)/))
+ arg += res[2];
+ else if (res = str.match(/^(")((?:[^\\"]|\\.)*)("?)/))
+ arg += eval(res[0] + (res[3] ? "" : '"'));
+ else if (res = str.match(/^(')((?:[^\\']|\\.)*)('?)/))
+ arg += res[2].replace(/\\(.)/g, function (n0, n1) /[\\']/.test(n1) ? n1 : n0);
+ break;
+ }
- liberator.registerObserver("load_mappings", function () {
+ if (!res)
+ break;
+ if (!res[3])
+ quote = res[1];
+ if (!res[1])
+ quote = res[3];
+ str = str.substr(res[0].length);
+ }
+
+ return [len - str.length, arg, quote];
+ },
+}, {
+ mappings: function () {
mappings.add(config.browserModes,
["@:"], "Repeat the last Ex command",
- function (count)
- {
- if (commands.repeat)
- {
+ function (count) {
+ if (commands.repeat) {
for (let i in util.interruptibleRange(0, Math.max(count, 1), 100))
liberator.execute(commands.repeat);
}
@@ -977,203 +867,9 @@ function Commands() //{{{
liberator.echoerr("E30: No previous command line");
},
{ count: true });
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMMANDS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- function userCommand(args, modifiers)
- {
- let tokens = {
- args: this.argCount && args.string,
- bang: this.bang && args.bang ? "!" : "",
- count: this.count && args.count
- };
-
- liberator.execute(commands.replaceTokens(this.replacementText, tokens));
- }
-
- // TODO: offer completion.ex?
- // : make this config specific
- var completeOptionMap = {
- abbreviation: "abbreviation", altstyle: "alternateStyleSheet",
- bookmark: "bookmark", buffer: "buffer", color: "colorScheme",
- command: "command", dialog: "dialog", dir: "directory",
- environment: "environment", event: "autocmdEvent", file: "file",
- help: "help", highlight: "highlightGroup", javascript: "javascript",
- macro: "macro", mapping: "userMapping", menu: "menuItem",
- option: "option", preference: "preference", search: "search",
- shellcmd: "shellCommand", sidebar: "sidebar", url: "url",
- usercommand: "userCommand"
- };
-
- // TODO: Vim allows commands to be defined without {rep} if there are {attr}s
- // specified - useful?
- self.add(["com[mand]"],
- "List and define commands",
- function (args)
- {
- let cmd = args[0];
-
- liberator.assert(!/\W/.test(cmd || ''), "E182: Invalid command name");
-
- if (args.literalArg)
- {
- let nargsOpt = args["-nargs"] || "0";
- let bangOpt = "-bang" in args;
- let countOpt = "-count" in args;
- let descriptionOpt = args["-description"] || "User-defined command";
- let completeOpt = args["-complete"];
-
- let completeFunc = null; // default to no completion for user commands
-
- if (completeOpt)
- {
- if (/^custom,/.test(completeOpt))
- {
- completeOpt = completeOpt.substr(7);
- completeFunc = function ()
- {
- try
- {
- var completer = liberator.eval(completeOpt);
-
- if (!(completer instanceof Function))
- throw new TypeError("User-defined custom completer '" + completeOpt + "' is not a function");
- }
- catch (e)
- {
- liberator.echo(":" + this.name + " ...");
- liberator.echoerr("E117: Unknown function: " + completeOpt);
- liberator.log(e);
- return undefined;
- }
- return completer.apply(this, Array.slice(arguments));
- };
- }
- else
- completeFunc = completion[completeOptionMap[completeOpt]];
- }
-
- let added = commands.addUserCommand([cmd],
- descriptionOpt,
- userCommand,
- {
- argCount: nargsOpt,
- bang: bangOpt,
- count: countOpt,
- completer: completeFunc,
- replacementText: args.literalArg
- }, args.bang);
-
- if (!added)
- liberator.echoerr("E174: Command already exists: add ! to replace it");
- }
- else
- {
- function completerToString(completer)
- {
- if (completer)
- return [k for ([k, v] in Iterator(completeOptionMap)) if (completer == completion[v])][0] || "custom";
- else
- return "";
- }
-
- // TODO: using an array comprehension here generates flakey results across repeated calls
- // : perhaps we shouldn't allow options in a list call but just ignore them for now
- let cmds = exCommands.filter(function (c) c.user && (!cmd || c.name.match("^" + cmd)));
-
- if (cmds.length > 0)
- {
- let str = template.tabular(["", "Name", "Args", "Range", "Complete", "Definition"], ["padding-right: 2em;"],
- ([cmd.bang ? "!" : " ",
- cmd.name,
- cmd.argCount,
- cmd.count ? "0c" : "",
- completerToString(cmd.completer),
- cmd.replacementText || "function () { ... }"]
- for ([, cmd] in Iterator(cmds))));
-
- commandline.echo(str, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
- }
- else
- liberator.echomsg("No user-defined commands found");
- }
- },
- {
- bang: true,
- completer: function (context, args)
- {
- if (args.completeArg == 0)
- completion.userCommand(context);
- else
- completion.ex(context);
- },
- options: [
- [["-nargs"], self.OPTION_STRING,
- function (arg) /^[01*?+]$/.test(arg),
- [["0", "No arguments are allowed (default)"],
- ["1", "One argument is allowed"],
- ["*", "Zero or more arguments are allowed"],
- ["?", "Zero or one argument is allowed"],
- ["+", "One or more arguments is allowed"]]],
- [["-bang"], self.OPTION_NOARG],
- [["-count"], self.OPTION_NOARG],
- [["-description"], self.OPTION_STRING],
- // TODO: "E180: invalid complete value: " + arg
- [["-complete"], self.OPTION_STRING,
- function (arg) arg in completeOptionMap || /custom,\w+/.test(arg),
- function (context) [[k, ""] for ([k, v] in Iterator(completeOptionMap))]]
- ],
- literal: 1,
- serial: function () [
- {
- command: this.name,
- bang: true,
- options: util.Array.toObject(
- [[v, typeof cmd[k] == "boolean" ? null : cmd[k]]
- // FIXME: this map is expressed multiple times
- for ([k, v] in Iterator({ argCount: "-nargs", bang: "-bang", count: "-count", description: "-description" }))
- // FIXME: add support for default values to parseArgs
- if (k in cmd && cmd[k] != "0" && cmd[k] != "User-defined command")]),
- arguments: [cmd.name],
- literalArg: cmd.replacementText
- }
- for ([k, cmd] in Iterator(exCommands))
- if (cmd.user && cmd.replacementText)
- ]
- });
-
- self.add(["comc[lear]"],
- "Delete all user-defined commands",
- function ()
- {
- commands.getUserCommands().forEach(function (cmd) { commands.removeUserCommand(cmd.name); });
- },
- { argCount: "0" });
-
- self.add(["delc[ommand]"],
- "Delete the specified user-defined command",
- function (args)
- {
- let name = args[0];
-
- if (commands.get(name))
- commands.removeUserCommand(name);
- else
- liberator.echoerr("E184: No such user-defined command: " + name);
- },
- {
- argCount: "1",
- completer: function (context) completion.userCommand(context)
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMPLETIONS /////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
+ },
- liberator.registerObserver("load_completion", function () {
+ completion: function () {
completion.setFunctionCompleter(commands.get, [function () ([c.name, c.description] for (c in commands))]);
completion.command = function command(context) {
@@ -1194,8 +890,7 @@ function Commands() //{{{
// dynamically get completions as specified with the command's completer function
let command = commands.get(cmd);
- if (!command)
- {
+ if (!command) {
context.highlight(0, cmd.length, "SPELLCHECK");
return;
}
@@ -1204,30 +899,25 @@ function Commands() //{{{
let cmdContext = context.fork(cmd, prefix.length);
let argContext = context.fork("args", prefix.length);
args = command.parseArgs(cmdContext.filter, argContext, { count: count, bang: bang });
- if (args)
- {
+ if (args) {
// FIXME: Move to parseCommand
args.count = count;
args.bang = bang;
- if (!args.completeOpt && command.completer)
- {
+ if (!args.completeOpt && command.completer) {
cmdContext.advance(args.completeStart);
cmdContext.quote = args.quote;
cmdContext.filter = args.completeFilter;
- try
- {
+ try {
let compObject = command.completer.call(command, cmdContext, args);
if (compObject instanceof Array) // for now at least, let completion functions return arrays instead of objects
compObject = { start: compObject[0], items: compObject[1] };
- if (compObject != null)
- {
+ if (compObject != null) {
cmdContext.advance(compObject.start);
cmdContext.filterFunc = null;
cmdContext.completions = compObject.items;
}
}
- catch (e)
- {
+ catch (e) {
liberator.reportError(e);
}
}
@@ -1239,11 +929,216 @@ function Commands() //{{{
context.keys = { text: "name", description: "replacementText" };
context.completions = commands.getUserCommands();
};
- });
- //}}}
+ },
+
+ commands: function () {
+ function userCommand(args, modifiers) {
+ let tokens = {
+ args: this.argCount && args.string,
+ bang: this.bang && args.bang ? "!" : "",
+ count: this.count && args.count
+ };
+
+ liberator.execute(commands.replaceTokens(this.replacementText, tokens));
+ }
+
+ // TODO: offer completion.ex?
+ // : make this config specific
+ var completeOptionMap = {
+ abbreviation: "abbreviation", altstyle: "alternateStyleSheet",
+ bookmark: "bookmark", buffer: "buffer", color: "colorScheme",
+ command: "command", dialog: "dialog", dir: "directory",
+ environment: "environment", event: "autocmdEvent", file: "file",
+ help: "help", highlight: "highlightGroup", javascript: "javascript",
+ macro: "macro", mapping: "userMapping", menu: "menuItem",
+ option: "option", preference: "preference", search: "search",
+ shellcmd: "shellCommand", sidebar: "sidebar", url: "url",
+ usercommand: "userCommand"
+ };
+
+ // TODO: Vim allows commands to be defined without {rep} if there are {attr}s
+ // specified - useful?
+ commands.add(["com[mand]"],
+ "List and define commands",
+ function (args) {
+ let cmd = args[0];
+
+ liberator.assert(!/\W/.test(cmd || ''), "E182: Invalid command name");
+
+ if (args.literalArg) {
+ let nargsOpt = args["-nargs"] || "0";
+ let bangOpt = "-bang" in args;
+ let countOpt = "-count" in args;
+ let descriptionOpt = args["-description"] || "User-defined command";
+ let completeOpt = args["-complete"];
+
+ let completeFunc = null; // default to no completion for user commands
+
+ if (completeOpt) {
+ if (/^custom,/.test(completeOpt)) {
+ completeOpt = completeOpt.substr(7);
+ completeFunc = function () {
+ try {
+ var completer = liberator.eval(completeOpt);
+
+ if (!(completer instanceof Function))
+ throw new TypeError("User-defined custom completer '" + completeOpt + "' is not a function");
+ }
+ catch (e) {
+ liberator.echo(":" + this.name + " ...");
+ liberator.echoerr("E117: Unknown function: " + completeOpt);
+ liberator.log(e);
+ return undefined;
+ }
+ return completer.apply(this, Array.slice(arguments));
+ };
+ }
+ else
+ completeFunc = completion[completeOptionMap[completeOpt]];
+ }
- return self;
+ let added = commands.addUserCommand([cmd],
+ descriptionOpt,
+ userCommand, {
+ argCount: nargsOpt,
+ bang: bangOpt,
+ count: countOpt,
+ completer: completeFunc,
+ replacementText: args.literalArg
+ }, args.bang);
+
+ if (!added)
+ liberator.echoerr("E174: Command already exists: add ! to replace it");
+ }
+ else {
+ function completerToString(completer) {
+ if (completer)
+ return [k for ([k, v] in Iterator(completeOptionMap)) if (completer == completion[v])][0] || "custom";
+ else
+ return "";
+ }
-} //}}}
+ // TODO: using an array comprehension here generates flakey results across repeated calls
+ // : perhaps we shouldn't allow options in a list call but just ignore them for now
+ let cmds = this._exCommands.filter(function (c) c.user && (!cmd || c.name.match("^" + cmd)));
+
+ if (cmds.length > 0) {
+ let str = template.tabular(["", "Name", "Args", "Range", "Complete", "Definition"], ["padding-right: 2em;"],
+ ([cmd.bang ? "!" : " ",
+ cmd.name,
+ cmd.argCount,
+ cmd.count ? "0c" : "",
+ completerToString(cmd.completer),
+ cmd.replacementText || "function () { ... }"]
+ for ([, cmd] in Iterator(cmds))));
+
+ commandline.echo(str, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
+ }
+ else
+ liberator.echomsg("No user-defined commands found");
+ }
+ }, {
+ bang: true,
+ completer: function (context, args) {
+ if (args.completeArg == 0)
+ completion.userCommand(context);
+ else
+ completion.ex(context);
+ },
+ options: [
+ [["-nargs"], commands.OPTION_STRING,
+ function (arg) /^[01*?+]$/.test(arg),
+ [["0", "No arguments are allowed (default)"],
+ ["1", "One argument is allowed"],
+ ["*", "Zero or more arguments are allowed"],
+ ["?", "Zero or one argument is allowed"],
+ ["+", "One or more arguments is allowed"]]],
+ [["-bang"], commands.OPTION_NOARG],
+ [["-count"], commands.OPTION_NOARG],
+ [["-description"], commands.OPTION_STRING],
+ // TODO: "E180: invalid complete value: " + arg
+ [["-complete"], commands.OPTION_STRING,
+ function (arg) arg in completeOptionMap || /custom,\w+/.test(arg),
+ function (context) [[k, ""] for ([k, v] in Iterator(completeOptionMap))]]
+ ],
+ literal: 1,
+ serial: function () [ {
+ command: this.name,
+ bang: true,
+ options: util.Array.toObject(
+ [[v, typeof cmd[k] == "boolean" ? null : cmd[k]]
+ // FIXME: this map is expressed multiple times
+ for ([k, v] in Iterator({ argCount: "-nargs", bang: "-bang", count: "-count", description: "-description" }))
+ // FIXME: add support for default values to parseArgs
+ if (k in cmd && cmd[k] != "0" && cmd[k] != "User-defined command")]),
+ arguments: [cmd.name],
+ literalArg: cmd.replacementText
+ }
+ for ([k, cmd] in Iterator(this._exCommands))
+ if (cmd.user && cmd.replacementText)
+ ]
+ });
+
+ commands.add(["comc[lear]"],
+ "Delete all user-defined commands",
+ function () {
+ commands.getUserCommands().forEach(function (cmd) { commands.removeUserCommand(cmd.name); });
+ },
+ { argCount: "0" });
+
+ commands.add(["delc[ommand]"],
+ "Delete the specified user-defined command",
+ function (args) {
+ let name = args[0];
+
+ if (commands.get(name))
+ commands.removeUserCommand(name);
+ else
+ liberator.echoerr("E184: No such user-defined command: " + name);
+ }, {
+ argCount: "1",
+ completer: function (context) completion.userCommand(context)
+ });
+ }
+});
+
+(function () {
+
+ Commands.quoteMap = {
+ "\n": "n",
+ "\t": "t"
+ };
+ function quote(q, list) {
+ let re = RegExp("[" + list + "]", "g");
+ return function (str) q + String.replace(str, re, function ($0) $0 in Commands.quoteMap ? Commands.quoteMap[$0] : ("\\" + $0)) + q;
+ };
+ Commands.complQuote = { // FIXME
+ '"': ['"', quote("", '\n\t"\\\\'), '"'],
+ "'": ["'", quote("", "\\\\'"), "'"],
+ "": ["", quote("", "\\\\ "), ""]
+ };
+ Commands.quoteArg = {
+ '"': quote('"', '\n\t"\\\\'),
+ "'": quote("'", "\\\\'"),
+ "": quote("", "\\\\ ")
+ };
+
+ Commands.parseBool = function (arg) {
+ if (/^(true|1|on)$/i.test(arg))
+ return true;
+ if (/^(false|0|off)$/i.test(arg))
+ return false;
+ return NaN;
+ };
+ Commands.argTypes = [
+ null,
+ ArgType("no arg", function (arg) !arg || null),
+ ArgType("boolean", Commands.parseBool),
+ ArgType("string", function (val) val),
+ ArgType("int", parseInt),
+ ArgType("float", parseFloat),
+ ArgType("list", function (arg) arg && arg.split(/\s*,\s*/))
+ ];
+})();
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/completion.js b/common/content/completion.js
index 5414583f..7734f4d1 100644
--- a/common/content/completion.js
+++ b/common/content/completion.js
@@ -25,194 +25,162 @@
* @author Kris Maglione <maglione.k@gmail.com>
* @constructor
*/
-function CompletionContext(editor, name, offset) //{{{
-{
- if (!(this instanceof arguments.callee))
- return new arguments.callee(editor, name, offset);
- if (!name)
- name = "";
-
- let self = this;
- if (editor instanceof arguments.callee)
- {
- let parent = editor;
- name = parent.name + "/" + name;
- this.contexts = parent.contexts;
- if (name in this.contexts)
- self = this.contexts[name];
- else
- self.contexts[name] = this;
+const CompletionContext = Class("CompletionContext", {
+ init: function (editor, name, offset) {
+ if (!name)
+ name = "";
+ let self = this;
+ if (editor instanceof this.constructor) {
+ let parent = editor;
+ name = parent.name + "/" + name;
+ this.contexts = parent.contexts;
+ if (name in this.contexts)
+ self = this.contexts[name];
+ else
+ self.contexts[name] = this;
+
+ /**
+ * @property {CompletionContext} This context's parent. {null} when
+ * this is a top-level context.
+ */
+ self.parent = parent;
+
+ ["filters", "keys", "title", "quote"].forEach(function (key)
+ self[key] = parent[key] && util.cloneObject(parent[key]));
+ ["anchored", "compare", "editor", "_filter", "filterFunc", "keys", "_process", "top"].forEach(function (key)
+ self[key] = parent[key]);
+
+ self.__defineGetter__("value", function () this.top.value);
+
+ self.offset = parent.offset;
+ self.advance(offset);
+
+ /**
+ * @property {boolean} Specifies that this context is not finished
+ * generating results.
+ * @default false
+ */
+ self.incomplete = false;
+ self.message = null;
+ /**
+ * @property {boolean} Specifies that this context is waiting for the
+ * user to press <Tab>. Useful when fetching completions could be
+ * dangerous or slow, and the user has enabled autocomplete.
+ */
+ self.waitingForTab = false;
+
+ delete self._generate;
+ delete self._ignoreCase;
+ if (self != this)
+ return self;
+ ["_caret", "contextList", "maxItems", "onUpdate", "selectionTypes", "tabPressed", "updateAsync", "value"].forEach(function (key) {
+ self.__defineGetter__(key, function () this.top[key]);
+ self.__defineSetter__(key, function (val) this.top[key] = val);
+ });
+ }
+ else {
+ if (typeof editor == "string")
+ this._value = editor;
+ else
+ this.editor = editor;
+ this.compare = function (a, b) String.localeCompare(a.text, b.text);
+
+ /**
+ * @property {function} This function is called when we close
+ * a completion window with Esc or Ctrl-c. Usually this callback
+ * is only needed for long, asynchronous completions
+ */
+ this.cancel = null;
+ /**
+ * @property {function} The function used to filter the results.
+ * @default Selects all results which match every predicate in the
+ * {@link #filters} array.
+ */
+ this.filterFunc = function (items) {
+ let self = this;
+ return this.filters.
+ reduce(function (res, filter) res.filter(function (item) filter.call(self, item)),
+ items);
+ };
+ /**
+ * @property {Array} An array of predicates on which to filter the
+ * results.
+ */
+ this.filters = [CompletionContext.Filter.text];
+ /**
+ * @property {boolean} Specifies whether this context results must
+ * match the filter at the beginning of the string.
+ * @default true
+ */
+ this.anchored = true;
+ /**
+ * @property {Object} A map of all contexts, keyed on their names.
+ * Names are assigned when a context is forked, with its specified
+ * name appended, after a '/', to its parent's name. May
+ * contain inactive contexts. For active contexts, see
+ * {@link #contextList}.
+ */
+ this.contexts = { "": this };
+ /**
+ * @property {Object} A mapping of keys, for {@link #getKey}. Given
+ * { key: value }, getKey(item, key) will return values as such:
+ * if value is a string, it will return item.item[value]. If it's a
+ * function, it will return value(item.item).
+ */
+ this.keys = { text: 0, description: 1, icon: "icon" };
+ /**
+ * @property {number} This context's offset from the beginning of
+ * {@link #editor}'s value.
+ */
+ this.offset = offset || 0;
+ /**
+ * @property {function} A function which is called when any subcontext
+ * changes its completion list. Only called when
+ * {@link #updateAsync} is true.
+ */
+ this.onUpdate = function () true;
+ /**
+ * @property {CompletionContext} The top-level completion context.
+ */
+ this.top = this;
+ this.__defineGetter__("incomplete", function () this.contextList.some(function (c) c.parent && c.incomplete));
+ this.__defineGetter__("waitingForTab", function () this.contextList.some(function (c) c.parent && c.waitingForTab));
+ this.reset();
+ }
/**
- * @property {CompletionContext} This context's parent. {null} when
- * this is a top-level context.
- */
- self.parent = parent;
-
- ["filters", "keys", "title", "quote"].forEach(function (key)
- self[key] = parent[key] && util.cloneObject(parent[key]));
- ["anchored", "compare", "editor", "_filter", "filterFunc", "keys", "_process", "top"].forEach(function (key)
- self[key] = parent[key]);
-
- self.__defineGetter__("value", function () this.top.value);
-
- self.offset = parent.offset;
- self.advance(offset);
-
- /**
- * @property {boolean} Specifies that this context is not finished
- * generating results.
- * @default false
- */
- self.incomplete = false;
- self.message = null;
- /**
- * @property {boolean} Specifies that this context is waiting for the
- * user to press <Tab>. Useful when fetching completions could be
- * dangerous or slow, and the user has enabled autocomplete.
- */
- self.waitingForTab = false;
-
- delete self._generate;
- delete self._ignoreCase;
- if (self != this)
- return self;
- ["_caret", "contextList", "maxItems", "onUpdate", "selectionTypes", "tabPressed", "updateAsync", "value"].forEach(function (key) {
- self.__defineGetter__(key, function () this.top[key]);
- self.__defineSetter__(key, function (val) this.top[key] = val);
- });
- }
- else
- {
- if (typeof editor == "string")
- this._value = editor;
- else
- this.editor = editor;
- this.compare = function (a, b) String.localeCompare(a.text, b.text);
-
- /**
- * @property {function} This function is called when we close
- * a completion window with Esc or Ctrl-c. Usually this callback
- * is only needed for long, asynchronous completions
- */
- this.cancel = null;
- /**
- * @property {function} The function used to filter the results.
- * @default Selects all results which match every predicate in the
- * {@link #filters} array.
- */
- this.filterFunc = function (items)
- {
- let self = this;
- return this.filters.
- reduce(function (res, filter) res.filter(function (item) filter.call(self, item)),
- items);
- };
- /**
- * @property {Array} An array of predicates on which to filter the
- * results.
- */
- this.filters = [CompletionContext.Filter.text];
- /**
- * @property {boolean} Specifies whether this context results must
- * match the filter at the beginning of the string.
- * @default true
- */
- this.anchored = true;
- /**
- * @property {Object} A map of all contexts, keyed on their names.
- * Names are assigned when a context is forked, with its specified
- * name appended, after a '/', to its parent's name. May
- * contain inactive contexts. For active contexts, see
- * {@link #contextList}.
+ * @property {Object} A general-purpose store for functions which need to
+ * cache data between calls.
*/
- this.contexts = { "": this };
+ this.cache = {};
/**
- * @property {Object} A mapping of keys, for {@link #getKey}. Given
- * { key: value }, getKey(item, key) will return values as such:
- * if value is a string, it will return item.item[value]. If it's a
- * function, it will return value(item.item).
+ * @private
+ * @property {Object} A cache for return values of {@link #generate}.
*/
- this.keys = { text: 0, description: 1, icon: "icon" };
+ this.itemCache = {};
/**
- * @property {number} This context's offset from the beginning of
- * {@link #editor}'s value.
+ * @property {string} A key detailing when the cached value of
+ * {@link #generate} may be used. Every call to
+ * {@link #generate} stores its result in {@link #itemCache}.
+ * When itemCache[key] exists, its value is returned, and
+ * {@link #generate} is not called again.
*/
- this.offset = offset || 0;
+ this.key = "";
/**
- * @property {function} A function which is called when any subcontext
- * changes its completion list. Only called when
- * {@link #updateAsync} is true.
+ * @property {string} A message to be shown before any results.
*/
- this.onUpdate = function () true;
+ this.message = null;
+ this.name = name || "";
+ /** @private */
+ this._completions = []; // FIXME
/**
- * @property {CompletionContext} The top-level completion context.
+ * Returns a key, as detailed in {@link #keys}.
+ * @function
*/
- this.top = this;
- this.__defineGetter__("incomplete", function () this.contextList.some(function (c) c.parent && c.incomplete));
- this.__defineGetter__("waitingForTab", function () this.contextList.some(function (c) c.parent && c.waitingForTab));
- this.reset();
- }
- /**
- * @property {Object} A general-purpose store for functions which need to
- * cache data between calls.
- */
- this.cache = {};
- /**
- * @private
- * @property {Object} A cache for return values of {@link #generate}.
- */
- this.itemCache = {};
- /**
- * @property {string} A key detailing when the cached value of
- * {@link #generate} may be used. Every call to
- * {@link #generate} stores its result in {@link #itemCache}.
- * When itemCache[key] exists, its value is returned, and
- * {@link #generate} is not called again.
- */
- this.key = "";
- /**
- * @property {string} A message to be shown before any results.
- */
- this.message = null;
- this.name = name || "";
- /** @private */
- this._completions = []; // FIXME
- /**
- * Returns a key, as detailed in {@link #keys}.
- * @function
- */
- this.getKey = function (item, key) (typeof self.keys[key] == "function") ? self.keys[key].call(this, item.item) :
- key in self.keys ? item.item[self.keys[key]]
- : item.item[key];
- return this;
-}
-
-CompletionContext.Sort = {
- number: function (a, b) parseInt(b) - parseInt(a) || String.localeCompare(a, b),
-
- unsorted: null
-};
-
-CompletionContext.Filter = {
- text: function (item) {
- let text = Array.concat(item.text);
- for (let [i, str] in Iterator(text))
- {
- if (this.match(String(str)))
- {
- item.text = String(text[i]);
- return true;
- }
- }
- return false;
+ this.getKey = function (item, key) (typeof self.keys[key] == "function") ? self.keys[key].call(this, item.item) :
+ key in self.keys ? item.item[self.keys[key]]
+ : item.item[key];
},
- textDescription: function (item) {
- return CompletionContext.Filter.text.call(this, item) || this.match(item.description);
- }
-};
-
-CompletionContext.prototype = {
// Temporary
/**
* @property {Object}
@@ -222,10 +190,8 @@ CompletionContext.prototype = {
*
* @deprecated
*/
- get allItems()
- {
- try
- {
+ get allItems() {
+ try {
let self = this;
let minStart = Math.min.apply(Math, [context.offset for ([k, context] in Iterator(this.contexts)) if (context.items.length && context.hasItems)]);
if (minStart == Infinity)
@@ -241,15 +207,13 @@ CompletionContext.prototype = {
});
return { start: minStart, items: util.Array.flatten(items), longestSubstring: this.longestAllSubstring };
}
- catch (e)
- {
+ catch (e) {
liberator.reportError(e);
return { start: 0, items: [], longestAllSubstring: "" };
}
},
// Temporary
- get allSubstrings()
- {
+ get allSubstrings() {
let contexts = this.contextList.filter(function (c) c.hasItems && c.items.length);
let minStart = Math.min.apply(Math, contexts.map(function (c) c.offset));
let lists = contexts.map(function (context) {
@@ -265,8 +229,7 @@ CompletionContext.prototype = {
return util.Array.uniq(Array.slice(substrings));
},
// Temporary
- get longestAllSubstring()
- {
+ get longestAllSubstring() {
return this.allSubstrings.reduce(function (a, b) a.length > b.length ? a : b, "");
},
@@ -277,8 +240,7 @@ CompletionContext.prototype = {
set compare(val) this._compare = val,
get completions() this._completions || [],
- set completions(items)
- {
+ set completions(items) {
// Accept a generator
if ({}.toString.call(items) != '[object Array]')
items = [x for (x in Iterator(items))];
@@ -299,8 +261,7 @@ CompletionContext.prototype = {
set filterFunc(val) this._filterFunc = val,
get filter() this._filter != null ? this._filter : this.value.substr(this.offset, this.caret),
- set filter(val)
- {
+ set filter(val) {
delete this._ignoreCase;
return this._filter = val;
},
@@ -311,8 +272,7 @@ CompletionContext.prototype = {
keys: this.keys,
process: this.process
}),
- set format(format)
- {
+ set format(format) {
this.anchored = format.anchored,
this.title = format.title || this.title;
this.keys = format.keys || this.keys;
@@ -322,11 +282,9 @@ CompletionContext.prototype = {
get message() this._message || (this.waitingForTab ? "Waiting for <Tab>" : null),
set message(val) this._message = val,
- get proto()
- {
+ get proto() {
let res = {};
- for (let i in Iterator(this.keys))
- {
+ for (let i in Iterator(this.keys)) {
let [k, v] = i;
let _k = "_" + k;
if (typeof v == "string" && /^[.[]/.test(v))
@@ -343,8 +301,7 @@ CompletionContext.prototype = {
get regenerate() this._generate && (!this.completions || !this.itemCache[this.key] || this.cache.offset != this.offset),
set regenerate(val) { if (val) delete this.itemCache[this.key]; },
- get generate() !this._generate ? null : function ()
- {
+ get generate() !this._generate ? null : function () {
if (this.offset != this.cache.offset)
this.itemCache = {};
this.cache.offset = this.offset;
@@ -352,12 +309,10 @@ CompletionContext.prototype = {
this.itemCache[this.key] = this._generate.call(this) || [];
return this.itemCache[this.key];
},
- set generate(arg)
- {
+ set generate(arg) {
this.hasItems = true;
this._generate = arg;
- if (this.background && this.regenerate)
- {
+ if (this.background && this.regenerate) {
let lock = {};
this.cache.backgroundLock = lock;
this.incomplete = true;
@@ -374,8 +329,7 @@ CompletionContext.prototype = {
}
},
- get ignoreCase()
- {
+ get ignoreCase() {
if ("_ignoreCase" in this)
return this._ignoreCase;
let mode = options["wildcase"];
@@ -387,16 +341,14 @@ CompletionContext.prototype = {
},
set ignoreCase(val) this._ignoreCase = val,
- get items()
- {
+ get items() {
if (!this.hasItems || this.backgroundLock)
return [];
if (this.cache.filtered && this.cache.filter == this.filter)
return this.cache.filtered;
this.cache.rows = [];
let items = this.completions;
- if (this.generate && !this.background)
- {
+ if (this.generate && !this.background) {
// XXX
this.noUpdate = true;
this.completions = items = this.generate();
@@ -425,8 +377,7 @@ CompletionContext.prototype = {
return this.cache.filtered = filtered;
},
- get process() // FIXME
- {
+ get process() { // FIXME
let self = this;
let process = this._process;
process = [process[0] || template.icon, process[1] || function (item, k) k];
@@ -436,13 +387,11 @@ CompletionContext.prototype = {
process[0] = function (item, text) first.call(self, item, template.highlightFilter(item.text, filter));
return process;
},
- set process(process)
- {
+ set process(process) {
this._process = process;
},
- get substrings()
- {
+ get substrings() {
let items = this.items;
if (items.length == 0 || !this.hasItems)
return [];
@@ -452,21 +401,18 @@ CompletionContext.prototype = {
let fixCase = this.ignoreCase ? String.toLowerCase : util.identity;
let text = fixCase(items[0].unquoted || items[0].text);
let filter = fixCase(this.filter);
- if (this.anchored)
- {
+ if (this.anchored) {
var compare = function compare(text, s) text.substr(0, s.length) == s;
substrings = util.map(util.range(filter.length, text.length + 1),
function (end) text.substring(0, end));
}
- else
- {
+ else {
var compare = function compare(text, s) text.indexOf(s) >= 0;
substrings = [];
let start = 0;
let idx;
let length = filter.length;
- while ((idx = text.indexOf(filter, start)) > -1 && idx < text.length)
- {
+ while ((idx = text.indexOf(filter, start)) > -1 && idx < text.length) {
for (let end in util.range(idx + length, text.length + 1))
substrings.push(text.substring(idx, end));
start = idx + 1;
@@ -494,11 +440,9 @@ CompletionContext.prototype = {
*
* @param {number} count The number of characters to advance the context.
*/
- advance: function advance(count)
- {
+ advance: function advance(count) {
delete this._ignoreCase;
- if (this.quote)
- {
+ if (this.quote) {
count = this.quote[0].length + this.quote[1](this.filter.substr(0, count)).length;
this.quote[0] = "";
this.quote[2] = "";
@@ -508,10 +452,8 @@ CompletionContext.prototype = {
this._filter = this._filter.substr(count);
},
- cancelAll: function ()
- {
- for (let [, context] in Iterator(this.contextList))
- {
+ cancelAll: function () {
+ for (let [, context] in Iterator(this.contextList)) {
if (context.cancel)
context.cancel();
}
@@ -524,15 +466,13 @@ CompletionContext.prototype = {
* @param {string} key
* @param defVal
*/
- getCache: function (key, defVal)
- {
+ getCache: function (key, defVal) {
if (!(key in this.cache))
this.cache[key] = defVal();
return this.cache[key];
},
- getItems: function getItems(start, end)
- {
+ getItems: function getItems(start, end) {
let self = this;
let items = this.items;
let step = start > end ? -1 : 1;
@@ -541,8 +481,7 @@ CompletionContext.prototype = {
return util.map(util.range(start, end, step), function (i) items[i]);
},
- getRows: function getRows(start, end, doc)
- {
+ getRows: function getRows(start, end, doc) {
let self = this;
let items = this.items;
let cache = this.cache.rows;
@@ -553,8 +492,7 @@ CompletionContext.prototype = {
yield [i, cache[i] = cache[i] || util.xmlToDom(self.createRow(items[i]), doc)];
},
- fork: function fork(name, offset, self, completer)
- {
+ fork: function fork(name, offset, self, completer) {
if (typeof completer == "string")
completer = self[completer];
let context = new CompletionContext(this, name, offset);
@@ -564,26 +502,22 @@ CompletionContext.prototype = {
return context;
},
- getText: function getText(item)
- {
+ getText: function getText(item) {
let text = item[self.keys["text"]];
if (self.quote)
return self.quote(text);
return text;
},
- highlight: function highlight(start, length, type)
- {
- try // Gecko < 1.9.1 doesn't have repaintSelection
- {
+ highlight: function highlight(start, length, type) {
+ try { // Gecko < 1.9.1 doesn't have repaintSelection
this.selectionTypes[type] = null;
const selType = Ci.nsISelectionController["SELECTION_" + type];
const editor = this.editor;
let sel = editor.selectionController.getSelection(selType);
if (length == 0)
sel.removeAllRanges();
- else
- {
+ else {
let range = editor.selection.getRangeAt(0).cloneRange();
range.setStart(range.startContainer, this.offset + start);
range.setEnd(range.startContainer, this.offset + start + length);
@@ -595,10 +529,8 @@ CompletionContext.prototype = {
},
// FIXME
- _match: function _match(filter, str)
- {
- if (this.ignoreCase)
- {
+ _match: function _match(filter, str) {
+ if (this.ignoreCase) {
filter = filter.toLowerCase();
str = str.toLowerCase();
}
@@ -607,13 +539,11 @@ CompletionContext.prototype = {
return str.indexOf(filter) > -1;
},
- match: function match(str)
- {
+ match: function match(str) {
return this._match(this.filter, str);
},
- reset: function reset()
- {
+ reset: function reset() {
let self = this;
if (this.parent)
throw Error();
@@ -633,34 +563,23 @@ CompletionContext.prototype = {
this.tabPressed = false;
this.title = ["Completions"];
this.updateAsync = false;
- try
- {
- this.waitingForTab = false;
- }
- catch (e) {}
this.cancelAll();
- if (this.editor)
- {
+ if (this.editor) {
this.value = this.editor.selection.focusNode.textContent;
this._caret = this.editor.selection.focusOffset;
}
- else
- {
+ else {
this.value = this._value;
this._caret = this.value.length;
}
//for (let key in (k for ([k, v] in Iterator(self.contexts)) if (v.offset > this.caret)))
// delete this.contexts[key];
- for each (let context in this.contexts)
- {
+ for each (let context in this.contexts) {
context.hasItems = false;
- try
- {
+ if (context != context.top)
context.incomplete = false;
- }
- catch (e) {}
}
},
@@ -672,255 +591,373 @@ CompletionContext.prototype = {
* @param {number} timeout The maximum time, in milliseconds, to wait.
* If 0 or null, wait indefinately.
*/
- wait: function wait(interruptable, timeout)
- {
+ wait: function wait(interruptable, timeout) {
let end = Date.now() + timeout;
while (this.incomplete && (!timeout || Date.now() > end))
liberator.threadYield(false, interruptable);
return this.incomplete;
}
-}; //}}}
+}, {
+ Sort: {
+ number: function (a, b) parseInt(b) - parseInt(a) || String.localeCompare(a, b),
+
+ unsorted: null
+ },
+
+ Filter: {
+ text: function (item) {
+ let text = Array.concat(item.text);
+ for (let [i, str] in Iterator(text)) {
+ if (this.match(String(str))) {
+ item.text = String(text[i]);
+ return true;
+ }
+ }
+ return false;
+ },
+ textDescription: function (item) {
+ return CompletionContext.Filter.text.call(this, item) || this.match(item.description);
+ }
+ },
+});
/**
* @instance completion
*/
-function Completion() //{{{
-{
+const Completion = Module("completion", {
+ init: function () {
+ this.javascriptCompleter = Completion.Javascript();
+ },
+
+ setFunctionCompleter: function setFunctionCompleter(funcs, completers) {
+ funcs = Array.concat(funcs);
+ for (let [, func] in Iterator(funcs)) {
+ func.liberatorCompleter = function liberatorCompleter(context, func, obj, args) {
+ let completer = completers[args.length - 1];
+ if (!completer)
+ return [];
+ return completer.call(this, context, obj, args);
+ };
+ }
+ },
+
+ // FIXME
+ _runCompleter: function _runCompleter(name, filter, maxItems) {
+ let context = CompletionContext(filter);
+ context.maxItems = maxItems;
+ let res = context.fork.apply(context, ["run", 0, this, name].concat(Array.slice(arguments, 3)));
+ if (res) // FIXME
+ return { items: res.map(function (i) ({ item: i })) };
+ context.wait(true);
+ return context.allItems;
+ },
+
+ runCompleter: function runCompleter(name, filter, maxItems) {
+ return this._runCompleter.apply(this, Array.slice(arguments))
+ .items.map(function (i) i.item);
+ },
+
+ listCompleter: function listCompleter(name, filter, maxItems) {
+ let context = CompletionContext(filter || "");
+ context.maxItems = maxItems;
+ context.fork.apply(context, ["list", 0, completion, name].concat(Array.slice(arguments, 3)));
+ context = context.contexts["/list"];
+ context.wait();
+
+ let list = template.commandOutput(
+ <div highlight="Completions">
+ { template.completionRow(context.title, "CompTitle") }
+ { template.map(context.items, function (item) context.createRow(item), null, 100) }
+ </div>);
+ commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
+ },
+
////////////////////////////////////////////////////////////////////////////////
- ////////////////////// PRIVATE SECTION /////////////////////////////////////////
+ ////////////////////// COMPLETION TYPES ////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
- const EVAL_TMP = "__liberator_eval_tmp";
+ javascript: function (context) this.javascriptCompleter.complete(context),
- function Javascript()
- {
- const OFFSET = 0, CHAR = 1, STATEMENTS = 2, DOTS = 3, FULL_STATEMENTS = 4, COMMA = 5, FUNCTIONS = 6;
- let stack = [];
- let functions = [];
- let top = []; // The element on the top of the stack.
- let last = ""; // The last opening char pushed onto the stack.
- let lastNonwhite = ""; // Last non-whitespace character we saw.
- let lastChar = ""; // Last character we saw, used for \ escaping quotes.
- let compl = [];
- let str = "";
+ // filter a list of urls
+ //
+ // may consist of search engines, filenames, bookmarks and history,
+ // depending on the 'complete' option
+ // if the 'complete' argument is passed like "h", it temporarily overrides the complete option
+ url: function url(context, complete) {
+ let numLocationCompletions = 0; // how many async completions did we already return to the caller?
+ let start = 0;
+ let skip = 0;
- let lastIdx = 0;
+ if (options["urlseparator"])
+ skip = context.filter.match("^.*" + options["urlseparator"]); // start after the last 'urlseparator'
- let cacheKey = null;
+ if (skip)
+ context.advance(skip[0].length);
- this.completers = {};
+ // Will, and should, throw an error if !(c in opts)
+ Array.forEach(complete || options["complete"], function (c) {
+ let completer = completion.urlCompleters[c];
+ context.fork.apply(context, [c, 0, completion, completer.completer].concat(completer.args));
+ });
+ },
- // Some object members are only accessible as function calls
- function getKey(obj, key)
- {
- try
- {
- return obj[key];
- }
- catch (e)
- {
- return undefined;
- }
+ urlCompleters: {},
+
+ addUrlCompleter: function addUrlCompleter(opt) {
+ let completer = Completion.UrlCompleter.apply(null, Array.slice(arguments));
+ completer.args = Array.slice(arguments, completer.length);
+ this.urlCompleters[opt] = completer;
+ },
+
+ urls: function (context, tags) {
+ let compare = String.localeCompare;
+ let contains = String.indexOf;
+ if (context.ignoreCase) {
+ compare = util.compareIgnoreCase;
+ contains = function (a, b) a && a.toLowerCase().indexOf(b.toLowerCase()) > -1;
}
- this.iter = function iter(obj)
- {
- let iterator = ([k, getKey(obj, k)] for (k in obj));
- try
- {
- // The point of 'for k in obj' is to get keys
- // that are accessible via . or [] notation.
- // Iterators quite often return values of no
- // use whatsoever for this purpose, so, we try
- // this rather dirty hack of getting a standard
- // object iterator for any object that defines its
- // own.
- if ("__iterator__" in obj)
- {
- let oldIter = obj.__iterator__;
- delete obj.__iterator__;
- iterator = Iterator(obj);
- obj.__iterator__ = oldIter;
+ if (tags)
+ context.filters.push(function (item) tags.
+ every(function (tag) (item.tags || []).
+ some(function (t) !compare(tag, t))));
+
+ context.anchored = false;
+ if (!context.title)
+ context.title = ["URL", "Title"];
+
+ context.fork("additional", 0, this, function (context) {
+ context.title[0] += " (additional)";
+ context.filter = context.parent.filter; // FIXME
+ context.completions = context.parent.completions;
+ // For items whose URL doesn't exactly match the filter,
+ // accept them if all tokens match either the URL or the title.
+ // Filter out all directly matching strings.
+ let match = context.filters[0];
+ context.filters[0] = function (item) !match.call(this, item);
+ // and all that don't match the tokens.
+ let tokens = context.filter.split(/\s+/);
+ context.filters.push(function (item) tokens.every(
+ function (tok) contains(item.url, tok) ||
+ contains(item.title, tok)));
+
+ let re = RegExp(tokens.filter(util.identity).map(util.escapeRegex).join("|"), "g");
+ function highlight(item, text, i) process[i].call(this, item, template.highlightRegexp(text, re));
+ let process = [template.icon, function (item, k) k];
+ context.process = [
+ function (item, text) highlight.call(this, item, item.text, 0),
+ function (item, text) highlight.call(this, item, text, 1)
+ ];
+ });
+ }
+ //}}}
+}, {
+ UrlCompleter: new Struct("name", "description", "completer"),
+
+ Javascript: Class("Javascript", {
+ init: function () {
+ const OFFSET = 0, CHAR = 1, STATEMENTS = 2, DOTS = 3, FULL_STATEMENTS = 4, COMMA = 5, FUNCTIONS = 6;
+ let stack = [];
+ let functions = [];
+ let top = []; // The element on the top of the stack.
+ let last = ""; // The last opening char pushed onto the stack.
+ let lastNonwhite = ""; // Last non-whitespace character we saw.
+ let lastChar = ""; // Last character we saw, used for \ escaping quotes.
+ let compl = [];
+ let str = "";
+
+ let lastIdx = 0;
+
+ let cacheKey = null;
+
+ this.completers = {};
+
+ // Some object members are only accessible as function calls
+ function getKey(obj, key) {
+ try {
+ return obj[key];
+ }
+ catch (e) {
+ return undefined;
}
}
- catch (e) {}
- return iterator;
- };
-
- // Search the object for strings starting with @key.
- // If @last is defined, key is a quoted string, it's
- // wrapped in @last after @offset characters are sliced
- // off of it and it's quoted.
- this.objectKeys = function objectKeys(obj)
- {
- // Things we can dereference
- if (["object", "string", "function"].indexOf(typeof obj) == -1)
- return [];
- if (!obj)
- return [];
-
- // XPCNativeWrappers, etc, don't show all accessible
- // members until they're accessed, so, we look at
- // the wrappedJSObject instead, and return any keys
- // available in the object itself.
- let orig = obj;
-
- if (modules.isPrototypeOf(obj))
- compl = [v for (v in Iterator(obj))];
- else
- {
- if (getKey(obj, 'wrappedJSObject'))
- obj = obj.wrappedJSObject;
- // v[0] in orig and orig[v[0]] catch different cases. XPCOM
- // objects are problematic, to say the least.
- compl = [v for (v in this.iter(obj))
- if ((typeof orig == "object" && v[0] in orig) || getKey(orig, v[0]) !== undefined)];
- }
-
- // And if wrappedJSObject happens to be available,
- // return that, too.
- if (getKey(orig, 'wrappedJSObject'))
- compl.push(["wrappedJSObject", obj]);
-
- // Add keys for sorting later.
- // Numbers are parsed to ints.
- // Constants, which should be unsorted, are found and marked null.
- compl.forEach(function (item) {
- let key = item[0];
- if (!isNaN(key))
- key = parseInt(key);
- else if (/^[A-Z_][A-Z0-9_]*$/.test(key))
- key = "";
- item.key = key;
- });
- return compl;
- };
+ this.iter = function iter(obj) {
+ let iterator = ([k, getKey(obj, k)] for (k in obj));
+ try {
+ // The point of 'for k in obj' is to get keys
+ // that are accessible via . or [] notation.
+ // Iterators quite often return values of no
+ // use whatsoever for this purpose, so, we try
+ // this rather dirty hack of getting a standard
+ // object iterator for any object that defines its
+ // own.
+ if ("__iterator__" in obj) {
+ let oldIter = obj.__iterator__;
+ delete obj.__iterator__;
+ iterator = Iterator(obj);
+ obj.__iterator__ = oldIter;
+ }
+ }
+ catch (e) {}
+ return iterator;
+ };
- this.eval = function eval(arg, key, tmp)
- {
- let cache = this.context.cache.eval;
- let context = this.context.cache.evalContext;
+ // Search the object for strings starting with @key.
+ // If @last is defined, key is a quoted string, it's
+ // wrapped in @last after @offset characters are sliced
+ // off of it and it's quoted.
+ this.objectKeys = function objectKeys(obj) {
+ // Things we can dereference
+ if (["object", "string", "function"].indexOf(typeof obj) == -1)
+ return [];
+ if (!obj)
+ return [];
- if (!key)
- key = arg;
- if (key in cache)
- return cache[key];
+ // XPCNativeWrappers, etc, don't show all accessible
+ // members until they're accessed, so, we look at
+ // the wrappedJSObject instead, and return any keys
+ // available in the object itself.
+ let orig = obj;
+
+ if (modules.isPrototypeOf(obj))
+ compl = [v for (v in Iterator(obj))];
+ else {
+ if (getKey(obj, 'wrappedJSObject'))
+ obj = obj.wrappedJSObject;
+ // v[0] in orig and orig[v[0]] catch different cases. XPCOM
+ // objects are problematic, to say the least.
+ compl = [v for (v in this.iter(obj))
+ if ((typeof orig == "object" && v[0] in orig) || getKey(orig, v[0]) !== undefined)];
+ }
- context[EVAL_TMP] = tmp;
- try
- {
- return cache[key] = liberator.eval(arg, context);
- }
- catch (e)
- {
- return null;
- }
- finally
- {
- delete context[EVAL_TMP];
- }
- };
-
- // Get an element from the stack. If @n is negative,
- // count from the top of the stack, otherwise, the bottom.
- // If @m is provided, return the @mth value of element @o
- // of the stack entry at @n.
- let get = function get(n, m, o)
- {
- let a = stack[n >= 0 ? n : stack.length + n];
- if (o != null)
- a = a[o];
- if (m == null)
- return a;
- return a[a.length - m - 1];
- };
-
- function buildStack(filter)
- {
- let self = this;
- // Push and pop the stack, maintaining references to 'top' and 'last'.
- let push = function push(arg)
- {
- top = [i, arg, [i], [], [], [], []];
- last = top[CHAR];
- stack.push(top);
+ // And if wrappedJSObject happens to be available,
+ // return that, too.
+ if (getKey(orig, 'wrappedJSObject'))
+ compl.push(["wrappedJSObject", obj]);
+
+ // Add keys for sorting later.
+ // Numbers are parsed to ints.
+ // Constants, which should be unsorted, are found and marked null.
+ compl.forEach(function (item) {
+ let key = item[0];
+ if (!isNaN(key))
+ key = parseInt(key);
+ else if (/^[A-Z_][A-Z0-9_]*$/.test(key))
+ key = "";
+ item.key = key;
+ });
+
+ return compl;
};
- let pop = function pop(arg)
- {
- if (top[CHAR] != arg)
- {
- self.context.highlight(top[OFFSET], i - top[OFFSET], "SPELLCHECK");
- self.context.highlight(top[OFFSET], 1, "FIND");
- throw new Error("Invalid JS");
+
+ this.eval = function eval(arg, key, tmp) {
+ let cache = this.context.cache.eval;
+ let context = this.context.cache.evalContext;
+
+ if (!key)
+ key = arg;
+ if (key in cache)
+ return cache[key];
+
+ context[Completion.Javascript.EVAL_TMP] = tmp;
+ try {
+ return cache[key] = liberator.eval(arg, context);
+ }
+ catch (e) {
+ return null;
+ }
+ finally {
+ delete context[Completion.Javascript.EVAL_TMP];
}
- if (i == self.context.caret - 1)
- self.context.highlight(top[OFFSET], 1, "FIND");
- // The closing character of this stack frame will have pushed a new
- // statement, leaving us with an empty statement. This doesn't matter,
- // now, as we simply throw away the frame when we pop it, but it may later.
- if (top[STATEMENTS][top[STATEMENTS].length - 1] == i)
- top[STATEMENTS].pop();
- top = get(-2);
- last = top[CHAR];
- let ret = stack.pop();
- return ret;
};
- let i = 0, c = ""; // Current index and character, respectively.
-
- // Reuse the old stack.
- if (str && filter.substr(0, str.length) == str)
- {
- i = str.length;
- if (this.popStatement)
- top[STATEMENTS].pop();
- }
- else
- {
- stack = [];
- functions = [];
- push("#root");
- }
+ // Get an element from the stack. If @n is negative,
+ // count from the top of the stack, otherwise, the bottom.
+ // If @m is provided, return the @mth value of element @o
+ // of the stack entry at @n.
+ let get = function get(n, m, o) {
+ let a = stack[n >= 0 ? n : stack.length + n];
+ if (o != null)
+ a = a[o];
+ if (m == null)
+ return a;
+ return a[a.length - m - 1];
+ };
- // Build a parse stack, discarding entries as opening characters
- // match closing characters. The stack is walked from the top entry
- // and down as many levels as it takes us to figure out what it is
- // that we're completing.
- str = filter;
- let length = str.length;
- for (; i < length; lastChar = c, i++)
- {
- c = str[i];
- if (last == '"' || last == "'" || last == "/")
- {
- if (lastChar == "\\") // Escape. Skip the next char, whatever it may be.
- {
- c = "";
- i++;
+ function buildStack(filter) {
+ let self = this;
+ // Push and pop the stack, maintaining references to 'top' and 'last'.
+ let push = function push(arg) {
+ top = [i, arg, [i], [], [], [], []];
+ last = top[CHAR];
+ stack.push(top);
+ };
+ let pop = function pop(arg) {
+ if (top[CHAR] != arg) {
+ self.context.highlight(top[OFFSET], i - top[OFFSET], "SPELLCHECK");
+ self.context.highlight(top[OFFSET], 1, "FIND");
+ throw new Error("Invalid JS");
}
- else if (c == last)
- pop(c);
+ if (i == self.context.caret - 1)
+ self.context.highlight(top[OFFSET], 1, "FIND");
+ // The closing character of this stack frame will have pushed a new
+ // statement, leaving us with an empty statement. This doesn't matter,
+ // now, as we simply throw away the frame when we pop it, but it may later.
+ if (top[STATEMENTS][top[STATEMENTS].length - 1] == i)
+ top[STATEMENTS].pop();
+ top = get(-2);
+ last = top[CHAR];
+ let ret = stack.pop();
+ return ret;
+ };
+
+ let i = 0, c = ""; // Current index and character, respectively.
+
+ // Reuse the old stack.
+ if (str && filter.substr(0, str.length) == str) {
+ i = str.length;
+ if (this.popStatement)
+ top[STATEMENTS].pop();
+ }
+ else {
+ stack = [];
+ functions = [];
+ push("#root");
}
- else
- {
- // A word character following a non-word character, or simply a non-word
- // character. Start a new statement.
- if (/[a-zA-Z_$]/.test(c) && !/[\w$]/.test(lastChar) || !/[\w\s$]/.test(c))
- top[STATEMENTS].push(i);
-
- // A "." or a "[" dereferences the last "statement" and effectively
- // joins it to this logical statement.
- if ((c == "." || c == "[") && /[\w$\])"']/.test(lastNonwhite)
- || lastNonwhite == "." && /[a-zA-Z_$]/.test(c))
- top[STATEMENTS].pop();
-
- switch (c)
- {
+
+ // Build a parse stack, discarding entries as opening characters
+ // match closing characters. The stack is walked from the top entry
+ // and down as many levels as it takes us to figure out what it is
+ // that we're completing.
+ str = filter;
+ let length = str.length;
+ for (; i < length; lastChar = c, i++) {
+ c = str[i];
+ if (last == '"' || last == "'" || last == "/") {
+ if (lastChar == "\\") { // Escape. Skip the next char, whatever it may be.
+ c = "";
+ i++;
+ }
+ else if (c == last)
+ pop(c);
+ }
+ else {
+ // A word character following a non-word character, or simply a non-word
+ // character. Start a new statement.
+ if (/[a-zA-Z_$]/.test(c) && !/[\w$]/.test(lastChar) || !/[\w\s$]/.test(c))
+ top[STATEMENTS].push(i);
+
+ // A "." or a "[" dereferences the last "statement" and effectively
+ // joins it to this logical statement.
+ if ((c == "." || c == "[") && /[\w$\])"']/.test(lastNonwhite)
+ || lastNonwhite == "." && /[a-zA-Z_$]/.test(c))
+ top[STATEMENTS].pop();
+
+ switch (c) {
case "(":
// Function call, or if/while/for/...
- if (/[\w$]/.test(lastNonwhite))
- {
+ if (/[\w$]/.test(lastNonwhite)) {
functions.push(i);
top[FUNCTIONS].push(i);
top[STATEMENTS].pop();
@@ -946,464 +983,294 @@ function Completion() //{{{
case ",":
top[COMMA].push(i);
break;
- }
+ }
- if (/\S/.test(c))
- lastNonwhite = c;
+ if (/\S/.test(c))
+ lastNonwhite = c;
+ }
}
- }
- this.popStatement = false;
- if (!/[\w$]/.test(lastChar) && lastNonwhite != ".")
- {
- this.popStatement = true;
- top[STATEMENTS].push(i);
- }
-
- lastIdx = i;
- }
-
- this.complete = function _complete(context)
- {
- this.context = context;
+ this.popStatement = false;
+ if (!/[\w$]/.test(lastChar) && lastNonwhite != ".") {
+ this.popStatement = true;
+ top[STATEMENTS].push(i);
+ }
- let self = this;
- try
- {
- buildStack.call(this, context.filter);
+ lastIdx = i;
}
- catch (e)
- {
- if (e.message != "Invalid JS")
- liberator.reportError(e);
- lastIdx = 0;
- return;
- }
-
- let cache = this.context.cache;
- this.context.getCache("eval", Object);
- this.context.getCache("evalContext", function () ({ __proto__: userContext }));
- // Okay, have parse stack. Figure out what we're completing.
+ this.complete = function _complete(context) {
+ this.context = context;
- // Find any complete statements that we can eval before we eval our object.
- // This allows for things like: let doc = window.content.document; let elem = doc.createElement...; elem.<Tab>
- let prev = 0;
- for (let [, v] in Iterator(get(0)[FULL_STATEMENTS]))
- {
- let key = str.substring(prev, v + 1);
- if (checkFunction(prev, v, key))
+ let self = this;
+ try {
+ buildStack.call(this, context.filter);
+ }
+ catch (e) {
+ if (e.message != "Invalid JS")
+ liberator.reportError(e);
+ lastIdx = 0;
return;
- this.eval(key);
- prev = v + 1;
- }
-
- // Don't eval any function calls unless the user presses tab.
- function checkFunction(start, end, key)
- {
- let res = functions.some(function (idx) idx >= start && idx < end);
- if (!res || self.context.tabPressed || key in cache.eval)
- return false;
- self.context.waitingForTab = true;
- return true;
- }
-
- // For each DOT in a statement, prefix it with TMP, eval it,
- // and save the result back to TMP. The point of this is to
- // cache the entire path through an object chain, mainly in
- // the presence of function calls. There are drawbacks. For
- // instance, if the value of a variable changes in the course
- // of inputting a command (let foo=bar; frob(foo); foo=foo.bar; ...),
- // we'll still use the old value. But, it's worth it.
- function getObj(frame, stop)
- {
- let statement = get(frame, 0, STATEMENTS) || 0; // Current statement.
- let prev = statement;
- let obj;
- let cacheKey;
- for (let [i, dot] in Iterator(get(frame)[DOTS].concat(stop)))
- {
- if (dot < statement)
- continue;
- if (dot > stop || dot <= prev)
- break;
- let s = str.substring(prev, dot);
-
- if (prev != statement)
- s = EVAL_TMP + "." + s;
- cacheKey = str.substring(statement, dot);
-
- if (checkFunction(prev, dot, cacheKey))
- return [];
-
- prev = dot + 1;
- obj = self.eval(s, cacheKey, obj);
}
- return [[obj, cacheKey]];
- }
-
- function getObjKey(frame)
- {
- let dot = get(frame, 0, DOTS) || -1; // Last dot in frame.
- let statement = get(frame, 0, STATEMENTS) || 0; // Current statement.
- let end = (frame == -1 ? lastIdx : get(frame + 1)[OFFSET]);
-
- cacheKey = null;
- let obj = [[cache.evalContext, "Local Variables"],
- [userContext, "Global Variables"],
- [modules, "modules"],
- [window, "window"]]; // Default objects;
- // Is this an object dereference?
- if (dot < statement) // No.
- dot = statement - 1;
- else // Yes. Set the object to the string before the dot.
- obj = getObj(frame, dot);
-
- let [, space, key] = str.substring(dot + 1, end).match(/^(\s*)(.*)/);
- return [dot + 1 + space.length, obj, key];
- }
-
- function fill(context, obj, name, compl, anchored, key, last, offset)
- {
- context.title = [name];
- context.anchored = anchored;
- context.filter = key;
- context.itemCache = context.parent.itemCache;
- context.key = name;
- if (last != null)
- context.quote = [last, function (text) util.escapeString(text.substr(offset), ""), last];
- else // We're not looking for a quoted string, so filter out anything that's not a valid identifier
- context.filters.push(function (item) /^[a-zA-Z_$][\w$]*$/.test(item.text));
+ let cache = this.context.cache;
+ this.context.getCache("eval", Object);
+ this.context.getCache("evalContext", function () ({ __proto__: userContext }));
- compl.call(self, context, obj);
- }
+ // Okay, have parse stack. Figure out what we're completing.
- function complete(objects, key, compl, string, last)
- {
- let orig = compl;
- if (!compl)
- {
- compl = function (context, obj)
- {
- context.process = [null, function highlight(item, v) template.highlight(v, true)];
- // Sort in a logical fashion for object keys:
- // Numbers are sorted as numbers, rather than strings, and appear first.
- // Constants are unsorted, and appear before other non-null strings.
- // Other strings are sorted in the default manner.
- let compare = context.compare;
- function isnan(item) item != '' && isNaN(item);
- context.compare = function (a, b)
- {
- if (!isnan(a.item.key) && !isnan(b.item.key))
- return a.item.key - b.item.key;
- return isnan(b.item.key) - isnan(a.item.key) || compare(a, b);
- };
- if (!context.anchored) // We've already listed anchored matches, so don't list them again here.
- context.filters.push(function (item) util.compareIgnoreCase(item.text.substr(0, this.filter.length), this.filter));
- if (obj == cache.evalContext)
- context.regenerate = true;
- context.generate = function () self.objectKeys(obj);
- };
- }
- // TODO: Make this a generic completion helper function.
- let filter = key + (string || "");
- for (let [, obj] in Iterator(objects))
- {
- this.context.fork(obj[1], top[OFFSET], this, fill,
- obj[0], obj[1], compl, compl != orig, filter, last, key.length);
- }
- if (orig)
- return;
- for (let [, obj] in Iterator(objects))
- {
- obj[1] += " (substrings)";
- this.context.fork(obj[1], top[OFFSET], this, fill,
- obj[0], obj[1], compl, false, filter, last, key.length);
+ // Find any complete statements that we can eval before we eval our object.
+ // This allows for things like: let doc = window.content.document; let elem = doc.createElement...; elem.<Tab>
+ let prev = 0;
+ for (let [, v] in Iterator(get(0)[FULL_STATEMENTS])) {
+ let key = str.substring(prev, v + 1);
+ if (checkFunction(prev, v, key))
+ return;
+ this.eval(key);
+ prev = v + 1;
}
- }
- // In a string. Check if we're dereferencing an object.
- // Otherwise, do nothing.
- if (last == "'" || last == '"')
- {
- //
- // str = "foo[bar + 'baz"
- // obj = "foo"
- // key = "bar + ''"
- //
-
- // The top of the stack is the sting we're completing.
- // Wrap it in its delimiters and eval it to process escape sequences.
- let string = str.substring(get(-1)[OFFSET] + 1, lastIdx);
- string = eval(last + string + last);
-
- function getKey()
- {
- if (last == "")
- return "";
- // After the opening [ upto the opening ", plus '' to take care of any operators before it
- let key = str.substring(get(-2, 0, STATEMENTS), get(-1, null, OFFSET)) + "''";
- // Now eval the key, to process any referenced variables.
- return this.eval(key);
+ // Don't eval any function calls unless the user presses tab.
+ function checkFunction(start, end, key) {
+ let res = functions.some(function (idx) idx >= start && idx < end);
+ if (!res || self.context.tabPressed || key in cache.eval)
+ return false;
+ self.context.waitingForTab = true;
+ return true;
}
- // Is this an object accessor?
- if (get(-2)[CHAR] == "[") // Are we inside of []?
- {
- // Stack:
- // [-1]: "...
- // [-2]: [...
- // [-3]: base statement
+ // For each DOT in a statement, prefix it with TMP, eval it,
+ // and save the result back to TMP. The point of this is to
+ // cache the entire path through an object chain, mainly in
+ // the presence of function calls. There are drawbacks. For
+ // instance, if the value of a variable changes in the course
+ // of inputting a command (let foo=bar; frob(foo); foo=foo.bar; ...),
+ // we'll still use the old value. But, it's worth it.
+ function getObj(frame, stop) {
+ let statement = get(frame, 0, STATEMENTS) || 0; // Current statement.
+ let prev = statement;
+ let obj;
+ let cacheKey;
+ for (let [i, dot] in Iterator(get(frame)[DOTS].concat(stop))) {
+ if (dot < statement)
+ continue;
+ if (dot > stop || dot <= prev)
+ break;
+ let s = str.substring(prev, dot);
- // Yes. If the [ starts at the beginning of a logical
- // statement, we're in an array literal, and we're done.
- if (get(-3, 0, STATEMENTS) == get(-2)[OFFSET])
- return null;
+ if (prev != statement)
+ s = Completion.Javascript.EVAL_TMP + "." + s;
+ cacheKey = str.substring(statement, dot);
- // Beginning of the statement upto the opening [
- let obj = getObj(-3, get(-2)[OFFSET]);
+ if (checkFunction(prev, dot, cacheKey))
+ return [];
- return void complete.call(this, obj, getKey(), null, string, last);
+ prev = dot + 1;
+ obj = self.eval(s, cacheKey, obj);
+ }
+ return [[obj, cacheKey]];
}
- // Is this a function call?
- if (get(-2)[CHAR] == "(")
- {
- // Stack:
- // [-1]: "...
- // [-2]: (...
- // [-3]: base statement
+ function getObjKey(frame) {
+ let dot = get(frame, 0, DOTS) || -1; // Last dot in frame.
+ let statement = get(frame, 0, STATEMENTS) || 0; // Current statement.
+ let end = (frame == -1 ? lastIdx : get(frame + 1)[OFFSET]);
+
+ cacheKey = null;
+ let obj = [[cache.evalContext, "Local Variables"],
+ [userContext, "Global Variables"],
+ [modules, "modules"],
+ [window, "window"]]; // Default objects;
+ // Is this an object dereference?
+ if (dot < statement) // No.
+ dot = statement - 1;
+ else // Yes. Set the object to the string before the dot.
+ obj = getObj(frame, dot);
+
+ let [, space, key] = str.substring(dot + 1, end).match(/^(\s*)(.*)/);
+ return [dot + 1 + space.length, obj, key];
+ }
- // Does the opening "(" mark a function call?
- if (get(-3, 0, FUNCTIONS) != get(-2)[OFFSET])
- return; // No. We're done.
+ function fill(context, obj, name, compl, anchored, key, last, offset) {
+ context.title = [name];
+ context.anchored = anchored;
+ context.filter = key;
+ context.itemCache = context.parent.itemCache;
+ context.key = name;
- let [offset, obj, func] = getObjKey(-3);
- if (!obj.length)
- return;
- obj = obj.slice(0, 1);
+ if (last != null)
+ context.quote = [last, function (text) util.escapeString(text.substr(offset), ""), last];
+ else // We're not looking for a quoted string, so filter out anything that's not a valid identifier
+ context.filters.push(function (item) /^[a-zA-Z_$][\w$]*$/.test(item.text));
+
+ compl.call(self, context, obj);
+ }
- try
- {
- var completer = obj[0][0][func].liberatorCompleter;
+ function complete(objects, key, compl, string, last) {
+ let orig = compl;
+ if (!compl) {
+ compl = function (context, obj) {
+ context.process = [null, function highlight(item, v) template.highlight(v, true)];
+ // Sort in a logical fashion for object keys:
+ // Numbers are sorted as numbers, rather than strings, and appear first.
+ // Constants are unsorted, and appear before other non-null strings.
+ // Other strings are sorted in the default manner.
+ let compare = context.compare;
+ function isnan(item) item != '' && isNaN(item);
+ context.compare = function (a, b) {
+ if (!isnan(a.item.key) && !isnan(b.item.key))
+ return a.item.key - b.item.key;
+ return isnan(b.item.key) - isnan(a.item.key) || compare(a, b);
+ };
+ if (!context.anchored) // We've already listed anchored matches, so don't list them again here.
+ context.filters.push(function (item) util.compareIgnoreCase(item.text.substr(0, this.filter.length), this.filter));
+ if (obj == cache.evalContext)
+ context.regenerate = true;
+ context.generate = function () self.objectKeys(obj);
+ };
}
- catch (e) {}
- if (!completer)
- completer = this.completers[func];
- if (!completer)
+ // TODO: Make this a generic completion helper function.
+ let filter = key + (string || "");
+ for (let [, obj] in Iterator(objects)) {
+ this.context.fork(obj[1], top[OFFSET], this, fill,
+ obj[0], obj[1], compl, compl != orig, filter, last, key.length);
+ }
+ if (orig)
return;
-
- // Split up the arguments
- let prev = get(-2)[OFFSET];
- let args = [];
- for (let [i, idx] in Iterator(get(-2)[COMMA]))
- {
- let arg = str.substring(prev + 1, idx);
- prev = idx;
- util.memoize(args, i, function () self.eval(arg));
+ for (let [, obj] in Iterator(objects)) {
+ obj[1] += " (substrings)";
+ this.context.fork(obj[1], top[OFFSET], this, fill,
+ obj[0], obj[1], compl, false, filter, last, key.length);
}
- let key = getKey();
- args.push(key + string);
-
- compl = function (context, obj)
- {
- let res = completer.call(self, context, func, obj, args);
- if (res)
- context.completions = res;
- };
-
- obj[0][1] += "." + func + "(... [" + args.length + "]";
- return void complete.call(this, obj, key, compl, string, last);
}
- // In a string that's not an obj key or a function arg.
- // Nothing to do.
- return;
- }
-
- //
- // str = "foo.bar.baz"
- // obj = "foo.bar"
- // key = "baz"
- //
- // str = "foo"
- // obj = [modules, window]
- // key = "foo"
- //
-
- let [offset, obj, key] = getObjKey(-1);
-
- // Wait for a keypress before completing the default objects.
- if (!this.context.tabPressed && key == "" && obj.length > 1)
- {
- this.context.waitingForTab = true;
- this.context.message = "Waiting for key press";
- return;
- }
-
- if (!/^(?:[a-zA-Z_$][\w$]*)?$/.test(key))
- return; // Not a word. Forget it. Can this even happen?
-
- try
- { // FIXME
- var o = top[OFFSET];
- top[OFFSET] = offset;
- return void complete.call(this, obj, key);
- }
- finally
- {
- top[OFFSET] = o;
- }
- }
- };
- let javascript = new Javascript();
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// PUBLIC SECTION //////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- const self = {
-
- setFunctionCompleter: function setFunctionCompleter(funcs, completers)
- {
- funcs = Array.concat(funcs);
- for (let [, func] in Iterator(funcs))
- {
- func.liberatorCompleter = function liberatorCompleter(context, func, obj, args) {
- let completer = completers[args.length - 1];
- if (!completer)
- return [];
- return completer.call(this, context, obj, args);
- };
- }
- },
-
- // FIXME
- _runCompleter: function _runCompleter(name, filter, maxItems)
- {
- let context = CompletionContext(filter);
- context.maxItems = maxItems;
- let res = context.fork.apply(context, ["run", 0, this, name].concat(Array.slice(arguments, 3)));
- if (res) // FIXME
- return { items: res.map(function (i) ({ item: i })) };
- context.wait(true);
- return context.allItems;
- },
+ // In a string. Check if we're dereferencing an object.
+ // Otherwise, do nothing.
+ if (last == "'" || last == '"') {
+ //
+ // str = "foo[bar + 'baz"
+ // obj = "foo"
+ // key = "bar + ''"
+ //
+
+ // The top of the stack is the sting we're completing.
+ // Wrap it in its delimiters and eval it to process escape sequences.
+ let string = str.substring(get(-1)[OFFSET] + 1, lastIdx);
+ string = eval(last + string + last);
+
+ function getKey() {
+ if (last == "")
+ return "";
+ // After the opening [ upto the opening ", plus '' to take care of any operators before it
+ let key = str.substring(get(-2, 0, STATEMENTS), get(-1, null, OFFSET)) + "''";
+ // Now eval the key, to process any referenced variables.
+ return this.eval(key);
+ }
- runCompleter: function runCompleter(name, filter, maxItems)
- {
- return this._runCompleter.apply(this, Array.slice(arguments))
- .items.map(function (i) i.item);
- },
+ // Is this an object accessor?
+ if (get(-2)[CHAR] == "[") { // Are we inside of []?
+ // Stack:
+ // [-1]: "...
+ // [-2]: [...
+ // [-3]: base statement
- listCompleter: function listCompleter(name, filter, maxItems)
- {
- let context = CompletionContext(filter || "");
- context.maxItems = maxItems;
- context.fork.apply(context, ["list", 0, completion, name].concat(Array.slice(arguments, 3)));
- context = context.contexts["/list"];
- context.wait();
-
- let list = template.commandOutput(
- <div highlight="Completions">
- { template.completionRow(context.title, "CompTitle") }
- { template.map(context.items, function (item) context.createRow(item), null, 100) }
- </div>);
- commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
- },
+ // Yes. If the [ starts at the beginning of a logical
+ // statement, we're in an array literal, and we're done.
+ if (get(-3, 0, STATEMENTS) == get(-2)[OFFSET])
+ return null;
- ////////////////////////////////////////////////////////////////////////////////
- ////////////////////// COMPLETION TYPES ////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
+ // Beginning of the statement upto the opening [
+ let obj = getObj(-3, get(-2)[OFFSET]);
- get javascriptCompleter() javascript,
+ return void complete.call(this, obj, getKey(), null, string, last);
+ }
- javascript: function _javascript(context) javascript.complete(context),
+ // Is this a function call?
+ if (get(-2)[CHAR] == "(") {
+ // Stack:
+ // [-1]: "...
+ // [-2]: (...
+ // [-3]: base statement
+
+ // Does the opening "(" mark a function call?
+ if (get(-3, 0, FUNCTIONS) != get(-2)[OFFSET])
+ return; // No. We're done.
+
+ let [offset, obj, func] = getObjKey(-3);
+ if (!obj.length)
+ return;
+ obj = obj.slice(0, 1);
+
+ try {
+ var completer = obj[0][0][func].liberatorCompleter;
+ }
+ catch (e) {}
+ if (!completer)
+ completer = this.completers[func];
+ if (!completer)
+ return;
+
+ // Split up the arguments
+ let prev = get(-2)[OFFSET];
+ let args = [];
+ for (let [i, idx] in Iterator(get(-2)[COMMA])) {
+ let arg = str.substring(prev + 1, idx);
+ prev = idx;
+ util.memoize(args, i, function () self.eval(arg));
+ }
+ let key = getKey();
+ args.push(key + string);
+
+ compl = function (context, obj) {
+ let res = completer.call(self, context, func, obj, args);
+ if (res)
+ context.completions = res;
+ };
- // filter a list of urls
- //
- // may consist of search engines, filenames, bookmarks and history,
- // depending on the 'complete' option
- // if the 'complete' argument is passed like "h", it temporarily overrides the complete option
- url: function url(context, complete)
- {
- let numLocationCompletions = 0; // how many async completions did we already return to the caller?
- let start = 0;
- let skip = 0;
+ obj[0][1] += "." + func + "(... [" + args.length + "]";
+ return void complete.call(this, obj, key, compl, string, last);
+ }
- if (options["urlseparator"])
- skip = context.filter.match("^.*" + options["urlseparator"]); // start after the last 'urlseparator'
+ // In a string that's not an obj key or a function arg.
+ // Nothing to do.
+ return;
+ }
- if (skip)
- context.advance(skip[0].length);
+ //
+ // str = "foo.bar.baz"
+ // obj = "foo.bar"
+ // key = "baz"
+ //
+ // str = "foo"
+ // obj = [modules, window]
+ // key = "foo"
+ //
- // Will, and should, throw an error if !(c in opts)
- Array.forEach(complete || options["complete"], function (c) {
- let completer = completion.urlCompleters[c];
- context.fork.apply(context, [c, 0, completion, completer.completer].concat(completer.args));
- });
- },
+ let [offset, obj, key] = getObjKey(-1);
- urlCompleters: {},
+ // Wait for a keypress before completing the default objects.
+ if (!this.context.tabPressed && key == "" && obj.length > 1) {
+ this.context.waitingForTab = true;
+ this.context.message = "Waiting for key press";
+ return;
+ }
- addUrlCompleter: function addUrlCompleter(opt)
- {
- let completer = UrlCompleter.apply(null, Array.slice(arguments));
- completer.args = Array.slice(arguments, completer.length);
- this.urlCompleters[opt] = completer;
- },
+ if (!/^(?:[a-zA-Z_$][\w$]*)?$/.test(key))
+ return; // Not a word. Forget it. Can this even happen?
- urls: function (context, tags)
- {
- let compare = String.localeCompare;
- let contains = String.indexOf;
- if (context.ignoreCase)
- {
- compare = util.compareIgnoreCase;
- contains = function (a, b) a && a.toLowerCase().indexOf(b.toLowerCase()) > -1;
+ try { // FIXME
+ var o = top[OFFSET];
+ top[OFFSET] = offset;
+ return void complete.call(this, obj, key);
+ }
+ finally {
+ top[OFFSET] = o;
+ }
}
-
- if (tags)
- context.filters.push(function (item) tags.
- every(function (tag) (item.tags || []).
- some(function (t) !compare(tag, t))));
-
- context.anchored = false;
- if (!context.title)
- context.title = ["URL", "Title"];
-
- context.fork("additional", 0, this, function (context) {
- context.title[0] += " (additional)";
- context.filter = context.parent.filter; // FIXME
- context.completions = context.parent.completions;
- // For items whose URL doesn't exactly match the filter,
- // accept them if all tokens match either the URL or the title.
- // Filter out all directly matching strings.
- let match = context.filters[0];
- context.filters[0] = function (item) !match.call(this, item);
- // and all that don't match the tokens.
- let tokens = context.filter.split(/\s+/);
- context.filters.push(function (item) tokens.every(
- function (tok) contains(item.url, tok) ||
- contains(item.title, tok)));
-
- let re = RegExp(tokens.filter(util.identity).map(util.escapeRegex).join("|"), "g");
- function highlight(item, text, i) process[i].call(this, item, template.highlightRegexp(text, re));
- let process = [template.icon, function (item, k) k];
- context.process = [
- function (item, text) highlight.call(this, item, item.text, 0),
- function (item, text) highlight.call(this, item, text, 1)
- ];
- });
- }
- //}}}
- };
-
- const UrlCompleter = new Struct("name", "description", "completer");
-
- return self;
- //}}}
-} //}}}
+ },
+ }, {
+ EVAL_TMP: "__liberator_eval_tmp",
+ }),
+});
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/editor.js b/common/content/editor.js
index 7b752a3f..2755d8ea 100644
--- a/common/content/editor.js
+++ b/common/content/editor.js
@@ -10,1187 +10,1064 @@
// http://developer.mozilla.org/en/docs/Editor_Embedding_Guide
/** @instance editor */
-function Editor() //{{{
-{
- ////////////////////////////////////////////////////////////////////////////////
- ////////////////////// PRIVATE SECTION /////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- // store our last search with f, F, t or T
- var lastFindChar = null;
- var lastFindCharFunc = null;
- // XXX: this strikes me as a rather odd ds; everyone's a critic --djk
- var abbreviations = {}; // abbreviations["lhr"][0]["{i,c,!}","rhs"]
-
- // (summarized from Vim's ":help abbreviations")
- //
- // There are three types of abbreviations:
- //
- // full-id: Consists entirely of keyword characters.
- // ("foo", "g3", "-1")
- //
- // end-id: Ends in a keyword character, but all other
- // are not keyword characters.
- // ("#i", "..f", "$/7")
- //
- // non-id: Ends in a non-keyword character, but the
- // others can be of any type other than space
- // and tab.
- // ("def#", "4/7$")
- //
- // Example strings that cannot be abbreviations:
- // "a.b", "#def", "a b", "_$r"
- //
- // For now, a keyword character is anything except for \s, ", or '
- // (i.e., whitespace and quotes). In Vim, a keyword character is
- // specified by the 'iskeyword' setting and is a much less inclusive
- // list.
- //
- // TODO: Make keyword definition closer to Vim's default keyword
- // definition (which differs across platforms).
- //
+const Editor = Module("editor", {
+ init: function () {
+ // store our last search with f, F, t or T
+ //
+ this._lastFindChar = null;
+ this._lastFindCharFunc = null;
+ // XXX: this strikes me as a rather odd ds; everyone's a critic --djk
+ this._abbreviations = {}; // this._abbreviations["lhr"][0]["{i,c,!}","rhs"]
- let nonkw = "\\s\"'";
- let keyword = "[^" + nonkw + "]";
- let nonkeyword = "[" + nonkw + "]";
+ // (summarized from Vim's ":help this._abbreviations")
+ //
+ // There are three types of this._abbreviations:
+ //
+ // full-id: Consists entirely of keyword characters.
+ // ("foo", "g3", "-1")
+ //
+ // end-id: Ends in a keyword character, but all other
+ // are not keyword characters.
+ // ("#i", "..f", "$/7")
+ //
+ // non-id: Ends in a non-keyword character, but the
+ // others can be of any type other than space
+ // and tab.
+ // ("def#", "4/7$")
+ //
+ // Example strings that cannot be this._abbreviations:
+ // "a.b", "#def", "a b", "_$r"
+ //
+ // For now, a keyword character is anything except for \s, ", or '
+ // (i.e., whitespace and quotes). In Vim, a keyword character is
+ // specified by the 'iskeyword' setting and is a much less inclusive
+ // list.
+ //
+ // TODO: Make keyword definition closer to Vim's default keyword
+ // definition (which differs across platforms).
+ //
- let fullId = keyword + "+";
- let endId = nonkeyword + "+" + keyword;
- let nonId = "\\S*" + nonkeyword;
+ let nonkw = "\\s\"'";
+ let keyword = "[^" + nonkw + "]";
+ let nonkeyword = "[" + nonkw + "]";
- // Used in addAbbrevation and expandAbbreviation
- var abbrevmatch = fullId + "|" + endId + "|" + nonId;
+ let fullId = keyword + "+";
+ let endId = nonkeyword + "+" + keyword;
+ let nonId = "\\S*" + nonkeyword;
- function getEditor() liberator.focus;
+ // Used in addAbbrevation and expandAbbreviation
+ this._abbrevmatch = fullId + "|" + endId + "|" + nonId;
- function getController()
- {
- let ed = getEditor();
- if (!ed || !ed.controllers)
- return null;
+ },
- return ed.controllers.getControllerForCommand("cmd_beginLine");
- }
- function selectPreviousLine()
- {
- editor.executeCommand("cmd_selectLinePrevious");
- if ((modes.extended & modes.LINE) && !editor.selectedText())
- editor.executeCommand("cmd_selectLinePrevious");
- }
- function selectNextLine()
- {
- editor.executeCommand("cmd_selectLineNext");
- if ((modes.extended & modes.LINE) && !editor.selectedText())
- editor.executeCommand("cmd_selectLineNext");
- }
+ // For the record, some of this code I've just finished throwing
+ // away makes me want to pull someone else's hair out. --Kris
+ abbrevs: function () {
+ for (let [lhs, abbr] in Iterator(this._abbreviations))
+ for (let [, rhs] in Iterator(abbr))
+ yield [lhs, rhs];
+ },
+
+ line: function () {
+ let line = 1;
+ let text = Editor.getEditor().value;
+ for (let i = 0; i < Editor.getEditor().selectionStart; i++)
+ if (text[i] == "\n")
+ line++;
+ return line;
+ },
+
+ col: function () {
+ let col = 1;
+ let text = Editor.getEditor().value;
+ for (let i = 0; i < Editor.getEditor().selectionStart; i++) {
+ col++;
+ if (text[i] == "\n")
+ col = 1;
+ }
+ return col;
+ },
+
+ unselectText: function () {
+ let elem = liberator.focus;
+ // A error occurs if the element has been removed when "elem.selectionStart" is executed.
+ try {
+ if (elem && elem.selectionEnd)
+ elem.selectionEnd = elem.selectionStart;
+ }
+ catch (e) {}
+ },
+
+ selectedText: function () {
+ let text = Editor.getEditor().value;
+ return text.substring(Editor.getEditor().selectionStart, Editor.getEditor().selectionEnd);
+ },
+
+ pasteClipboard: function () {
+ if (liberator.has("Win32")) {
+ this.executeCommand("cmd_paste");
+ return;
+ }
- // add mappings for commands like h,j,k,l,etc. in CARET, VISUAL and TEXTAREA mode
- function addMovementMap(keys, hasCount, caretModeMethod, caretModeArg, textareaCommand, visualTextareaCommand)
- {
- let extraInfo = {};
- if (hasCount)
- extraInfo.count = true;
+ // FIXME: #93 (<s-insert> in the bottom of a long textarea bounces up)
+ let elem = liberator.focus;
+
+ if (elem.setSelectionRange && util.readFromClipboard()) {
+ // readFromClipboard would return 'undefined' if not checked
+ // dunno about .setSelectionRange
+ // This is a hacky fix - but it works.
+ let curTop = elem.scrollTop;
+ let curLeft = elem.scrollLeft;
+
+ let rangeStart = elem.selectionStart; // caret position
+ let rangeEnd = elem.selectionEnd;
+ let tempStr1 = elem.value.substring(0, rangeStart);
+ let tempStr2 = util.readFromClipboard();
+ let tempStr3 = elem.value.substring(rangeEnd);
+ elem.value = tempStr1 + tempStr2 + tempStr3;
+ elem.selectionStart = rangeStart + tempStr2.length;
+ elem.selectionEnd = elem.selectionStart;
+
+ elem.scrollTop = curTop;
+ elem.scrollLeft = curLeft;
+ }
+ },
- mappings.add([modes.CARET], keys, "",
- function (count)
- {
- if (typeof count != "number" || count < 1)
- count = 1;
+ // count is optional, defaults to 1
+ executeCommand: function (cmd, count) {
+ let controller = Editor.getController();
+ if (!controller || !controller.supportsCommand(cmd) || !controller.isCommandEnabled(cmd)) {
+ liberator.beep();
+ return false;
+ }
- let controller = buffer.selectionController;
- while (count--)
- controller[caretModeMethod](caretModeArg, false);
- },
- extraInfo);
+ if (typeof count != "number" || count < 1)
+ count = 1;
+
+ let didCommand = false;
+ while (count--) {
+ // some commands need this try/catch workaround, because a cmd_charPrevious triggered
+ // at the beginning of the textarea, would hang the doCommand()
+ // good thing is, we need this code anyway for proper beeping
+ try {
+ controller.doCommand(cmd);
+ didCommand = true;
+ }
+ catch (e) {
+ if (!didCommand)
+ liberator.beep();
+ return false;
+ }
+ }
- mappings.add([modes.VISUAL], keys, "",
- function (count)
- {
- if (typeof count != "number" || count < 1 || !hasCount)
- count = 1;
+ return true;
+ },
- let controller = buffer.selectionController;
- while (count--)
- {
- if (modes.extended & modes.TEXTAREA)
- {
- if (typeof visualTextareaCommand == "function")
- visualTextareaCommand();
- else
- editor.executeCommand(visualTextareaCommand);
- }
- else
- controller[caretModeMethod](caretModeArg, true);
- }
- },
- extraInfo);
+ // cmd = y, d, c
+ // motion = b, 0, gg, G, etc.
+ executeCommandWithMotion: function (cmd, motion, count) {
+ if (typeof count != "number" || count < 1)
+ count = 1;
- mappings.add([modes.TEXTAREA], keys, "",
- function (count)
- {
- if (typeof count != "number" || count < 1)
- count = 1;
+ if (cmd == motion) {
+ motion = "j";
+ count--;
+ }
- editor.executeCommand(textareaCommand, count);
- },
- extraInfo);
- }
+ modes.set(modes.VISUAL, modes.TEXTAREA);
+
+ switch (motion) {
+ case "j":
+ this.executeCommand("cmd_beginLine", 1);
+ this.executeCommand("cmd_selectLineNext", count + 1);
+ break;
+ case "k":
+ this.executeCommand("cmd_beginLine", 1);
+ this.executeCommand("cmd_lineNext", 1);
+ this.executeCommand("cmd_selectLinePrevious", count + 1);
+ break;
+ case "h":
+ this.executeCommand("cmd_selectCharPrevious", count);
+ break;
+ case "l":
+ this.executeCommand("cmd_selectCharNext", count);
+ break;
+ case "e":
+ case "w":
+ this.executeCommand("cmd_selectWordNext", count);
+ break;
+ case "b":
+ this.executeCommand("cmd_selectWordPrevious", count);
+ break;
+ case "0":
+ case "^":
+ this.executeCommand("cmd_selectBeginLine", 1);
+ break;
+ case "$":
+ this.executeCommand("cmd_selectEndLine", 1);
+ break;
+ case "gg":
+ this.executeCommand("cmd_endLine", 1);
+ this.executeCommand("cmd_selectTop", 1);
+ this.executeCommand("cmd_selectBeginLine", 1);
+ break;
+ case "G":
+ this.executeCommand("cmd_beginLine", 1);
+ this.executeCommand("cmd_selectBottom", 1);
+ this.executeCommand("cmd_selectEndLine", 1);
+ break;
+
+ default:
+ liberator.beep();
+ return false;
+ }
- // add mappings for commands like i,a,s,c,etc. in TEXTAREA mode
- function addBeginInsertModeMap(keys, commands)
- {
- mappings.add([modes.TEXTAREA], keys, "",
- function (count)
- {
- commands.forEach(function (cmd)
- editor.executeCommand(cmd, 1));
- modes.set(modes.INSERT, modes.TEXTAREA);
- });
- }
+ switch (cmd) {
+ case "d":
+ this.executeCommand("cmd_delete", 1);
+ // need to reset the mode as the visual selection changes it
+ modes.main = modes.TEXTAREA;
+ break;
+ case "c":
+ this.executeCommand("cmd_delete", 1);
+ modes.set(modes.INSERT, modes.TEXTAREA);
+ break;
+ case "y":
+ this.executeCommand("cmd_copy", 1);
+ this.unselectText();
+ break;
- function addMotionMap(key)
- {
- mappings.add([modes.TEXTAREA], [key],
- "Motion command",
- function (motion, count) { editor.executeCommandWithMotion(key, motion, count); },
- { count: true, motion: true });
- }
+ default:
+ liberator.beep();
+ return false;
+ }
+ return true;
+ },
+
+ // This function will move/select up to given "pos"
+ // Simple setSelectionRange() would be better, but we want to maintain the correct
+ // order of selectionStart/End (a Gecko bug always makes selectionStart <= selectionEnd)
+ // Use only for small movements!
+ moveToPosition: function (pos, forward, select) {
+ if (!select) {
+ Editor.getEditor().setSelectionRange(pos, pos);
+ return;
+ }
- // For the record, some of this code I've just finished throwing
- // away makes me want to pull someone else's hair out. --Kris
- function abbrevs()
- {
- for (let [lhs, abbr] in Iterator(abbreviations))
- for (let [, rhs] in Iterator(abbr))
- yield [lhs, rhs];
- }
+ if (forward) {
+ if (pos <= Editor.getEditor().selectionEnd || pos > Editor.getEditor().value.length)
+ return false;
- // mode = "i" -> add :iabbrev, :iabclear and :iunabbrev commands
- function addAbbreviationCommands(ch, modeDescription)
- {
- let mode = ch || "!";
- modeDescription = modeDescription ? " in " + modeDescription + " mode" : "";
-
- commands.add([ch ? ch + "a[bbrev]" : "ab[breviate]"],
- "Abbreviate a key sequence" + modeDescription,
- function (args)
- {
- let matches = args.string.match(RegExp("^\\s*($|" + abbrevmatch + ")(?:\\s*$|\\s+(.*))"));
- liberator.assert(matches, "E474: Invalid argument");
-
- let [, lhs, rhs] = matches;
- if (rhs)
- editor.addAbbreviation(mode, lhs, rhs);
- else
- editor.listAbbreviations(mode, lhs || "");
- },
- {
- completer: function (context, args) completion.abbreviation(context, args, mode),
- literal: 0,
- serial: function () [
- {
- command: this.name,
- arguments: [lhs],
- literalArg: abbr[1]
- }
- for ([lhs, abbr] in abbrevs())
- if (abbr[0] == mode)
- ]
- });
+ do { // TODO: test code for endless loops
+ this.executeCommand("cmd_selectCharNext", 1);
+ }
+ while (Editor.getEditor().selectionEnd != pos);
+ }
+ else {
+ if (pos >= Editor.getEditor().selectionStart || pos < 0)
+ return false;
- commands.add([ch ? ch + "una[bbrev]" : "una[bbreviate]"],
- "Remove an abbreviation" + modeDescription,
- function (args) { editor.removeAbbreviation(mode, args.literalArg); },
- {
- argCount: "1",
- completer: function (context, args) completion.abbreviation(context, args, mode),
- literal: 0
- });
+ do { // TODO: test code for endless loops
+ this.executeCommand("cmd_selectCharPrevious", 1);
+ }
+ while (Editor.getEditor().selectionStart != pos);
+ }
+ },
- commands.add([ch + "abc[lear]"],
- "Remove all abbreviations" + modeDescription,
- function () { editor.removeAllAbbreviations(mode); },
- { argCount: "0" });
- }
+ // returns the position of char
+ findCharForward: function (ch, count) {
+ if (!Editor.getEditor())
+ return -1;
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// OPTIONS /////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- options.add(["editor"],
- "Set the external text editor",
- "string", "gvim -f");
-
- options.add(["insertmode", "im"],
- "Use Insert mode as the default for text areas",
- "boolean", true);
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// MAPPINGS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- var myModes = [modes.INSERT, modes.COMMAND_LINE];
-
- // KEYS COUNT CARET TEXTAREA VISUAL_TEXTAREA
- addMovementMap(["k", "<Up>"], true, "lineMove", false, "cmd_linePrevious", selectPreviousLine);
- addMovementMap(["j", "<Down>", "<Return>"], true, "lineMove", true, "cmd_lineNext", selectNextLine);
- addMovementMap(["h", "<Left>", "<BS>"], true, "characterMove", false, "cmd_charPrevious", "cmd_selectCharPrevious");
- addMovementMap(["l", "<Right>", "<Space>"], true, "characterMove", true, "cmd_charNext", "cmd_selectCharNext");
- addMovementMap(["b", "B", "<C-Left>"], true, "wordMove", false, "cmd_wordPrevious", "cmd_selectWordPrevious");
- addMovementMap(["w", "W", "e", "<C-Right>"], true, "wordMove", true, "cmd_wordNext", "cmd_selectWordNext");
- addMovementMap(["<C-f>", "<PageDown>"], true, "pageMove", true, "cmd_movePageDown", "cmd_selectNextPage");
- addMovementMap(["<C-b>", "<PageUp>"], true, "pageMove", false, "cmd_movePageUp", "cmd_selectPreviousPage");
- addMovementMap(["gg", "<C-Home>"], false, "completeMove", false, "cmd_moveTop", "cmd_selectTop");
- addMovementMap(["G", "<C-End>"], false, "completeMove", true, "cmd_moveBottom", "cmd_selectBottom");
- addMovementMap(["0", "^", "<Home>"], false, "intraLineMove", false, "cmd_beginLine", "cmd_selectBeginLine");
- addMovementMap(["$", "<End>"], false, "intraLineMove", true, "cmd_endLine" , "cmd_selectEndLine" );
-
- addBeginInsertModeMap(["i", "<Insert>"], []);
- addBeginInsertModeMap(["a"], ["cmd_charNext"]);
- addBeginInsertModeMap(["I", "gI"], ["cmd_beginLine"]);
- addBeginInsertModeMap(["A"], ["cmd_endLine"]);
- addBeginInsertModeMap(["s"], ["cmd_deleteCharForward"]);
- addBeginInsertModeMap(["S"], ["cmd_deleteToEndOfLine", "cmd_deleteToBeginningOfLine"]);
- addBeginInsertModeMap(["C"], ["cmd_deleteToEndOfLine"]);
-
- addMotionMap("d"); // delete
- addMotionMap("c"); // change
- addMotionMap("y"); // yank
-
- // insert mode mappings
- mappings.add(myModes,
- ["<C-w>"], "Delete previous word",
- function () { editor.executeCommand("cmd_deleteWordBackward", 1); });
-
- mappings.add(myModes,
- ["<C-u>"], "Delete until beginning of current line",
- function ()
- {
- // broken in FF3, deletes the whole line:
- // editor.executeCommand("cmd_deleteToBeginningOfLine", 1);
- editor.executeCommand("cmd_selectBeginLine", 1);
- if (getController().isCommandEnabled("cmd_delete"))
- editor.executeCommand("cmd_delete", 1);
- });
-
- mappings.add(myModes,
- ["<C-k>"], "Delete until end of current line",
- function () { editor.executeCommand("cmd_deleteToEndOfLine", 1); });
-
- mappings.add(myModes,
- ["<C-a>"], "Move cursor to beginning of current line",
- function () { editor.executeCommand("cmd_beginLine", 1); });
-
- mappings.add(myModes,
- ["<C-e>"], "Move cursor to end of current line",
- function () { editor.executeCommand("cmd_endLine", 1); });
-
- mappings.add(myModes,
- ["<C-h>"], "Delete character to the left",
- function () { editor.executeCommand("cmd_deleteCharBackward", 1); });
-
- mappings.add(myModes,
- ["<C-d>"], "Delete character to the right",
- function () { editor.executeCommand("cmd_deleteCharForward", 1); });
-
- /*mappings.add(myModes,
- ["<C-Home>"], "Move cursor to beginning of text field",
- function () { editor.executeCommand("cmd_moveTop", 1); });
-
- mappings.add(myModes,
- ["<C-End>"], "Move cursor to end of text field",
- function () { editor.executeCommand("cmd_moveBottom", 1); });*/
-
- mappings.add(myModes,
- ["<S-Insert>"], "Insert clipboard/selection",
- function () { editor.pasteClipboard(); });
-
- mappings.add([modes.INSERT, modes.TEXTAREA, modes.COMPOSE],
- ["<C-i>"], "Edit text field with an external editor",
- function () { editor.editFieldExternally(); });
-
- mappings.add([modes.INSERT],
- ["<C-t>"], "Edit text field in Vi mode",
- function () { liberator.mode = modes.TEXTAREA; });
-
- mappings.add([modes.INSERT],
- ["<Space>", "<Return>"], "Expand insert mode abbreviation",
- function () { editor.expandAbbreviation("i"); },
- { route: true });
-
- mappings.add([modes.INSERT],
- ["<Tab>"], "Expand insert mode abbreviation",
- function () { editor.expandAbbreviation("i"); document.commandDispatcher.advanceFocus(); });
-
- mappings.add([modes.INSERT],
- ["<C-]>", "<C-5>"], "Expand insert mode abbreviation",
- function () { editor.expandAbbreviation("i"); });
-
- // textarea mode
- mappings.add([modes.TEXTAREA],
- ["u"], "Undo",
- function (count)
- {
- editor.executeCommand("cmd_undo", count);
- liberator.mode = modes.TEXTAREA;
- },
- { count: true });
-
- mappings.add([modes.TEXTAREA],
- ["<C-r>"], "Redo",
- function (count)
- {
- editor.executeCommand("cmd_redo", count);
- liberator.mode = modes.TEXTAREA;
- },
- { count: true });
-
- mappings.add([modes.TEXTAREA],
- ["D"], "Delete the characters under the cursor until the end of the line",
- function () { editor.executeCommand("cmd_deleteToEndOfLine"); });
-
- mappings.add([modes.TEXTAREA],
- ["o"], "Open line below current",
- function (count)
- {
- editor.executeCommand("cmd_endLine", 1);
- modes.set(modes.INSERT, modes.TEXTAREA);
- events.feedkeys("<Return>");
- });
-
- mappings.add([modes.TEXTAREA],
- ["O"], "Open line above current",
- function (count)
- {
- editor.executeCommand("cmd_beginLine", 1);
- modes.set(modes.INSERT, modes.TEXTAREA);
- events.feedkeys("<Return>");
- editor.executeCommand("cmd_linePrevious", 1);
- });
-
- mappings.add([modes.TEXTAREA],
- ["X"], "Delete character to the left",
- function (count) { editor.executeCommand("cmd_deleteCharBackward", count); },
- { count: true });
-
- mappings.add([modes.TEXTAREA],
- ["x"], "Delete character to the right",
- function (count) { editor.executeCommand("cmd_deleteCharForward", count); },
- { count: true });
-
- // visual mode
- mappings.add([modes.CARET, modes.TEXTAREA],
- ["v"], "Start visual mode",
- function (count) { modes.set(modes.VISUAL, liberator.mode); });
-
- mappings.add([modes.VISUAL],
- ["v"], "End visual mode",
- function (count) { events.onEscape(); });
-
- mappings.add([modes.TEXTAREA],
- ["V"], "Start visual line mode",
- function (count)
- {
- modes.set(modes.VISUAL, modes.TEXTAREA | modes.LINE);
- editor.executeCommand("cmd_beginLine", 1);
- editor.executeCommand("cmd_selectLineNext", 1);
- });
-
- mappings.add([modes.VISUAL],
- ["c", "s"], "Change selected text",
- function (count)
- {
- if (modes.extended & modes.TEXTAREA)
- {
- editor.executeCommand("cmd_cut");
- modes.set(modes.INSERT, modes.TEXTAREA);
- }
- else
- liberator.beep();
- });
-
- mappings.add([modes.VISUAL],
- ["d"], "Delete selected text",
- function (count)
- {
- if (modes.extended & modes.TEXTAREA)
- {
- editor.executeCommand("cmd_cut");
- modes.set(modes.TEXTAREA);
- }
- else
- liberator.beep();
- });
-
- mappings.add([modes.VISUAL],
- ["y"], "Yank selected text",
- function (count)
- {
- if (modes.extended & modes.TEXTAREA)
- {
- editor.executeCommand("cmd_copy");
- modes.set(modes.TEXTAREA);
- }
- else
- {
- let sel = window.content.document.getSelection();
- if (sel)
- util.copyToClipboard(sel, true);
- else
- liberator.beep();
- }
- });
-
- mappings.add([modes.VISUAL, modes.TEXTAREA],
- ["p"], "Paste clipboard contents",
- function (count)
- {
- if (!(modes.extended & modes.CARET))
- {
- if (!count) count = 1;
- while (count--)
- editor.executeCommand("cmd_paste");
- liberator.mode = modes.TEXTAREA;
- }
- else
- liberator.beep();
- });
-
- // finding characters
- mappings.add([modes.TEXTAREA, modes.VISUAL],
- ["f"], "Move to a character on the current line after the cursor",
- function (count, arg)
- {
- let pos = editor.findCharForward(arg, count);
- if (pos >= 0)
- editor.moveToPosition(pos, true, liberator.mode == modes.VISUAL);
- },
- { arg: true, count: true });
-
- mappings.add([modes.TEXTAREA, modes.VISUAL],
- ["F"], "Move to a charater on the current line before the cursor",
- function (count, arg)
- {
- let pos = editor.findCharBackward(arg, count);
- if (pos >= 0)
- editor.moveToPosition(pos, false, liberator.mode == modes.VISUAL);
- },
- { arg: true, count: true });
-
- mappings.add([modes.TEXTAREA, modes.VISUAL],
- ["t"], "Move before a character on the current line",
- function (count, arg)
- {
- let pos = editor.findCharForward(arg, count);
- if (pos >= 0)
- editor.moveToPosition(pos - 1, true, liberator.mode == modes.VISUAL);
- },
- { arg: true, count: true });
-
- mappings.add([modes.TEXTAREA, modes.VISUAL],
- ["T"], "Move before a character on the current line, backwards",
- function (count, arg)
- {
- let pos = editor.findCharBackward(arg, count);
- if (pos >= 0)
- editor.moveToPosition(pos + 1, false, liberator.mode == modes.VISUAL);
- },
- { arg: true, count: true });
-
- // textarea and visual mode
- mappings.add([modes.TEXTAREA, modes.VISUAL],
- ["~"], "Switch case of the character under the cursor and move the cursor to the right",
- function (count)
- {
- if (modes.main == modes.VISUAL)
- count = getEditor().selectionEnd - getEditor().selectionStart;
- if (typeof count != "number" || count < 1)
- count = 1;
-
- while (count-- > 0)
- {
- let text = getEditor().value;
- let pos = getEditor().selectionStart;
- if (pos >= text.length)
- return void liberator.beep();
- let chr = text[pos];
- getEditor().value = text.substring(0, pos) +
- (chr == chr.toLocaleLowerCase() ? chr.toLocaleUpperCase() : chr.toLocaleLowerCase()) +
- text.substring(pos + 1);
- editor.moveToPosition(pos + 1, true, false);
- }
- modes.set(modes.TEXTAREA);
- },
- { count: true });
+ this._lastFindChar = ch;
+ this._lastFindCharFunc = this.findCharForward;
+
+ let text = Editor.getEditor().value;
+ if (!typeof count == "number" || count < 1)
+ count = 1;
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMMANDS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
+ for (let i = Editor.getEditor().selectionEnd + 1; i < text.length; i++) {
+ if (text[i] == "\n")
+ break;
+ if (text[i] == ch)
+ count--;
+ if (count == 0)
+ return i + 1; // always position the cursor after the char
+ }
- addAbbreviationCommands("", "");
- addAbbreviationCommands("i", "insert");
- addAbbreviationCommands("c", "command line");
+ liberator.beep();
+ return -1;
+ },
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMPLETIONS /////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
+ // returns the position of char
+ findCharBackward: function (ch, count) {
+ if (!Editor.getEditor())
+ return -1;
- liberator.registerObserver("load_completion", function () {
- // TODO: shouldn't all of these have a standard signature (context, args, ...)? --djk
- completion.abbreviation = function abbreviation(context, args, mode) {
- mode = mode || "!";
+ this._lastFindChar = ch;
+ this._lastFindCharFunc = this.findCharBackward;
- if (args.completeArg == 0)
- {
- let abbreviations = editor.getAbbreviations(mode);
- context.completions = [[lhs, ""] for ([, [, lhs,]] in Iterator(abbreviations))];
- }
- };
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// PUBLIC SECTION //////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- return {
-
- line: function ()
- {
- let line = 1;
- let text = getEditor().value;
- for (let i = 0; i < getEditor().selectionStart; i++)
- if (text[i] == "\n")
- line++;
- return line;
- },
-
- col: function ()
- {
- let col = 1;
- let text = getEditor().value;
- for (let i = 0; i < getEditor().selectionStart; i++)
- {
- col++;
- if (text[i] == "\n")
- col = 1;
- }
- return col;
- },
-
- unselectText: function ()
- {
- let elem = liberator.focus;
- // A error occurs if the element has been removed when "elem.selectionStart" is executed.
- try
- {
- if (elem && elem.selectionEnd)
- elem.selectionEnd = elem.selectionStart;
- }
- catch (e) {}
- },
-
- selectedText: function ()
- {
- let text = getEditor().value;
- return text.substring(getEditor().selectionStart, getEditor().selectionEnd);
- },
-
- pasteClipboard: function ()
- {
- if (liberator.has("Win32"))
- {
- this.executeCommand("cmd_paste");
- return;
- }
+ let text = Editor.getEditor().value;
+ if (!typeof count == "number" || count < 1)
+ count = 1;
- // FIXME: #93 (<s-insert> in the bottom of a long textarea bounces up)
- let elem = liberator.focus;
-
- if (elem.setSelectionRange && util.readFromClipboard())
- // readFromClipboard would return 'undefined' if not checked
- // dunno about .setSelectionRange
- {
- // This is a hacky fix - but it works.
- let curTop = elem.scrollTop;
- let curLeft = elem.scrollLeft;
-
- let rangeStart = elem.selectionStart; // caret position
- let rangeEnd = elem.selectionEnd;
- let tempStr1 = elem.value.substring(0, rangeStart);
- let tempStr2 = util.readFromClipboard();
- let tempStr3 = elem.value.substring(rangeEnd);
- elem.value = tempStr1 + tempStr2 + tempStr3;
- elem.selectionStart = rangeStart + tempStr2.length;
- elem.selectionEnd = elem.selectionStart;
+ for (let i = Editor.getEditor().selectionStart - 1; i >= 0; i--) {
+ if (text[i] == "\n")
+ break;
+ if (text[i] == ch)
+ count--;
+ if (count == 0)
+ return i;
+ }
- elem.scrollTop = curTop;
- elem.scrollLeft = curLeft;
- }
- },
-
- // count is optional, defaults to 1
- executeCommand: function (cmd, count)
- {
- let controller = getController();
- if (!controller || !controller.supportsCommand(cmd) || !controller.isCommandEnabled(cmd))
- {
- liberator.beep();
- return false;
- }
+ liberator.beep();
+ return -1;
+ },
- if (typeof count != "number" || count < 1)
- count = 1;
-
- let didCommand = false;
- while (count--)
- {
- // some commands need this try/catch workaround, because a cmd_charPrevious triggered
- // at the beginning of the textarea, would hang the doCommand()
- // good thing is, we need this code anyway for proper beeping
- try
- {
- controller.doCommand(cmd);
- didCommand = true;
+ editFileExternally: function (path) {
+ // TODO: save return value in v:shell_error
+ let args = commands.parseArgs(options["editor"], [], "*", true);
+
+ liberator.assert(args.length >= 1, "No editor specified");
+
+ args.push(path);
+ liberator.callFunctionInThread(null, io.run, io.expandPath(args.shift()), args, true);
+ },
+
+ // TODO: clean up with 2 functions for textboxes and currentEditor?
+ editFieldExternally: function (forceEditing) {
+ if (!options["editor"])
+ return false;
+
+ let textBox = null;
+ if (!(config.isComposeWindow))
+ textBox = liberator.focus;
+
+ if (!forceEditing && textBox && textBox.type == "password") {
+ commandline.input("Editing a password field externally will reveal the password. Would you like to continue? (yes/[no]): ",
+ function (resp) {
+ if (resp && resp.match(/^y(es)?$/i))
+ return editor.editFieldExternally(true);
+ });
+ return;
+ }
+
+ let text = ""; // XXX
+ if (textBox)
+ text = textBox.value;
+ else if (typeof GetCurrentEditor == "function") // Thunderbird composer
+ text = GetCurrentEditor().outputToString("text/plain", 2);
+ else
+ return false;
+
+ let oldBg, tmpBg;
+ try {
+ let res = io.withTempFiles(function (tmpfile) {
+ if (textBox) {
+ textBox.setAttribute("readonly", "true");
+ oldBg = textBox.style.backgroundColor;
+ tmpBg = "yellow";
+ textBox.style.backgroundColor = "#bbbbbb";
}
- catch (e)
- {
- if (!didCommand)
- liberator.beep();
- return false;
+
+ if (!tmpfile.write(text))
+ throw Error("Input contains characters not valid in the current " +
+ "file encoding");
+
+ this.editFileExternally(tmpfile.path);
+
+ if (textBox)
+ textBox.removeAttribute("readonly");
+
+ let val = tmpfile.read();
+ if (textBox)
+ textBox.value = val;
+ else {
+ let editor = GetCurrentEditor();
+ let wholeDocRange = editor.document.createRange();
+ let rootNode = editor.rootElement.QueryInterface(Ci.nsIDOMNode);
+ wholeDocRange.selectNodeContents(rootNode);
+ editor.selection.addRange(wholeDocRange);
+ editor.selection.deleteFromDocument();
+ editor.insertText(val);
}
- }
+ }, this);
+ if (res == false)
+ throw "Couldn't create temporary file";
+ }
+ catch (e) {
+ // Errors are unlikely, and our error messages won't
+ // likely be any more helpful than that given in the
+ // exception.
+ liberator.echoerr(e);
+ tmpBg = "red";
+ }
- return true;
- },
-
- // cmd = y, d, c
- // motion = b, 0, gg, G, etc.
- executeCommandWithMotion: function (cmd, motion, count)
- {
- if (typeof count != "number" || count < 1)
- count = 1;
-
- if (cmd == motion)
- {
- motion = "j";
- count--;
- }
+ // blink the textbox after returning
+ if (textBox) {
+ let colors = [tmpBg, oldBg, tmpBg, oldBg];
+ (function () {
+ textBox.style.backgroundColor = colors.shift();
+ if (colors.length > 0)
+ setTimeout(arguments.callee, 100);
+ })();
+ }
- modes.set(modes.VISUAL, modes.TEXTAREA);
+ return true;
+ },
- switch (motion)
- {
- case "j":
- this.executeCommand("cmd_beginLine", 1);
- this.executeCommand("cmd_selectLineNext", count + 1);
- break;
- case "k":
- this.executeCommand("cmd_beginLine", 1);
- this.executeCommand("cmd_lineNext", 1);
- this.executeCommand("cmd_selectLinePrevious", count + 1);
- break;
- case "h":
- this.executeCommand("cmd_selectCharPrevious", count);
- break;
- case "l":
- this.executeCommand("cmd_selectCharNext", count);
- break;
- case "e":
- case "w":
- this.executeCommand("cmd_selectWordNext", count);
- break;
- case "b":
- this.executeCommand("cmd_selectWordPrevious", count);
- break;
- case "0":
- case "^":
- this.executeCommand("cmd_selectBeginLine", 1);
- break;
- case "$":
- this.executeCommand("cmd_selectEndLine", 1);
- break;
- case "gg":
- this.executeCommand("cmd_endLine", 1);
- this.executeCommand("cmd_selectTop", 1);
- this.executeCommand("cmd_selectBeginLine", 1);
- break;
- case "G":
- this.executeCommand("cmd_beginLine", 1);
- this.executeCommand("cmd_selectBottom", 1);
- this.executeCommand("cmd_selectEndLine", 1);
- break;
+ // Abbreviations {{{
- default:
- liberator.beep();
- return false;
- }
+ // NOTE: I think this comment block is trying to say something but no
+ // one is listening. In space, no one can hear you scream. --djk
+ //
+ // System for adding this._abbreviations:
+ //
+ // filter == ! delete all, and set first (END)
+ //
+ // if filter == ! remove all and add it as only END
+ //
+ // variant 1: rhs matches anywhere in loop
+ //
+ // 1 mod matches anywhere in loop
+ // a) simple replace and
+ // I) (maybe there's another rhs that matches? not possible)
+ // (when there's another item, it's opposite mod with different rhs)
+ // (so do nothing further but END)
+ //
+ // 2 mod does not match
+ // a) the opposite is there -> make a ! and put it as only and END
+ // (b) a ! is there. do nothing END)
+ //
+ // variant 2: rhs matches *no*were in loop and filter is c or i
+ // every kind of current combo is possible to 1 {c,i,!} or two {c and i}
+ //
+ // 1 mod is ! split into two i + c END
+ // 1 not !: opposite mode (first), add/change 'second' and END
+ // 1 not !: same mode (first), overwrite first this END
+ //
+ // TODO: I don't like these funky filters, I am a funky filter hater. --djk
+ // : make this a separate object
+ // : use Struct for individual this._abbreviations
+ // : rename "filter" arg "mode"
+ /**
+ * Adds a new abbreviation. Abbreviations consist of a LHS (the text
+ * that is replaced when the abbreviation is expanded) and a RHS (the
+ * replacement text).
+ *
+ * @param {string} filter The mode filter. This specifies the modes in
+ * which this abbreviation is available. Either:
+ * "c" - applies in command-line mode
+ * "i" - applies in insert mode
+ * "!" - applies in both command-line and insert modes
+ * @param {string} lhs The LHS of the abbreviation.
+ * @param {string} rhs The RHS of the abbreviation.
+ */
+ addAbbreviation: function (filter, lhs, rhs) {
+ if (!this._abbreviations[lhs]) {
+ this._abbreviations[lhs] = [];
+ this._abbreviations[lhs][0] = [filter, rhs];
+ return;
+ }
- switch (cmd)
- {
- case "d":
- this.executeCommand("cmd_delete", 1);
- // need to reset the mode as the visual selection changes it
- modes.main = modes.TEXTAREA;
- break;
- case "c":
- this.executeCommand("cmd_delete", 1);
- modes.set(modes.INSERT, modes.TEXTAREA);
- break;
- case "y":
- this.executeCommand("cmd_copy", 1);
- this.unselectText();
- break;
+ if (filter == "!") {
+ if (this._abbreviations[lhs][1])
+ this._abbreviations[lhs][1] = "";
+ this._abbreviations[lhs][0] = [filter, rhs];
+ return;
+ }
- default:
- liberator.beep();
- return false;
- }
- return true;
- },
-
- // This function will move/select up to given "pos"
- // Simple setSelectionRange() would be better, but we want to maintain the correct
- // order of selectionStart/End (a Gecko bug always makes selectionStart <= selectionEnd)
- // Use only for small movements!
- moveToPosition: function (pos, forward, select)
- {
- if (!select)
- {
- getEditor().setSelectionRange(pos, pos);
- return;
+ for (let i = 0; i < this._abbreviations[lhs].length; i++) {
+ if (this._abbreviations[lhs][i][1] == rhs) {
+ if (this._abbreviations[lhs][i][0] == filter) {
+ this._abbreviations[lhs][i] = [filter, rhs];
+ return;
+ }
+ else {
+ if (this._abbreviations[lhs][i][0] != "!") {
+ if (this._abbreviations[lhs][1])
+ this._abbreviations[lhs][1] = "";
+ this._abbreviations[lhs][0] = ["!", rhs];
+ return;
+ }
+ else
+ return;
+ }
}
+ }
+
+ if (this._abbreviations[lhs][0][0] == "!") {
+ let tmpOpp = ("i" == filter) ? "c" : "i";
+ this._abbreviations[lhs][1] = [tmpOpp, this._abbreviations[lhs][0][1]];
+ this._abbreviations[lhs][0] = [filter, rhs];
+ return;
+ }
- if (forward)
- {
- if (pos <= getEditor().selectionEnd || pos > getEditor().value.length)
- return false;
+ if (this._abbreviations[lhs][0][0] != filter)
+ this._abbreviations[lhs][1] = [filter, rhs];
+ else
+ this._abbreviations[lhs][0] = [filter, rhs];
+ },
+
+ /**
+ * Expands an abbreviation in the currently active textbox.
+ *
+ * @param {string} filter The mode filter.
+ * @see #addAbbreviation
+ */
+ expandAbbreviation: function (filter) {
+ let textbox = Editor.getEditor();
+ if (!textbox)
+ return;
+ let text = textbox.value;
+ let currStart = textbox.selectionStart;
+ let currEnd = textbox.selectionEnd;
+ let foundWord = text.substring(0, currStart).replace(RegExp("^(.|\\n)*?\\s*(" + this._abbrevmatch + ")$", "m"), "$2"); // get last word \b word boundary
+ if (!foundWord)
+ return true;
- do // TODO: test code for endless loops
- {
- this.executeCommand("cmd_selectCharNext", 1);
+ for (let lhs in this._abbreviations) {
+ for (let i = 0; i < this._abbreviations[lhs].length; i++) {
+ if (lhs == foundWord && (this._abbreviations[lhs][i][0] == filter || this._abbreviations[lhs][i][0] == "!")) {
+ // if found, replace accordingly
+ let len = foundWord.length;
+ let abbrText = this._abbreviations[lhs][i][1];
+ text = text.substring(0, currStart - len) + abbrText + text.substring(currStart);
+ textbox.value = text;
+ textbox.selectionStart = currStart - len + abbrText.length;
+ textbox.selectionEnd = currEnd - len + abbrText.length;
+ break;
}
- while (getEditor().selectionEnd != pos);
}
- else
- {
- if (pos >= getEditor().selectionStart || pos < 0)
- return false;
-
- do // TODO: test code for endless loops
- {
- this.executeCommand("cmd_selectCharPrevious", 1);
+ }
+ return true;
+ },
+
+ /**
+ * Returns all this._abbreviations matching <b>filter</b> and <b>lhs</b>.
+ *
+ * @param {string} filter The mode filter.
+ * @param {string} lhs The LHS of the abbreviation.
+ * @returns {Array} The matching this._abbreviations [mode, lhs, rhs]
+ * @see #addAbbreviation
+ */
+ getAbbreviations: function (filter, lhs) {
+ // ! -> list all, on c or i ! matches too
+ let searchFilter = (filter == "!") ? "!ci" : filter + "!";
+ return list = [[mode, left, right] for ([left, [mode, right]] in this.abbrevs())
+ if (searchFilter.indexOf(mode) >= 0 && left.indexOf(lhs || "") == 0)];
+ },
+
+ /**
+ * Lists all this._abbreviations matching <b>filter</b> and <b>lhs</b>.
+ *
+ * @param {string} filter The mode filter.
+ * @param {string} lhs The LHS of the abbreviation.
+ * @see #addAbbreviation
+ */
+ listAbbreviations: function (filter, lhs) {
+ let list = this.getAbbreviations(filter, lhs);
+
+ if (!list.length)
+ liberator.echomsg("No this._abbreviations found");
+ else if (list.length == 1) {
+ let [mode, lhs, rhs] = list[0];
+
+ liberator.echo(mode + " " + lhs + " " + rhs, commandline.FORCE_SINGLELINE); // 2 spaces, 3 spaces
+ }
+ else {
+ list = template.tabular(["", "LHS", "RHS"], [], list);
+ commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
+ }
+ },
+
+ /**
+ * Deletes all this._abbreviations matching <b>filter</b> and <b>lhs</b>.
+ *
+ * @param {string} filter The mode filter.
+ * @param {string} lhs The LHS of the abbreviation.
+ * @see #addAbbreviation
+ */
+ removeAbbreviation: function (filter, lhs) {
+ if (!lhs) {
+ liberator.echoerr("E474: Invalid argument");
+ return false;
+ }
+
+ if (this._abbreviations[lhs]) { // this._abbreviations exists
+ if (filter == "!") {
+ this._abbreviations[lhs] = "";
+ return true;
+ }
+ else {
+ if (!this._abbreviations[lhs][1]) { // only one exists
+ if (this._abbreviations[lhs][0][0] == "!") { // exists as ! -> no 'full' delete
+ this._abbreviations[lhs][0][0] = (filter == "i") ? "c" : "i"; // ! - i = c; ! - c = i
+ return true;
+ }
+ else if (this._abbreviations[lhs][0][0] == filter) {
+ this._abbreviations[lhs] = "";
+ return true;
+ }
+ }
+ else { // two this._abbreviations exist ( 'i' or 'c' (filter as well))
+ if (this._abbreviations[lhs][0][0] == "c" && filter == "c")
+ this._abbreviations[lhs][0] = this._abbreviations[lhs][1];
+
+ this._abbreviations[lhs][1] = "";
+
+ return true;
}
- while (getEditor().selectionStart != pos);
}
- },
+ }
- // returns the position of char
- findCharForward: function (ch, count)
- {
- if (!getEditor())
- return -1;
+ liberator.echoerr("E24: No such abbreviation");
+ return false;
+ },
+
+ /**
+ * Removes all this._abbreviations matching <b>filter</b>.
+ *
+ * @param {string} filter The mode filter.
+ * @see #addAbbreviation
+ */
+ removeAllAbbreviations: function (filter) {
+ let searchFilter = (filter == "!") ? "!ci" : filter + "!";
+ for (let [lhs, [mode, rhs]] in this.abbrevs())
+ if (searchFilter.indexOf(mode) >= 0)
+ this.removeAbbreviation(filter, lhs);
+ }
+}, {
+ getEditor: function () liberator.focus,
- lastFindChar = ch;
- lastFindCharFunc = this.findCharForward;
+ getController: function () {
+ let ed = Editor.getEditor();
+ if (!ed || !ed.controllers)
+ return null;
- let text = getEditor().value;
- if (!typeof count == "number" || count < 1)
- count = 1;
+ return ed.controllers.getControllerForCommand("cmd_beginLine");
+ },
+
+}, {
+ commands: function () {
+ // mode = "i" -> add :iabbrev, :iabclear and :iunabbrev commands
+ function addAbbreviationCommands(ch, modeDescription) {
+ let mode = ch || "!";
+ modeDescription = modeDescription ? " in " + modeDescription + " mode" : "";
+
+ commands.add([ch ? ch + "a[bbrev]" : "ab[breviate]"],
+ "Abbreviate a key sequence" + modeDescription,
+ function (args) {
+ let matches = args.string.match(RegExp("^\\s*($|" + this._abbrevmatch + ")(?:\\s*$|\\s+(.*))"));
+ liberator.assert(matches, "E474: Invalid argument");
+
+ let [, lhs, rhs] = matches;
+ if (rhs)
+ editor.addAbbreviation(mode, lhs, rhs);
+ else
+ editor.listAbbreviations(mode, lhs || "");
+ }, {
+ completer: function (context, args) completion.abbreviation(context, args, mode),
+ literal: 0,
+ serial: function () [ {
+ command: this.name,
+ arguments: [lhs],
+ literalArg: abbr[1]
+ }
+ for ([lhs, abbr] in editor.abbrevs())
+ if (abbr[0] == mode)
+ ]
+ });
+
+ commands.add([ch ? ch + "una[bbrev]" : "una[bbreviate]"],
+ "Remove an abbreviation" + modeDescription,
+ function (args) { editor.removeAbbreviation(mode, args.literalArg); }, {
+ argCount: "1",
+ completer: function (context, args) completion.abbreviation(context, args, mode),
+ literal: 0
+ });
+
+ commands.add([ch + "abc[lear]"],
+ "Remove all this._abbreviations" + modeDescription,
+ function () { editor.removeAllAbbreviations(mode); },
+ { argCount: "0" });
+ }
- for (let i = getEditor().selectionEnd + 1; i < text.length; i++)
- {
- if (text[i] == "\n")
- break;
- if (text[i] == ch)
- count--;
- if (count == 0)
- return i + 1; // always position the cursor after the char
- }
+ addAbbreviationCommands("", "");
+ addAbbreviationCommands("i", "insert");
+ addAbbreviationCommands("c", "command line");
+ },
- liberator.beep();
- return -1;
- },
+ completion: function () {
+ // TODO: shouldn't all of these have a standard signature (context, args, ...)? --djk
+ completion.abbreviation = function abbreviation(context, args, mode) {
+ mode = mode || "!";
- // returns the position of char
- findCharBackward: function (ch, count)
- {
- if (!getEditor())
- return -1;
+ if (args.completeArg == 0) {
+ let abbreviations = editor.getAbbreviations(mode);
+ context.completions = [[lhs, ""] for ([, [, lhs,]] in Iterator(abbreviations))];
+ }
+ };
+ },
+
+ mappings: function () {
+ var myModes = [modes.INSERT, modes.COMMAND_LINE];
+
+ // add mappings for commands like h,j,k,l,etc. in CARET, VISUAL and TEXTAREA mode
+ function addMovementMap(keys, hasCount, caretModeMethod, caretModeArg, textareaCommand, visualTextareaCommand) {
+ let extraInfo = {};
+ if (hasCount)
+ extraInfo.count = true;
+
+ mappings.add([modes.CARET], keys, "",
+ function (count) {
+ if (typeof count != "number" || count < 1)
+ count = 1;
+
+ let controller = buffer.selectionController;
+ while (count--)
+ controller[caretModeMethod](caretModeArg, false);
+ },
+ extraInfo);
+
+ mappings.add([modes.VISUAL], keys, "",
+ function (count) {
+ if (typeof count != "number" || count < 1 || !hasCount)
+ count = 1;
+
+ let controller = buffer.selectionController;
+ while (count--) {
+ if (modes.extended & modes.TEXTAREA) {
+ if (typeof visualTextareaCommand == "function")
+ visualTextareaCommand();
+ else
+ editor.executeCommand(visualTextareaCommand);
+ }
+ else
+ controller[caretModeMethod](caretModeArg, true);
+ }
+ },
+ extraInfo);
- lastFindChar = ch;
- lastFindCharFunc = this.findCharBackward;
+ mappings.add([modes.TEXTAREA], keys, "",
+ function (count) {
+ if (typeof count != "number" || count < 1)
+ count = 1;
- let text = getEditor().value;
- if (!typeof count == "number" || count < 1)
- count = 1;
+ editor.executeCommand(textareaCommand, count);
+ },
+ extraInfo);
+ }
- for (let i = getEditor().selectionStart - 1; i >= 0; i--)
- {
- if (text[i] == "\n")
- break;
- if (text[i] == ch)
- count--;
- if (count == 0)
- return i;
- }
+ // add mappings for commands like i,a,s,c,etc. in TEXTAREA mode
+ function addBeginInsertModeMap(keys, commands) {
+ mappings.add([modes.TEXTAREA], keys, "",
+ function (count) {
+ commands.forEach(function (cmd)
+ editor.executeCommand(cmd, 1));
+ modes.set(modes.INSERT, modes.TEXTAREA);
+ });
+ }
- liberator.beep();
- return -1;
- },
+ function addMotionMap(key) {
+ mappings.add([modes.TEXTAREA], [key],
+ "Motion command",
+ function (motion, count) { editor.executeCommandWithMotion(key, motion, count); },
+ { count: true, motion: true });
+ }
+ function selectPreviousLine() {
+ editor.executeCommand("cmd_selectLinePrevious");
+ if ((modes.extended & modes.LINE) && !editor.selectedText())
+ editor.executeCommand("cmd_selectLinePrevious");
+ }
+ function selectNextLine() {
+ editor.executeCommand("cmd_selectLineNext");
+ if ((modes.extended & modes.LINE) && !editor.selectedText())
+ editor.executeCommand("cmd_selectLineNext");
+ }
- editFileExternally: function (path)
- {
- // TODO: save return value in v:shell_error
- let args = commands.parseArgs(options["editor"], [], "*", true);
+ // KEYS COUNT CARET TEXTAREA VISUAL_TEXTAREA
+ addMovementMap(["k", "<Up>"], true, "lineMove", false, "cmd_linePrevious", selectPreviousLine);
+ addMovementMap(["j", "<Down>", "<Return>"], true, "lineMove", true, "cmd_lineNext", selectNextLine);
+ addMovementMap(["h", "<Left>", "<BS>"], true, "characterMove", false, "cmd_charPrevious", "cmd_selectCharPrevious");
+ addMovementMap(["l", "<Right>", "<Space>"], true, "characterMove", true, "cmd_charNext", "cmd_selectCharNext");
+ addMovementMap(["b", "B", "<C-Left>"], true, "wordMove", false, "cmd_wordPrevious", "cmd_selectWordPrevious");
+ addMovementMap(["w", "W", "e", "<C-Right>"], true, "wordMove", true, "cmd_wordNext", "cmd_selectWordNext");
+ addMovementMap(["<C-f>", "<PageDown>"], true, "pageMove", true, "cmd_movePageDown", "cmd_selectNextPage");
+ addMovementMap(["<C-b>", "<PageUp>"], true, "pageMove", false, "cmd_movePageUp", "cmd_selectPreviousPage");
+ addMovementMap(["gg", "<C-Home>"], false, "completeMove", false, "cmd_moveTop", "cmd_selectTop");
+ addMovementMap(["G", "<C-End>"], false, "completeMove", true, "cmd_moveBottom", "cmd_selectBottom");
+ addMovementMap(["0", "^", "<Home>"], false, "intraLineMove", false, "cmd_beginLine", "cmd_selectBeginLine");
+ addMovementMap(["$", "<End>"], false, "intraLineMove", true, "cmd_endLine" , "cmd_selectEndLine" );
+
+ addBeginInsertModeMap(["i", "<Insert>"], []);
+ addBeginInsertModeMap(["a"], ["cmd_charNext"]);
+ addBeginInsertModeMap(["I", "gI"], ["cmd_beginLine"]);
+ addBeginInsertModeMap(["A"], ["cmd_endLine"]);
+ addBeginInsertModeMap(["s"], ["cmd_deleteCharForward"]);
+ addBeginInsertModeMap(["S"], ["cmd_deleteToEndOfLine", "cmd_deleteToBeginningOfLine"]);
+ addBeginInsertModeMap(["C"], ["cmd_deleteToEndOfLine"]);
+
+ addMotionMap("d"); // delete
+ addMotionMap("c"); // change
+ addMotionMap("y"); // yank
+
+ // insert mode mappings
+ mappings.add(myModes,
+ ["<C-w>"], "Delete previous word",
+ function () { editor.executeCommand("cmd_deleteWordBackward", 1); });
+
+ mappings.add(myModes,
+ ["<C-u>"], "Delete until beginning of current line",
+ function () {
+ // broken in FF3, deletes the whole line:
+ // editor.executeCommand("cmd_deleteToBeginningOfLine", 1);
+ editor.executeCommand("cmd_selectBeginLine", 1);
+ if (Editor.getController().isCommandEnabled("cmd_delete"))
+ editor.executeCommand("cmd_delete", 1);
+ });
- liberator.assert(args.length >= 1, "No editor specified");
+ mappings.add(myModes,
+ ["<C-k>"], "Delete until end of current line",
+ function () { editor.executeCommand("cmd_deleteToEndOfLine", 1); });
- args.push(path);
- liberator.callFunctionInThread(null, io.run, io.expandPath(args.shift()), args, true);
- },
+ mappings.add(myModes,
+ ["<C-a>"], "Move cursor to beginning of current line",
+ function () { editor.executeCommand("cmd_beginLine", 1); });
- // TODO: clean up with 2 functions for textboxes and currentEditor?
- editFieldExternally: function (forceEditing)
- {
- if (!options["editor"])
- return false;
+ mappings.add(myModes,
+ ["<C-e>"], "Move cursor to end of current line",
+ function () { editor.executeCommand("cmd_endLine", 1); });
- let textBox = null;
- if (!(config.isComposeWindow))
- textBox = liberator.focus;
-
- if (!forceEditing && textBox && textBox.type == "password")
- {
- commandline.input("Editing a password field externally will reveal the password. Would you like to continue? (yes/[no]): ",
- function (resp)
- {
- if (resp && resp.match(/^y(es)?$/i))
- return editor.editFieldExternally(true);
- });
- return;
- }
+ mappings.add(myModes,
+ ["<C-h>"], "Delete character to the left",
+ function () { editor.executeCommand("cmd_deleteCharBackward", 1); });
- let text = ""; // XXX
- if (textBox)
- text = textBox.value;
- else if (typeof GetCurrentEditor == "function") // Thunderbird composer
- text = GetCurrentEditor().outputToString("text/plain", 2);
- else
- return false;
+ mappings.add(myModes,
+ ["<C-d>"], "Delete character to the right",
+ function () { editor.executeCommand("cmd_deleteCharForward", 1); });
- let oldBg, tmpBg;
- try
- {
- let res = io.withTempFiles(function (tmpfile) {
- if (textBox)
- {
- textBox.setAttribute("readonly", "true");
- oldBg = textBox.style.backgroundColor;
- tmpBg = "yellow";
- textBox.style.backgroundColor = "#bbbbbb";
- }
+ /*mappings.add(myModes,
+ ["<C-Home>"], "Move cursor to beginning of text field",
+ function () { editor.executeCommand("cmd_moveTop", 1); });
- if (!tmpfile.write(text))
- throw Error("Input contains characters not valid in the current " +
- "file encoding");
+ mappings.add(myModes,
+ ["<C-End>"], "Move cursor to end of text field",
+ function () { editor.executeCommand("cmd_moveBottom", 1); });*/
- this.editFileExternally(tmpfile.path);
+ mappings.add(myModes,
+ ["<S-Insert>"], "Insert clipboard/selection",
+ function () { editor.pasteClipboard(); });
- if (textBox)
- textBox.removeAttribute("readonly");
+ mappings.add([modes.INSERT, modes.TEXTAREA, modes.COMPOSE],
+ ["<C-i>"], "Edit text field with an external editor",
+ function () { editor.editFieldExternally(); });
- let val = tmpfile.read();
- if (textBox)
- textBox.value = val;
- else
- {
- let editor = GetCurrentEditor();
- let wholeDocRange = editor.document.createRange();
- let rootNode = editor.rootElement.QueryInterface(Ci.nsIDOMNode);
- wholeDocRange.selectNodeContents(rootNode);
- editor.selection.addRange(wholeDocRange);
- editor.selection.deleteFromDocument();
- editor.insertText(val);
- }
- }, this);
- if (res == false)
- throw "Couldn't create temporary file";
- }
- catch (e)
- {
- // Errors are unlikely, and our error messages won't
- // likely be any more helpful than that given in the
- // exception.
- liberator.echoerr(e);
- tmpBg = "red";
- }
+ mappings.add([modes.INSERT],
+ ["<C-t>"], "Edit text field in Vi mode",
+ function () { liberator.mode = modes.TEXTAREA; });
- // blink the textbox after returning
- if (textBox)
- {
- let colors = [tmpBg, oldBg, tmpBg, oldBg];
- (function () {
- textBox.style.backgroundColor = colors.shift();
- if (colors.length > 0)
- setTimeout(arguments.callee, 100);
- })();
- }
+ mappings.add([modes.INSERT],
+ ["<Space>", "<Return>"], "Expand insert mode abbreviation",
+ function () { editor.expandAbbreviation("i"); },
+ { route: true });
- return true;
- },
+ mappings.add([modes.INSERT],
+ ["<Tab>"], "Expand insert mode abbreviation",
+ function () { editor.expandAbbreviation("i"); document.commandDispatcher.advanceFocus(); });
- // Abbreviations {{{
+ mappings.add([modes.INSERT],
+ ["<C-]>", "<C-5>"], "Expand insert mode abbreviation",
+ function () { editor.expandAbbreviation("i"); });
- // NOTE: I think this comment block is trying to say something but no
- // one is listening. In space, no one can hear you scream. --djk
- //
- // System for adding abbreviations:
- //
- // filter == ! delete all, and set first (END)
- //
- // if filter == ! remove all and add it as only END
- //
- // variant 1: rhs matches anywhere in loop
- //
- // 1 mod matches anywhere in loop
- // a) simple replace and
- // I) (maybe there's another rhs that matches? not possible)
- // (when there's another item, it's opposite mod with different rhs)
- // (so do nothing further but END)
- //
- // 2 mod does not match
- // a) the opposite is there -> make a ! and put it as only and END
- // (b) a ! is there. do nothing END)
- //
- // variant 2: rhs matches *no*were in loop and filter is c or i
- // every kind of current combo is possible to 1 {c,i,!} or two {c and i}
- //
- // 1 mod is ! split into two i + c END
- // 1 not !: opposite mode (first), add/change 'second' and END
- // 1 not !: same mode (first), overwrite first this END
- //
- // TODO: I don't like these funky filters, I am a funky filter hater. --djk
- // : make this a separate object
- // : use Struct for individual abbreviations
- // : rename "filter" arg "mode"
- /**
- * Adds a new abbreviation. Abbreviations consist of a LHS (the text
- * that is replaced when the abbreviation is expanded) and a RHS (the
- * replacement text).
- *
- * @param {string} filter The mode filter. This specifies the modes in
- * which this abbreviation is available. Either:
- * "c" - applies in command-line mode
- * "i" - applies in insert mode
- * "!" - applies in both command-line and insert modes
- * @param {string} lhs The LHS of the abbreviation.
- * @param {string} rhs The RHS of the abbreviation.
- */
- addAbbreviation: function (filter, lhs, rhs)
- {
- if (!abbreviations[lhs])
- {
- abbreviations[lhs] = [];
- abbreviations[lhs][0] = [filter, rhs];
- return;
- }
+ // textarea mode
+ mappings.add([modes.TEXTAREA],
+ ["u"], "Undo",
+ function (count) {
+ editor.executeCommand("cmd_undo", count);
+ liberator.mode = modes.TEXTAREA;
+ },
+ { count: true });
- if (filter == "!")
- {
- if (abbreviations[lhs][1])
- abbreviations[lhs][1] = "";
- abbreviations[lhs][0] = [filter, rhs];
- return;
- }
+ mappings.add([modes.TEXTAREA],
+ ["<C-r>"], "Redo",
+ function (count) {
+ editor.executeCommand("cmd_redo", count);
+ liberator.mode = modes.TEXTAREA;
+ },
+ { count: true });
- for (let i = 0; i < abbreviations[lhs].length; i++)
- {
- if (abbreviations[lhs][i][1] == rhs)
- {
- if (abbreviations[lhs][i][0] == filter)
- {
- abbreviations[lhs][i] = [filter, rhs];
- return;
- }
- else
- {
- if (abbreviations[lhs][i][0] != "!")
- {
- if (abbreviations[lhs][1])
- abbreviations[lhs][1] = "";
- abbreviations[lhs][0] = ["!", rhs];
- return;
- }
- else
- return;
- }
- }
- }
+ mappings.add([modes.TEXTAREA],
+ ["D"], "Delete the characters under the cursor until the end of the line",
+ function () { editor.executeCommand("cmd_deleteToEndOfLine"); });
- if (abbreviations[lhs][0][0] == "!")
- {
- let tmpOpp = ("i" == filter) ? "c" : "i";
- abbreviations[lhs][1] = [tmpOpp, abbreviations[lhs][0][1]];
- abbreviations[lhs][0] = [filter, rhs];
- return;
- }
+ mappings.add([modes.TEXTAREA],
+ ["o"], "Open line below current",
+ function (count) {
+ editor.executeCommand("cmd_endLine", 1);
+ modes.set(modes.INSERT, modes.TEXTAREA);
+ events.feedkeys("<Return>");
+ });
- if (abbreviations[lhs][0][0] != filter)
- abbreviations[lhs][1] = [filter, rhs];
- else
- abbreviations[lhs][0] = [filter, rhs];
- },
-
- /**
- * Expands an abbreviation in the currently active textbox.
- *
- * @param {string} filter The mode filter.
- * @see #addAbbreviation
- */
- expandAbbreviation: function (filter)
- {
- let textbox = getEditor();
- if (!textbox)
- return;
- let text = textbox.value;
- let currStart = textbox.selectionStart;
- let currEnd = textbox.selectionEnd;
- let foundWord = text.substring(0, currStart).replace(RegExp("^(.|\\n)*?\\s*(" + abbrevmatch + ")$", "m"), "$2"); // get last word \b word boundary
- if (!foundWord)
- return true;
+ mappings.add([modes.TEXTAREA],
+ ["O"], "Open line above current",
+ function (count) {
+ editor.executeCommand("cmd_beginLine", 1);
+ modes.set(modes.INSERT, modes.TEXTAREA);
+ events.feedkeys("<Return>");
+ editor.executeCommand("cmd_linePrevious", 1);
+ });
- for (let lhs in abbreviations)
- {
- for (let i = 0; i < abbreviations[lhs].length; i++)
- {
- if (lhs == foundWord && (abbreviations[lhs][i][0] == filter || abbreviations[lhs][i][0] == "!"))
- {
- // if found, replace accordingly
- let len = foundWord.length;
- let abbrText = abbreviations[lhs][i][1];
- text = text.substring(0, currStart - len) + abbrText + text.substring(currStart);
- textbox.value = text;
- textbox.selectionStart = currStart - len + abbrText.length;
- textbox.selectionEnd = currEnd - len + abbrText.length;
- break;
- }
+ mappings.add([modes.TEXTAREA],
+ ["X"], "Delete character to the left",
+ function (count) { editor.executeCommand("cmd_deleteCharBackward", count); },
+ { count: true });
+
+ mappings.add([modes.TEXTAREA],
+ ["x"], "Delete character to the right",
+ function (count) { editor.executeCommand("cmd_deleteCharForward", count); },
+ { count: true });
+
+ // visual mode
+ mappings.add([modes.CARET, modes.TEXTAREA],
+ ["v"], "Start visual mode",
+ function (count) { modes.set(modes.VISUAL, liberator.mode); });
+
+ mappings.add([modes.VISUAL],
+ ["v"], "End visual mode",
+ function (count) { events.onEscape(); });
+
+ mappings.add([modes.TEXTAREA],
+ ["V"], "Start visual line mode",
+ function (count) {
+ modes.set(modes.VISUAL, modes.TEXTAREA | modes.LINE);
+ editor.executeCommand("cmd_beginLine", 1);
+ editor.executeCommand("cmd_selectLineNext", 1);
+ });
+
+ mappings.add([modes.VISUAL],
+ ["c", "s"], "Change selected text",
+ function (count) {
+ if (modes.extended & modes.TEXTAREA) {
+ editor.executeCommand("cmd_cut");
+ modes.set(modes.INSERT, modes.TEXTAREA);
}
- }
- return true;
- },
-
- /**
- * Returns all abbreviations matching <b>filter</b> and <b>lhs</b>.
- *
- * @param {string} filter The mode filter.
- * @param {string} lhs The LHS of the abbreviation.
- * @returns {Array} The matching abbreviations [mode, lhs, rhs]
- * @see #addAbbreviation
- */
- getAbbreviations: function (filter, lhs)
- {
- // ! -> list all, on c or i ! matches too
- let searchFilter = (filter == "!") ? "!ci" : filter + "!";
- return list = [[mode, left, right] for ([left, [mode, right]] in abbrevs())
- if (searchFilter.indexOf(mode) >= 0 && left.indexOf(lhs || "") == 0)];
- },
-
- /**
- * Lists all abbreviations matching <b>filter</b> and <b>lhs</b>.
- *
- * @param {string} filter The mode filter.
- * @param {string} lhs The LHS of the abbreviation.
- * @see #addAbbreviation
- */
- listAbbreviations: function (filter, lhs)
- {
- let list = this.getAbbreviations(filter, lhs);
-
- if (!list.length)
- liberator.echomsg("No abbreviations found");
- else if (list.length == 1)
- {
- let [mode, lhs, rhs] = list[0];
-
- liberator.echo(mode + " " + lhs + " " + rhs, commandline.FORCE_SINGLELINE); // 2 spaces, 3 spaces
- }
- else
- {
- list = template.tabular(["", "LHS", "RHS"], [], list);
- commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
- }
- },
-
- /**
- * Deletes all abbreviations matching <b>filter</b> and <b>lhs</b>.
- *
- * @param {string} filter The mode filter.
- * @param {string} lhs The LHS of the abbreviation.
- * @see #addAbbreviation
- */
- removeAbbreviation: function (filter, lhs)
- {
- if (!lhs)
- {
- liberator.echoerr("E474: Invalid argument");
- return false;
- }
+ else
+ liberator.beep();
+ });
- if (abbreviations[lhs]) // abbreviations exists
- {
- if (filter == "!")
- {
- abbreviations[lhs] = "";
- return true;
+ mappings.add([modes.VISUAL],
+ ["d"], "Delete selected text",
+ function (count) {
+ if (modes.extended & modes.TEXTAREA) {
+ editor.executeCommand("cmd_cut");
+ modes.set(modes.TEXTAREA);
}
else
- {
- if (!abbreviations[lhs][1]) // only one exists
- {
- if (abbreviations[lhs][0][0] == "!") // exists as ! -> no 'full' delete
- {
- abbreviations[lhs][0][0] = (filter == "i") ? "c" : "i"; // ! - i = c; ! - c = i
- return true;
- }
- else if (abbreviations[lhs][0][0] == filter)
- {
- abbreviations[lhs] = "";
- return true;
- }
- }
- else // two abbreviations exist ( 'i' or 'c' (filter as well))
- {
- if (abbreviations[lhs][0][0] == "c" && filter == "c")
- abbreviations[lhs][0] = abbreviations[lhs][1];
+ liberator.beep();
+ });
- abbreviations[lhs][1] = "";
+ mappings.add([modes.VISUAL],
+ ["y"], "Yank selected text",
+ function (count) {
+ if (modes.extended & modes.TEXTAREA) {
+ editor.executeCommand("cmd_copy");
+ modes.set(modes.TEXTAREA);
+ }
+ else {
+ let sel = window.content.document.getSelection();
+ if (sel)
+ util.copyToClipboard(sel, true);
+ else
+ liberator.beep();
+ }
+ });
- return true;
- }
+ mappings.add([modes.VISUAL, modes.TEXTAREA],
+ ["p"], "Paste clipboard contents",
+ function (count) {
+ if (!(modes.extended & modes.CARET)) {
+ if (!count) count = 1;
+ while (count--)
+ editor.executeCommand("cmd_paste");
+ liberator.mode = modes.TEXTAREA;
}
- }
+ else
+ liberator.beep();
+ });
- liberator.echoerr("E24: No such abbreviation");
- return false;
- },
-
- /**
- * Removes all abbreviations matching <b>filter</b>.
- *
- * @param {string} filter The mode filter.
- * @see #addAbbreviation
- */
- removeAllAbbreviations: function (filter)
- {
- let searchFilter = (filter == "!") ? "!ci" : filter + "!";
- for (let [lhs, [mode, rhs]] in abbrevs())
- if (searchFilter.indexOf(mode) >= 0)
- this.removeAbbreviation(filter, lhs);
- }
- //}}}
- };
- //}}}
-} //}}}
+ // finding characters
+ mappings.add([modes.TEXTAREA, modes.VISUAL],
+ ["f"], "Move to a character on the current line after the cursor",
+ function (count, arg) {
+ let pos = editor.findCharForward(arg, count);
+ if (pos >= 0)
+ editor.moveToPosition(pos, true, liberator.mode == modes.VISUAL);
+ },
+ { arg: true, count: true });
+
+ mappings.add([modes.TEXTAREA, modes.VISUAL],
+ ["F"], "Move to a charater on the current line before the cursor",
+ function (count, arg) {
+ let pos = editor.findCharBackward(arg, count);
+ if (pos >= 0)
+ editor.moveToPosition(pos, false, liberator.mode == modes.VISUAL);
+ },
+ { arg: true, count: true });
+
+ mappings.add([modes.TEXTAREA, modes.VISUAL],
+ ["t"], "Move before a character on the current line",
+ function (count, arg) {
+ let pos = editor.findCharForward(arg, count);
+ if (pos >= 0)
+ editor.moveToPosition(pos - 1, true, liberator.mode == modes.VISUAL);
+ },
+ { arg: true, count: true });
+
+ mappings.add([modes.TEXTAREA, modes.VISUAL],
+ ["T"], "Move before a character on the current line, backwards",
+ function (count, arg) {
+ let pos = editor.findCharBackward(arg, count);
+ if (pos >= 0)
+ editor.moveToPosition(pos + 1, false, liberator.mode == modes.VISUAL);
+ },
+ { arg: true, count: true });
+
+ // textarea and visual mode
+ mappings.add([modes.TEXTAREA, modes.VISUAL],
+ ["~"], "Switch case of the character under the cursor and move the cursor to the right",
+ function (count) {
+ if (modes.main == modes.VISUAL)
+ count = Editor.getEditor().selectionEnd - Editor.getEditor().selectionStart;
+ if (typeof count != "number" || count < 1)
+ count = 1;
+
+ while (count-- > 0) {
+ let text = Editor.getEditor().value;
+ let pos = Editor.getEditor().selectionStart;
+ if (pos >= text.length)
+ return void liberator.beep();
+ let chr = text[pos];
+ Editor.getEditor().value = text.substring(0, pos) +
+ (chr == chr.toLocaleLowerCase() ? chr.toLocaleUpperCase() : chr.toLocaleLowerCase()) +
+ text.substring(pos + 1);
+ editor.moveToPosition(pos + 1, true, false);
+ }
+ modes.set(modes.TEXTAREA);
+ },
+ { count: true });
+ },
+
+ options: function () {
+ options.add(["editor"],
+ "Set the external text editor",
+ "string", "gvim -f");
+
+ options.add(["insertmode", "im"],
+ "Use Insert mode as the default for text areas",
+ "boolean", true);
+ },
+});
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/eval.js b/common/content/eval.js
index 2a0c9d66..c6029d01 100644
--- a/common/content/eval.js
+++ b/common/content/eval.js
@@ -1,7 +1,6 @@
try { __liberator_eval_result = eval(__liberator_eval_string);
}
-catch (e)
-{
+catch (e) {
__liberator_eval_error = e;
}
// IMPORTANT: The eval statement *must* remain on the first line
diff --git a/common/content/events.js b/common/content/events.js
index 89fb1a54..8dcfc550 100644
--- a/common/content/events.js
+++ b/common/content/events.js
@@ -7,1833 +7,1352 @@
/** @scope modules */
/**
- * @instance autocommands
+ * @instance events
*/
-function AutoCommands() //{{{
-{
- ////////////////////////////////////////////////////////////////////////////////
- ////////////////////// PRIVATE SECTION /////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- const AutoCommand = new Struct("event", "pattern", "command");
- var store = [];
-
- function matchAutoCmd(autoCmd, event, regex)
- {
- return (!event || autoCmd.event == event) && (!regex || autoCmd.pattern.source == regex);
- }
+const Events = Module("events", {
+ requires: ["autocommands"],
+
+ init: function () {
+ const self = this;
+
+ this._fullscreen = window.fullScreen;
+ this._lastFocus = null;
+ this._currentMacro = "";
+ this._lastMacro = "";
+
+ this._macros = storage.newMap("macros", true, { privateData: true });
+
+ // NOTE: the order of ["Esc", "Escape"] or ["Escape", "Esc"]
+ // matters, so use that string as the first item, that you
+ // want to refer to within liberator's source code for
+ // comparisons like if (key == "<Esc>") { ... }
+ this._keyTable = {
+ add: ["Plus", "Add"],
+ back_space: ["BS"],
+ delete: ["Del"],
+ escape: ["Esc", "Escape"],
+ insert: ["Insert", "Ins"],
+ left_shift: ["LT", "<"],
+ return: ["Return", "CR", "Enter"],
+ right_shift: [">"],
+ space: ["Space", " "],
+ subtract: ["Minus", "Subtract"]
+ };
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// OPTIONS /////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- options.add(["eventignore", "ei"],
- "List of autocommand event names which should be ignored",
- "stringlist", "",
- {
- completer: function () config.autocommands.concat([["all", "All events"]]),
- validator: Option.validateCompleter
- });
-
- options.add(["focuscontent", "fc"],
- "Try to stay in normal mode after loading a web page",
- "boolean", false);
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMMANDS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- commands.add(["au[tocmd]"],
- "Execute commands automatically on events",
- function (args)
- {
- let [event, regex, cmd] = args;
- let events = [];
-
- try
- {
- RegExp(regex);
- }
- catch (e)
- {
- liberator.assert(false, "E475: Invalid argument: " + regex);
+ this._code_key = {};
+ this._key_code = {};
+
+ for (let [k, v] in Iterator(KeyEvent))
+ if (/^DOM_VK_(?![A-Z0-9]$)/.test(k)) {
+ k = k.substr(7).toLowerCase();
+ let names = [k.replace(/(^|_)(.)/g, function (m, n1, n2) n2.toUpperCase())
+ .replace(/^NUMPAD/, "k")];
+ if (k in this._keyTable)
+ names = this._keyTable[k];
+ this._code_key[v] = names[0];
+ for (let [, name] in Iterator(names))
+ this._key_code[name.toLowerCase()] = v;
}
- if (event)
- {
- // NOTE: event can only be a comma separated list for |:au {event} {pat} {cmd}|
- let validEvents = config.autocommands.map(function (event) event[0]);
- validEvents.push("*");
-
- events = event.split(",");
- liberator.assert(events.every(function (event) validEvents.indexOf(event) >= 0),
- "E216: No such group or event: " + event);
- }
+ // HACK: as Gecko does not include an event for <, we must add this in manually.
+ if (!("<" in this._key_code)) {
+ this._key_code["<"] = 60;
+ this._key_code["lt"] = 60;
+ this._code_key[60] = "lt";
+ }
- if (cmd) // add new command, possibly removing all others with the same event/pattern
- {
- if (args.bang)
- autocommands.remove(event, regex);
- if (args["-javascript"])
- cmd = eval("(function (args) { with(args) {" + cmd + "} })");
- autocommands.add(events, regex, cmd);
- }
- else
- {
- if (event == "*")
- event = null;
+ this._input = {
+ buffer: "", // partial command storage
+ pendingMotionMap: null, // e.g. "d{motion}" if we wait for a motion of the "d" command
+ pendingArgMap: null, // pending map storage for commands like m{a-z}
+ count: -1 // parsed count from the input buffer
+ };
- if (args.bang)
- {
- // TODO: "*" only appears to work in Vim when there is a {group} specified
- if (args[0] != "*" || regex)
- autocommands.remove(event, regex); // remove all
- }
- else
- autocommands.list(event, regex); // list all
+ function onResize(event) {
+ if (window.fullScreen != this._fullscreen) {
+ this._fullscreen = window.fullScreen;
+ liberator.triggerObserver("fullscreen", this._fullscreen);
+ autocommands.trigger("Fullscreen", { state: this._fullscreen });
}
- },
- {
- bang: true,
- completer: function (context) completion.autocmdEvent(context),
- literal: 2,
- options: [[["-javascript", "-js"], commands.OPTION_NOARG]]
- });
-
- [
- {
- name: "do[autocmd]",
- description: "Apply the autocommands matching the specified URL pattern to the current buffer"
- },
- {
- name: "doautoa[ll]",
- description: "Apply the autocommands matching the specified URL pattern to all buffers"
}
- ].forEach(function (command) {
- commands.add([command.name],
- command.description,
- // TODO: Perhaps this should take -args to pass to the command?
- function (args)
- {
- // Vim compatible
- if (args.length == 0)
- return void liberator.echomsg("No matching autocommands");
-
- let [event, url] = args;
- let defaultURL = url || buffer.URL;
- let validEvents = config.autocommands.map(function (e) e[0]);
-
- // TODO: add command validators
- liberator.assert(event != "*",
- "E217: Can't execute autocommands for ALL events");
- liberator.assert(validEvents.indexOf(event) >= 0,
- "E216: No such group or event: " + args);
- liberator.assert(autocommands.get(event).some(function (c) c.pattern.test(defaultURL)),
- "No matching autocommands");
-
- if (this.name == "doautoall" && liberator.has("tabs"))
- {
- let current = tabs.index();
-
- for (let i = 0; i < tabs.count; i++)
- {
- tabs.select(i);
- // if no url arg is specified use the current buffer's URL
- autocommands.trigger(event, { url: url || buffer.URL });
- }
- tabs.select(current);
+ /////////////////////////////////////////////////////////
+ // track if a popup is open or the menubar is active
+ this._activeMenubar = false;
+ window.addEventListener("popupshown", this._enterPopupMode, true);
+ window.addEventListener("popuphidden", this._exitPopupMode, true);
+ window.addEventListener("DOMMenuBarActive", this._enterMenuMode, true);
+ window.addEventListener("DOMMenuBarInactive", this._exitMenuMode, true);
+ window.addEventListener("resize", onResize, true);
+
+ // load all macros
+ // setTimeout needed since io. is loaded after events.
+ setTimeout(function () {
+ try {
+ let dirs = io.getRuntimeDirectories("macros");
+
+ if (dirs.length > 0) {
+ for (let [, dir] in Iterator(dirs)) {
+ liberator.echomsg('Searching for "macros/*" in "' + dir.path + '"', 2);
+ liberator.log("Sourcing macros directory: " + dir.path + "...", 3);
+
+ let files = io.readDirectory(dir.path);
+ for (let file in dir.iterDirectory()) {
+ if (file.exists() && !file.isDirectory() && file.isReadable() &&
+ /^[\w_-]+(\.vimp)?$/i.test(file.leafName)) {
+ let name = file.leafName.replace(/\.vimp$/i, "");
+ this._macros.set(name, file.read().split("\n")[0]);
+
+ liberator.log("Macro " + name + " added: " + this._macros.get(name), 5);
+ }
+ }
+ }
}
else
- autocommands.trigger(event, { url: defaultURL });
- },
- {
- argCount: "*", // FIXME: kludged for proper error message should be "1".
- completer: function (context) completion.autocmdEvent(context)
- });
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMPLETIONS /////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- liberator.registerObserver("load_completion", function () {
- completion.setFunctionCompleter(autocommands.get, [function () config.autocommands]);
-
- completion.autocmdEvent = function autocmdEvent(context) {
- context.completions = config.autocommands;
- };
-
- completion.macro = function macro(context) {
- context.title = ["Macro", "Keys"];
- context.completions = [item for (item in events.getMacros())];
- };
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// PUBLIC SECTION //////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- return {
-
- __iterator__: function () util.Array.itervalues(store),
-
- /**
- * Adds a new autocommand. <b>cmd</b> will be executed when one of the
- * specified <b>events</b> occurs and the URL of the applicable buffer
- * matches <b>regex</b>.
- *
- * @param {Array} events The array of event names for which this
- * autocommand should be executed.
- * @param {string} regex The URL pattern to match against the buffer URL.
- * @param {string} cmd The Ex command to run.
- */
- add: function (events, regex, cmd)
- {
- if (typeof events == "string")
- {
- events = events.split(",");
- liberator.log("DEPRECATED: the events list arg to autocommands.add() should be an array of event names");
+ liberator.log("No user macros directory found", 3);
}
- events.forEach(function (event) {
- store.push(new AutoCommand(event, RegExp(regex), cmd));
- });
- },
-
- /**
- * Returns all autocommands with a matching <b>event</b> and
- * <b>regex</b>.
- *
- * @param {string} event The event name filter.
- * @param {string} regex The URL pattern filter.
- * @returns {AutoCommand[]}
- */
- get: function (event, regex)
- {
- return store.filter(function (autoCmd) matchAutoCmd(autoCmd, event, regex));
- },
-
- /**
- * Deletes all autocommands with a matching <b>event</b> and
- * <b>regex</b>.
- *
- * @param {string} event The event name filter.
- * @param {string} regex The URL pattern filter.
- */
- remove: function (event, regex)
- {
- store = store.filter(function (autoCmd) !matchAutoCmd(autoCmd, event, regex));
- },
+ catch (e) {
+ // thrown if directory does not exist
+ liberator.log("Error sourcing macros directory: " + e, 9);
+ }
+ }, 100);
+
+ window.XULBrowserWindow = this.progressListener;
+ window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .treeOwner
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIXULWindow)
+ .XULBrowserWindow = this.progressListener;
+ try {
+ getBrowser().addProgressListener(this.progressListener, Ci.nsIWebProgress.NOTIFY_ALL);
+ }
+ catch (e) {}
- /**
- * Lists all autocommands with a matching <b>event</b> and
- * <b>regex</b>.
- *
- * @param {string} event The event name filter.
- * @param {string} regex The URL pattern filter.
- */
- list: function (event, regex)
- {
- let cmds = {};
-
- // XXX
- store.forEach(function (autoCmd) {
- if (matchAutoCmd(autoCmd, event, regex))
- {
- cmds[autoCmd.event] = cmds[autoCmd.event] || [];
- cmds[autoCmd.event].push(autoCmd);
+ function wrapListener(method) {
+ return function (event) {
+ try {
+ self[method](event);
}
- });
-
- let list = template.commandOutput(
- <table>
- <tr highlight="Title">
- <td colspan="2">----- Auto Commands -----</td>
- </tr>
- {
- template.map(cmds, function ([event, items])
- <tr highlight="Title">
- <td colspan="2">{event}</td>
- </tr>
- +
- template.map(items, function (item)
- <tr>
- <td>&#160;{item.pattern.source}</td>
- <td>{item.command}</td>
- </tr>))
- }
- </table>);
-
- commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
- },
-
- /**
- * Triggers the execution of all autocommands registered for
- * <b>event</b>. A map of <b>args</b> is passed to each autocommand
- * when it is being executed.
- *
- * @param {string} event The event to fire.
- * @param {Object} args The args to pass to each autocommand.
- */
- trigger: function (event, args)
- {
- if (options.get("eventignore").has("all", event))
- return;
-
- let autoCmds = store.filter(function (autoCmd) autoCmd.event == event);
-
- liberator.echomsg("Executing " + event + " Auto commands for \"*\"", 8);
-
- let lastPattern = null;
- let url = args.url || "";
-
- for (let [, autoCmd] in Iterator(autoCmds))
- {
- if (autoCmd.pattern.test(url))
- {
- if (!lastPattern || lastPattern.source != autoCmd.pattern.source)
- liberator.echomsg("Executing " + event + " Auto commands for \"" + autoCmd.pattern.source + "\"", 8);
-
- lastPattern = autoCmd.pattern;
- liberator.echomsg("autocommand " + autoCmd.command, 9);
-
- if (typeof autoCmd.command == "function")
- {
- try
- {
- autoCmd.command.call(autoCmd, args);
- }
- catch (e)
- {
- liberator.reportError(e);
- liberator.echoerr(e);
- }
- }
+ catch (e) {
+ if (e.message == "Interrupted")
+ liberator.echoerr("Interrupted");
else
- liberator.execute(commands.replaceTokens(autoCmd.command, args), null, true);
+ liberator.echoerr("Processing " + event.type + " event: " + (e.echoerr || e));
+ liberator.reportError(e);
}
- }
+ };
}
- };
- //}}}
-} //}}}
-
-/**
- * @instance events
- */
-function Events() //{{{
-{
- ////////////////////////////////////////////////////////////////////////////////
- ////////////////////// PRIVATE SECTION /////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- const input = {
- buffer: "", // partial command storage
- pendingMotionMap: null, // e.g. "d{motion}" if we wait for a motion of the "d" command
- pendingArgMap: null, // pending map storage for commands like m{a-z}
- count: -1 // parsed count from the input buffer
- };
-
- var fullscreen = window.fullScreen;
-
- var lastFocus = null;
-
- var macros = storage.newMap("macros", { store: true, privateData: true });
-
- var currentMacro = "";
- var lastMacro = "";
-
- if (liberator.has("tabs"))
- {
- // FIXME: most of this doesn't work for Muttator yet.
- liberator.registerObserver("load_tabs", function () {
- let tabContainer = tabs.getBrowser().mTabContainer;
-
- tabContainer.addEventListener("TabMove", function (event) {
- statusline.updateTabCount(true);
- }, false);
- tabContainer.addEventListener("TabOpen", function (event) {
- statusline.updateTabCount(true);
- }, false);
- tabContainer.addEventListener("TabClose", function (event) {
- statusline.updateTabCount(true);
- }, false);
- tabContainer.addEventListener("TabSelect", function (event) {
- // TODO: is all of that necessary?
- // I vote no. --Kris
- modes.reset();
- statusline.updateTabCount(true);
- tabs.updateSelectionHistory();
-
- if (options["focuscontent"])
- setTimeout(function () { liberator.focusContent(true); }, 10); // just make sure, that no widget has focus
- }, false);
-
- tabs.getBrowser().addEventListener("DOMContentLoaded", onDOMContentLoaded, true);
-
- // this adds an event which is is called on each page load, even if the
- // page is loaded in a background tab
- tabs.getBrowser().addEventListener("load", onPageLoad, true);
+ window.addEventListener("keypress", wrapListener("onKeyPress"), true);
+ window.addEventListener("keydown", wrapListener("onKeyUpOrDown"), true);
+ window.addEventListener("keyup", wrapListener("onKeyUpOrDown"), true);
+ },
+
+ /**
+ * A destructor called when this module is destroyed.
+ */
+ destroy: function () {
+ // removeEventListeners() to avoid mem leaks
+ liberator.dump("TODO: remove all eventlisteners");
+
+ try {
+ getBrowser().removeProgressListener(this.progressListener);
+ }
+ catch (e) {}
- // called when the active document is scrolled
- tabs.getBrowser().addEventListener("scroll", function (event) {
- statusline.updateBufferPosition();
- modes.show();
- }, null);
- });
- }
+ window.removeEventListener("popupshown", this._enterPopupMode, true);
+ window.removeEventListener("popuphidden", this._exitPopupMode, true);
+ window.removeEventListener("DOMMenuBarActive", this._enterMenuMode, true);
+ window.removeEventListener("DOMMenuBarInactive", this._exitMenuMode, true);
-// getBrowser().addEventListener("submit", function (event) {
-// // reset buffer loading state as early as possible, important for macros
-// buffer.loaded = 0;
-// }, null);
+ window.removeEventListener("keypress", this.onKeyPress, true);
+ window.removeEventListener("keydown", this.onKeyDown, true);
+ },
- /////////////////////////////////////////////////////////
- // track if a popup is open or the menubar is active
- var activeMenubar = false;
- function enterPopupMode(event)
- {
+ _enterPopupMode: function (event) {
if (event.originalTarget.localName == "tooltip" || event.originalTarget.id == "liberator-visualbell")
return;
modes.add(modes.MENU);
- }
- function exitPopupMode()
- {
- // gContextMenu is set to NULL, when a context menu is closed
- if (typeof gContextMenu != "undefined" && gContextMenu == null && !activeMenubar)
+ },
+ _exitPopupMode: function () { // gContextMenu is set to NULL, when a context menu is closed
+ if (window.gContextMenu == null && !this._activeMenubar)
modes.remove(modes.MENU);
- }
- function enterMenuMode()
- {
- activeMenubar = true;
+ },
+ _enterMenuMode: function () {
+ this._activeMenubar = true;
modes.add(modes.MENU);
- }
- function exitMenuMode()
- {
- activeMenubar = false;
+ },
+ _exitMenuMode: function () {
+ this._activeMenubar = false;
modes.remove(modes.MENU);
- }
- window.addEventListener("popupshown", enterPopupMode, true);
- window.addEventListener("popuphidden", exitPopupMode, true);
- window.addEventListener("DOMMenuBarActive", enterMenuMode, true);
- window.addEventListener("DOMMenuBarInactive", exitMenuMode, true);
- window.addEventListener("resize", onResize, true);
-
- // NOTE: the order of ["Esc", "Escape"] or ["Escape", "Esc"]
- // matters, so use that string as the first item, that you
- // want to refer to within liberator's source code for
- // comparisons like if (key == "<Esc>") { ... }
- var keyTable = {
- add: ["Plus", "Add"],
- back_space: ["BS"],
- delete: ["Del"],
- escape: ["Esc", "Escape"],
- insert: ["Insert", "Ins"],
- left_shift: ["LT", "<"],
- return: ["Return", "CR", "Enter"],
- right_shift: [">"],
- space: ["Space", " "],
- subtract: ["Minus", "Subtract"]
- };
-
- const code_key = {};
- const key_code = {};
-
- for (let [k, v] in Iterator(KeyEvent))
- if (/^DOM_VK_(?![A-Z0-9]$)/.test(k))
- {
- k = k.substr(7).toLowerCase();
- let names = [k.replace(/(^|_)(.)/g, function (m, n1, n2) n2.toUpperCase())
- .replace(/^NUMPAD/, "k")];
- if (k in keyTable)
- names = keyTable[k];
- code_key[v] = names[0];
- for (let [, name] in Iterator(names))
- key_code[name.toLowerCase()] = v;
- }
-
- // HACK: as Gecko does not include an event for <, we must add this in manually.
- if (!("<" in key_code))
- {
- key_code["<"] = 60;
- key_code["lt"] = 60;
- code_key[60] = "lt";
- }
-
- function isInputElemFocused()
- {
- let elem = liberator.focus;
- return ((elem instanceof HTMLInputElement && !/image/.test(elem.type)) ||
- elem instanceof HTMLTextAreaElement ||
- elem instanceof HTMLIsIndexElement ||
- elem instanceof HTMLObjectElement ||
- elem instanceof HTMLEmbedElement);
- }
+ },
+
+ /**
+ * @property {boolean} Whether synthetic key events are currently being
+ * processed.
+ */
+ feedingKeys: false,
+
+ wantsModeReset: true, // used in onFocusChange since Firefox is so buggy here
+
+ /**
+ * Initiates the recording of a key event macro.
+ *
+ * @param {string} macro The name for the macro.
+ */
+ startRecording: function (macro) {
+ // TODO: ignore this like Vim?
+ liberator.assert(/[a-zA-Z0-9]/.test(macro),
+ "E354: Invalid register name: '" + macro + "'");
- function triggerLoadAutocmd(name, doc)
- {
- let args = {
- url: doc.location.href,
- title: doc.title
- };
+ modes.isRecording = true;
- if (liberator.has("tabs"))
- {
- args.tab = tabs.getContentIndex(doc) + 1;
- args.doc = "tabs.getTab(" + (args.tab - 1) + ").linkedBrowser.contentDocument";
+ if (/[A-Z]/.test(macro)) { // uppercase (append)
+ this._currentMacro = macro.toLowerCase();
+ if (!this._macros.get(this._currentMacro))
+ this._macros.set(this._currentMacro, ""); // initialize if it does not yet exist
}
-
- autocommands.trigger(name, args);
- }
-
- function onResize(event)
- {
- if (window.fullScreen != fullscreen)
- {
- fullscreen = window.fullScreen;
- liberator.triggerObserver("fullscreen", fullscreen);
- autocommands.trigger("Fullscreen", { state: fullscreen });
+ else {
+ this._currentMacro = macro;
+ this._macros.set(this._currentMacro, "");
}
- }
-
- function onDOMContentLoaded(event)
- {
- let doc = event.originalTarget;
- if (doc instanceof HTMLDocument && !doc.defaultView.frameElement)
- triggerLoadAutocmd("DOMLoad", doc);
- }
-
- // TODO: see what can be moved to onDOMContentLoaded()
- function onPageLoad(event)
- {
- if (event.originalTarget instanceof HTMLDocument)
- {
- let doc = event.originalTarget;
- // document is part of a frameset
- if (doc.defaultView.frameElement)
- {
- // hacky way to get rid of "Transfering data from ..." on sites with frames
- // when you click on a link inside a frameset, because asyncUpdateUI
- // is not triggered there (Gecko bug?)
- setTimeout(function () { statusline.updateUrl(); }, 10);
- return;
- }
-
- // code which should happen for all (also background) newly loaded tabs goes here:
-
- // mark the buffer as loaded, we can't use buffer.loaded
- // since that always refers to the current buffer, while doc can be
- // any buffer, even in a background tab
- doc.pageIsFullyLoaded = 1;
-
- // code which is only relevant if the page load is the current tab goes here:
- if (doc == getBrowser().contentDocument)
- {
- // we want to stay in command mode after a page has loaded
- // TODO: move somewhere else, as focusing can already happen earlier than on "load"
- if (options["focuscontent"])
- {
- setTimeout(function () {
- let focused = liberator.focus;
- if (focused && (focused.value != null) && focused.value.length == 0)
- focused.blur();
- }, 0);
- }
- }
- else // background tab
- liberator.echomsg("Background tab loaded: " + doc.title || doc.location.href, 3);
-
- triggerLoadAutocmd("PageLoad", doc);
+ },
+
+ /**
+ * Replays a macro.
+ *
+ * @param {string} The name of the macro to replay.
+ * @return {boolean}
+ */
+ playMacro: function (macro) {
+ let res = false;
+ if (!/[a-zA-Z0-9@]/.test(macro) && macro.length == 1) {
+ liberator.echoerr("E354: Invalid register name: '" + macro + "'");
+ return false;
}
- }
- // return true when load successful, or false otherwise
- function waitForPageLoad() events.waitForPageLoad();
-
- // load all macros
- // setTimeout needed since io. is loaded after events.
- setTimeout(function () {
- try
- {
- let dirs = io.getRuntimeDirectories("macros");
-
- if (dirs.length > 0)
- {
- for (let [, dir] in Iterator(dirs))
- {
- liberator.echomsg('Searching for "macros/*" in "' + dir.path + '"', 2);
- liberator.log("Sourcing macros directory: " + dir.path + "...", 3);
-
- let files = io.readDirectory(dir.path);
- for (let file in dir.iterDirectory())
- {
- if (file.exists() && !file.isDirectory() && file.isReadable() &&
- /^[\w_-]+(\.vimp)?$/i.test(file.leafName))
- {
- let name = file.leafName.replace(/\.vimp$/i, "");
- macros.set(name, file.read().split("\n")[0]);
-
- liberator.log("Macro " + name + " added: " + macros.get(name), 5);
- }
- }
- }
+ if (macro == "@") { // use lastMacro if it's set
+ if (!this._lastMacro) {
+ liberator.echoerr("E748: No previously used register");
+ return false;
}
- else
- liberator.log("No user macros directory found", 3);
}
- catch (e)
- {
- // thrown if directory does not exist
- liberator.log("Error sourcing macros directory: " + e, 9);
+ else {
+ if (macro.length == 1)
+ this._lastMacro = macro.toLowerCase(); // XXX: sets last played macro, even if it does not yet exist
+ else
+ this._lastMacro = macro; // e.g. long names are case sensitive
}
- }, 100);
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// MAPPINGS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- liberator.registerObserver("load_mappings", function () {
- mappings.add(modes.all,
- ["<Esc>", "<C-[>"], "Focus content",
- function () { events.onEscape(); });
-
- // add the ":" mapping in all but insert mode mappings
- mappings.add([modes.NORMAL, modes.PLAYER, modes.VISUAL, modes.HINTS, modes.MESSAGE, modes.COMPOSE, modes.CARET, modes.TEXTAREA],
- [":"], "Enter command line mode",
- function () { commandline.open(":", "", modes.EX); });
-
- // focus events
- mappings.add([modes.NORMAL, modes.PLAYER, modes.VISUAL, modes.CARET],
- ["<Tab>"], "Advance keyboard focus",
- function () { document.commandDispatcher.advanceFocus(); });
-
- mappings.add([modes.NORMAL, modes.PLAYER, modes.VISUAL, modes.CARET, modes.INSERT, modes.TEXTAREA],
- ["<S-Tab>"], "Rewind keyboard focus",
- function () { document.commandDispatcher.rewindFocus(); });
- mappings.add(modes.all,
- ["<C-z>"], "Temporarily ignore all " + config.name + " key bindings",
- function () { modes.passAllKeys = true; });
-
- mappings.add(modes.all,
- ["<C-v>"], "Pass through next key",
- function () { modes.passNextKey = true; });
-
- mappings.add(modes.all,
- ["<Nop>"], "Do nothing",
- function () { return; });
-
- // macros
- mappings.add([modes.NORMAL, modes.PLAYER, modes.MESSAGE],
- ["q"], "Record a key sequence into a macro",
- function (arg) { events.startRecording(arg); },
- { arg: true });
-
- mappings.add([modes.NORMAL, modes.PLAYER, modes.MESSAGE],
- ["@"], "Play a macro",
- function (count, arg)
- {
- if (count < 1) count = 1;
- while (count-- && events.playMacro(arg))
- ;
- },
- { arg: true, count: true });
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMMANDS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- commands.add(["delmac[ros]"],
- "Delete macros",
- function (args)
- {
- liberator.assert(!args.bang || !args.string, "E474: Invalid argument");
-
- if (args.bang)
- events.deleteMacros();
- else if (args.string)
- events.deleteMacros(args.string);
- else
- liberator.echoerr("E471: Argument required");
- },
- {
- bang: true,
- completer: function (context) completion.macro(context)
- });
-
- commands.add(["macros"],
- "List all macros",
- function (args) { completion.listCompleter("macro", args[0]); },
- {
- argCount: "?",
- completer: function (context) completion.macro(context)
- });
-
- commands.add(["pl[ay]"],
- "Replay a recorded macro",
- function (args) { events.playMacro(args[0]); },
- {
- argCount: "1",
- completer: function (context) completion.macro(context)
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// PUBLIC SECTION //////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- const self = {
-
- /**
- * @property {boolean} Whether synthetic key events are currently being
- * processed.
- */
- feedingKeys: false,
-
- wantsModeReset: true, // used in onFocusChange since Firefox is so buggy here
-
- /**
- * A destructor called when this module is destroyed.
- */
- destroy: function ()
- {
- // removeEventListeners() to avoid mem leaks
- liberator.dump("TODO: remove all eventlisteners");
-
- try
- {
- getBrowser().removeProgressListener(this.progressListener);
+ if (this._macros.get(this._lastMacro)) {
+ // make sure the page is stopped before starting to play the macro
+ try {
+ window.getWebNavigation().stop(nsIWebNavigation.STOP_ALL);
}
catch (e) {}
- window.removeEventListener("popupshown", enterPopupMode, true);
- window.removeEventListener("popuphidden", exitPopupMode, true);
- window.removeEventListener("DOMMenuBarActive", enterMenuMode, true);
- window.removeEventListener("DOMMenuBarInactive", exitMenuMode, true);
-
- window.removeEventListener("keypress", this.onKeyPress, true);
- window.removeEventListener("keydown", this.onKeyDown, true);
- },
-
- /**
- * Initiates the recording of a key event macro.
- *
- * @param {string} macro The name for the macro.
- */
- startRecording: function (macro)
- {
+ buffer.loaded = 1; // even if not a full page load, assume it did load correctly before starting the macro
+ modes.isReplaying = true;
+ res = events.feedkeys(this._macros.get(this._lastMacro), { noremap: true });
+ modes.isReplaying = false;
+ }
+ else {
+ if (this._lastMacro.length == 1)
// TODO: ignore this like Vim?
- liberator.assert(/[a-zA-Z0-9]/.test(macro),
- "E354: Invalid register name: '" + macro + "'");
-
- modes.isRecording = true;
-
- if (/[A-Z]/.test(macro)) // uppercase (append)
- {
- currentMacro = macro.toLowerCase();
- if (!macros.get(currentMacro))
- macros.set(currentMacro, ""); // initialize if it does not yet exist
- }
+ liberator.echoerr("Exxx: Register '" + this._lastMacro + "' not set");
else
- {
- currentMacro = macro;
- macros.set(currentMacro, "");
- }
- },
-
- /**
- * Replays a macro.
- *
- * @param {string} The name of the macro to replay.
- * @return {boolean}
- */
- playMacro: function (macro)
- {
- let res = false;
- if (!/[a-zA-Z0-9@]/.test(macro) && macro.length == 1)
- {
- liberator.echoerr("E354: Invalid register name: '" + macro + "'");
- return false;
- }
-
- if (macro == "@") // use lastMacro if it's set
- {
- if (!lastMacro)
- {
- liberator.echoerr("E748: No previously used register");
- return false;
- }
- }
- else
- {
- if (macro.length == 1)
- lastMacro = macro.toLowerCase(); // XXX: sets last played macro, even if it does not yet exist
+ liberator.echoerr("Exxx: Named macro '" + this._lastMacro + "' not set");
+ }
+ return res;
+ },
+
+ /**
+ * Returns all macros matching <b>filter</b>.
+ *
+ * @param {string} filter A regular expression filter string. A null
+ * filter selects all macros.
+ */
+ getMacros: function (filter) {
+ if (!filter)
+ return this._macros;
+
+ let re = RegExp(filter);
+ return ([macro, keys] for ([macro, keys] in this._macros) if (re.test(macro)));
+ },
+
+ /**
+ * Deletes all macros matching <b>filter</b>.
+ *
+ * @param {string} filter A regular expression filter string. A null
+ * filter deletes all macros.
+ */
+ deleteMacros: function (filter) {
+ let re = RegExp(filter);
+
+ for (let [item, ] in this._macros) {
+ if (re.test(item) || !filter)
+ this._macros.remove(item);
+ }
+ },
+
+ /**
+ * Pushes keys onto the event queue from liberator. It is similar to
+ * Vim's feedkeys() method, but cannot cope with 2 partially-fed
+ * strings, you have to feed one parsable string.
+ *
+ * @param {string} keys A string like "2<C-f>" to push onto the event
+ * queue. If you want "<" to be taken literally, prepend it with a
+ * "\\".
+ * @param {boolean} noremap Allow recursive mappings.
+ * @param {boolean} silent Whether the command should be echoed to the
+ * command line.
+ * @returns {boolean}
+ */
+ feedkeys: function (keys, noremap, quiet) {
+ let doc = window.document;
+ let view = window.document.defaultView;
+
+ let wasFeeding = this.feedingKeys;
+ this.feedingKeys = true;
+ this.duringFeed = this.duringFeed || [];
+ let wasQuiet = commandline.quiet;
+ if (quiet)
+ commandline.quiet = quiet;
+
+ try {
+ liberator.threadYield(1, true);
+
+ for (let [, evt_obj] in Iterator(events.fromString(keys))) {
+ let elem = liberator.focus || window.content;
+ let evt = events.create(doc, "keypress", evt_obj);
+
+ if (typeof noremap == "object")
+ for (let [k, v] in Iterator(noremap))
+ evt[k] = v;
else
- lastMacro = macro; // e.g. long names are case sensitive
- }
-
- if (macros.get(lastMacro))
- {
- // make sure the page is stopped before starting to play the macro
- try
- {
- window.getWebNavigation().stop(nsIWebNavigation.STOP_ALL);
+ evt.noremap = !!noremap;
+ evt.isMacro = true;
+ // A special hack for liberator-specific key names.
+ if (evt_obj.liberatorString || evt_obj.liberatorShift) {
+ evt.liberatorString = evt_obj.liberatorString; // for key-less keypress events e.g. <Nop>
+ evt.liberatorShift = evt_obj.liberatorShift; // for untypable shift keys e.g. <S-1>
+ events.onKeyPress(evt);
}
- catch (e) {}
- buffer.loaded = 1; // even if not a full page load, assume it did load correctly before starting the macro
- modes.isReplaying = true;
- res = events.feedkeys(macros.get(lastMacro), { noremap: true });
- modes.isReplaying = false;
- }
- else
- {
- if (lastMacro.length == 1)
- // TODO: ignore this like Vim?
- liberator.echoerr("Exxx: Register '" + lastMacro + "' not set");
else
- liberator.echoerr("Exxx: Named macro '" + lastMacro + "' not set");
- }
- return res;
- },
+ elem.dispatchEvent(evt);
- /**
- * Returns all macros matching <b>filter</b>.
- *
- * @param {string} filter A regular expression filter string. A null
- * filter selects all macros.
- */
- getMacros: function (filter)
- {
- if (!filter)
- return macros;
-
- let re = RegExp(filter);
- return ([macro, keys] for ([macro, keys] in macros) if (re.test(macro)));
- },
+ if (!this.feedingKeys)
+ break;
- /**
- * Deletes all macros matching <b>filter</b>.
- *
- * @param {string} filter A regular expression filter string. A null
- * filter deletes all macros.
- */
- deleteMacros: function (filter)
- {
- let re = RegExp(filter);
-
- for (let [item, ] in macros)
- {
- if (re.test(item) || !filter)
- macros.remove(item);
+ // Stop feeding keys if page loading failed.
+ if (modes.isReplaying && !this.waitForPageLoad())
+ break;
}
- },
-
- /**
- * Pushes keys onto the event queue from liberator. It is similar to
- * Vim's feedkeys() method, but cannot cope with 2 partially-fed
- * strings, you have to feed one parsable string.
- *
- * @param {string} keys A string like "2<C-f>" to push onto the event
- * queue. If you want "<" to be taken literally, prepend it with a
- * "\\".
- * @param {boolean} noremap Allow recursive mappings.
- * @param {boolean} silent Whether the command should be echoed to the
- * command line.
- * @returns {boolean}
- */
- feedkeys: function (keys, noremap, quiet)
- {
- let doc = window.document;
- let view = window.document.defaultView;
-
- let wasFeeding = this.feedingKeys;
- this.feedingKeys = true;
- this.duringFeed = this.duringFeed || [];
- let wasQuiet = commandline.quiet;
+ }
+ finally {
+ this.feedingKeys = wasFeeding;
if (quiet)
- commandline.quiet = quiet;
+ commandline.quiet = wasQuiet;
- try
- {
- liberator.threadYield(1, true);
-
- for (let [, evt_obj] in Iterator(events.fromString(keys)))
- {
- let elem = liberator.focus || window.content;
- let evt = events.create(doc, "keypress", evt_obj);
+ if (this.duringFeed.length) {
+ let duringFeed = this.duringFeed;
+ this.duringFeed = [];
+ for (let [, evt] in Iterator(duringFeed))
+ evt.target.dispatchEvent(evt);
+ }
+ }
+ },
+
+ /**
+ * Creates an actual event from a pseudo-event object.
+ *
+ * The pseudo-event object (such as may be retrieved from events.fromString)
+ * should have any properties you want the event to have.
+ *
+ * @param {Document} doc The DOM document to associate this event with
+ * @param {Type} type The type of event (keypress, click, etc.)
+ * @param {Object} opts The pseudo-event.
+ */
+ create: function (doc, type, opts) {
+ var DEFAULTS = {
+ Key: {
+ type: type,
+ bubbles: true, cancelable: true,
+ view: doc.defaultView,
+ ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
+ keyCode: 0, charCode: 0
+ },
+ Mouse: {
+ type: type,
+ bubbles: true, cancelable: true,
+ view: doc.defaultView,
+ detail: 1,
+ screenX: 0, screenY: 0,
+ clientX: 0, clientY: 0,
+ ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
+ button: 0,
+ relatedTarget: null
+ }
+ };
+ const TYPES = {
+ click: "Mouse", mousedown: "Mouse", mouseup: "Mouse",
+ mouseover: "Mouse", mouseout: "Mouse",
+ keypress: "Key", keyup: "Key", keydown: "Key"
+ };
+ var t = TYPES[type];
+ var evt = doc.createEvent(t + "Events");
+ evt["init" + t + "Event"].apply(evt,
+ [v for ([k, v] in Iterator(util.extend(DEFAULTS[t], opts)))]);
+ return evt;
+ },
+
+ /**
+ * Converts a user-input string of keys into a canonical
+ * representation.
+ *
+ * <C-A> maps to <C-a>, <C-S-a> maps to <C-S-A>
+ * <C- > maps to <C-Space>, <S-a> maps to A
+ * << maps to <lt><lt>
+ *
+ * <S-@> is preserved, as in vim, to allow untypable key-combinations
+ * in macros.
+ *
+ * canonicalKeys(canonicalKeys(x)) == canonicalKeys(x) for all values
+ * of x.
+ *
+ * @param {string} keys Messy form.
+ * @returns {string} Canonical form.
+ */
+ canonicalKeys: function (keys) {
+ return events.fromString(keys).map(events.closure.toString).join("");
+ },
+
+ /**
+ * Converts an event string into an array of pseudo-event objects.
+ *
+ * These objects can be used as arguments to events.toString or
+ * events.create, though they are unlikely to be much use for other
+ * purposes. They have many of the properties you'd expect to find on a
+ * real event, but none of the methods.
+ *
+ * Also may contain two "special" parameters, .liberatorString and
+ * .liberatorShift these are set for characters that can never by
+ * typed, but may appear in mappings, for example <Nop> is passed as
+ * liberatorString, and liberatorShift is set when a user specifies
+ * <S-@> where @ is a non-case-changable, non-space character.
+ *
+ * @param {string} keys The string to parse.
+ * @return {Array[Object]}
+ */
+ fromString: function (input) {
+ let out = [];
+
+ let re = RegExp("<.*?>?>|[^<]|<(?!.*>)", "g");
+ let match;
+
+ while (match = re.exec(input)) {
+ let evt_str = match[0];
+ let evt_obj = { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false,
+ keyCode: 0, charCode: 0, type: "keypress" };
+
+ if (evt_str.length > 1) { // <.*?>
+ let [match, modifier, keyname] = evt_str.match(/^<((?:[CSMA]-)*)(.+?)>$/i) || [false, '', ''];
+ modifier = modifier.toUpperCase();
+ keyname = keyname.toLowerCase();
+
+ if (keyname && !(keyname.length == 1 && modifier.length == 0 || // disallow <> and <a>
+ !(keyname.length == 1 || this._key_code[keyname] || keyname == "nop" || /mouse$/.test(keyname)))) { // disallow <misteak>
+ evt_obj.ctrlKey = /C-/.test(modifier);
+ evt_obj.altKey = /A-/.test(modifier);
+ evt_obj.shiftKey = /S-/.test(modifier);
+ evt_obj.metaKey = /M-/.test(modifier);
+
+ if (keyname.length == 1) { // normal characters
+ if (evt_obj.shiftKey) {
+ keyname = keyname.toUpperCase();
+ if (keyname == keyname.toLowerCase())
+ evt_obj.liberatorShift = true;
+ }
- if (typeof noremap == "object")
- for (let [k, v] in Iterator(noremap))
- evt[k] = v;
- else
- evt.noremap = !!noremap;
- evt.isMacro = true;
- // A special hack for liberator-specific key names.
- if (evt_obj.liberatorString || evt_obj.liberatorShift)
- {
- evt.liberatorString = evt_obj.liberatorString; // for key-less keypress events e.g. <Nop>
- evt.liberatorShift = evt_obj.liberatorShift; // for untypable shift keys e.g. <S-1>
- events.onKeyPress(evt);
+ evt_obj.charCode = keyname.charCodeAt(0);
+ }
+ else if (keyname == "nop") {
+ evt_obj.liberatorString = "<Nop>";
+ }
+ else if (/mouse$/.test(keyname)) { // mouse events
+ evt_obj.type = (/2-/.test(modifier) ? "dblclick" : "click");
+ evt_obj.button = ["leftmouse", "middlemouse", "rightmouse"].indexOf(keyname);
+ delete evt_obj.keyCode;
+ delete evt_obj.charCode;
+ }
+ else { // spaces, control characters, and <
+ evt_obj.keyCode = this._key_code[keyname];
+ evt_obj.charCode = 0;
}
-
- else
- elem.dispatchEvent(evt);
-
- if (!this.feedingKeys)
- break;
-
- // Stop feeding keys if page loading failed.
- if (modes.isReplaying && !waitForPageLoad())
- break;
}
- }
- finally
- {
- this.feedingKeys = wasFeeding;
- if (quiet)
- commandline.quiet = wasQuiet;
-
- if (this.duringFeed.length)
- {
- let duringFeed = this.duringFeed;
- this.duringFeed = [];
- for (let [, evt] in Iterator(duringFeed))
- evt.target.dispatchEvent(evt);
+ else { // an invalid sequence starting with <, treat as a literal
+ out = out.concat(events.fromString("<lt>" + evt_str.substr(1)));
+ continue;
}
}
- },
+ else // a simple key (no <...>)
+ evt_obj.charCode = evt_str.charCodeAt(0);
- /**
- * Creates an actual event from a pseudo-event object.
- *
- * The pseudo-event object (such as may be retrieved from events.fromString)
- * should have any properties you want the event to have.
- *
- * @param {Document} doc The DOM document to associate this event with
- * @param {Type} type The type of event (keypress, click, etc.)
- * @param {Object} opts The pseudo-event.
- */
- create: function (doc, type, opts)
- {
- var DEFAULTS = {
- Key: {
- type: type,
- bubbles: true, cancelable: true,
- view: doc.defaultView,
- ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
- keyCode: 0, charCode: 0
- },
- Mouse: {
- type: type,
- bubbles: true, cancelable: true,
- view: doc.defaultView,
- detail: 1,
- screenX: 0, screenY: 0,
- clientX: 0, clientY: 0,
- ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
- button: 0,
- relatedTarget: null
- }
- };
- const TYPES = {
- click: "Mouse", mousedown: "Mouse", mouseup: "Mouse",
- mouseover: "Mouse", mouseout: "Mouse",
- keypress: "Key", keyup: "Key", keydown: "Key"
- };
- var t = TYPES[type];
- var evt = doc.createEvent(t + "Events");
- evt["init" + t + "Event"].apply(evt,
- [v for ([k, v] in Iterator(util.extend(DEFAULTS[t], opts)))]);
- return evt;
- },
+ // TODO: make a list of characters that need keyCode and charCode somewhere
+ if (evt_obj.keyCode == 32 || evt_obj.charCode == 32)
+ evt_obj.charCode = evt_obj.keyCode = 32; // <Space>
+ if (evt_obj.keyCode == 60 || evt_obj.charCode == 60)
+ evt_obj.charCode = evt_obj.keyCode = 60; // <lt>
- /**
- * Converts a user-input string of keys into a canonical
- * representation.
- *
- * <C-A> maps to <C-a>, <C-S-a> maps to <C-S-A>
- * <C- > maps to <C-Space>, <S-a> maps to A
- * << maps to <lt><lt>
- *
- * <S-@> is preserved, as in vim, to allow untypable key-combinations
- * in macros.
- *
- * canonicalKeys(canonicalKeys(x)) == canonicalKeys(x) for all values
- * of x.
- *
- * @param {string} keys Messy form.
- * @returns {string} Canonical form.
- */
- canonicalKeys: function (keys)
- {
- return events.fromString(keys).map(events.toString).join("");
- },
-
- /**
- * Converts an event string into an array of pseudo-event objects.
- *
- * These objects can be used as arguments to events.toString or
- * events.create, though they are unlikely to be much use for other
- * purposes. They have many of the properties you'd expect to find on a
- * real event, but none of the methods.
- *
- * Also may contain two "special" parameters, .liberatorString and
- * .liberatorShift these are set for characters that can never by
- * typed, but may appear in mappings, for example <Nop> is passed as
- * liberatorString, and liberatorShift is set when a user specifies
- * <S-@> where @ is a non-case-changable, non-space character.
- *
- * @param {string} keys The string to parse.
- * @return {Array[Object]}
- */
- fromString: function (input)
- {
- let out = [];
-
- let re = RegExp("<.*?>?>|[^<]|<(?!.*>)", "g");
- let match;
-
- while (match = re.exec(input))
- {
- let evt_str = match[0];
- let evt_obj = { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false,
- keyCode: 0, charCode: 0, type: "keypress" };
-
- if (evt_str.length > 1) // <.*?>
- {
- let [match, modifier, keyname] = evt_str.match(/^<((?:[CSMA]-)*)(.+?)>$/i) || [false, '', ''];
- modifier = modifier.toUpperCase();
- keyname = keyname.toLowerCase();
-
- if (keyname && !(keyname.length == 1 && modifier.length == 0 || // disallow <> and <a>
- !(keyname.length == 1 || key_code[keyname] || keyname == "nop" || /mouse$/.test(keyname)))) // disallow <misteak>
- {
- evt_obj.ctrlKey = /C-/.test(modifier);
- evt_obj.altKey = /A-/.test(modifier);
- evt_obj.shiftKey = /S-/.test(modifier);
- evt_obj.metaKey = /M-/.test(modifier);
-
- if (keyname.length == 1) // normal characters
- {
- if (evt_obj.shiftKey)
- {
- keyname = keyname.toUpperCase();
- if (keyname == keyname.toLowerCase())
- evt_obj.liberatorShift = true;
- }
+ out.push(evt_obj);
+ }
+ return out;
+ },
+
+ /**
+ * Converts the specified event to a string in liberator key-code
+ * notation. Returns null for an unknown event.
+ *
+ * E.g. pressing ctrl+n would result in the string "<C-n>".
+ *
+ * @param {Event} event
+ * @returns {string}
+ */
+ toString: function (event) {
+ if (!event)
+ return "[object Mappings]";
+
+ if (event.liberatorString)
+ return event.liberatorString;
+
+ let key = null;
+ let modifier = "";
+
+ if (event.ctrlKey)
+ modifier += "C-";
+ if (event.altKey)
+ modifier += "A-";
+ if (event.metaKey)
+ modifier += "M-";
+
+ if (/^key/.test(event.type)) {
+ if (event.charCode == 0) {
+ if (event.shiftKey)
+ modifier += "S-";
- evt_obj.charCode = keyname.charCodeAt(0);
- }
- else if (keyname == "nop")
- {
- evt_obj.liberatorString = "<Nop>";
- }
- else if (/mouse$/.test(keyname)) // mouse events
- {
- evt_obj.type = (/2-/.test(modifier) ? "dblclick" : "click");
- evt_obj.button = ["leftmouse", "middlemouse", "rightmouse"].indexOf(keyname);
- delete evt_obj.keyCode;
- delete evt_obj.charCode;
- }
- else // spaces, control characters, and <
- {
- evt_obj.keyCode = key_code[keyname];
- evt_obj.charCode = 0;
- }
- }
- else // an invalid sequence starting with <, treat as a literal
- {
- out = out.concat(events.fromString("<lt>" + evt_str.substr(1)));
- continue;
- }
+ if (event.keyCode in this._code_key)
+ key = this._code_key[event.keyCode];
+ }
+ // [Ctrl-Bug] special handling of mysterious <C-[>, <C-\\>, <C-]>, <C-^>, <C-_> bugs (OS/X)
+ // (i.e., cntrl codes 27--31)
+ // ---
+ // For more information, see:
+ // [*] Vimp FAQ: http://vimperator.org/trac/wiki/Vimperator/FAQ#WhydoesntC-workforEscMacOSX
+ // [*] Referenced mailing list msg: http://www.mozdev.org/pipermail/vimperator/2008-May/001548.html
+ // [*] Mozilla bug 416227: event.charCode in keypress handler has unexpected values on Mac for Ctrl with chars in "[ ] _ \"
+ // https://bugzilla.mozilla.org/show_bug.cgi?query_format=specific&order=relevance+desc&bug_status=__open__&id=416227
+ // [*] Mozilla bug 432951: Ctrl+'foo' doesn't seem same charCode as Meta+'foo' on Cocoa
+ // https://bugzilla.mozilla.org/show_bug.cgi?query_format=specific&order=relevance+desc&bug_status=__open__&id=432951
+ // ---
+ //
+ // The following fixes are only activated if liberator.has("MacUnix").
+ // Technically, they prevent mappings from <C-Esc> (and
+ // <C-C-]> if your fancy keyboard permits such things<?>), but
+ // these <C-control> mappings are probably pathological (<C-Esc>
+ // certainly is on Windows), and so it is probably
+ // harmless to remove the has("MacUnix") if desired.
+ //
+ else if (liberator.has("MacUnix") && event.ctrlKey && event.charCode >= 27 && event.charCode <= 31) {
+ if (event.charCode == 27) { // [Ctrl-Bug 1/5] the <C-[> bug
+ key = "Esc";
+ modifier = modifier.replace("C-", "");
}
- else // a simple key (no <...>)
- evt_obj.charCode = evt_str.charCodeAt(0);
-
- // TODO: make a list of characters that need keyCode and charCode somewhere
- if (evt_obj.keyCode == 32 || evt_obj.charCode == 32)
- evt_obj.charCode = evt_obj.keyCode = 32; // <Space>
- if (evt_obj.keyCode == 60 || evt_obj.charCode == 60)
- evt_obj.charCode = evt_obj.keyCode = 60; // <lt>
-
- out.push(evt_obj);
+ else // [Ctrl-Bug 2,3,4,5/5] the <C-\\>, <C-]>, <C-^>, <C-_> bugs
+ key = String.fromCharCode(event.charCode + 64);
}
- return out;
- },
+ // a normal key like a, b, c, 0, etc.
+ else if (event.charCode > 0) {
+ key = String.fromCharCode(event.charCode);
- /**
- * Converts the specified event to a string in liberator key-code
- * notation. Returns null for an unknown event.
- *
- * E.g. pressing ctrl+n would result in the string "<C-n>".
- *
- * @param {Event} event
- * @returns {string}
- */
- toString: function (event)
- {
- if (!event)
- return "[object Mappings]";
-
- if (event.liberatorString)
- return event.liberatorString;
-
- let key = null;
- let modifier = "";
-
- if (event.ctrlKey)
- modifier += "C-";
- if (event.altKey)
- modifier += "A-";
- if (event.metaKey)
- modifier += "M-";
-
- if (/^key/.test(event.type))
- {
- if (event.charCode == 0)
- {
- if (event.shiftKey)
+ if (key in this._key_code) {
+ // a named charcode key (<Space> and <lt>) space can be shifted, <lt> must be forced
+ if ((key.match(/^\s$/) && event.shiftKey) || event.liberatorShift)
modifier += "S-";
- if (event.keyCode in code_key)
- key = code_key[event.keyCode];
- }
- // [Ctrl-Bug] special handling of mysterious <C-[>, <C-\\>, <C-]>, <C-^>, <C-_> bugs (OS/X)
- // (i.e., cntrl codes 27--31)
- // ---
- // For more information, see:
- // [*] Vimp FAQ: http://vimperator.org/trac/wiki/Vimperator/FAQ#WhydoesntC-workforEscMacOSX
- // [*] Referenced mailing list msg: http://www.mozdev.org/pipermail/vimperator/2008-May/001548.html
- // [*] Mozilla bug 416227: event.charCode in keypress handler has unexpected values on Mac for Ctrl with chars in "[ ] _ \"
- // https://bugzilla.mozilla.org/show_bug.cgi?query_format=specific&order=relevance+desc&bug_status=__open__&id=416227
- // [*] Mozilla bug 432951: Ctrl+'foo' doesn't seem same charCode as Meta+'foo' on Cocoa
- // https://bugzilla.mozilla.org/show_bug.cgi?query_format=specific&order=relevance+desc&bug_status=__open__&id=432951
- // ---
- //
- // The following fixes are only activated if liberator.has("MacUnix").
- // Technically, they prevent mappings from <C-Esc> (and
- // <C-C-]> if your fancy keyboard permits such things<?>), but
- // these <C-control> mappings are probably pathological (<C-Esc>
- // certainly is on Windows), and so it is probably
- // harmless to remove the has("MacUnix") if desired.
- //
- else if (liberator.has("MacUnix") && event.ctrlKey && event.charCode >= 27 && event.charCode <= 31)
- {
- if (event.charCode == 27) // [Ctrl-Bug 1/5] the <C-[> bug
- {
- key = "Esc";
- modifier = modifier.replace("C-", "");
- }
- else // [Ctrl-Bug 2,3,4,5/5] the <C-\\>, <C-]>, <C-^>, <C-_> bugs
- key = String.fromCharCode(event.charCode + 64);
- }
- // a normal key like a, b, c, 0, etc.
- else if (event.charCode > 0)
- {
- key = String.fromCharCode(event.charCode);
-
- if (key in key_code)
- {
- // a named charcode key (<Space> and <lt>) space can be shifted, <lt> must be forced
- if ((key.match(/^\s$/) && event.shiftKey) || event.liberatorShift)
- modifier += "S-";
-
- key = code_key[key_code[key]];
- }
- else
- {
- // a shift modifier is only allowed if the key is alphabetical and used in a C-A-M- mapping in the uppercase,
- // or if the shift has been forced for a non-alphabetical character by the user while :map-ping
- if ((key != key.toLowerCase() && (event.ctrlKey || event.altKey || event.metaKey)) || event.liberatorShift)
- modifier += "S-";
- else if (modifier.length == 0)
- return key;
- }
+ key = this._code_key[this._key_code[key]];
}
- if (key == null)
- return;
- }
- else if (event.type == "click" || event.type == "dblclick")
- {
- if (event.shiftKey)
- modifier += "S-";
- if (event.type == "dblclick")
- modifier += "2-";
- // TODO: triple and quadruple click
-
- switch (event.button)
- {
- case 0:
- key = "LeftMouse";
- break;
- case 1:
- key = "MiddleMouse";
- break;
- case 2:
- key = "RightMouse";
- break;
+ else {
+ // a shift modifier is only allowed if the key is alphabetical and used in a C-A-M- mapping in the uppercase,
+ // or if the shift has been forced for a non-alphabetical character by the user while :map-ping
+ if ((key != key.toLowerCase() && (event.ctrlKey || event.altKey || event.metaKey)) || event.liberatorShift)
+ modifier += "S-";
+ else if (modifier.length == 0)
+ return key;
}
}
-
if (key == null)
- return null;
+ return;
+ }
+ else if (event.type == "click" || event.type == "dblclick") {
+ if (event.shiftKey)
+ modifier += "S-";
+ if (event.type == "dblclick")
+ modifier += "2-";
+ // TODO: triple and quadruple click
+
+ switch (event.button) {
+ case 0:
+ key = "LeftMouse";
+ break;
+ case 1:
+ key = "MiddleMouse";
+ break;
+ case 2:
+ key = "RightMouse";
+ break;
+ }
+ }
- return "<" + modifier + key + ">";
- },
+ if (key == null)
+ return null;
+
+ return "<" + modifier + key + ">";
+ },
+
+ /**
+ * Whether <b>key</b> is a key code defined to accept/execute input on
+ * the command line.
+ *
+ * @param {string} key The key code to test.
+ * @returns {boolean}
+ */
+ isAcceptKey: function (key) key == "<Return>" || key == "<C-j>" || key == "<C-m>",
+
+ /**
+ * Whether <b>key</b> is a key code defined to reject/cancel input on
+ * the command line.
+ *
+ * @param {string} key The key code to test.
+ * @returns {boolean}
+ */
+ isCancelKey: function (key) key == "<Esc>" || key == "<C-[>" || key == "<C-c>",
+
+ /**
+ * Waits for the current buffer to successfully finish loading. Returns
+ * true for a successful page load otherwise false.
+ *
+ * @returns {boolean}
+ */
+ waitForPageLoad: function () {
+ //liberator.dump("start waiting in loaded state: " + buffer.loaded);
+ liberator.threadYield(true); // clear queue
+
+ if (buffer.loaded == 1)
+ return true;
+
+ const maxWaitTime = 25;
+ let start = Date.now();
+ let end = start + (maxWaitTime * 1000); // maximum time to wait - TODO: add option
+ let now;
+ while (now = Date.now(), now < end) {
+ liberator.threadYield();
+ //if ((now - start) % 1000 < 10)
+ // liberator.dump("waited: " + (now - start) + " ms");
+
+ if (!events.feedingKeys)
+ return false;
- /**
- * Whether <b>key</b> is a key code defined to accept/execute input on
- * the command line.
- *
- * @param {string} key The key code to test.
- * @returns {boolean}
- */
- isAcceptKey: function (key) key == "<Return>" || key == "<C-j>" || key == "<C-m>",
-
- /**
- * Whether <b>key</b> is a key code defined to reject/cancel input on
- * the command line.
- *
- * @param {string} key The key code to test.
- * @returns {boolean}
- */
- isCancelKey: function (key) key == "<Esc>" || key == "<C-[>" || key == "<C-c>",
-
- /**
- * Waits for the current buffer to successfully finish loading. Returns
- * true for a successful page load otherwise false.
- *
- * @returns {boolean}
- */
- waitForPageLoad: function ()
- {
- //liberator.dump("start waiting in loaded state: " + buffer.loaded);
- liberator.threadYield(true); // clear queue
-
- if (buffer.loaded == 1)
- return true;
-
- const maxWaitTime = 25;
- let start = Date.now();
- let end = start + (maxWaitTime * 1000); // maximum time to wait - TODO: add option
- let now;
- while (now = Date.now(), now < end)
- {
- liberator.threadYield();
- //if ((now - start) % 1000 < 10)
- // liberator.dump("waited: " + (now - start) + " ms");
-
- if (!events.feedingKeys)
- return false;
-
- if (buffer.loaded > 0)
- {
- liberator.sleep(250);
- break;
- }
- else
- liberator.echo("Waiting for page to load...", commandline.DISALLOW_MULTILINE);
+ if (buffer.loaded > 0) {
+ liberator.sleep(250);
+ break;
}
- modes.show();
+ else
+ liberator.echo("Waiting for page to load...", commandline.DISALLOW_MULTILINE);
+ }
+ modes.show();
+
+ // TODO: allow macros to be continued when page does not fully load with an option
+ let ret = (buffer.loaded == 1);
+ if (!ret)
+ liberator.echoerr("Page did not load completely in " + maxWaitTime + " seconds. Macro stopped.");
+ //liberator.dump("done waiting: " + ret);
+
+ // sometimes the input widget had focus when replaying a macro
+ // maybe this call should be moved somewhere else?
+ // liberator.focusContent(true);
+
+ return ret;
+ },
+
+ // argument "event" is deliberately not used, as i don't seem to have
+ // access to the real focus target
+ // Huh? --djk
+ onFocusChange: function (event) {
+ // command line has it's own focus change handler
+ if (liberator.mode == modes.COMMAND_LINE)
+ return;
- // TODO: allow macros to be continued when page does not fully load with an option
- let ret = (buffer.loaded == 1);
- if (!ret)
- liberator.echoerr("Page did not load completely in " + maxWaitTime + " seconds. Macro stopped.");
- //liberator.dump("done waiting: " + ret);
+ function hasHTMLDocument(win) win && win.document && win.document instanceof HTMLDocument
- // sometimes the input widget had focus when replaying a macro
- // maybe this call should be moved somewhere else?
- // liberator.focusContent(true);
+ let win = window.document.commandDispatcher.focusedWindow;
+ let elem = window.document.commandDispatcher.focusedElement;
- return ret;
- },
+ if (win && win.top == content && liberator.has("tabs"))
+ tabs.localStore.focusedFrame = win;
- // argument "event" is deliberately not used, as i don't seem to have
- // access to the real focus target
- // Huh? --djk
- onFocusChange: function (event)
- {
- // command line has it's own focus change handler
- if (liberator.mode == modes.COMMAND_LINE)
+ try {
+ if (elem && elem.readOnly)
return;
- function hasHTMLDocument(win) win && win.document && win.document instanceof HTMLDocument
-
- let win = window.document.commandDispatcher.focusedWindow;
- let elem = window.document.commandDispatcher.focusedElement;
-
- if (win && win.top == content && liberator.has("tabs"))
- tabs.localStore.focusedFrame = win;
-
- try
- {
- if (elem && elem.readOnly)
- return;
-
- if ((elem instanceof HTMLInputElement && /^(text|password)$/.test(elem.type)) ||
- (elem instanceof HTMLSelectElement))
- {
- liberator.mode = modes.INSERT;
- if (hasHTMLDocument(win))
- buffer.lastInputField = elem;
- return;
- }
- if (elem instanceof HTMLEmbedElement || elem instanceof HTMLObjectElement)
- {
- liberator.mode = modes.EMBED;
- return;
- }
-
- if (elem instanceof HTMLTextAreaElement || (elem && elem.contentEditable == "true"))
- {
- if (options["insertmode"])
- modes.set(modes.INSERT);
- else if (elem.selectionEnd - elem.selectionStart > 0)
- modes.set(modes.VISUAL, modes.TEXTAREA);
- else
- modes.main = modes.TEXTAREA;
- if (hasHTMLDocument(win))
- buffer.lastInputField = elem;
- return;
- }
-
- if (config.focusChange)
- return void config.focusChange(win);
-
- let urlbar = document.getElementById("urlbar");
- if (elem == null && urlbar && urlbar.inputField == lastFocus)
- liberator.threadYield(true);
-
- if (liberator.mode & (modes.EMBED | modes.INSERT | modes.TEXTAREA | modes.VISUAL))
- modes.reset();
+ if ((elem instanceof HTMLInputElement && /^(text|password)$/.test(elem.type)) ||
+ (elem instanceof HTMLSelectElement)) {
+ liberator.mode = modes.INSERT;
+ if (hasHTMLDocument(win))
+ buffer.lastInputField = elem;
+ return;
}
- finally
- {
- lastFocus = elem;
+ if (elem instanceof HTMLEmbedElement || elem instanceof HTMLObjectElement) {
+ liberator.mode = modes.EMBED;
+ return;
}
- },
- onSelectionChange: function (event)
- {
- let couldCopy = false;
- let controller = document.commandDispatcher.getControllerForCommand("cmd_copy");
- if (controller && controller.isCommandEnabled("cmd_copy"))
- couldCopy = true;
-
- if (liberator.mode != modes.VISUAL)
- {
- if (couldCopy)
- {
- if ((liberator.mode == modes.TEXTAREA ||
- (modes.extended & modes.TEXTAREA))
- && !options["insertmode"])
- modes.set(modes.VISUAL, modes.TEXTAREA);
- else if (liberator.mode == modes.CARET)
- modes.set(modes.VISUAL, modes.CARET);
- }
+ if (elem instanceof HTMLTextAreaElement || (elem && elem.contentEditable == "true")) {
+ if (options["insertmode"])
+ modes.set(modes.INSERT);
+ else if (elem.selectionEnd - elem.selectionStart > 0)
+ modes.set(modes.VISUAL, modes.TEXTAREA);
+ else
+ modes.main = modes.TEXTAREA;
+ if (hasHTMLDocument(win))
+ buffer.lastInputField = elem;
+ return;
}
- // XXX: disabled, as i think automatically starting visual caret mode does more harm than help
- // else
- // {
- // if (!couldCopy && modes.extended & modes.CARET)
- // liberator.mode = modes.CARET;
- // }
- },
- /**
- * The global escape key handler. This is called in ALL modes.
- */
- onEscape: function ()
- {
- if (modes.passNextKey)
- return;
+ if (config.focusChange)
+ return void config.focusChange(win);
- if (modes.passAllKeys)
- {
- modes.passAllKeys = false;
- return;
+ let urlbar = document.getElementById("urlbar");
+ if (elem == null && urlbar && urlbar.inputField == this._lastFocus)
+ liberator.threadYield(true);
+
+ if (liberator.mode & (modes.EMBED | modes.INSERT | modes.TEXTAREA | modes.VISUAL))
+ modes.reset();
+ }
+ finally {
+ this._lastFocus = elem;
+ }
+ },
+
+ onSelectionChange: function (event) {
+ let couldCopy = false;
+ let controller = document.commandDispatcher.getControllerForCommand("cmd_copy");
+ if (controller && controller.isCommandEnabled("cmd_copy"))
+ couldCopy = true;
+
+ if (liberator.mode != modes.VISUAL) {
+ if (couldCopy) {
+ if ((liberator.mode == modes.TEXTAREA ||
+ (modes.extended & modes.TEXTAREA))
+ && !options["insertmode"])
+ modes.set(modes.VISUAL, modes.TEXTAREA);
+ else if (liberator.mode == modes.CARET)
+ modes.set(modes.VISUAL, modes.CARET);
}
+ }
+ // XXX: disabled, as i think automatically starting visual caret mode does more harm than help
+ // else
+ // {
+ // if (!couldCopy && modes.extended & modes.CARET)
+ // liberator.mode = modes.CARET;
+ // }
+ },
+
+ /**
+ * The global escape key handler. This is called in ALL modes.
+ */
+ onEscape: function () {
+ if (modes.passNextKey)
+ return;
- switch (liberator.mode)
- {
- case modes.NORMAL:
- // clear any selection made
- let selection = window.content.getSelection();
- try
- { // a simple if (selection) does not seem to work
- selection.collapseToStart();
- }
- catch (e) {}
+ if (modes.passAllKeys) {
+ modes.passAllKeys = false;
+ return;
+ }
- modes.reset();
- break;
+ switch (liberator.mode) {
+ case modes.NORMAL:
+ // clear any selection made
+ let selection = window.content.getSelection();
+ try { // a simple if (selection) does not seem to work
+ selection.collapseToStart();
+ }
+ catch (e) {}
- case modes.VISUAL:
- if (modes.extended & modes.TEXTAREA)
- liberator.mode = modes.TEXTAREA;
- else if (modes.extended & modes.CARET)
- liberator.mode = modes.CARET;
- break;
+ modes.reset();
+ break;
+
+ case modes.VISUAL:
+ if (modes.extended & modes.TEXTAREA)
+ liberator.mode = modes.TEXTAREA;
+ else if (modes.extended & modes.CARET)
+ liberator.mode = modes.CARET;
+ break;
+
+ case modes.CARET:
+ // setting this option will trigger an observer which will
+ // take care of all other details like setting the NORMAL
+ // mode
+ options.setPref("accessibility.browsewithcaret", false);
+ break;
+
+ case modes.TEXTAREA:
+ // TODO: different behaviour for text areas and other input
+ // fields seems unnecessarily complicated. If the user
+ // likes Vi-mode then they probably like it for all input
+ // fields, if not they can enter it explicitly for only
+ // text areas. The mode name TEXTAREA is confusing and
+ // would be better replaced with something indicating that
+ // it's a Vi editing mode. Extended modes really need to be
+ // displayed too. --djk
+ function isInputField() {
+ let elem = liberator.focus;
+ return ((elem instanceof HTMLInputElement && !/image/.test(elem.type))
+ || elem instanceof HTMLIsIndexElement);
+ }
- case modes.CARET:
- // setting this option will trigger an observer which will
- // take care of all other details like setting the NORMAL
- // mode
- options.setPref("accessibility.browsewithcaret", false);
- break;
+ if (options["insertmode"] || isInputField())
+ liberator.mode = modes.INSERT;
+ else
+ modes.reset();
+ break;
- case modes.TEXTAREA:
- // TODO: different behaviour for text areas and other input
- // fields seems unnecessarily complicated. If the user
- // likes Vi-mode then they probably like it for all input
- // fields, if not they can enter it explicitly for only
- // text areas. The mode name TEXTAREA is confusing and
- // would be better replaced with something indicating that
- // it's a Vi editing mode. Extended modes really need to be
- // displayed too. --djk
- function isInputField()
- {
- let elem = liberator.focus;
- return ((elem instanceof HTMLInputElement && !/image/.test(elem.type))
- || elem instanceof HTMLIsIndexElement);
- }
+ case modes.INSERT:
+ if ((modes.extended & modes.TEXTAREA))
+ liberator.mode = modes.TEXTAREA;
+ else
+ modes.reset();
+ break;
- if (options["insertmode"] || isInputField())
- liberator.mode = modes.INSERT;
- else
- modes.reset();
- break;
+ default: // HINTS, CUSTOM or COMMAND_LINE
+ modes.reset();
+ break;
+ }
+ },
- case modes.INSERT:
- if ((modes.extended & modes.TEXTAREA))
- liberator.mode = modes.TEXTAREA;
- else
- modes.reset();
- break;
+ // this keypress handler gets always called first, even if e.g.
+ // the commandline has focus
+ onKeyPress: function (event) {
+ function isEscapeKey(key) key == "<Esc>" || key == "<C-[>";
- default: // HINTS, CUSTOM or COMMAND_LINE
- modes.reset();
- break;
- }
- },
+ function killEvent() {
+ event.preventDefault();
+ event.stopPropagation();
+ }
- // this keypress handler gets always called first, even if e.g.
- // the commandline has focus
- onKeyPress: function (event)
- {
- function isEscapeKey(key) key == "<Esc>" || key == "<C-[>";
+ let key = events.toString(event);
+ if (!key)
+ return;
- function killEvent()
- {
- event.preventDefault();
- event.stopPropagation();
+ if (modes.isRecording) {
+ if (key == "q") { // TODO: should not be hardcoded
+ modes.isRecording = false;
+ liberator.log("Recorded " + this._currentMacro + ": " + this._macros.get(this._currentMacro), 9);
+ liberator.echomsg("Recorded macro '" + this._currentMacro + "'");
+ return void killEvent();
}
+ else if (!mappings.hasMap(liberator.mode, this._input.buffer + key))
+ this._macros.set(this._currentMacro, this._macros.get(this._currentMacro) + key);
+ }
- let key = events.toString(event);
- if (!key)
- return;
-
- if (modes.isRecording)
- {
- if (key == "q") // TODO: should not be hardcoded
- {
- modes.isRecording = false;
- liberator.log("Recorded " + currentMacro + ": " + macros.get(currentMacro), 9);
- liberator.echomsg("Recorded macro '" + currentMacro + "'");
- return void killEvent();
+ if (key == "<C-c>")
+ liberator.interrupted = true;
+
+ // feedingKeys needs to be separate from interrupted so
+ // we can differentiate between a recorded <C-c>
+ // interrupting whatever it's started and a real <C-c>
+ // interrupting our playback.
+ if (events.feedingKeys && !event.isMacro) {
+ if (key == "<C-c>") {
+ events.feedingKeys = false;
+ if (modes.isReplaying) {
+ modes.isReplaying = false;
+ setTimeout(function () { liberator.echomsg("Canceled playback of macro '" + this._lastMacro + "'"); }, 100);
}
- else if (!mappings.hasMap(liberator.mode, input.buffer + key))
- macros.set(currentMacro, macros.get(currentMacro) + key);
}
+ else
+ events.duringFeed.push(event);
- if (key == "<C-c>")
- liberator.interrupted = true;
-
- // feedingKeys needs to be separate from interrupted so
- // we can differentiate between a recorded <C-c>
- // interrupting whatever it's started and a real <C-c>
- // interrupting our playback.
- if (events.feedingKeys && !event.isMacro)
- {
- if (key == "<C-c>")
- {
- events.feedingKeys = false;
- if (modes.isReplaying)
- {
- modes.isReplaying = false;
- setTimeout(function () { liberator.echomsg("Canceled playback of macro '" + lastMacro + "'"); }, 100);
- }
- }
+ return void killEvent();
+ }
+
+ try {
+ let stop = false;
+
+ let win = document.commandDispatcher.focusedWindow;
+ if (win && win.document && win.document.designMode == "on" && !config.isComposeWindow)
+ stop = true;
+ // menus have their own command handlers
+ if (modes.extended & modes.MENU)
+ stop = true;
+ // handle Escape-one-key mode (Ctrl-v)
+ else if (modes.passNextKey && !modes.passAllKeys) {
+ modes.passNextKey = false;
+ stop = true;
+ }
+ // handle Escape-all-keys mode (Ctrl-q)
+ else if (modes.passAllKeys) {
+ if (modes.passNextKey)
+ modes.passNextKey = false; // and then let flow continue
+ else if (isEscapeKey(key) || key == "<C-v>")
+ ; // let flow continue to handle these keys to cancel escape-all-keys mode
else
- events.duringFeed.push(event);
+ stop = true;
+ }
- return void killEvent();
+ if (stop) {
+ this._input.buffer = "";
+ return;
}
- try
- {
- let stop = false;
+ stop = true; // set to false if we should NOT consume this event but let the host app handle it
- let win = document.commandDispatcher.focusedWindow;
- if (win && win.document && win.document.designMode == "on" && !config.isComposeWindow)
- stop = true;
- // menus have their own command handlers
- if (modes.extended & modes.MENU)
- stop = true;
- // handle Escape-one-key mode (Ctrl-v)
- else if (modes.passNextKey && !modes.passAllKeys)
- {
- modes.passNextKey = false;
- stop = true;
- }
- // handle Escape-all-keys mode (Ctrl-q)
- else if (modes.passAllKeys)
- {
- if (modes.passNextKey)
- modes.passNextKey = false; // and then let flow continue
- else if (isEscapeKey(key) || key == "<C-v>")
- ; // let flow continue to handle these keys to cancel escape-all-keys mode
- else
- stop = true;
- }
+ // just forward event without checking any mappings when the MOW is open
+ if (liberator.mode == modes.COMMAND_LINE && (modes.extended & modes.OUTPUT_MULTILINE)) {
+ commandline.onMultilineOutputEvent(event);
+ return void killEvent();
+ }
- if (stop)
- {
- input.buffer = "";
- return;
- }
+ // XXX: ugly hack for now pass certain keys to the host app as
+ // they are without beeping also fixes key navigation in combo
+ // boxes, submitting forms, etc.
+ // FIXME: breaks iabbr for now --mst
+ if (key in config.ignoreKeys && (config.ignoreKeys[key] & liberator.mode)) {
+ this._input.buffer = "";
+ return;
+ }
- stop = true; // set to false if we should NOT consume this event but let the host app handle it
+ // TODO: handle middle click in content area
- // just forward event without checking any mappings when the MOW is open
- if (liberator.mode == modes.COMMAND_LINE && (modes.extended & modes.OUTPUT_MULTILINE))
- {
- commandline.onMultilineOutputEvent(event);
+ if (!isEscapeKey(key)) {
+ // custom mode...
+ if (liberator.mode == modes.CUSTOM) {
+ plugins.onEvent(event);
return void killEvent();
}
- // XXX: ugly hack for now pass certain keys to the host app as
- // they are without beeping also fixes key navigation in combo
- // boxes, submitting forms, etc.
- // FIXME: breaks iabbr for now --mst
- if (key in config.ignoreKeys && (config.ignoreKeys[key] & liberator.mode))
- {
- input.buffer = "";
+ // All of these special cases for hint mode are driving
+ // me insane! -Kris
+ if (modes.extended & modes.HINTS) {
+ // under HINT mode, certain keys are redirected to hints.onEvent
+ if (key == "<Return>" || key == "<Tab>" || key == "<S-Tab>"
+ || key == mappings.getMapLeader()
+ || (key == "<BS>" && hints.previnput == "number")
+ || (/^[0-9]$/.test(key) && !hints.escNumbers)) {
+ hints.onEvent(event);
+ this._input.buffer = "";
+ return void killEvent();
+ }
+
+ // others are left to generate the 'input' event or handled by the host app
return;
}
+ }
- // TODO: handle middle click in content area
+ // FIXME (maybe): (is an ESC or C-] here): on HINTS mode, it enters
+ // into 'if (map && !skipMap) below. With that (or however) it
+ // triggers the onEscape part, where it resets mode. Here I just
+ // return true, with the effect that it also gets to there (for
+ // whatever reason). if that happens to be correct, well..
+ // XXX: why not just do that as well for HINTS mode actually?
- if (!isEscapeKey(key))
- {
- // custom mode...
- if (liberator.mode == modes.CUSTOM)
- {
- plugins.onEvent(event);
- return void killEvent();
- }
+ if (liberator.mode == modes.CUSTOM)
+ return;
- // All of these special cases for hint mode are driving
- // me insane! -Kris
- if (modes.extended & modes.HINTS)
- {
- // under HINT mode, certain keys are redirected to hints.onEvent
- if (key == "<Return>" || key == "<Tab>" || key == "<S-Tab>"
- || key == mappings.getMapLeader()
- || (key == "<BS>" && hints.previnput == "number")
- || (/^[0-9]$/.test(key) && !hints.escNumbers))
- {
- hints.onEvent(event);
- input.buffer = "";
- return void killEvent();
- }
+ let inputStr = this._input.buffer + key;
+ let countStr = inputStr.match(/^[1-9][0-9]*|/)[0];
+ let candidateCommand = inputStr.substr(countStr.length);
+ let map = mappings[event.noremap ? "getDefault" : "get"](liberator.mode, candidateCommand);
+
+ let candidates = mappings.getCandidates(liberator.mode, candidateCommand);
+ if (candidates.length == 0 && !map) {
+ map = this._input.pendingMap;
+ this._input.pendingMap = null;
+ if (map && map.arg)
+ this._input.pendingArgMap = map;
+ }
- // others are left to generate the 'input' event or handled by the host app
+ // counts must be at the start of a complete mapping (10j -> go 10 lines down)
+ if (countStr && !candidateCommand) {
+ // no count for insert mode mappings
+ if (!modes.mainMode.count || modes.mainMode.input)
+ stop = false;
+ else
+ this._input.buffer = inputStr;
+ }
+ else if (this._input.pendingArgMap) {
+ this._input.buffer = "";
+ let map = this._input.pendingArgMap;
+ this._input.pendingArgMap = null;
+ if (!isEscapeKey(key)) {
+ if (modes.isReplaying && !this.waitForPageLoad())
return;
- }
+ map.execute(null, this._input.count, key);
}
-
- // FIXME (maybe): (is an ESC or C-] here): on HINTS mode, it enters
- // into 'if (map && !skipMap) below. With that (or however) it
- // triggers the onEscape part, where it resets mode. Here I just
- // return true, with the effect that it also gets to there (for
- // whatever reason). if that happens to be correct, well..
- // XXX: why not just do that as well for HINTS mode actually?
-
- if (liberator.mode == modes.CUSTOM)
- return;
-
- let inputStr = input.buffer + key;
- let countStr = inputStr.match(/^[1-9][0-9]*|/)[0];
- let candidateCommand = inputStr.substr(countStr.length);
- let map = mappings[event.noremap ? "getDefault" : "get"](liberator.mode, candidateCommand);
-
- let candidates = mappings.getCandidates(liberator.mode, candidateCommand);
- if (candidates.length == 0 && !map)
- {
- map = input.pendingMap;
- input.pendingMap = null;
- if (map && map.arg)
- input.pendingArgMap = map;
+ }
+ // only follow a map if there isn't a longer possible mapping
+ // (allows you to do :map z yy, when zz is a longer mapping than z)
+ else if (map && !event.skipmap && candidates.length == 0) {
+ this._input.pendingMap = null;
+ this._input.count = parseInt(countStr, 10);
+ if (isNaN(this._input.count))
+ this._input.count = -1;
+ this._input.buffer = "";
+ if (map.arg) {
+ this._input.buffer = inputStr;
+ this._input.pendingArgMap = map;
+ }
+ else if (this._input.pendingMotionMap) {
+ if (!isEscapeKey(key))
+ this._input.pendingMotionMap.execute(candidateCommand, this._input.count, null);
+ this._input.pendingMotionMap = null;
+ }
+ // no count support for these commands yet
+ else if (map.motion) {
+ this._input.pendingMotionMap = map;
}
+ else {
+ if (modes.isReplaying && !this.waitForPageLoad())
+ return void killEvent();
- // counts must be at the start of a complete mapping (10j -> go 10 lines down)
- if (countStr && !candidateCommand)
- {
- // no count for insert mode mappings
- if (!modes.mainMode.count || modes.mainMode.input)
+ let ret = map.execute(null, this._input.count);
+ if (map.route && ret)
stop = false;
- else
- input.buffer = inputStr;
}
- else if (input.pendingArgMap)
- {
- input.buffer = "";
- let map = input.pendingArgMap;
- input.pendingArgMap = null;
- if (!isEscapeKey(key))
- {
- if (modes.isReplaying && !waitForPageLoad())
- return;
- map.execute(null, input.count, key);
+ }
+ else if (mappings.getCandidates(liberator.mode, candidateCommand).length > 0 && !event.skipmap) {
+ this._input.pendingMap = map;
+ this._input.buffer += key;
+ }
+ else { // if the key is neither a mapping nor the start of one
+ // the mode checking is necessary so that things like g<esc> do not beep
+ if (this._input.buffer != "" && !event.skipmap &&
+ (liberator.mode & (modes.INSERT | modes.COMMAND_LINE | modes.TEXTAREA)))
+ events.feedkeys(this._input.buffer, { noremap: true, skipmap: true });
+
+ this._input.buffer = "";
+ this._input.pendingArgMap = null;
+ this._input.pendingMotionMap = null;
+ this._input.pendingMap = null;
+
+ if (!isEscapeKey(key)) {
+ // allow key to be passed to the host app if we can't handle it
+ stop = false;
+
+ if (liberator.mode == modes.COMMAND_LINE) {
+ if (!(modes.extended & modes.INPUT_MULTILINE))
+ commandline.onEvent(event); // reroute event in command line mode
}
+ else if (!modes.mainMode.input)
+ liberator.beep();
}
- // only follow a map if there isn't a longer possible mapping
- // (allows you to do :map z yy, when zz is a longer mapping than z)
- else if (map && !event.skipmap && candidates.length == 0)
- {
- input.pendingMap = null;
- input.count = parseInt(countStr, 10);
- if (isNaN(input.count))
- input.count = -1;
- input.buffer = "";
- if (map.arg)
- {
- input.buffer = inputStr;
- input.pendingArgMap = map;
- }
- else if (input.pendingMotionMap)
- {
- if (!isEscapeKey(key))
- input.pendingMotionMap.execute(candidateCommand, input.count, null);
- input.pendingMotionMap = null;
- }
- // no count support for these commands yet
- else if (map.motion)
- {
- input.pendingMotionMap = map;
- }
- else
- {
- if (modes.isReplaying && !waitForPageLoad())
- return void killEvent();
+ }
- let ret = map.execute(null, input.count);
- if (map.route && ret)
- stop = false;
- }
- }
- else if (mappings.getCandidates(liberator.mode, candidateCommand).length > 0 && !event.skipmap)
- {
- input.pendingMap = map;
- input.buffer += key;
- }
- else // if the key is neither a mapping nor the start of one
- {
- // the mode checking is necessary so that things like g<esc> do not beep
- if (input.buffer != "" && !event.skipmap &&
- (liberator.mode & (modes.INSERT | modes.COMMAND_LINE | modes.TEXTAREA)))
- events.feedkeys(input.buffer, { noremap: true, skipmap: true });
-
- input.buffer = "";
- input.pendingArgMap = null;
- input.pendingMotionMap = null;
- input.pendingMap = null;
+ if (stop)
+ killEvent();
+ }
+ finally {
+ let motionMap = (this._input.pendingMotionMap && this._input.pendingMotionMap.names[0]) || "";
+ statusline.updateInputBuffer(motionMap + this._input.buffer);
+ }
+ },
- if (!isEscapeKey(key))
- {
- // allow key to be passed to the host app if we can't handle it
- stop = false;
+ // this is need for sites like msn.com which focus the input field on keydown
+ onKeyUpOrDown: function (event) {
+ if (modes.passNextKey ^ modes.passAllKeys || Events.isInputElemFocused())
+ return;
- if (liberator.mode == modes.COMMAND_LINE)
- {
- if (!(modes.extended & modes.INPUT_MULTILINE))
- commandline.onEvent(event); // reroute event in command line mode
- }
- else if (!modes.mainMode.input)
- liberator.beep();
+ event.stopPropagation();
+ },
+
+ // TODO: move to buffer.js?
+ /**
+ * The liberator document loading progress listener.
+ */
+ progressListener: {
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIWebProgressListener,
+ Ci.nsIXULBrowserWindow
+ ]),
+
+ // XXX: function may later be needed to detect a canceled synchronous openURL()
+ onStateChange: function (webProgress, request, flags, status) {
+ // STATE_IS_DOCUMENT | STATE_IS_WINDOW is important, because we also
+ // receive statechange events for loading images and other parts of the web page
+ if (flags & (Ci.nsIWebProgressListener.STATE_IS_DOCUMENT | Ci.nsIWebProgressListener.STATE_IS_WINDOW)) {
+ // This fires when the load event is initiated
+ // only thrown for the current tab, not when another tab changes
+ if (flags & Ci.nsIWebProgressListener.STATE_START) {
+ buffer.loaded = 0;
+ statusline.updateProgress(0);
+
+ autocommands.trigger("PageLoadPre", { url: buffer.URL });
+
+ // don't reset mode if a frame of the frameset gets reloaded which
+ // is not the focused frame
+ if (document.commandDispatcher.focusedWindow == webProgress.DOMWindow) {
+ setTimeout(function () { modes.reset(false); },
+ liberator.mode == modes.HINTS ? 500 : 0);
}
}
+ else if (flags & Ci.nsIWebProgressListener.STATE_STOP) {
+ buffer.loaded = (status == 0 ? 1 : 2);
+ statusline.updateUrl();
+ }
+ }
+ },
+ // for notifying the user about secure web pages
+ onSecurityChange: function (webProgress, request, state) {
+ // TODO: do something useful with STATE_SECURE_MED and STATE_SECURE_LOW
+ if (state & Ci.nsIWebProgressListener.STATE_IS_INSECURE)
+ statusline.setClass("insecure");
+ else if (state & Ci.nsIWebProgressListener.STATE_IS_BROKEN)
+ statusline.setClass("broken");
+ else if (state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL)
+ statusline.setClass("extended");
+ else if (state & Ci.nsIWebProgressListener.STATE_SECURE_HIGH)
+ statusline.setClass("secure");
+ },
+ onStatusChange: function (webProgress, request, status, message) {
+ statusline.updateUrl(message);
+ },
+ onProgressChange: function (webProgress, request, curSelfProgress, maxSelfProgress, curTotalProgress, maxTotalProgress) {
+ statusline.updateProgress(curTotalProgress/maxTotalProgress);
+ },
+ // happens when the users switches tabs
+ onLocationChange: function () {
+ statusline.updateUrl();
+ statusline.updateProgress();
- if (stop)
- killEvent();
+ autocommands.trigger("LocationChange", { url: buffer.URL });
+
+ // if this is not delayed we get the position of the old buffer
+ setTimeout(function () { statusline.updateBufferPosition(); }, 500);
+ },
+ // called at the very end of a page load
+ asyncUpdateUI: function () {
+ setTimeout(function () { statusline.updateUrl(); }, 100);
+ },
+ setOverLink: function (link, b) {
+ let ssli = options["showstatuslinks"];
+ if (link && ssli) {
+ if (ssli == 1)
+ statusline.updateUrl("Link: " + link);
+ else if (ssli == 2)
+ liberator.echo("Link: " + link, commandline.DISALLOW_MULTILINE);
}
- finally
- {
- let motionMap = (input.pendingMotionMap && input.pendingMotionMap.names[0]) || "";
- statusline.updateInputBuffer(motionMap + input.buffer);
+
+ if (link == "") {
+ if (ssli == 1)
+ statusline.updateUrl();
+ else if (ssli == 2)
+ modes.show();
}
},
- // this is need for sites like msn.com which focus the input field on keydown
- onKeyUpOrDown: function (event)
- {
- if (modes.passNextKey ^ modes.passAllKeys || isInputElemFocused())
- return;
+ // nsIXULBrowserWindow stubs
+ setJSDefaultStatus: function (status) {},
+ setJSStatus: function (status) {},
- event.stopPropagation();
- },
+ // Stub for something else, presumably. Not in any documented
+ // interface.
+ onLinkIconAvailable: function () {}
+ }
+}, {
+ isInputElemFocused: function () {
+ let elem = liberator.focus;
+ return ((elem instanceof HTMLInputElement && !/image/.test(elem.type)) ||
+ elem instanceof HTMLTextAreaElement ||
+ elem instanceof HTMLIsIndexElement ||
+ elem instanceof HTMLObjectElement ||
+ elem instanceof HTMLEmbedElement);
+ },
- // TODO: move to buffer.js?
- /**
- * The liberator document loading progress listener.
- */
- progressListener: {
- QueryInterface: XPCOMUtils.generateQI([
- Ci.nsIWebProgressListener,
- Ci.nsIXULBrowserWindow
- ]),
-
- // XXX: function may later be needed to detect a canceled synchronous openURL()
- onStateChange: function (webProgress, request, flags, status)
- {
- // STATE_IS_DOCUMENT | STATE_IS_WINDOW is important, because we also
- // receive statechange events for loading images and other parts of the web page
- if (flags & (Ci.nsIWebProgressListener.STATE_IS_DOCUMENT | Ci.nsIWebProgressListener.STATE_IS_WINDOW))
- {
- // This fires when the load event is initiated
- // only thrown for the current tab, not when another tab changes
- if (flags & Ci.nsIWebProgressListener.STATE_START)
- {
- buffer.loaded = 0;
- statusline.updateProgress(0);
-
- autocommands.trigger("PageLoadPre", { url: buffer.URL });
-
- // don't reset mode if a frame of the frameset gets reloaded which
- // is not the focused frame
- if (document.commandDispatcher.focusedWindow == webProgress.DOMWindow)
- {
- setTimeout(function () { modes.reset(false); },
- liberator.mode == modes.HINTS ? 500 : 0);
- }
- }
- else if (flags & Ci.nsIWebProgressListener.STATE_STOP)
- {
- buffer.loaded = (status == 0 ? 1 : 2);
- statusline.updateUrl();
- }
- }
- },
- // for notifying the user about secure web pages
- onSecurityChange: function (webProgress, request, state)
- {
- // TODO: do something useful with STATE_SECURE_MED and STATE_SECURE_LOW
- if (state & Ci.nsIWebProgressListener.STATE_IS_INSECURE)
- statusline.setClass("insecure");
- else if (state & Ci.nsIWebProgressListener.STATE_IS_BROKEN)
- statusline.setClass("broken");
- else if (state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL)
- statusline.setClass("extended");
- else if (state & Ci.nsIWebProgressListener.STATE_SECURE_HIGH)
- statusline.setClass("secure");
- },
- onStatusChange: function (webProgress, request, status, message)
- {
- statusline.updateUrl(message);
- },
- onProgressChange: function (webProgress, request, curSelfProgress, maxSelfProgress, curTotalProgress, maxTotalProgress)
- {
- statusline.updateProgress(curTotalProgress/maxTotalProgress);
- },
- // happens when the users switches tabs
- onLocationChange: function ()
- {
- statusline.updateUrl();
- statusline.updateProgress();
+}, {
+ commands: function () {
+ commands.add(["delmac[ros]"],
+ "Delete macros",
+ function (args) {
+ liberator.assert(!args.bang || !args.string, "E474: Invalid argument");
- autocommands.trigger("LocationChange", { url: buffer.URL });
+ if (args.bang)
+ events.deleteMacros();
+ else if (args.string)
+ events.deleteMacros(args.string);
+ else
+ liberator.echoerr("E471: Argument required");
+ }, {
+ bang: true,
+ completer: function (context) completion.macro(context)
+ });
- // if this is not delayed we get the position of the old buffer
- setTimeout(function () { statusline.updateBufferPosition(); }, 500);
- },
- // called at the very end of a page load
- asyncUpdateUI: function ()
- {
- setTimeout(function () { statusline.updateUrl(); }, 100);
- },
- setOverLink: function (link, b)
- {
- let ssli = options["showstatuslinks"];
- if (link && ssli)
- {
- if (ssli == 1)
- statusline.updateUrl("Link: " + link);
- else if (ssli == 2)
- liberator.echo("Link: " + link, commandline.DISALLOW_MULTILINE);
- }
+ commands.add(["macros"],
+ "List all macros",
+ function (args) { completion.listCompleter("macro", args[0]); }, {
+ argCount: "?",
+ completer: function (context) completion.macro(context)
+ });
- if (link == "")
- {
- if (ssli == 1)
- statusline.updateUrl();
- else if (ssli == 2)
- modes.show();
- }
+ commands.add(["pl[ay]"],
+ "Replay a recorded macro",
+ function (args) { events.playMacro(args[0]); }, {
+ argCount: "1",
+ completer: function (context) completion.macro(context)
+ });
+ },
+ mappings: function () {
+ mappings.add(modes.all,
+ ["<Esc>", "<C-[>"], "Focus content",
+ function () { events.onEscape(); });
+
+ // add the ":" mapping in all but insert mode mappings
+ mappings.add([modes.NORMAL, modes.PLAYER, modes.VISUAL, modes.HINTS, modes.MESSAGE, modes.COMPOSE, modes.CARET, modes.TEXTAREA],
+ [":"], "Enter command line mode",
+ function () { commandline.open(":", "", modes.EX); });
+
+ // focus events
+ mappings.add([modes.NORMAL, modes.PLAYER, modes.VISUAL, modes.CARET],
+ ["<Tab>"], "Advance keyboard focus",
+ function () { document.commandDispatcher.advanceFocus(); });
+
+ mappings.add([modes.NORMAL, modes.PLAYER, modes.VISUAL, modes.CARET, modes.INSERT, modes.TEXTAREA],
+ ["<S-Tab>"], "Rewind keyboard focus",
+ function () { document.commandDispatcher.rewindFocus(); });
+
+ mappings.add(modes.all,
+ ["<C-z>"], "Temporarily ignore all " + config.name + " key bindings",
+ function () { modes.passAllKeys = true; });
+
+ mappings.add(modes.all,
+ ["<C-v>"], "Pass through next key",
+ function () { modes.passNextKey = true; });
+
+ mappings.add(modes.all,
+ ["<Nop>"], "Do nothing",
+ function () { return; });
+
+ // macros
+ mappings.add([modes.NORMAL, modes.PLAYER, modes.MESSAGE],
+ ["q"], "Record a key sequence into a macro",
+ function (arg) { events.startRecording(arg); },
+ { arg: true });
+
+ mappings.add([modes.NORMAL, modes.PLAYER, modes.MESSAGE],
+ ["@"], "Play a macro",
+ function (count, arg) {
+ if (count < 1) count = 1;
+ while (count-- && events.playMacro(arg))
+ ;
},
+ { arg: true, count: true });
+ },
+ tabs: function () {
- // nsIXULBrowserWindow stubs
- setJSDefaultStatus: function (status) {},
- setJSStatus: function (status) {},
+ function triggerLoadAutocmd(name, doc) {
+ let args = {
+ url: doc.location.href,
+ title: doc.title
+ };
- // Stub for something else, presumably. Not in any documented
- // interface.
- onLinkIconAvailable: function () {}
+ if (liberator.has("tabs")) {
+ args.tab = tabs.getContentIndex(doc) + 1;
+ args.doc = "tabs.getTab(" + (args.tab - 1) + ").linkedBrowser.contentDocument";
+ }
+
+ autocommands.trigger(name, args);
+ }
+
+ function onDOMContentLoaded(event) {
+ let doc = event.originalTarget;
+ if (doc instanceof HTMLDocument && !doc.defaultView.frameElement)
+ triggerLoadAutocmd("DOMLoad", doc);
}
- }; //}}}
-
- window.XULBrowserWindow = self.progressListener;
- window.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIWebNavigation)
- .QueryInterface(Ci.nsIDocShellTreeItem)
- .treeOwner
- .QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIXULWindow)
- .XULBrowserWindow = self.progressListener;
- try
- {
- getBrowser().addProgressListener(self.progressListener, Ci.nsIWebProgress.NOTIFY_ALL);
- }
- catch (e) {}
- liberator.registerObserver("shutdown", function () { self.destroy(); });
+ // TODO: see what can be moved to onDOMContentLoaded()
+ function onPageLoad(event) {
+ if (event.originalTarget instanceof HTMLDocument) {
+ let doc = event.originalTarget;
+ // document is part of a frameset
+ if (doc.defaultView.frameElement) {
+ // hacky way to get rid of "Transfering data from ..." on sites with frames
+ // when you click on a link inside a frameset, because asyncUpdateUI
+ // is not triggered there (Gecko bug?)
+ setTimeout(function () { statusline.updateUrl(); }, 10);
+ return;
+ }
- function wrapListener(method)
- {
- return function (event) {
- try
- {
- self[method](event);
- }
- catch (e)
- {
- if (e.message == "Interrupted")
- liberator.echoerr("Interrupted");
- else
- liberator.echoerr("Processing " + event.type + " event: " + (e.echoerr || e));
- liberator.reportError(e);
- }
- };
- }
- window.addEventListener("keypress", wrapListener("onKeyPress"), true);
- window.addEventListener("keydown", wrapListener("onKeyUpOrDown"), true);
- window.addEventListener("keyup", wrapListener("onKeyUpOrDown"), true);
+ // code which should happen for all (also background) newly loaded tabs goes here:
+
+ // mark the buffer as loaded, we can't use buffer.loaded
+ // since that always refers to the current buffer, while doc can be
+ // any buffer, even in a background tab
+ doc.pageIsFullyLoaded = 1;
+
+ // code which is only relevant if the page load is the current tab goes here:
+ if (doc == getBrowser().contentDocument) {
+ // we want to stay in command mode after a page has loaded
+ // TODO: move somewhere else, as focusing can already happen earlier than on "load"
+ if (options["focuscontent"]) {
+ setTimeout(function () {
+ let focused = liberator.focus;
+ if (focused && (focused.value != null) && focused.value.length == 0)
+ focused.blur();
+ }, 0);
+ }
+ }
+ else // background tab
+ liberator.echomsg("Background tab loaded: " + doc.title || doc.location.href, 3);
- return self;
+ triggerLoadAutocmd("PageLoad", doc);
+ }
+ }
-} //}}}
+ let tabContainer = tabs.getBrowser().mTabContainer;
+
+ tabContainer.addEventListener("TabMove", function (event) {
+ statusline.updateTabCount(true);
+ }, false);
+ tabContainer.addEventListener("TabOpen", function (event) {
+ statusline.updateTabCount(true);
+ }, false);
+ tabContainer.addEventListener("TabClose", function (event) {
+ statusline.updateTabCount(true);
+ }, false);
+ tabContainer.addEventListener("TabSelect", function (event) {
+ // TODO: is all of that necessary?
+ // I vote no. --Kris
+ modes.reset();
+ statusline.updateTabCount(true);
+ tabs.updateSelectionHistory();
+
+ if (options["focuscontent"])
+ setTimeout(function () { liberator.focusContent(true); }, 10); // just make sure, that no widget has focus
+ }, false);
+
+ tabs.getBrowser().addEventListener("DOMContentLoaded", onDOMContentLoaded, true);
+
+ // this adds an event which is is called on each page load, even if the
+ // page is loaded in a background tab
+ tabs.getBrowser().addEventListener("load", onPageLoad, true);
+
+ // called when the active document is scrolled
+ tabs.getBrowser().addEventListener("scroll", function (event) {
+ statusline.updateBufferPosition();
+ modes.show();
+ }, null);
+ },
+});
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/finder.js b/common/content/finder.js
index 36660945..33334036 100644
--- a/common/content/finder.js
+++ b/common/content/finder.js
@@ -13,70 +13,192 @@
// : 'linksearch' searches should highlight link matches only
// : changing any search settings should also update the search state including highlighting
// : incremental searches shouldn't permanently update search modifiers
+//
+// TODO: Clean up this rat's nest. --Kris
/**
* @instance finder
*/
-function Finder() //{{{
-{
- ////////////////////////////////////////////////////////////////////////////////
- ////////////////////// PRIVATE SECTION /////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- var found = false; // true if the last search was successful
- var backwards = false; // currently searching backwards
- var searchString = ""; // current search string (without modifiers)
- var searchPattern = ""; // current search string (includes modifiers)
- var lastSearchPattern = ""; // the last searched pattern (includes modifiers)
- var lastSearchString = ""; // the last searched string (without modifiers)
- var lastSearchBackwards = false; // like "backwards", but for the last search, so if you cancel a search with <esc> this is not set
- var caseSensitive = false; // search string is case sensitive
- var linksOnly = false; // search is limited to link text only
-
- // Event handlers for search - closure is needed
- commandline.registerCallback("change", modes.SEARCH_FORWARD, function (str) { finder.onKeyPress(str); });
- commandline.registerCallback("submit", modes.SEARCH_FORWARD, function (str) { finder.onSubmit(str); });
- commandline.registerCallback("cancel", modes.SEARCH_FORWARD, function () { finder.onCancel(); });
- // TODO: allow advanced myModes in register/triggerCallback
- commandline.registerCallback("change", modes.SEARCH_BACKWARD, function (str) { finder.onKeyPress(str); });
- commandline.registerCallback("submit", modes.SEARCH_BACKWARD, function (str) { finder.onSubmit(str); });
- commandline.registerCallback("cancel", modes.SEARCH_BACKWARD, function () { finder.onCancel(); });
+const Finder = Module("finder", {
+ init: function () {
+ const self = this;
+
+ this._found = false; // true if the last search was successful
+ this._backwards = false; // currently searching backwards
+ this._searchString = ""; // current search string (without modifiers)
+ this._searchPattern = ""; // current search string (includes modifiers)
+ this._lastSearchPattern = ""; // the last searched pattern (includes modifiers)
+ this._lastSearchString = ""; // the last searched string (without modifiers)
+ this._lastSearchBackwards = false; // like "backwards", but for the last search, so if you cancel a search with <esc> this is not set
+ this._caseSensitive = false; // search string is case sensitive
+ this._linksOnly = false; // search is limited to link text only
+
+ /* Stolen from toolkit.jar in Firefox, for the time being. The private
+ * methods were unstable, and changed. The new version is not remotely
+ * compatible with what we do.
+ * The following only applies to this object, and may not be
+ * necessary, or accurate, but, just in case:
+ * The Original Code is mozilla.org viewsource frontend.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (c) 2003
+ * by the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Blake Ross <blake@cs.stanford.edu> (Original Author)
+ * Masayuki Nakano <masayuki@d-toybox.com>
+ * Ben Basson <contact@cusser.net>
+ * Jason Barnabe <jason_barnabe@fastmail.fm>
+ * Asaf Romano <mano@mozilla.com>
+ * Ehsan Akhgari <ehsan.akhgari@gmail.com>
+ * Graeme McCutcheon <graememcc_firefox@graeme-online.co.uk>
+ */
+ this._highlighter = {
+
+ doc: null,
+
+ spans: [],
+
+ search: function (aWord, matchCase) {
+ var finder = services.create("find");
+ if (matchCase !== undefined)
+ self._caseSensitive = matchCase;
+
+ var range;
+ while ((range = finder.Find(aWord, this.searchRange, this.startPt, this.endPt)))
+ yield range;
+ },
+
+ highlightDoc: function highlightDoc(win, aWord) {
+ this.doc = content.document; // XXX
+ Array.forEach(win.frames, function (frame) this._highlighter.highlightDoc(frame, aWord));
+
+ var doc = win.document;
+ if (!doc || !(doc instanceof HTMLDocument))
+ return;
+
+ if (!aWord) {
+ let elems = this._highlighter.spans;
+ for (let i = elems.length; --i >= 0;) {
+ let elem = elems[i];
+ let docfrag = doc.createDocumentFragment();
+ let next = elem.nextSibling;
+ let parent = elem.parentNode;
+
+ let child;
+ while (child = elem.firstChild)
+ docfrag.appendChild(child);
+
+ parent.removeChild(elem);
+ parent.insertBefore(docfrag, next);
+ parent.normalize();
+ }
+ return;
+ }
+
+ var baseNode = <span highlight="Search"/>;
+ baseNode = util.xmlToDom(baseNode, window.content.document);
+
+ var body = doc.body;
+ var count = body.childNodes.length;
+ this.searchRange = doc.createRange();
+ this.startPt = doc.createRange();
+ this.endPt = doc.createRange();
+
+ this.searchRange.setStart(body, 0);
+ this.searchRange.setEnd(body, count);
+
+ this.startPt.setStart(body, 0);
+ this.startPt.setEnd(body, 0);
+ this.endPt.setStart(body, count);
+ this.endPt.setEnd(body, count);
+
+ liberator.interrupted = false;
+ let n = 0;
+ for (let retRange in this.search(aWord, this._caseSensitive)) {
+ // Highlight
+ var nodeSurround = baseNode.cloneNode(true);
+ var node = this.highlight(retRange, nodeSurround);
+ this.startPt = node.ownerDocument.createRange();
+ this.startPt.setStart(node, node.childNodes.length);
+ this.startPt.setEnd(node, node.childNodes.length);
+ if (n++ % 20 == 0)
+ liberator.threadYield(true);
+ if (liberator.interrupted)
+ break;
+ }
+ },
+
+ highlight: function highlight(aRange, aNode) {
+ var startContainer = aRange.startContainer;
+ var startOffset = aRange.startOffset;
+ var endOffset = aRange.endOffset;
+ var docfrag = aRange.extractContents();
+ var before = startContainer.splitText(startOffset);
+ var parent = before.parentNode;
+ aNode.appendChild(docfrag);
+ parent.insertBefore(aNode, before);
+ this.spans.push(aNode);
+ return aNode;
+ },
+
+ /**
+ * Clears all search highlighting.
+ */
+ clear: function () {
+ this.spans.forEach(function (span) {
+ if (span.parentNode) {
+ let el = span.firstChild;
+ while (el) {
+ span.removeChild(el);
+ span.parentNode.insertBefore(el, span);
+ el = span.firstChild;
+ }
+ span.parentNode.removeChild(span);
+ }
+ });
+ this.spans = [];
+ },
+
+ isHighlighted: function (doc) this.doc == doc && this.spans.length > 0
+ };
+ },
// set searchString, searchPattern, caseSensitive, linksOnly
- function processUserPattern(pattern)
- {
+ processUserPattern: function (pattern) {
//// strip off pattern terminator and offset
//if (backwards)
// pattern = pattern.replace(/\?.*/, "");
//else
// pattern = pattern.replace(/\/.*/, "");
- searchPattern = pattern;
+ this._searchPattern = pattern;
// links only search - \l wins if both modifiers specified
if (/\\l/.test(pattern))
- linksOnly = true;
+ this._linksOnly = true;
else if (/\L/.test(pattern))
- linksOnly = false;
+ this._linksOnly = false;
else if (options["linksearch"])
- linksOnly = true;
+ this._linksOnly = true;
else
- linksOnly = false;
+ this._linksOnly = false;
// strip links-only modifiers
pattern = pattern.replace(/(\\)?\\[lL]/g, function ($0, $1) { return $1 ? $0 : ""; });
// case sensitivity - \c wins if both modifiers specified
if (/\c/.test(pattern))
- caseSensitive = false;
+ this._caseSensitive = false;
else if (/\C/.test(pattern))
- caseSensitive = true;
+ this._caseSensitive = true;
else if (options["ignorecase"] && options["smartcase"] && /[A-Z]/.test(pattern))
- caseSensitive = true;
+ this._caseSensitive = true;
else if (options["ignorecase"])
- caseSensitive = false;
+ this._caseSensitive = false;
else
- caseSensitive = true;
+ this._caseSensitive = true;
// strip case-sensitive modifiers
pattern = pattern.replace(/(\\)?\\[cC]/g, function ($0, $1) { return $1 ? $0 : ""; });
@@ -84,414 +206,253 @@ function Finder() //{{{
// remove any modifier escape \
pattern = pattern.replace(/\\(\\[cClL])/g, "$1");
- searchString = pattern;
- }
+ this._searchString = pattern;
+ },
- /* Stolen from toolkit.jar in Firefox, for the time being. The private
- * methods were unstable, and changed. The new version is not remotely
- * compatible with what we do.
- * The following only applies to this object, and may not be
- * necessary, or accurate, but, just in case:
- * The Original Code is mozilla.org viewsource frontend.
+ /**
+ * Called when the search dialog is requested.
*
- * The Initial Developer of the Original Code is
- * Netscape Communications Corporation.
- * Portions created by the Initial Developer are Copyright (c) 2003
- * by the Initial Developer. All Rights Reserved.
+ * @param {number} mode The search mode, either modes.SEARCH_FORWARD or
+ * modes.SEARCH_BACKWARD.
+ * @default modes.SEARCH_FORWARD
+ */
+ openPrompt: function (mode) {
+ this._backwards = mode == modes.SEARCH_BACKWARD;
+ commandline.open(this._backwards ? "?" : "/", "", mode);
+ // TODO: focus the top of the currently visible screen
+ },
+
+ // TODO: backwards seems impossible i fear :(
+ /**
+ * Searches the current buffer for <b>str</b>.
*
- * Contributor(s):
- * Blake Ross <blake@cs.stanford.edu> (Original Author)
- * Masayuki Nakano <masayuki@d-toybox.com>
- * Ben Basson <contact@cusser.net>
- * Jason Barnabe <jason_barnabe@fastmail.fm>
- * Asaf Romano <mano@mozilla.com>
- * Ehsan Akhgari <ehsan.akhgari@gmail.com>
- * Graeme McCutcheon <graememcc_firefox@graeme-online.co.uk>
+ * @param {string} str The string to find.
*/
- var highlighter = {
-
- doc: null,
-
- spans: [],
-
- search: function (aWord, matchCase)
- {
- var finder = services.create("find");
- if (matchCase !== undefined)
- finder.caseSensitive = matchCase;
-
- var range;
- while ((range = finder.Find(aWord, this.searchRange, this.startPt, this.endPt)))
- yield range;
- },
-
- highlightDoc: function highlightDoc(win, aWord)
- {
- this.doc = content.document; // XXX
- Array.forEach(win.frames, function (frame) highlighter.highlightDoc(frame, aWord));
-
- var doc = win.document;
- if (!doc || !(doc instanceof HTMLDocument))
- return;
-
- if (!aWord)
- {
- let elems = highlighter.spans;
- for (let i = elems.length; --i >= 0;)
- {
- let elem = elems[i];
- let docfrag = doc.createDocumentFragment();
- let next = elem.nextSibling;
- let parent = elem.parentNode;
-
- let child;
- while (child = elem.firstChild)
- docfrag.appendChild(child);
-
- parent.removeChild(elem);
- parent.insertBefore(docfrag, next);
- parent.normalize();
- }
- return;
- }
-
- var baseNode = <span highlight="Search"/>;
- baseNode = util.xmlToDom(baseNode, window.content.document);
-
- var body = doc.body;
- var count = body.childNodes.length;
- this.searchRange = doc.createRange();
- this.startPt = doc.createRange();
- this.endPt = doc.createRange();
-
- this.searchRange.setStart(body, 0);
- this.searchRange.setEnd(body, count);
-
- this.startPt.setStart(body, 0);
- this.startPt.setEnd(body, 0);
- this.endPt.setStart(body, count);
- this.endPt.setEnd(body, count);
-
- liberator.interrupted = false;
- let n = 0;
- for (let retRange in this.search(aWord, caseSensitive))
- {
- // Highlight
- var nodeSurround = baseNode.cloneNode(true);
- var node = this.highlight(retRange, nodeSurround);
- this.startPt = node.ownerDocument.createRange();
- this.startPt.setStart(node, node.childNodes.length);
- this.startPt.setEnd(node, node.childNodes.length);
- if (n++ % 20 == 0)
- liberator.threadYield(true);
- if (liberator.interrupted)
- break;
- }
- },
-
- highlight: function highlight(aRange, aNode)
- {
- var startContainer = aRange.startContainer;
- var startOffset = aRange.startOffset;
- var endOffset = aRange.endOffset;
- var docfrag = aRange.extractContents();
- var before = startContainer.splitText(startOffset);
- var parent = before.parentNode;
- aNode.appendChild(docfrag);
- parent.insertBefore(aNode, before);
- this.spans.push(aNode);
- return aNode;
- },
-
- /**
- * Clears all search highlighting.
- */
- clear: function ()
- {
- this.spans.forEach(function (span)
- {
- if (span.parentNode)
- {
- let el = span.firstChild;
- while (el)
- {
- span.removeChild(el);
- span.parentNode.insertBefore(el, span);
- el = span.firstChild;
- }
- span.parentNode.removeChild(span);
- }
- });
- this.spans = [];
- },
-
- isHighlighted: function (doc) this.doc == doc && this.spans.length > 0
- };
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// OPTIONS /////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- options.add(["hlsearch", "hls"],
- "Highlight previous search pattern matches",
- "boolean", "false",
- {
- setter: function (value)
- {
- if (value)
- finder.highlight();
- else
- finder.clear();
-
- return value;
- }
- });
-
- options.add(["ignorecase", "ic"],
- "Ignore case in search patterns",
- "boolean", true);
-
- options.add(["incsearch", "is"],
- "Show where the search pattern matches as it is typed",
- "boolean", true);
-
- options.add(["linksearch", "lks"],
- "Limit the search to hyperlink text",
- "boolean", false);
-
- options.add(["smartcase", "scs"],
- "Override the 'ignorecase' option if the pattern contains uppercase characters",
- "boolean", true);
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// MAPPINGS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- var myModes = config.browserModes;
- myModes = myModes.concat([modes.CARET]);
-
- mappings.add(myModes,
- ["/"], "Search forward for a pattern",
- function () { finder.openPrompt(modes.SEARCH_FORWARD); });
-
- mappings.add(myModes,
- ["?"], "Search backwards for a pattern",
- function () { finder.openPrompt(modes.SEARCH_BACKWARD); });
-
- mappings.add(myModes,
- ["n"], "Find next",
- function () { finder.findAgain(false); });
-
- mappings.add(myModes,
- ["N"], "Find previous",
- function () { finder.findAgain(true); });
-
- mappings.add(myModes.concat([modes.CARET, modes.TEXTAREA]), ["*"],
- "Find word under cursor",
- function ()
- {
- found = false;
- finder.onSubmit(buffer.getCurrentWord(), false);
- });
-
- mappings.add(myModes.concat([modes.CARET, modes.TEXTAREA]), ["#"],
- "Find word under cursor backwards",
- function ()
- {
- found = false;
- finder.onSubmit(buffer.getCurrentWord(), true);
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMMANDS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- commands.add(["noh[lsearch]"],
- "Remove the search highlighting",
- function () { finder.clear(); },
- { argCount: "0" });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// PUBLIC SECTION //////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- return {
-
- /**
- * Called when the search dialog is requested.
- *
- * @param {number} mode The search mode, either modes.SEARCH_FORWARD or
- * modes.SEARCH_BACKWARD.
- * @default modes.SEARCH_FORWARD
- */
- openPrompt: function (mode)
- {
- backwards = mode == modes.SEARCH_BACKWARD;
- commandline.open(backwards ? "?" : "/", "", mode);
- // TODO: focus the top of the currently visible screen
- },
-
- // TODO: backwards seems impossible i fear :(
- /**
- * Searches the current buffer for <b>str</b>.
- *
- * @param {string} str The string to find.
- */
- find: function (str)
- {
- let fastFind = getBrowser().fastFind;
+ find: function (str) {
+ let fastFind = getBrowser().fastFind;
- processUserPattern(str);
- fastFind.caseSensitive = caseSensitive;
- found = fastFind.find(searchString, linksOnly) != Ci.nsITypeAheadFind.FIND_NOTFOUND;
+ this._processUserPattern(str);
+ fastFind.caseSensitive = this._caseSensitive;
+ this._found = fastFind.find(this._searchString, this._linksOnly) != Ci.nsITypeAheadFind.FIND_NOTFOUND;
- if (!found)
- setTimeout(function () liberator.echoerr("E486: Pattern not found: " + searchPattern, commandline.FORCE_SINGLELINE), 0);
- },
+ if (!this._found)
+ setTimeout(function () liberator.echoerr("E486: Pattern not found: " + this._searchPattern, commandline.FORCE_SINGLELINE), 0);
+ },
- /**
- * Searches the current buffer again for the most recently used search
- * string.
- *
- * @param {boolean} reverse Whether to search forwards or backwards.
- * @default false
- */
- findAgain: function (reverse)
- {
- // This hack is needed to make n/N work with the correct string, if
- // we typed /foo<esc> after the original search. Since searchString is
- // readonly we have to call find() again to update it.
- if (getBrowser().fastFind.searchString != lastSearchString)
- this.find(lastSearchString);
-
- let up = reverse ? !lastSearchBackwards : lastSearchBackwards;
- let result = getBrowser().fastFind.findAgain(up, linksOnly);
-
- if (result == Ci.nsITypeAheadFind.FIND_NOTFOUND)
- liberator.echoerr("E486: Pattern not found: " + lastSearchPattern, commandline.FORCE_SINGLELINE);
- else if (result == Ci.nsITypeAheadFind.FIND_WRAPPED)
- {
- // hack needed, because wrapping causes a "scroll" event which clears
- // our command line
- setTimeout(function () {
- let msg = up ? "search hit TOP, continuing at BOTTOM" : "search hit BOTTOM, continuing at TOP";
- commandline.echo(msg, commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES | commandline.FORCE_SINGLELINE);
- }, 0);
- }
- else
- {
- commandline.echo((up ? "?" : "/") + lastSearchPattern, null, commandline.FORCE_SINGLELINE);
-
- if (options["hlsearch"])
- this.highlight(lastSearchString);
- }
- },
-
- /**
- * Called when the user types a key in the search dialog. Triggers a
- * search attempt if 'incsearch' is set.
- *
- * @param {string} str The search string.
- */
- onKeyPress: function (str)
- {
- if (options["incsearch"])
- this.find(str);
- },
-
- /**
- * Called when the <Enter> key is pressed to trigger a search.
- *
- * @param {string} str The search string.
- * @param {boolean} forcedBackward Whether to search forwards or
- * backwards. This overrides the direction set in
- * (@link #openPrompt).
- * @default false
- */
- onSubmit: function (str, forcedBackward)
- {
- if (typeof forcedBackward === "boolean")
- backwards = forcedBackward;
-
- if (str)
- var pattern = str;
- else
- {
- liberator.assert(lastSearchPattern, "E35: No previous search pattern");
- pattern = lastSearchPattern;
- }
-
- this.clear();
-
- if (!options["incsearch"] || !str || !found)
- {
- // prevent any current match from matching again
- if (!window.content.getSelection().isCollapsed)
- window.content.getSelection().getRangeAt(0).collapse(backwards);
-
- this.find(pattern);
- }
-
- lastSearchBackwards = backwards;
- //lastSearchPattern = pattern.replace(backwards ? /\?.*/ : /\/.*/, ""); // XXX
- lastSearchPattern = pattern;
- lastSearchString = searchString;
-
- // TODO: move to find() when reverse incremental searching is kludged in
- // need to find again for reverse searching
- if (backwards)
- setTimeout(function () { finder.findAgain(false); }, 0);
+ /**
+ * Searches the current buffer again for the most recently used search
+ * string.
+ *
+ * @param {boolean} reverse Whether to search forwards or backwards.
+ * @default false
+ */
+ findAgain: function (reverse) {
+ // This hack is needed to make n/N work with the correct string, if
+ // we typed /foo<esc> after the original search. Since searchString is
+ // readonly we have to call find() again to update it.
+ if (getBrowser().fastFind.searchString != this._lastSearchString)
+ this.find(this._lastSearchString);
+
+ let up = reverse ? !this._lastSearchBackwards : this._lastSearchBackwards;
+ let result = getBrowser().fastFind.findAgain(up, this._linksOnly);
+
+ if (result == Ci.nsITypeAheadFind.FIND_NOTFOUND)
+ liberator.echoerr("E486: Pattern not found: " + this._lastSearchPattern, commandline.FORCE_SINGLELINE);
+ else if (result == Ci.nsITypeAheadFind.FIND_WRAPPED) {
+ // hack needed, because wrapping causes a "scroll" event which clears
+ // our command line
+ setTimeout(function () {
+ let msg = up ? "search hit TOP, continuing at BOTTOM" : "search hit BOTTOM, continuing at TOP";
+ commandline.echo(msg, commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES | commandline.FORCE_SINGLELINE);
+ }, 0);
+ }
+ else {
+ commandline.echo((up ? "?" : "/") + this._lastSearchPattern, null, commandline.FORCE_SINGLELINE);
if (options["hlsearch"])
- this.highlight(searchString);
+ this.highlight(this._lastSearchString);
+ }
+ },
- modes.reset();
- },
+ /**
+ * Called when the user types a key in the search dialog. Triggers a
+ * search attempt if 'incsearch' is set.
+ *
+ * @param {string} str The search string.
+ */
+ onKeyPress: function (str) {
+ if (options["incsearch"])
+ this.find(str);
+ },
- /**
- * Called when the search is canceled. For example, if someone presses
- * <Esc> while typing a search.
- */
- onCancel: function ()
- {
- // TODO: code to reposition the document to the place before search started
- },
+ /**
+ * Called when the <Enter> key is pressed to trigger a search.
+ *
+ * @param {string} str The search string.
+ * @param {boolean} forcedBackward Whether to search forwards or
+ * backwards. This overrides the direction set in
+ * (@link #openPrompt).
+ * @default false
+ */
+ onSubmit: function (str, forcedBackward) {
+ if (typeof forcedBackward === "boolean")
+ this._backwards = forcedBackward;
+
+ if (str)
+ var pattern = str;
+ else {
+ liberator.assert(this._lastSearchPattern, "E35: No previous search pattern");
+ pattern = this._lastSearchPattern;
+ }
- /**
- * Highlights all occurances of <b>str</b> in the buffer.
- *
- * @param {string} str The string to highlight.
- */
- highlight: function (str)
- {
- // FIXME: Thunderbird incompatible
- if (config.name == "Muttator")
- return;
+ this.clear();
- if (highlighter.isHighlighted(content.document))
- return;
+ if (!options["incsearch"] || !str || !this._found) {
+ // prevent any current match from matching again
+ if (!window.content.getSelection().isCollapsed)
+ window.content.getSelection().getRangeAt(0).collapse(this._backwards);
- if (!str)
- str = lastSearchString;
+ this.find(pattern);
+ }
- highlighter.highlightDoc(window.content, str);
+ this._lastSearchBackwards = this._backwards;
+ //lastSearchPattern = pattern.replace(backwards ? /\?.*/ : /\/.*/, ""); // XXX
+ this._lastSearchPattern = pattern;
+ this._lastSearchString = this._searchString;
- // recreate selection since highlightDoc collapses the selection
- if (window.content.getSelection().isCollapsed)
- getBrowser().fastFind.findAgain(backwards, linksOnly);
+ // TODO: move to find() when reverse incremental searching is kludged in
+ // need to find again for reverse searching
+ if (this._backwards)
+ setTimeout(function () { finder.findAgain(false); }, 0);
- // TODO: remove highlighting from non-link matches (HTML - A/AREA with href attribute; XML - Xlink [type="simple"])
- },
+ if (options["hlsearch"])
+ this.highlight(this._searchString);
- /**
- * Clears all search highlighting.
- */
- clear: function ()
- {
- highlighter.clear();
- }
- };
- //}}}
-} //}}}
+ modes.reset();
+ },
+
+ /**
+ * Called when the search is canceled. For example, if someone presses
+ * <Esc> while typing a search.
+ */
+ onCancel: function () {
+ // TODO: code to reposition the document to the place before search started
+ },
+
+ /**
+ * Highlights all occurances of <b>str</b> in the buffer.
+ *
+ * @param {string} str The string to highlight.
+ */
+ highlight: function (str) {
+ // FIXME: Thunderbird incompatible
+ if (config.name == "Muttator")
+ return;
+
+ if (this._highlighter.isHighlighted(content.document))
+ return;
+
+ if (!str)
+ str = this._lastSearchString;
+
+ this._highlighter.highlightDoc(window.content, str);
+
+ // recreate selection since highlightDoc collapses the selection
+ if (window.content.getSelection().isCollapsed)
+ getBrowser().fastFind.findAgain(this._backwards, this._linksOnly);
+
+ // TODO: remove highlighting from non-link matches (HTML - A/AREA with href attribute; XML - Xlink [type="simple"])
+ },
+
+ /**
+ * Clears all search highlighting.
+ */
+ clear: function () {
+ this._highlighter.clear();
+ }
+}, {
+ commandline: function () {
+ const self = this;
+ // Event handlers for search - closure is needed
+ commandline.registerCallback("change", modes.SEARCH_FORWARD, function (str) { self.onKeyPress(str); });
+ commandline.registerCallback("submit", modes.SEARCH_FORWARD, function (str) { self.onSubmit(str); });
+ commandline.registerCallback("cancel", modes.SEARCH_FORWARD, function () { self.onCancel(); });
+ // TODO: allow advanced myModes in register/triggerCallback
+ commandline.registerCallback("change", modes.SEARCH_BACKWARD, function (str) { self.onKeyPress(str); });
+ commandline.registerCallback("submit", modes.SEARCH_BACKWARD, function (str) { self.onSubmit(str); });
+ commandline.registerCallback("cancel", modes.SEARCH_BACKWARD, function () { self.onCancel(); });
+
+ },
+ commands: function () {
+ commands.add(["noh[lsearch]"],
+ "Remove the search highlighting",
+ function () { finder.clear(); },
+ { argCount: "0" });
+ },
+ mappings: function () {
+ var myModes = config.browserModes;
+ myModes = myModes.concat([modes.CARET]);
+
+ mappings.add(myModes,
+ ["/"], "Search forward for a pattern",
+ function () { finder.openPrompt(modes.SEARCH_FORWARD); });
+
+ mappings.add(myModes,
+ ["?"], "Search backwards for a pattern",
+ function () { finder.openPrompt(modes.SEARCH_BACKWARD); });
+
+ mappings.add(myModes,
+ ["n"], "Find next",
+ function () { finder.findAgain(false); });
+
+ mappings.add(myModes,
+ ["N"], "Find previous",
+ function () { finder.findAgain(true); });
+
+ mappings.add(myModes.concat([modes.CARET, modes.TEXTAREA]), ["*"],
+ "Find word under cursor",
+ function () {
+ this._found = false;
+ finder.onSubmit(buffer.getCurrentWord(), false);
+ });
+
+ mappings.add(myModes.concat([modes.CARET, modes.TEXTAREA]), ["#"],
+ "Find word under cursor backwards",
+ function () {
+ this._found = false;
+ finder.onSubmit(buffer.getCurrentWord(), true);
+ });
+ },
+ options: function () {
+ options.add(["hlsearch", "hls"],
+ "Highlight previous search pattern matches",
+ "boolean", "false", {
+ setter: function (value) {
+ if (value)
+ finder.highlight();
+ else
+ finder.clear();
+
+ return value;
+ }
+ });
+
+ options.add(["ignorecase", "ic"],
+ "Ignore case in search patterns",
+ "boolean", true);
+
+ options.add(["incsearch", "is"],
+ "Show where the search pattern matches as it is typed",
+ "boolean", true);
+
+ options.add(["linksearch", "lks"],
+ "Limit the search to hyperlink text",
+ "boolean", false);
+
+ options.add(["smartcase", "scs"],
+ "Override the 'ignorecase' option if the pattern contains uppercase characters",
+ "boolean", true);
+ },
+});
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/help.js b/common/content/help.js
index a07745f8..67fbba37 100644
--- a/common/content/help.js
+++ b/common/content/help.js
@@ -4,8 +4,7 @@
// given in the LICENSE.txt file included with this file.
-function checkFragment()
-{
+function checkFragment() {
document.title = document.getElementsByTagNameNS("http://www.w3.org/1999/xhtml", "title")[0].textContent;
var frag = document.location.hash.substr(1);
var elem = document.getElementById(frag);
diff --git a/common/content/hints.js b/common/content/hints.js
index 8a309f4d..a0fd440c 100644
--- a/common/content/hints.js
+++ b/common/content/hints.js
@@ -6,125 +6,116 @@
/** @scope modules */
/** @instance hints */
-function Hints() //{{{
-{
- ////////////////////////////////////////////////////////////////////////////////
- ////////////////////// PRIVATE SECTION /////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- const ELEM = 0, TEXT = 1, SPAN = 2, IMG_SPAN = 3;
-
- var myModes = config.browserModes;
-
- var hintMode;
- var submode = ""; // used for extended mode, can be "o", "t", "y", etc.
- var hintString = ""; // the typed string part of the hint is in this string
- var hintNumber = 0; // only the numerical part of the hint
- var usedTabKey = false; // when we used <Tab> to select an element
- var prevInput = ""; // record previous user input type, "text" || "number"
- var extendedhintCount; // for the count argument of Mode#action (extended hint only)
-
- // hints[] = [elem, text, span, imgSpan, elem.style.backgroundColor, elem.style.color]
- var pageHints = [];
- var validHints = []; // store the indices of the "hints" array with valid elements
-
- var activeTimeout = null; // needed for hinttimeout > 0
- var canUpdate = false;
-
- // keep track of the documents which we generated the hints for
- // docs = { doc: document, start: start_index in hints[], end: end_index in hints[] }
- var docs = [];
-
- const Mode = new Struct("prompt", "action", "tags");
- Mode.defaultValue("tags", function () function () options.hinttags);
- function extended() options.extendedhinttags;
- function images() util.makeXPath(["img"]);
-
- const hintModes = {
- ";": Mode("Focus hint", function (elem) buffer.focusElement(elem), extended),
- "?": Mode("Show information for hint", function (elem) buffer.showElementInfo(elem), extended),
- s: Mode("Save hint", function (elem) buffer.saveLink(elem, true)),
- a: Mode("Save hint with prompt", function (elem) buffer.saveLink(elem, false)),
- f: Mode("Focus frame", function (elem) elem.ownerDocument.defaultView.focus(), function () util.makeXPath(["body"])),
- o: Mode("Follow hint", function (elem) buffer.followLink(elem, liberator.CURRENT_TAB)),
- t: Mode("Follow hint in a new tab", function (elem) buffer.followLink(elem, liberator.NEW_TAB)),
- b: Mode("Follow hint in a background tab", function (elem) buffer.followLink(elem, liberator.NEW_BACKGROUND_TAB)),
- w: Mode("Follow hint in a new window", function (elem) buffer.followLink(elem, liberator.NEW_WINDOW), extended),
- F: Mode("Open multiple hints in tabs", followAndReshow),
- O: Mode("Generate an ':open URL' using hint", function (elem, loc) commandline.open(":", "open " + loc, modes.EX)),
- T: Mode("Generate a ':tabopen URL' using hint", function (elem, loc) commandline.open(":", "tabopen " + loc, modes.EX)),
- W: Mode("Generate a ':winopen URL' using hint", function (elem, loc) commandline.open(":", "winopen " + loc, modes.EX)),
- v: Mode("View hint source", function (elem, loc) buffer.viewSource(loc, false), extended),
- V: Mode("View hint source in external editor", function (elem, loc) buffer.viewSource(loc, true), extended),
- y: Mode("Yank hint location", function (elem, loc) util.copyToClipboard(loc, true)),
- Y: Mode("Yank hint description", function (elem) util.copyToClipboard(elem.textContent || "", true), extended),
- c: Mode("Open context menu", function (elem) buffer.openContextMenu(elem), extended),
- i: Mode("Show image", function (elem) liberator.open(elem.src), images),
- I: Mode("Show image in a new tab", function (elem) liberator.open(elem.src, liberator.NEW_TAB), images)
- };
+const ELEM = 0, TEXT = 1, SPAN = 2, IMG_SPAN = 3;
+const Hints = Module("hints", {
+ init: function () {
+
+ this._hintMode;
+ this._submode = ""; // used for extended mode, can be "o", "t", "y", etc.
+ this._hintString = ""; // the typed string part of the hint is in this string
+ this._hintNumber = 0; // only the numerical part of the hint
+ this._usedTabKey = false; // when we used <Tab> to select an element
+ this._prevInput = ""; // record previous user input type, "text" || "number"
+ this._extendedhintCount; // for the count argument of Mode#action (extended hint only)
+
+ // hints[] = [elem, text, span, imgSpan, elem.style.backgroundColor, elem.style.color]
+ this._pageHints = [];
+ this._validHints = []; // store the indices of the "hints" array with valid elements
+
+ this._activeTimeout = null; // needed for hinttimeout > 0
+ this._canUpdate = false;
+
+ // keep track of the documents which we generated the hints for
+ // this._docs = { doc: document, start: start_index in hints[], end: end_index in hints[] }
+ this._docs = [];
+
+ const Mode = Hints.Mode;
+ Mode.defaultValue("tags", function () function () options.hinttags);
+ function extended() options.extendedhinttags;
+ function images() util.makeXPath(["img"]);
+
+ this._hintModes = {
+ ";": Mode("Focus hint", function (elem) buffer.focusElement(elem), extended),
+ "?": Mode("Show information for hint", function (elem) buffer.showElementInfo(elem), extended),
+ s: Mode("Save hint", function (elem) buffer.saveLink(elem, true)),
+ a: Mode("Save hint with prompt", function (elem) buffer.saveLink(elem, false)),
+ f: Mode("Focus frame", function (elem) elem.ownerDocument.defaultView.focus(), function () util.makeXPath(["body"])),
+ o: Mode("Follow hint", function (elem) buffer.followLink(elem, liberator.CURRENT_TAB)),
+ t: Mode("Follow hint in a new tab", function (elem) buffer.followLink(elem, liberator.NEW_TAB)),
+ b: Mode("Follow hint in a background tab", function (elem) buffer.followLink(elem, liberator.NEW_BACKGROUND_TAB)),
+ w: Mode("Follow hint in a new window", function (elem) buffer.followLink(elem, liberator.NEW_WINDOW), extended),
+ F: Mode("Open multiple hints in tabs", followAndReshow),
+ O: Mode("Generate an ':open URL' using hint", function (elem, loc) commandline.open(":", "open " + loc, modes.EX)),
+ T: Mode("Generate a ':tabopen URL' using hint", function (elem, loc) commandline.open(":", "tabopen " + loc, modes.EX)),
+ W: Mode("Generate a ':winopen URL' using hint", function (elem, loc) commandline.open(":", "winopen " + loc, modes.EX)),
+ v: Mode("View hint source", function (elem, loc) buffer.viewSource(loc, false), extended),
+ V: Mode("View hint source in external editor", function (elem, loc) buffer.viewSource(loc, true), extended),
+ y: Mode("Yank hint location", function (elem, loc) util.copyToClipboard(loc, true)),
+ Y: Mode("Yank hint description", function (elem) util.copyToClipboard(elem.textContent || "", true), extended),
+ c: Mode("Open context menu", function (elem) buffer.openContextMenu(elem), extended),
+ i: Mode("Show image", function (elem) liberator.open(elem.src), images),
+ I: Mode("Show image in a new tab", function (elem) liberator.open(elem.src, liberator.NEW_TAB), images)
+ };
- /**
- * Follows the specified hint and then reshows all hints. Used to open
- * multiple hints in succession.
- *
- * @param {Node} elem The selected hint.
- */
- function followAndReshow(elem)
- {
- buffer.followLink(elem, liberator.NEW_BACKGROUND_TAB);
-
- // TODO: Maybe we find a *simple* way to keep the hints displayed rather than
- // showing them again, or is this short flash actually needed as a "usability
- // feature"? --mst
- hints.show("F");
- }
+ /**
+ * Follows the specified hint and then reshows all hints. Used to open
+ * multiple hints in succession.
+ *
+ * @param {Node} elem The selected hint.
+ */
+ function followAndReshow(elem) {
+ buffer.followLink(elem, liberator.NEW_BACKGROUND_TAB);
+
+ // TODO: Maybe we find a *simple* way to keep the hints displayed rather than
+ // showing them again, or is this short flash actually needed as a "usability
+ // feature"? --mst
+ hints.show("F");
+ }
+ },
/**
* Reset hints, so that they can be cleanly used again.
*/
- function reset()
- {
+ _reset: function () {
statusline.updateInputBuffer("");
- hintString = "";
- hintNumber = 0;
- usedTabKey = false;
- prevInput = "";
- pageHints = [];
- validHints = [];
- canUpdate = false;
- docs = [];
+ this._hintString = "";
+ this._hintNumber = 0;
+ this._usedTabKey = false;
+ this._prevInput = "";
+ this._pageHints = [];
+ this._validHints = [];
+ this._canUpdate = false;
+ this._docs = [];
hints.escNumbers = false;
- if (activeTimeout)
- clearTimeout(activeTimeout);
- activeTimeout = null;
- }
+ if (this._activeTimeout)
+ clearTimeout(this._activeTimeout);
+ this._activeTimeout = null;
+ },
/**
* Display the current status to the user.
*/
- function updateStatusline()
- {
- statusline.updateInputBuffer((hints.escNumbers ? mappings.getMapLeader() : "") + (hintNumber || ""));
- }
+ _updateStatusline: function () {
+ statusline.updateInputBuffer((hints.escNumbers ? mappings.getMapLeader() : "") + (this._hintNumber || ""));
+ },
/**
* Get a hint for "input", "textarea" and "select".
*
* Tries to use <label>s if possible but does not try to guess that a
- * neighbouring element might look like a label. Only called by generate().
+ * neighbouring element might look like a label. Only called by this._generate().
*
* If it finds a hint it returns it, if the hint is not the caption of the
* element it will return showText=true.
*
- * @param {Object} elem The element used to generate hint text.
+ * @param {Object} elem The element used to this._generate hint text.
* @param {Document} doc The containing document.
*
* @returns [text, showText]
*/
- function getInputHint(elem, doc)
- {
- // <input type="submit|button|reset"> Always use the value
+ _getInputHint: function (elem, doc) {
+ // <input type="submit|button|this._reset"> Always use the value
// <input type="radio|checkbox"> Use the value if it is not numeric or label or name
// <input type="password"> Never use the value, use label or name
// <input type="text|file"> <textarea> Use value if set or label or name
@@ -134,35 +125,27 @@ function Hints() //{{{
let type = elem.type;
- if (elem instanceof HTMLInputElement && /(submit|button|reset)/.test(type))
+ if (elem instanceof HTMLInputElement && /(submit|button|this._reset)/.test(type))
return [elem.value, false];
- else
- {
- for (let [, option] in Iterator(options["hintinputs"].split(",")))
- {
- if (option == "value")
- {
- if (elem instanceof HTMLSelectElement)
- {
+ else {
+ for (let [, option] in Iterator(options["hintinputs"].split(","))) {
+ if (option == "value") {
+ if (elem instanceof HTMLSelectElement) {
if (elem.selectedIndex >= 0)
return [elem.item(elem.selectedIndex).text.toLowerCase(), false];
}
- else if (type == "image")
- {
+ else if (type == "image") {
if (elem.alt)
return [elem.alt.toLowerCase(), true];
}
- else if (elem.value && type != "password")
- {
+ else if (elem.value && type != "password") {
// radio's and checkboxes often use internal ids as values - maybe make this an option too...
if (! ((type == "radio" || type == "checkbox") && !isNaN(elem.value)))
return [elem.value.toLowerCase(), (type == "radio" || type == "checkbox")];
}
}
- else if (option == "label")
- {
- if (elem.id)
- {
+ else if (option == "label") {
+ if (elem.id) {
// TODO: (possibly) do some guess work for label-like objects
let label = util.evaluateXPath("//label[@for='" + elem.id + "']", doc).snapshotItem(0);
if (label)
@@ -175,22 +158,20 @@ function Hints() //{{{
}
return ["", false];
- }
+ },
/**
* Gets the actual offset of an imagemap area.
*
- * Only called by generate().
+ * Only called by this._generate().
*
* @param {Object} elem The <area> element.
* @param {number} leftPos The left offset of the image.
* @param {number} topPos The top offset of the image.
* @returns [leftPos, topPos] The updated offsets.
*/
- function getAreaOffset(elem, leftPos, topPos)
- {
- try
- {
+ _getAreaOffset: function (elem, leftPos, topPos) {
+ try {
// Need to add the offset to the area element.
// Always try to find the top-left point, as per liberator default.
let shape = elem.getAttribute("shape").toLowerCase();
@@ -199,24 +180,20 @@ function Hints() //{{{
coordStr = coordStr.replace(/\s+[;,]\s+/g, ",").replace(/\s+/g, ",");
let coords = coordStr.split(",").map(Number);
- if ((shape == "rect" || shape == "rectangle") && coords.length == 4)
- {
+ if ((shape == "rect" || shape == "rectangle") && coords.length == 4) {
leftPos += coords[0];
topPos += coords[1];
}
- else if (shape == "circle" && coords.length == 3)
- {
+ else if (shape == "circle" && coords.length == 3) {
leftPos += coords[0] - coords[2] / Math.sqrt(2);
topPos += coords[1] - coords[2] / Math.sqrt(2);
}
- else if ((shape == "poly" || shape == "polygon") && coords.length % 2 == 0)
- {
+ else if ((shape == "poly" || shape == "polygon") && coords.length % 2 == 0) {
let leftBound = Infinity;
let topBound = Infinity;
// First find the top-left corner of the bounding rectangle (offset from image topleft can be noticably suboptimal)
- for (let i = 0; i < coords.length; i += 2)
- {
+ for (let i = 0; i < coords.length; i += 2) {
leftBound = Math.min(coords[i], leftBound);
topBound = Math.min(coords[i + 1], topBound);
}
@@ -226,13 +203,11 @@ function Hints() //{{{
let curDist = Infinity;
// Then find the closest vertex. (we could generalise to nearest point on an edge, but I doubt there is a need)
- for (let i = 0; i < coords.length; i += 2)
- {
+ for (let i = 0; i < coords.length; i += 2) {
let leftOffset = coords[i] - leftBound;
let topOffset = coords[i + 1] - topBound;
let dist = Math.sqrt(leftOffset * leftOffset + topOffset * topOffset);
- if (dist < curDist)
- {
+ if (dist < curDist) {
curDist = dist;
curLeft = coords[i];
curTop = coords[i + 1];
@@ -246,50 +221,46 @@ function Hints() //{{{
}
catch (e) {} // badly formed document, or shape == "default" in which case we don't move the hint
return [leftPos, topPos];
- }
+ },
// the containing block offsets with respect to the viewport
- function getContainerOffsets(doc)
- {
+ _getContainerOffsets: function (doc) {
let body = doc.body || doc.documentElement;
// TODO: getComputedStyle returns null for Facebook channel_iframe doc - probable Gecko bug.
let style = util.computedStyle(body);
- if (style && /^(absolute|fixed|relative)$/.test(style.position))
- {
+ if (style && /^(absolute|fixed|relative)$/.test(style.position)) {
let rect = body.getClientRects()[0];
return [-rect.left, -rect.top];
}
else
return [doc.defaultView.scrollX, doc.defaultView.scrollY];
- }
+ },
/**
* Generate the hints in a window.
*
- * Pushes the hints into the pageHints object, but does not display them.
+ * Pushes the hints into the this._pageHints object, but does not display them.
*
* @param {Window} win The window,defaults to window.content.
*/
- function generate(win)
- {
+ _generate: function (win) {
if (!win)
win = window.content;
let doc = win.document;
let height = win.innerHeight;
let width = win.innerWidth;
- let [offsetX, offsetY] = getContainerOffsets(doc);
+ let [offsetX, offsetY] = this._getContainerOffsets(doc);
let baseNodeAbsolute = util.xmlToDom(<span highlight="Hint"/>, doc);
let elem, text, span, rect, showText;
- let res = util.evaluateXPath(hintMode.tags(), doc, null, true);
+ let res = util.evaluateXPath(this._hintMode.tags(), doc, null, true);
let fragment = util.xmlToDom(<div highlight="hints"/>, doc);
- let start = pageHints.length;
- for (let elem in res)
- {
+ let start = this._pageHints.length;
+ for (let elem in res) {
showText = false;
// TODO: for iframes, this calculation is wrong
@@ -306,7 +277,7 @@ function Hints() //{{{
continue;
if (elem instanceof HTMLInputElement || elem instanceof HTMLSelectElement || elem instanceof HTMLTextAreaElement)
- [text, showText] = getInputHint(elem, doc);
+ [text, showText] = this._getInputHint(elem, doc);
else
text = elem.textContent.toLowerCase();
@@ -316,27 +287,26 @@ function Hints() //{{{
let topPos = Math.max((rect.top + offsetY), offsetY);
if (elem instanceof HTMLAreaElement)
- [leftPos, topPos] = getAreaOffset(elem, leftPos, topPos);
+ [leftPos, topPos] = this._getAreaOffset(elem, leftPos, topPos);
span.style.left = leftPos + "px";
span.style.top = topPos + "px";
fragment.appendChild(span);
- pageHints.push([elem, text, span, null, elem.style.backgroundColor, elem.style.color, showText]);
+ this._pageHints.push([elem, text, span, null, elem.style.backgroundColor, elem.style.color, showText]);
}
let body = doc.body || util.evaluateXPath(["body"], doc).snapshotItem(0);
- if (body)
- {
+ if (body) {
body.appendChild(fragment);
- docs.push({ doc: doc, start: start, end: pageHints.length - 1 });
+ this._docs.push({ doc: doc, start: start, end: this._pageHints.length - 1 });
}
- // also generate hints for frames
- Array.forEach(win.frames, generate);
+ // also _generate hints for frames
+ Array.forEach(win.frames, this.closure._generate);
return true;
- }
+ },
/**
* Update the activeHint.
@@ -346,16 +316,15 @@ function Hints() //{{{
* @param {number} newId The hint to make active.
* @param {number} oldId The currently active hint.
*/
- function showActiveHint(newId, oldId)
- {
- let oldElem = validHints[oldId - 1];
+ _showActiveHint: function (newId, oldId) {
+ let oldElem = this._validHints[oldId - 1];
if (oldElem)
- setClass(oldElem, false);
+ this._setClass(oldElem, false);
- let newElem = validHints[newId - 1];
+ let newElem = this._validHints[newId - 1];
if (newElem)
- setClass(newElem, true);
- }
+ this._setClass(newElem, true);
+ },
/**
* Toggle the highlight of a hint.
@@ -363,35 +332,31 @@ function Hints() //{{{
* @param {Object} elem The element to toggle.
* @param {boolean} active Whether it is the currently active hint or not.
*/
- function setClass(elem, active)
- {
+ _setClass: function (elem, active) {
let prefix = (elem.getAttributeNS(NS.uri, "class") || "") + " ";
if (active)
elem.setAttributeNS(NS.uri, "highlight", prefix + "HintActive");
else
elem.setAttributeNS(NS.uri, "highlight", prefix + "HintElem");
- }
+ },
/**
- * Display the hints in pageHints that are still valid.
+ * Display the hints in this._pageHints that are still valid.
*/
- function showHints()
- {
+ _showHints: function () {
let elem, text, rect, span, imgSpan, _a, _b, showText;
let hintnum = 1;
- let validHint = hintMatcher(hintString.toLowerCase());
- let activeHint = hintNumber || 1;
- validHints = [];
+ let validHint = this._hintMatcher(this._hintString.toLowerCase());
+ let activeHint = this._hintNumber || 1;
+ this._validHints = [];
- for (let [,{ doc: doc, start: start, end: end }] in Iterator(docs))
- {
- let [offsetX, offsetY] = getContainerOffsets(doc);
+ for (let [,{ doc: doc, start: start, end: end }] in Iterator(this._docs)) {
+ let [offsetX, offsetY] = this._getContainerOffsets(doc);
inner:
- for (let i in (util.interruptibleRange(start, end + 1, 500)))
- {
- let hint = pageHints[i];
+ for (let i in (util.interruptibleRange(start, end + 1, 500))) {
+ let hint = this._pageHints[i];
[elem, text, span, imgSpan, _a, _b, showText] = hint;
let valid = validHint(text);
@@ -399,16 +364,13 @@ function Hints() //{{{
if (imgSpan)
imgSpan.style.display = (valid ? "" : "none");
- if (!valid)
- {
+ if (!valid) {
elem.removeAttributeNS(NS.uri, "highlight");
continue inner;
}
- if (text == "" && elem.firstChild && elem.firstChild instanceof HTMLImageElement)
- {
- if (!imgSpan)
- {
+ if (text == "" && elem.firstChild && elem.firstChild instanceof HTMLImageElement) {
+ if (!imgSpan) {
rect = elem.firstChild.getBoundingClientRect();
if (!rect)
continue;
@@ -421,27 +383,24 @@ function Hints() //{{{
hint[IMG_SPAN] = imgSpan;
span.parentNode.appendChild(imgSpan);
}
- setClass(imgSpan, activeHint == hintnum);
+ this._setClass(imgSpan, activeHint == hintnum);
}
span.setAttribute("number", showText ? hintnum + ": " + text.substr(0, 50) : hintnum);
if (imgSpan)
imgSpan.setAttribute("number", hintnum);
else
- setClass(elem, activeHint == hintnum);
- validHints.push(elem);
+ this._setClass(elem, activeHint == hintnum);
+ this._validHints.push(elem);
hintnum++;
}
}
- if (getBrowser().markupDocumentViewer.authorStyleDisabled)
- {
+ if (getBrowser().markupDocumentViewer.authorStyleDisabled) {
let css = [];
// FIXME: Broken for imgspans.
- for (let [, { doc: doc }] in Iterator(docs))
- {
- for (let elem in util.evaluateXPath("//*[@liberator:highlight and @number]", doc))
- {
+ for (let [, { doc: doc }] in Iterator(this._docs)) {
+ for (let elem in util.evaluateXPath(" {//*[@liberator:highlight and @number]", doc)) {
let group = elem.getAttributeNS(NS.uri, "highlight");
css.push(highlight.selector(group) + "[number='" + elem.getAttribute("number") + "'] { " + elem.style.cssText + " }");
}
@@ -450,27 +409,24 @@ function Hints() //{{{
}
return true;
- }
+ },
/**
- * Remove all hints from the document, and reset the completions.
+ * Remove all hints from the document, and this._reset the completions.
*
* Lingers on the active hint briefly to confirm the selection to the user.
*
* @param {number} timeout The number of milliseconds before the active
* hint disappears.
*/
- function removeHints(timeout)
- {
- let firstElem = validHints[0] || null;
+ _removeHints: function (timeout) {
+ let firstElem = this._validHints[0] || null;
- for (let [,{ doc: doc, start: start, end: end }] in Iterator(docs))
- {
+ for (let [,{ doc: doc, start: start, end: end }] in Iterator(this._docs)) {
for (let elem in util.evaluateXPath("//*[@liberator:highlight='hints']", doc))
elem.parentNode.removeChild(elem);
- for (let i in util.range(start, end + 1))
- {
- let hint = pageHints[i];
+ for (let i in util.range(start, end + 1)) {
+ let hint = this._pageHints[i];
if (!timeout || hint[ELEM] != firstElem)
hint[ELEM].removeAttributeNS(NS.uri, "highlight");
}
@@ -481,8 +437,8 @@ function Hints() //{{{
}
styles.removeSheet(true, "hint-positions");
- reset();
- }
+ this._reset();
+ },
/**
* Finish hinting.
@@ -494,41 +450,36 @@ function Hints() //{{{
* link (when 'followhints' is 1 or 2)
*
*/
- function processHints(followFirst)
- {
- if (validHints.length == 0)
- {
+ __processHints: function (followFirst) {
+ if (this._validHints.length == 0) {
liberator.beep();
return false;
}
- if (options["followhints"] > 0)
- {
+ if (options["followhints"] > 0) {
if (!followFirst)
return false; // no return hit; don't examine uniqueness
// OK. return hit. But there's more than one hint, and
// there's no tab-selected current link. Do not follow in mode 2
- if (options["followhints"] == 2 && validHints.length > 1 && !hintNumber)
+ if (options["followhints"] == 2 && this._validHints.length > 1 && !this._hintNumber)
return liberator.beep();
}
- if (!followFirst)
- {
- let firstHref = validHints[0].getAttribute("href") || null;
- if (firstHref)
- {
- if (validHints.some(function (e) e.getAttribute("href") != firstHref))
+ if (!followFirst) {
+ let firstHref = this._validHints[0].getAttribute("href") || null;
+ if (firstHref) {
+ if (this._validHints.some(function (e) e.getAttribute("href") != firstHref))
return false;
}
- else if (validHints.length > 1)
+ else if (this._validHints.length > 1)
return false;
}
let timeout = followFirst || events.feedingKeys ? 0 : 500;
- let activeIndex = (hintNumber ? hintNumber - 1 : 0);
- let elem = validHints[activeIndex];
- removeHints(timeout);
+ let activeIndex = (this._hintNumber ? this._hintNumber - 1 : 0);
+ let elem = this._validHints[activeIndex];
+ this._removeHints(timeout);
if (timeout == 0)
// force a possible mode change, based on whether an input field has focus
@@ -536,29 +487,27 @@ function Hints() //{{{
setTimeout(function () {
if (modes.extended & modes.HINTS)
modes.reset();
- hintMode.action(elem, elem.href || "", extendedhintCount);
+ this._hintMode.action(elem, elem.href || "", this._extendedhintCount);
}, timeout);
return true;
- }
+ },
- function checkUnique()
- {
- if (hintNumber == 0)
+ _checkUnique: function () {
+ if (this._hintNumber == 0)
return;
- if (hintNumber > validHints.length)
+ if (this._hintNumber > this._validHints.length)
return void liberator.beep();
// if we write a numeric part like 3, but we have 45 hints, only follow
// the hint after a timeout, as the user might have wanted to follow link 34
- if (hintNumber > 0 && hintNumber * 10 <= validHints.length)
- {
+ if (this._hintNumber > 0 && this._hintNumber * 10 <= this._validHints.length) {
let timeout = options["hinttimeout"];
if (timeout > 0)
- activeTimeout = setTimeout(function () { processHints(true); }, timeout);
+ this._activeTimeout = setTimeout(function () { this._processHints(true); }, timeout);
}
else // we have a unique hint
- processHints(true);
- }
+ this._processHints(true);
+ },
/**
* Handle user input.
@@ -568,33 +517,30 @@ function Hints() //{{{
*
* @param {Event} event The keypress event.
*/
- function onInput(event)
- {
- prevInput = "text";
+ __onInput: function (event) {
+ this._prevInput = "text";
// clear any timeout which might be active after pressing a number
- if (activeTimeout)
- {
- clearTimeout(activeTimeout);
- activeTimeout = null;
+ if (this._activeTimeout) {
+ clearTimeout(this._activeTimeout);
+ this._activeTimeout = null;
}
- hintNumber = 0;
- hintString = commandline.command;
- updateStatusline();
- showHints();
- if (validHints.length == 1)
- processHints(false);
- }
+ this._hintNumber = 0;
+ this._hintString = commandline.command;
+ this._updateStatusline();
+ this._showHints();
+ if (this._validHints.length == 1)
+ this._processHints(false);
+ },
/**
- * Get the hintMatcher according to user preference.
+ * Get the this._hintMatcher according to user preference.
*
- * @param {string} hintString The currently typed hint.
- * @returns {hintMatcher}
+ * @param {string} this._hintString The currently typed hint.
+ * @returns {this._hintMatcher}
*/
- function hintMatcher(hintString) //{{{
- {
+ _hintMatcher: function (hintString) { //{{{
/**
* Divide a string by a regular expression.
*
@@ -607,7 +553,7 @@ function Hints() //{{{
/**
* Get a hint matcher for hintmatching=contains
*
- * The hintMatcher expects the user input to be space delimited and it
+ * The this._hintMatcher expects the user input to be space delimited and it
* returns true if each set of characters typed can be found, in any
* order, in the link.
*
@@ -616,8 +562,7 @@ function Hints() //{{{
* of a hint and returns true if all the (space-delimited) sets of
* characters typed by the user can be found in it.
*/
- function containsMatcher(hintString) //{{{
- {
+ function containsMatcher(hintString) { //{{{
let tokens = tokenize(/\s+/, hintString);
return function (linkText) {
linkText = linkText.toLowerCase();
@@ -626,9 +571,9 @@ function Hints() //{{{
} //}}}
/**
- * Get a hintMatcher for hintmatching=firstletters|wordstartswith
+ * Get a this._hintMatcher for hintmatching=firstletters|wordstartswith
*
- * The hintMatcher will look for any division of the user input that
+ * The this._hintMatcher will look for any division of the user input that
* would match the first letters of words. It will always only match
* words in order.
*
@@ -638,8 +583,7 @@ function Hints() //{{{
* @returns {function(String):boolean} A function that will filter only
* hints that match as above.
*/
- function wordStartsWithMatcher(hintString, allowWordOverleaping) //{{{
- {
+ function wordStartsWithMatcher(hintString, allowWordOverleaping) { //{{{
let hintStrings = tokenize(/\s+/, hintString);
let wordSplitRegex = RegExp(options["wordseparators"]);
@@ -657,13 +601,10 @@ function Hints() //{{{
* skipped during matching.
* @returns {boolean} Whether a match can be found.
*/
- function charsAtBeginningOfWords(chars, words, allowWordOverleaping)
- {
- function charMatches(charIdx, chars, wordIdx, words, inWordIdx, allowWordOverleaping)
- {
+ function charsAtBeginningOfWords(chars, words, allowWordOverleaping) {
+ function charMatches(charIdx, chars, wordIdx, words, inWordIdx, allowWordOverleaping) {
let matches = (chars[charIdx] == words[wordIdx][inWordIdx]);
- if ((matches == false && allowWordOverleaping) || words[wordIdx].length == 0)
- {
+ if ((matches == false && allowWordOverleaping) || words[wordIdx].length == 0) {
let nextWordIdx = wordIdx + 1;
if (nextWordIdx == words.length)
return false;
@@ -671,8 +612,7 @@ function Hints() //{{{
return charMatches(charIdx, chars, nextWordIdx, words, 0, allowWordOverleaping);
}
- if (matches)
- {
+ if (matches) {
let nextCharIdx = charIdx + 1;
if (nextCharIdx == chars.length)
return true;
@@ -686,8 +626,7 @@ function Hints() //{{{
if (charMatched)
return true;
- if (charMatched == false || beyondLastWord == true)
- {
+ if (charMatched == false || beyondLastWord == true) {
let nextInWordIdx = inWordIdx + 1;
if (nextInWordIdx == words[wordIdx].length)
return false;
@@ -717,11 +656,9 @@ function Hints() //{{{
* non-contiguous.
* @returns boolean Whether all the strings matched.
*/
- function stringsAtBeginningOfWords(strings, words, allowWordOverleaping)
- {
+ function stringsAtBeginningOfWords(strings, words, allowWordOverleaping) {
let strIdx = 0;
- for (let [, word] in Iterator(words))
- {
+ for (let [, word] in Iterator(words)) {
if (word.length == 0)
continue;
@@ -735,8 +672,7 @@ function Hints() //{{{
return true;
}
- for (; strIdx < strings.length; strIdx++)
- {
+ for (; strIdx < strings.length; strIdx++) {
if (strings[strIdx].length != 0)
return false;
}
@@ -755,321 +691,179 @@ function Hints() //{{{
};
} //}}}
- switch (options["hintmatching"])
- {
- case "contains" : return containsMatcher(hintString);
- case "wordstartswith": return wordStartsWithMatcher(hintString, /*allowWordOverleaping=*/ true);
- case "firstletters" : return wordStartsWithMatcher(hintString, /*allowWordOverleaping=*/ false);
- case "custom" : return liberator.plugins.customHintMatcher(hintString);
- default : liberator.echoerr("Invalid hintmatching type: " + hintMatching);
+ switch (options["hintmatching"]) {
+ case "contains" : return containsMatcher(hintString);
+ case "wordstartswith": return wordStartsWithMatcher(hintString, /*allowWordOverleaping=*/ true);
+ case "firstletters" : return wordStartsWithMatcher(hintString, /*allowWordOverleaping=*/ false);
+ case "custom" : return liberator.plugins.customHintMatcher(hintString);
+ default : liberator.echoerr("Invalid hintmatching type: " + hintMatching);
}
return null;
- } //}}}
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// OPTIONS /////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- const DEFAULT_HINTTAGS =
- util.makeXPath(["input[not(@type='hidden')]", "a", "area", "iframe", "textarea", "button", "select"])
- + " | //*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @role='link']";
-
- function checkXPath(val)
- {
- try
- {
- util.evaluateXPath(val, document.implementation.createDocument("", "", null));
- return true;
- }
- catch (e)
- {
- return false;
- }
- }
+ }, //}}}
- options.add(["extendedhinttags", "eht"],
- "XPath string of hintable elements activated by ';'",
- "string", DEFAULT_HINTTAGS,
- { validator: checkXPath });
-
- options.add(["hinttags", "ht"],
- "XPath string of hintable elements activated by 'f' and 'F'",
- "string", DEFAULT_HINTTAGS,
- { validator: checkXPath });
-
- options.add(["hinttimeout", "hto"],
- "Timeout before automatically following a non-unique numerical hint",
- "number", 0,
- { validator: function (value) value >= 0 });
-
- options.add(["followhints", "fh"],
- // FIXME: this description isn't very clear but I can't think of a
- // better one right now.
- "Change the behaviour of <Return> in hint mode",
- "number", 0,
- {
- completer: function () [
- ["0", "Follow the first hint as soon as typed text uniquely identifies it. Follow the selected hint on <Return>."],
- ["1", "Follow the selected hint on <Return>."],
- ["2", "Follow the selected hint on <Return> only it's been <Tab>-selected."]
- ],
- validator: Option.validateCompleter
- });
-
- options.add(["hintmatching", "hm"],
- "How links are matched",
- "string", "contains",
- {
- completer: function (context) [
- ["contains", "The typed characters are split on whitespace. The resulting groups must all appear in the hint."],
- ["wordstartswith", "The typed characters are split on whitespace. The resulting groups must all match the beginings of words, in order."],
- ["firstletters", "Behaves like wordstartswith, but all groups much match a sequence of words."],
- ["custom", "Delegate to a custom function: liberator.plugins.customHintMatcher(hintString)"]
- ],
- validator: Option.validateCompleter
- });
-
- options.add(["wordseparators", "wsp"],
- "How words are split for hintmatching",
- "string", '[.,!?:;/"^$%&?()[\\]{}<>#*+|=~ _-]');
-
- options.add(["hintinputs", "hin"],
- "How text input fields are hinted",
- "stringlist", "label,value",
- {
- completer: function (context) [
- ["value", "Match against the value contained by the input field"],
- ["label", "Match against the value of a label for the input field, if one can be found"],
- ["name", "Match against the name of an input field, only if neither a name or value could be found."]
- ],
- validator: Option.validateCompleter
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// MAPPINGS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- mappings.add(myModes, ["f"],
- "Start QuickHint mode",
- function () { hints.show("o"); });
-
- // At the moment, "F" calls
- // buffer.followLink(clicked_element, DO_WHAT_FIREFOX_DOES_WITH_CNTRL_CLICK)
- // It is not clear that it shouldn't be:
- // buffer.followLink(clicked_element, !DO_WHAT_FIREFOX_DOES_WITH_CNTRL_CLICK)
- // In fact, it might be nice if there was a "dual" to F (like H and
- // gH, except that gF is already taken). --tpp
- //
- // Likewise, it might be nice to have a liberator.NEW_FOREGROUND_TAB
- // and then make liberator.NEW_TAB always do what a Cntrl+Click
- // does. --tpp
- mappings.add(myModes, ["F"],
- "Start QuickHint mode, but open link in a new tab",
- function () { hints.show(options.getPref("browser.tabs.loadInBackground") ? "b" : "t"); });
-
- mappings.add(myModes, [";"],
- "Start an extended hint mode",
- function (count)
- {
- extendedhintCount = count;
- commandline.input(";", null,
- {
- promptHighlight: "Normal",
- completer: function (context)
- {
- context.compare = function () 0;
- context.completions = [[k, v.prompt] for ([k, v] in Iterator(hintModes))];
- },
- onChange: function () { modes.pop(); },
- onCancel: function (arg) { arg && setTimeout(function () hints.show(arg), 0); }
- });
- }, { count: true });
+ /**
+ * Creates a new hint mode.
+ *
+ * @param {string} mode The letter that identifies this mode.
+ * @param {string} prompt The description to display to the user
+ * about this mode.
+ * @param {function(Node)} action The function to be called with the
+ * element that matches.
+ * @param {function():string} tags The function that returns an
+ * XPath expression to decide which elements can be hinted (the
+ * default returns options.hinttags).
+ * @optional
+ */
+ addMode: function (mode, prompt, action, tags) {
+ this._hintModes[mode] = Hints.Mode.apply(Hints.Mode, Array.slice(arguments, 1));
+ },
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// PUBLIC SECTION //////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
+ /**
+ * Updates the display of hints.
+ *
+ * @param {string} minor Which hint mode to use.
+ * @param {string} filter The filter to use.
+ * @param {Object} win The window in which we are showing hints.
+ */
+ show: function (minor, filter, win) {
+ this._hintMode = this._hintModes[minor];
+ if (!this._hintMode)
+ return void liberator.beep();
+ commandline.input(this._hintMode.prompt + ": ", null, { onChange: this._onInput });
+ modes.extended = modes.HINTS;
- return {
+ this._submode = minor;
+ this._hintString = filter || "";
+ this._hintNumber = 0;
+ usedTab = false;
+ this._prevInput = "";
+ this._canUpdate = false;
- /**
- * Creates a new hint mode.
- *
- * @param {string} mode The letter that identifies this mode.
- * @param {string} prompt The description to display to the user
- * about this mode.
- * @param {function(Node)} action The function to be called with the
- * element that matches.
- * @param {function():string} tags The function that returns an
- * XPath expression to decide which elements can be hinted (the
- * default returns options.hinttags).
- * @optional
- */
- addMode: function (mode, prompt, action, tags)
- {
- hintModes[mode] = Mode.apply(Mode, Array.slice(arguments, 1));
- },
+ this._generate(win);
- /**
- * Updates the display of hints.
- *
- * @param {string} minor Which hint mode to use.
- * @param {string} filter The filter to use.
- * @param {Object} win The window in which we are showing hints.
- */
- show: function (minor, filter, win)
- {
- hintMode = hintModes[minor];
- if (!hintMode)
- return void liberator.beep();
- commandline.input(hintMode.prompt + ": ", null, { onChange: onInput });
- modes.extended = modes.HINTS;
+ // get all keys from the input queue
+ liberator.threadYield(true);
- submode = minor;
- hintString = filter || "";
- hintNumber = 0;
- usedTab = false;
- prevInput = "";
- canUpdate = false;
+ this._canUpdate = true;
+ this._showHints();
- generate(win);
-
- // get all keys from the input queue
- liberator.threadYield(true);
-
- canUpdate = true;
- showHints();
+ if (this._validHints.length == 0) {
+ liberator.beep();
+ modes.reset();
+ }
+ else if (this._validHints.length == 1)
+ this._processHints(false);
+ else // Ticket #185
+ this._checkUnique();
+ },
- if (validHints.length == 0)
- {
- liberator.beep();
- modes.reset();
- }
- else if (validHints.length == 1)
- processHints(false);
- else // Ticket #185
- checkUnique();
- },
+ /**
+ * Cancel all hinting.
+ */
+ hide: function () {
+ this._removeHints(0);
+ },
- /**
- * Cancel all hinting.
- */
- hide: function ()
- {
- removeHints(0);
- },
+ /**
+ * Handle a hint mode event.
+ *
+ * @param {Event} event The event to handle.
+ */
+ onEvent: function (event) {
+ let key = events.toString(event);
+ let followFirst = false;
- /**
- * Handle a hint mode event.
- *
- * @param {Event} event The event to handle.
- */
- onEvent: function (event)
- {
- let key = events.toString(event);
- let followFirst = false;
+ // clear any timeout which might be active after pressing a number
+ if (this._activeTimeout) {
+ clearTimeout(this._activeTimeout);
+ this._activeTimeout = null;
+ }
- // clear any timeout which might be active after pressing a number
- if (activeTimeout)
- {
- clearTimeout(activeTimeout);
- activeTimeout = null;
+ switch (key) {
+ case "<Return>":
+ followFirst = true;
+ break;
+
+ case "<Tab>":
+ case "<S-Tab>":
+ this._usedTabKey = true;
+ if (this._hintNumber == 0)
+ this._hintNumber = 1;
+
+ let oldId = this._hintNumber;
+ if (key == "<Tab>") {
+ if (++this._hintNumber > this._validHints.length)
+ this._hintNumber = 1;
}
+ else {
+ if (--this._hintNumber < 1)
+ this._hintNumber = this._validHints.length;
+ }
+ this._showActiveHint(this._hintNumber, oldId);
+ this._updateStatusline();
+ return;
- switch (key)
- {
- case "<Return>":
- followFirst = true;
- break;
-
- case "<Tab>":
- case "<S-Tab>":
- usedTabKey = true;
- if (hintNumber == 0)
- hintNumber = 1;
-
- let oldId = hintNumber;
- if (key == "<Tab>")
- {
- if (++hintNumber > validHints.length)
- hintNumber = 1;
- }
- else
- {
- if (--hintNumber < 1)
- hintNumber = validHints.length;
- }
- showActiveHint(hintNumber, oldId);
- updateStatusline();
- return;
-
- case "<BS>":
- if (hintNumber > 0 && !usedTabKey)
- {
- hintNumber = Math.floor(hintNumber / 10);
- if (hintNumber == 0)
- prevInput = "text";
- }
- else
- {
- usedTabKey = false;
- hintNumber = 0;
- return void liberator.beep();
- }
- break;
+ case "<BS>":
+ if (this._hintNumber > 0 && !this._usedTabKey) {
+ this._hintNumber = Math.floor(this._hintNumber / 10);
+ if (this._hintNumber == 0)
+ this._prevInput = "text";
+ }
+ else {
+ this._usedTabKey = false;
+ this._hintNumber = 0;
+ return void liberator.beep();
+ }
+ break;
- case mappings.getMapLeader():
- hints.escNumbers = !hints.escNumbers;
- if (hints.escNumbers && usedTabKey) // hintNumber not used normally, but someone may wants to toggle
- hintNumber = 0; // <tab>s ? reset. Prevent to show numbers not entered.
+ case mappings.getMapLeader():
+ hints.escNumbers = !hints.escNumbers;
+ if (hints.escNumbers && this._usedTabKey) // this._hintNumber not used normally, but someone may wants to toggle
+ this._hintNumber = 0; // <tab>s ? this._reset. Prevent to show numbers not entered.
- updateStatusline();
- return;
+ this._updateStatusline();
+ return;
- default:
- if (/^\d$/.test(key))
- {
- prevInput = "number";
+ default:
+ if (/^\d$/.test(key)) {
+ this._prevInput = "number";
- let oldHintNumber = hintNumber;
- if (hintNumber == 0 || usedTabKey)
- {
- usedTabKey = false;
- hintNumber = parseInt(key, 10);
- }
- else
- hintNumber = (hintNumber * 10) + parseInt(key, 10);
+ let oldHintNumber = this._hintNumber;
+ if (this._hintNumber == 0 || this._usedTabKey) {
+ this._usedTabKey = false;
+ this._hintNumber = parseInt(key, 10);
+ }
+ else
+ this._hintNumber = (this._hintNumber * 10) + parseInt(key, 10);
- updateStatusline();
+ this._updateStatusline();
- if (!canUpdate)
- return;
+ if (!this._canUpdate)
+ return;
- if (docs.length == 0)
- {
- generate();
- showHints();
- }
- showActiveHint(hintNumber, oldHintNumber || 1);
+ if (this._docs.length == 0) {
+ this._generate();
+ this._showHints();
+ }
+ this._showActiveHint(this._hintNumber, oldHintNumber || 1);
- if (hintNumber == 0)
- return void liberator.beep();
+ if (this._hintNumber == 0)
+ return void liberator.beep();
- checkUnique();
- }
+ this._checkUnique();
}
+ }
- updateStatusline();
+ this._updateStatusline();
- if (canUpdate)
- {
- if (docs.length == 0 && hintString.length > 0)
- generate();
+ if (this._canUpdate) {
+ if (this._docs.length == 0 && this._hintString.length > 0)
+ this._generate();
- showHints();
- processHints(followFirst);
- }
+ this._showHints();
+ this._processHints(followFirst);
}
- };
+ }
// FIXME: add resize support
// window.addEventListener("resize", onResize, null);
@@ -1083,6 +877,118 @@ function Hints() //{{{
// }
//}}}
-} //}}}
+}, {
+ Mode: new Struct("prompt", "action", "tags"),
+}, {
+ mappings: function () {
+ var myModes = config.browserModes;
+ mappings.add(myModes, ["f"],
+ "Start QuickHint mode",
+ function () { hints.show("o"); });
+
+ // At the moment, "F" calls
+ // buffer.followLink(clicked_element, DO_WHAT_FIREFOX_DOES_WITH_CNTRL_CLICK)
+ // It is not clear that it shouldn't be:
+ // buffer.followLink(clicked_element, !DO_WHAT_FIREFOX_DOES_WITH_CNTRL_CLICK)
+ // In fact, it might be nice if there was a "dual" to F (like H and
+ // gH, except that gF is already taken). --tpp
+ //
+ // Likewise, it might be nice to have a liberator.NEW_FOREGROUND_TAB
+ // and then make liberator.NEW_TAB always do what a Cntrl+Click
+ // does. --tpp
+ mappings.add(myModes, ["F"],
+ "Start QuickHint mode, but open link in a new tab",
+ function () { hints.show(options.getPref("browser.tabs.loadInBackground") ? "b" : "t"); });
+
+ mappings.add(myModes, [";"],
+ "Start an extended hint mode",
+ function (count) {
+ this._extendedhintCount = count;
+ commandline.input(";", null,
+ {
+ promptHighlight: "Normal",
+ completer: function (context) {
+ context.compare = function () 0;
+ context.completions = [[k, v.prompt] for ([k, v] in Iterator(this._hintModes))];
+ },
+ onChange: function () { modes.pop(); },
+ onCancel: function (arg) { arg && setTimeout(function () hints.show(arg), 0); }
+ });
+ }, { count: true });
+ },
+ options: function () {
+ const DEFAULT_HINTTAGS =
+ util.makeXPath(["input[not(@type='hidden')]", "a", "area", "iframe", "textarea", "button", "select"])
+ + " | //*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @role='link']";
+
+ function checkXPath(val) {
+ try {
+ util.evaluateXPath(val, document.implementation.createDocument("", "", null));
+ return true;
+ }
+ catch (e) {
+ return false;
+ }
+ }
+
+ options.add(["extendedhinttags", "eht"],
+ "XPath string of hintable elements activated by ';'",
+ "string", DEFAULT_HINTTAGS,
+ { validator: checkXPath });
+
+ options.add(["hinttags", "ht"],
+ "XPath string of hintable elements activated by 'f' and 'F'",
+ "string", DEFAULT_HINTTAGS,
+ { validator: checkXPath });
+
+ options.add(["hinttimeout", "hto"],
+ "Timeout before automatically following a non-unique numerical hint",
+ "number", 0,
+ { validator: function (value) value >= 0 });
+
+ options.add(["followhints", "fh"],
+ // FIXME: this description isn't very clear but I can't think of a
+ // better one right now.
+ "Change the behaviour of <Return> in hint mode",
+ "number", 0,
+ {
+ completer: function () [
+ ["0", "Follow the first hint as soon as typed text uniquely identifies it. Follow the selected hint on <Return>."],
+ ["1", "Follow the selected hint on <Return>."],
+ ["2", "Follow the selected hint on <Return> only it's been <Tab>-selected."]
+ ],
+ validator: Option.validateCompleter
+ });
+
+ options.add(["hintmatching", "hm"],
+ "How links are matched",
+ "string", "contains",
+ {
+ completer: function (context) [
+ ["contains", "The typed characters are split on whitespace. The resulting groups must all appear in the hint."],
+ ["wordstartswith", "The typed characters are split on whitespace. The resulting groups must all match the beginings of words, in order."],
+ ["firstletters", "Behaves like wordstartswith, but all groups much match a sequence of words."],
+ ["custom", "Delegate to a custom function: liberator.plugins.customHintMatcher(this._hintString)"]
+ ],
+ validator: Option.validateCompleter
+ });
+
+ options.add(["wordseparators", "wsp"],
+ "How words are split for hintmatching",
+ "string", '[.,!?:;/"^$%&?()[\\]{}<>#*+|=~ _-]');
+
+ options.add(["hintinputs", "hin"],
+ "How text input fields are hinted",
+ "stringlist", "label,value",
+ {
+ completer: function (context) [
+ ["value", "Match against the value contained by the input field"],
+ ["label", "Match against the value of a label for the input field, if one can be found"],
+ ["name", "Match against the name of an input field, only if neither a name or value could be found."]
+ ],
+ validator: Option.validateCompleter
+ });
+ }
+});
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/history.js b/common/content/history.js
new file mode 100644
index 00000000..c9427958
--- /dev/null
+++ b/common/content/history.js
@@ -0,0 +1,233 @@
+// Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
+//
+// This work is licensed for reuse under an MIT license. Details are
+// given in the LICENSE.txt file included with this file.
+
+const History = Module("history", {
+ get format() bookmarks.format,
+
+ get service() services.get("history"),
+
+ get: function get(filter, maxItems) {
+ // no query parameters will get all history
+ let query = services.get("history").getNewQuery();
+ let options = services.get("history").getNewQueryOptions();
+
+ if (typeof filter == "string")
+ filter = { searchTerms: filter };
+ for (let [k, v] in Iterator(filter))
+ query[k] = v;
+ options.sortingMode = options.SORT_BY_DATE_DESCENDING;
+ options.resultType = options.RESULTS_AS_URI;
+ if (maxItems > 0)
+ options.maxResults = maxItems;
+
+ // execute the query
+ let root = services.get("history").executeQuery(query, options).root;
+ root.containerOpen = true;
+ let items = util.map(util.range(0, root.childCount), function (i) {
+ let node = root.getChild(i);
+ return {
+ url: node.uri,
+ title: node.title,
+ icon: node.icon ? node.icon.spec : DEFAULT_FAVICON
+ };
+ });
+ root.containerOpen = false; // close a container after using it!
+
+ return items;
+ },
+
+ get session() {
+ let sh = window.getWebNavigation().sessionHistory;
+ let obj = [];
+ obj.index = sh.index;
+ obj.__iterator__ = function () util.Array.iteritems(this);
+ for (let i in util.range(0, sh.count)) {
+ obj[i] = { index: i, __proto__: sh.getEntryAtIndex(i, false) };
+ util.memoize(obj[i], "icon",
+ function (obj) services.get("favicon").getFaviconImageForPage(obj.URI).spec);
+ }
+ return obj;
+ },
+
+ // TODO: better names
+ stepTo: function stepTo(steps) {
+ let start = 0;
+ let end = window.getWebNavigation().sessionHistory.count - 1;
+ let current = window.getWebNavigation().sessionHistory.index;
+
+ if (current == start && steps < 0 || current == end && steps > 0)
+ liberator.beep();
+ else {
+ let index = util.Math.constrain(current + steps, start, end);
+ window.getWebNavigation().gotoIndex(index);
+ }
+ },
+
+ goToStart: function goToStart() {
+ let index = window.getWebNavigation().sessionHistory.index;
+
+ if (index > 0)
+ window.getWebNavigation().gotoIndex(0);
+ else
+ liberator.beep();
+
+ },
+
+ goToEnd: function goToEnd() {
+ let sh = window.getWebNavigation().sessionHistory;
+ let max = sh.count - 1;
+
+ if (sh.index < max)
+ window.getWebNavigation().gotoIndex(max);
+ else
+ liberator.beep();
+
+ },
+
+ // if openItems is true, open the matching history items in tabs rather than display
+ list: function list(filter, openItems, maxItems) {
+ // FIXME: returning here doesn't make sense
+ // Why the hell doesn't it make sense? --Kris
+ // See comment at bookmarks.list --djk
+ if (!openItems)
+ return completion.listCompleter("history", filter, maxItems);
+ let items = completion.runCompleter("history", filter, maxItems);
+
+ if (items.length)
+ return liberator.open(items.map(function (i) i.url), liberator.NEW_TAB);
+
+ if (filter.length > 0)
+ liberator.echoerr("E283: No history matching \"" + filter + "\"");
+ else
+ liberator.echoerr("No history set");
+ }
+}, {
+}, {
+ commands: function () {
+ commands.add(["ba[ck]"],
+ "Go back in the browser history",
+ function (args) {
+ let url = args.literalArg;
+
+ if (args.bang)
+ history.goToStart();
+ else {
+ if (url) {
+ let sh = history.session;
+ if (/^\d+(:|$)/.test(url) && sh.index - parseInt(url) in sh)
+ return void window.getWebNavigation().gotoIndex(sh.index - parseInt(url));
+
+ for (let [i, ent] in Iterator(sh.slice(0, sh.index).reverse()))
+ if (ent.URI.spec == url)
+ return void window.getWebNavigation().gotoIndex(i);
+ liberator.echoerr("Exxx: URL not found in history");
+ }
+ else
+ history.stepTo(-Math.max(args.count, 1));
+ }
+ },
+ {
+ argCount: "?",
+ bang: true,
+ completer: function completer(context) {
+ let sh = history.session;
+
+ context.anchored = false;
+ context.compare = CompletionContext.Sort.unsorted;
+ context.filters = [CompletionContext.Filter.textDescription];
+ context.completions = sh.slice(0, sh.index).reverse();
+ context.keys = { text: function (item) (sh.index - item.index) + ": " + item.URI.spec, description: "title", icon: "icon" };
+ },
+ count: true,
+ literal: 0
+ });
+
+ commands.add(["fo[rward]", "fw"],
+ "Go forward in the browser history",
+ function (args) {
+ let url = args.literalArg;
+
+ if (args.bang)
+ history.goToEnd();
+ else {
+ if (url) {
+ let sh = history.session;
+ if (/^\d+(:|$)/.test(url) && sh.index + parseInt(url) in sh)
+ return void window.getWebNavigation().gotoIndex(sh.index + parseInt(url));
+
+ for (let [i, ent] in Iterator(sh.slice(sh.index + 1)))
+ if (ent.URI.spec == url)
+ return void window.getWebNavigation().gotoIndex(i);
+ liberator.echoerr("Exxx: URL not found in history");
+ }
+ else
+ history.stepTo(Math.max(args.count, 1));
+ }
+ },
+ {
+ argCount: "?",
+ bang: true,
+ completer: function completer(context) {
+ let sh = history.session;
+
+ context.anchored = false;
+ context.compare = CompletionContext.Sort.unsorted;
+ context.filters = [CompletionContext.Filter.textDescription];
+ context.completions = sh.slice(sh.index + 1);
+ context.keys = { text: function (item) (item.index - sh.index) + ": " + item.URI.spec, description: "title", icon: "icon" };
+ },
+ count: true,
+ literal: 0
+ });
+
+ commands.add(["hist[ory]", "hs"],
+ "Show recently visited URLs",
+ function (args) { history.list(args.join(" "), args.bang, args["-max"] || 1000); }, {
+ bang: true,
+ completer: function (context) { context.quote = null; completion.history(context); },
+ // completer: function (filter) completion.history(filter)
+ options: [[["-max", "-m"], commands.OPTION_INT]]
+ });
+ },
+ completion: function () {
+ completion.history = function _history(context, maxItems) {
+ context.format = history.format;
+ context.title = ["History"];
+ context.compare = CompletionContext.Sort.unsorted;
+ //context.background = true;
+ if (context.maxItems == null)
+ context.maxItems = 100;
+ context.regenerate = true;
+ context.generate = function () history.get(context.filter, this.maxItems);
+ };
+
+ completion.addUrlCompleter("h", "History", completion.history);
+ },
+ mappings: function () {
+ var myModes = config.browserModes;
+
+ mappings.add(myModes,
+ ["<C-o>"], "Go to an older position in the jump list",
+ function (count) { history.stepTo(-Math.max(count, 1)); },
+ { count: true });
+
+ mappings.add(myModes,
+ ["<C-i>"], "Go to a newer position in the jump list",
+ function (count) { history.stepTo(Math.max(count, 1)); },
+ { count: true });
+
+ mappings.add(myModes,
+ ["H", "<A-Left>", "<M-Left>"], "Go back in the browser history",
+ function (count) { history.stepTo(-Math.max(count, 1)); },
+ { count: true });
+
+ mappings.add(myModes,
+ ["L", "<A-Right>", "<M-Right>"], "Go forward in the browser history",
+ function (count) { history.stepTo(Math.max(count, 1)); },
+ { count: true });
+ },
+});
+
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/io.js b/common/content/io.js
index ffc2c177..ed5f7143 100644
--- a/common/content/io.js
+++ b/common/content/io.js
@@ -8,88 +8,234 @@
/** @scope modules */
plugins.contexts = {};
-function Script(file)
-{
- let self = plugins.contexts[file.path];
- if (self)
- {
- if (self.onUnload)
- self.onUnload();
- return self;
- }
- plugins.contexts[file.path] = this;
- this.NAME = file.leafName.replace(/\..*/, "").replace(/-([a-z])/g, function (m, n1) n1.toUpperCase());
- this.PATH = file.path;
- this.__context__ = this;
-
- // This belongs elsewhere
- for (let [, dir] in Iterator(io.getRuntimeDirectories("plugin")))
- {
- if (dir.contains(file, false))
- plugins[this.NAME] = this;
+const Script = Class("Script", {
+ init: function (file) {
+ let self = plugins.contexts[file.path];
+ if (self) {
+ if (self.onUnload)
+ self.onUnload();
+ return self;
+ }
+ plugins.contexts[file.path] = this;
+ this.NAME = file.leafName.replace(/\..*/, "").replace(/-([a-z])/g, function (m, n1) n1.toUpperCase());
+ this.PATH = file.path;
+ this.toString = this.toString;
+ this.__context__ = this;
+ this.__proto__ = plugins;
+
+ // This belongs elsewhere
+ for (let [, dir] in Iterator(io.getRuntimeDirectories("plugin"))) {
+ if (dir.contains(file, false))
+ plugins[this.NAME] = this;
+ }
}
-}
-Script.prototype = plugins;
+});
-// TODO: why are we passing around strings rather than file objects?
/**
- * Provides a basic interface to common system I/O operations.
- * @instance io
+ * @class File A class to wrap nsIFile objects and simplify operations
+ * thereon.
+ *
+ * @param {nsIFile|string} path Expanded according to {@link IO#expandPath}
+ * @param {boolean} checkPWD Whether to allow expansion relative to the
+ * current directory. @default true
*/
-function IO() //{{{
-{
- ////////////////////////////////////////////////////////////////////////////////
- ////////////////////// PRIVATE SECTION /////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- const WINDOWS = liberator.has("Win32");
- const EXTENSION_NAME = config.name.toLowerCase();
-
- const downloadManager = Cc["@mozilla.org/download-manager;1"].createInstance(Ci.nsIDownloadManager);
-
- var processDir = services.get("directory").get("CurWorkD", Ci.nsIFile);
- var cwd = processDir;
- var oldcwd = null;
-
- var lastRunCommand = ""; // updated whenever the users runs a command with :!
- var scriptNames = [];
-
- // default option values
- var cdpath = "," + (services.get("environment").get("CDPATH").replace(/[:;]/g, ",") || ",");
- var shell, shellcmdflag;
-
- if (WINDOWS)
- {
- shell = "cmd.exe";
- // TODO: setting 'shell' to "something containing sh" updates
- // 'shellcmdflag' appropriately at startup on Windows in Vim
- shellcmdflag = "/c";
- }
- else
- {
- shell = services.get("environment").get("SHELL") || "sh";
- shellcmdflag = "-c";
- }
+const File = Class("File", {
+ init: function (path, checkPWD) {
+ if (arguments.length < 2)
+ checkPWD = true;
+
+ let file = services.create("file");
+
+ if (path instanceof Ci.nsIFile)
+ file = path;
+ else if (/file:\/\//.test(path))
+ file = services.create("file:").getFileFromURLSpec(path);
+ else {
+ let expandedPath = File.expandPath(path);
+
+ if (!File.isAbsolutePath(expandedPath) && checkPWD)
+ file = File.joinPaths(io.getCurrentDirectory().path, expandedPath);
+ else
+ file.initWithPath(expandedPath);
+ }
+ file = XPCSafeJSObjectWrapper(file);
+ file.__proto__ = File.prototype;
+ return file;
+ },
+
+ /**
+ * Iterates over the objects in this directory.
+ */
+ iterDirectory: function () {
+ if (!this.isDirectory())
+ throw Error("Not a directory");
+ let entries = this.directoryEntries;
+ while (entries.hasMoreElements())
+ yield File(entries.getNext().QueryInterface(Ci.nsIFile));
+ },
+ /**
+ * Returns the list of files in this directory.
+ *
+ * @param {boolean} sort Whether to sort the returned directory
+ * entries.
+ * @returns {nsIFile[]}
+ */
+ readDirectory: function (sort) {
+ if (!this.isDirectory())
+ throw Error("Not a directory");
+
+ let array = [e for (e in this.iterDirectory())];
+ if (sort)
+ array.sort(function (a, b) b.isDirectory() - a.isDirectory() || String.localeCompare(a.path, b.path));
+ return array;
+ },
+
+ /**
+ * Reads this file's entire contents in "text" mode and returns the
+ * content as a string.
+ *
+ * @param {string} encoding The encoding from which to decode the file.
+ * @default options["fileencoding"]
+ * @returns {string}
+ */
+ read: function (encoding) {
+ let ifstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream);
+ let icstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream);
+
+ if (!encoding)
+ encoding = options["fileencoding"];
+
+ ifstream.init(this, -1, 0, 0);
+ icstream.init(ifstream, encoding, 4096, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); // 4096 bytes buffering
+
+ let buffer = [];
+ let str = {};
+ while (icstream.readString(4096, str) != 0)
+ buffer.push(str.value);
+
+ icstream.close();
+ ifstream.close();
+ return buffer.join("");
+ },
+
+ /**
+ * Writes the string <b>buf</b> to this file.
+ *
+ * @param {string} buf The file content.
+ * @param {string|number} mode The file access mode, a bitwise OR of
+ * the following flags:
+ * {@link #MODE_RDONLY}: 0x01
+ * {@link #MODE_WRONLY}: 0x02
+ * {@link #MODE_RDWR}: 0x04
+ * {@link #MODE_CREATE}: 0x08
+ * {@link #MODE_APPEND}: 0x10
+ * {@link #MODE_TRUNCATE}: 0x20
+ * {@link #MODE_SYNC}: 0x40
+ * Alternatively, the following abbreviations may be used:
+ * ">" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_TRUNCATE}
+ * ">>" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_APPEND}
+ * @default ">"
+ * @param {number} perms The file mode bits of the created file. This
+ * is only used when creating a new file and does not change
+ * permissions if the file exists.
+ * @default 0644
+ * @param {string} encoding The encoding to used to write the file.
+ * @default options["fileencoding"]
+ */
+ write: function (buf, mode, perms, encoding) {
+ let ofstream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
+ function getStream(defaultChar) {
+ let stream = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream);
+ stream.init(ofstream, encoding, 0, defaultChar);
+ return stream;
+ }
+
+ if (!encoding)
+ encoding = options["fileencoding"];
- function expandPathList(list) list.split(",").map(io.expandPath).join(",")
+ if (mode == ">>")
+ mode = self.MODE_WRONLY | self.MODE_CREATE | self.MODE_APPEND;
+ else if (!mode || mode == ">")
+ mode = self.MODE_WRONLY | self.MODE_CREATE | self.MODE_TRUNCATE;
- function getPathsFromPathList(list)
- {
+ if (!perms)
+ perms = 0644;
+
+ ofstream.init(this, mode, perms, 0);
+ let ocstream = getStream(0);
+ try {
+ ocstream.writeString(buf);
+ }
+ catch (e) {
+ liberator.dump(e);
+ if (e.result == Cr.NS_ERROR_LOSS_OF_SIGNIFICANT_DATA) {
+ ocstream = getStream("?".charCodeAt(0));
+ ocstream.writeString(buf);
+ return false;
+ }
+ else
+ throw e;
+ }
+ finally {
+ try {
+ ocstream.close();
+ }
+ catch (e) {}
+ ofstream.close();
+ }
+ return true;
+ },
+}, {
+
+ expandPathList: function (list) list.split(",").map(this.expandPath).join(","),
+
+ expandPath: function (path, relative) {
+
+ // expand any $ENV vars - this is naive but so is Vim and we like to be compatible
+ // TODO: Vim does not expand variables set to an empty string (and documents it).
+ // Kris reckons we shouldn't replicate this 'bug'. --djk
+ // TODO: should we be doing this for all paths?
+ function expand(path) path.replace(
+ !liberator.has("Win32") ? /\$(\w+)\b|\${(\w+)}/g
+ : /\$(\w+)\b|\${(\w+)}|%(\w+)%/g,
+ function (m, n1, n2, n3) services.get("environment").get(n1 || n2 || n3) || m
+ );
+ path = expand(path);
+
+ // expand ~
+ // Yuck.
+ if (!relative && RegExp("~(?:$|[/" + util.escapeRegex(IO.PATH_SEP) + "])").test(path)) {
+ // Try $HOME first, on all systems
+ let home = services.get("environment").get("HOME");
+
+ // Windows has its own idiosyncratic $HOME variables.
+ if (!home && liberator.has("Win32"))
+ home = services.get("environment").get("USERPROFILE") ||
+ services.get("environment").get("HOMEDRIVE") + services.get("environment").get("HOMEPATH");
+
+ path = home + path.substr(1);
+ }
+
+ // TODO: Vim expands paths twice, once before checking for ~, once
+ // after, but doesn't document it. Is this just a bug? --Kris
+ path = expand(path);
+ return path.replace("/", IO.PATH_SEP, "g");
+ },
+
+ getPathsFromPathList: function (list) {
if (!list)
return [];
// empty list item means the current directory
return list.replace(/,$/, "").split(",")
.map(function (dir) dir == "" ? io.getCurrentDirectory().path : dir);
- }
+ },
- function replacePathSep(path) path.replace("/", IO.PATH_SEP, "g");
+ replacePathSep: function (path) path.replace("/", IO.PATH_SEP, "g"),
- function joinPaths(head, tail)
- {
- let path = self.File(head);
- try
- {
- path.appendRelativePath(self.expandPath(tail, true)); // FIXME: should only expand env vars and normalise path separators
+ joinPaths: function (head, tail) {
+ let path = this(head);
+ try {
+ path.appendRelativePath(this.expandPath(tail, true)); // FIXME: should only expand env vars and normalise path separators
// TODO: This code breaks the external editor at least in ubuntu
// because /usr/bin/gvim becomes /usr/bin/vim.gnome normalized and for
// some strange reason it will start without a gui then (which is not
@@ -98,277 +244,736 @@ function IO() //{{{
// if (path.exists() && path.normalize)
// path.normalize();
}
- catch (e)
- {
+ catch (e) {
return { exists: function () false, __noSuchMethod__: function () { throw e; } };
}
return path;
- }
+ },
- function isAbsolutePath(path)
- {
- try
- {
+ isAbsolutePath: function (path) {
+ try {
services.create("file").initWithPath(path);
return true;
}
- catch (e)
- {
+ catch (e) {
return false;
}
- }
+ },
+});
- var downloadListener = {
- onDownloadStateChange: function (state, download)
- {
- if (download.state == downloadManager.DOWNLOAD_FINISHED)
- {
- let url = download.source.spec;
- let title = download.displayName;
- let file = download.targetFile.path;
- let size = download.size;
-
- liberator.echomsg("Download of " + title + " to " + file + " finished", 1);
- autocommands.trigger("DownloadPost", { url: url, title: title, file: file, size: size });
- }
- },
- onStateChange: function () {},
- onProgressChange: function () {},
- onSecurityChange: function () {}
- };
-
- downloadManager.addListener(downloadListener);
- liberator.registerObserver("shutdown", function () {
- downloadManager.removeListener(downloadListener);
+// TODO: why are we passing around strings rather than file objects?
+/**
+ * Provides a basic interface to common system I/O operations.
+ * @instance io
+ */
+const IO = Module("io", {
+ requires: ["services"],
+
+ init: function () {
+ this._processDir = services.get("directory").get("CurWorkD", Ci.nsIFile);
+ this._cwd = this._processDir;
+ this._oldcwd = null;
+
+ this._lastRunCommand = ""; // updated whenever the users runs a command with :!
+ this._scriptNames = [];
+
+ this.downloadListener = {
+ onDownloadStateChange: function (state, download) {
+ if (download.state == services.get("downloadManager").DOWNLOAD_FINISHED) {
+ let url = download.source.spec;
+ let title = download.displayName;
+ let file = download.targetFile.path;
+ let size = download.size;
+
+ liberator.echomsg("Download of " + title + " to " + file + " finished", 1);
+ autocommands.trigger("DownloadPost", { url: url, title: title, file: file, size: size });
+ }
+ },
+ onStateChange: function () {},
+ onProgressChange: function () {},
+ onSecurityChange: function () {}
+ };
+
+ services.get("downloadManager").addListener(this.downloadListener);
+ },
+
+ destroy: function () {
+ services.get("downloadManager").removeListener(this.downloadListener);
for (let [, plugin] in Iterator(plugins.contexts))
if (plugin.onUnload)
plugin.onUnload();
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// OPTIONS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- options.add(["fileencoding", "fenc"],
- "Sets the character encoding of read and written files",
- "string", "UTF-8",
- {
- completer: function (context) completion.charset(context),
- validator: Option.validateCompleter
- });
- options.add(["cdpath", "cd"],
- "List of directories searched when executing :cd",
- "stringlist", cdpath,
- { setter: function (value) expandPathList(value) });
-
- options.add(["runtimepath", "rtp"],
- "List of directories searched for runtime files",
- "stringlist", IO.runtimePath,
- { setter: function (value) expandPathList(value) });
-
- options.add(["shell", "sh"],
- "Shell to use for executing :! and :run commands",
- "string", shell,
- { setter: function (value) io.expandPath(value) });
-
- options.add(["shellcmdflag", "shcf"],
- "Flag passed to shell when executing :! and :run commands",
- "string", shellcmdflag);
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMMANDS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- commands.add(["cd", "chd[ir]"],
- "Change the current directory",
- function (args)
- {
- let arg = args.literalArg;
-
- if (!arg)
- arg = "~";
- else if (arg == "-")
- {
- liberator.assert(oldcwd, "E186: No previous directory");
- arg = oldcwd.path;
- }
+ },
+
+ /**
+ * @property {function} File class.
+ * @final
+ */
+ File: File,
+
+ /**
+ * @property {number} Open for reading only.
+ * @final
+ */
+ MODE_RDONLY: 0x01,
+
+ /**
+ * @property {number} Open for writing only.
+ * @final
+ */
+ MODE_WRONLY: 0x02,
+
+ /**
+ * @property {number} Open for reading and writing.
+ * @final
+ */
+ MODE_RDWR: 0x04,
+
+ /**
+ * @property {number} If the file does not exist, the file is created.
+ * If the file exists, this flag has no effect.
+ * @final
+ */
+ MODE_CREATE: 0x08,
+
+ /**
+ * @property {number} The file pointer is set to the end of the file
+ * prior to each write.
+ * @final
+ */
+ MODE_APPEND: 0x10,
+
+ /**
+ * @property {number} If the file exists, its length is truncated to 0.
+ * @final
+ */
+ MODE_TRUNCATE: 0x20,
+
+ /**
+ * @property {number} If set, each write will wait for both the file
+ * data and file status to be physically updated.
+ * @final
+ */
+ MODE_SYNC: 0x40,
+
+ /**
+ * @property {number} With MODE_CREATE, if the file does not exist, the
+ * file is created. If the file already exists, no action and NULL
+ * is returned.
+ * @final
+ */
+ MODE_EXCL: 0x80,
+
+ /**
+ * @property {Object} The current file sourcing context. As a file is
+ * being sourced the 'file' and 'line' properties of this context
+ * object are updated appropriately.
+ */
+ sourcing: null,
+
+ /**
+ * Expands "~" and environment variables in <b>path</b>.
+ *
+ * "~" is expanded to to the value of $HOME. On Windows if this is not
+ * set then the following are tried in order:
+ * $USERPROFILE
+ * ${HOMDRIVE}$HOMEPATH
+ *
+ * The variable notation is $VAR (terminated by a non-word character)
+ * or ${VAR}. %VAR% is also supported on Windows.
+ *
+ * @param {string} path The unexpanded path string.
+ * @param {boolean} relative Whether the path is relative or absolute.
+ * @returns {string}
+ */
+ expandPath: File.expandPath,
+
+ // TODO: there seems to be no way, short of a new component, to change
+ // the process's CWD - see https://bugzilla.mozilla.org/show_bug.cgi?id=280953
+ /**
+ * Returns the current working directory.
+ *
+ * It's not possible to change the real CWD of the process so this
+ * state is maintained internally. External commands run via
+ * {@link #system} are executed in this directory.
+ *
+ * @returns {nsIFile}
+ */
+ getCurrentDirectory: function () {
+ let dir = File(this._cwd.path);
+
+ // NOTE: the directory could have been deleted underneath us so
+ // fallback to the process's CWD
+ if (dir.exists() && dir.isDirectory())
+ return dir;
+ else
+ return this._processDir;
+ },
+
+ /**
+ * Sets the current working directory.
+ *
+ * @param {string} newDir The new CWD. This may be a relative or
+ * absolute path and is expanded by {@link #expandPath}.
+ */
+ setCurrentDirectory: function (newDir) {
+ newDir = newDir || "~";
- arg = io.expandPath(arg);
+ if (newDir == "-")
+ [this._cwd, this._oldcwd] = [this._oldcwd, this.getCurrentDirectory()];
+ else {
+ let dir = File(newDir);
- // go directly to an absolute path or look for a relative path
- // match in 'cdpath'
- // TODO: handle ../ and ./ paths
- if (isAbsolutePath(arg))
- {
- if (io.setCurrentDirectory(arg))
- liberator.echomsg(io.getCurrentDirectory().path);
+ if (!dir.exists() || !dir.isDirectory()) {
+ liberator.echoerr("E344: Can't find directory \"" + dir.path + "\" in path");
+ return null;
}
- else
- {
- let dirs = getPathsFromPathList(options["cdpath"]);
- let found = false;
- for (let [, dir] in Iterator(dirs))
- {
- dir = joinPaths(dir, arg);
+ [this._cwd, this._oldcwd] = [dir, this.getCurrentDirectory()];
+ }
- if (dir.exists() && dir.isDirectory() && dir.isReadable())
- {
- io.setCurrentDirectory(dir.path);
- liberator.echomsg(io.getCurrentDirectory().path);
- found = true;
+ return self.getCurrentDirectory();
+ },
+
+ /**
+ * Returns all directories named <b>name<b/> in 'runtimepath'.
+ *
+ * @param {string} name
+ * @returns {nsIFile[])
+ */
+ getRuntimeDirectories: function (name) {
+ let dirs = File.getPathsFromPathList(options["runtimepath"]);
+
+ dirs = dirs.map(function (dir) File.joinPaths(dir, name))
+ .filter(function (dir) dir.exists() && dir.isDirectory() && dir.isReadable());
+ return dirs;
+ },
+
+ /**
+ * Returns the first user RC file found in <b>dir</b>.
+ *
+ * @param {string} dir The directory to search.
+ * @param {boolean} always When true, return a path whether
+ * the file exists or not.
+ * @default $HOME.
+ * @returns {nsIFile} The RC file or null if none is found.
+ */
+ getRCFile: function (dir, always) {
+ dir = dir || "~";
+
+ let rcFile1 = File.joinPaths(dir, "." + config.name.toLowerCase() + "rc");
+ let rcFile2 = File.joinPaths(dir, "_" + config.name.toLowerCase() + "rc");
+
+ if (liberator.has("Win32"))
+ [rcFile1, rcFile2] = [rcFile2, rcFile1];
+
+ if (rcFile1.exists() && rcFile1.isFile())
+ return rcFile1;
+ else if (rcFile2.exists() && rcFile2.isFile())
+ return rcFile2;
+ else if (always)
+ return rcFile1;
+ return null;
+ },
+
+ // TODO: make secure
+ /**
+ * Creates a temporary file.
+ *
+ * @returns {File}
+ */
+ createTempFile: function () {
+ let file = services.get("directory").get("TmpD", Ci.nsIFile);
+
+ file.append(config.tempFile);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
+
+ return File(file);
+ },
+
+
+ /**
+ * Runs an external program.
+ *
+ * @param {string} program The program to run.
+ * @param {string[]} args An array of arguments to pass to <b>program</b>.
+ * @param {boolean} blocking Whether to wait until the process terminates.
+ */
+ blockingProcesses: [],
+ run: function (program, args, blocking) {
+ args = args || [];
+ blocking = !!blocking;
+
+ let file;
+
+ if (File.isAbsolutePath(program))
+ file = File(program, true);
+ else {
+ let dirs = services.get("environment").get("PATH").split(liberator.has("Win32") ? ";" : ":");
+ // Windows tries the CWD first TODO: desirable?
+ if (liberator.has("Win32"))
+ dirs = [io.getCurrentDirectory().path].concat(dirs);
+
+lookup:
+ for (let [, dir] in Iterator(dirs)) {
+ file = File.joinPaths(dir, program);
+ try {
+ if (file.exists())
break;
+
+ // TODO: couldn't we just palm this off to the start command?
+ // automatically try to add the executable path extensions on windows
+ if (liberator.has("Win32")) {
+ let extensions = services.get("environment").get("PATHEXT").split(";");
+ for (let [, extension] in Iterator(extensions)) {
+ file = File.joinPaths(dir, program + extension);
+ if (file.exists())
+ break lookup;
+ }
}
}
+ catch (e) {}
+ }
+ }
+
+ if (!file || !file.exists()) {
+ liberator.echoerr("Command not found: " + program);
+ return -1;
+ }
+
+ let process = services.create("process");
+
+ process.init(file);
+ process.run(blocking, args.map(String), args.length);
+
+ return process.exitValue;
+ },
+
+ // FIXME: multiple paths?
+ /**
+ * Sources files found in 'runtimepath'. For each relative path in
+ * <b>paths</b> each directory in 'runtimepath' is searched and if a
+ * matching file is found it is sourced. Only the first file found (per
+ * specified path) is sourced unless <b>all</b> is specified, then
+ * all found files are sourced.
+ *
+ * @param {string[]} paths An array of relative paths to source.
+ * @param {boolean} all Whether all found files should be sourced.
+ */
+ sourceFromRuntimePath: function (paths, all) {
+ let dirs = File.getPathsFromPathList(options["runtimepath"]);
+ let found = false;
+
+ liberator.echomsg("Searching for \"" + paths.join(" ") + "\" in \"" + options["runtimepath"] + "\"", 2);
+
+ outer:
+ for (let [, dir] in Iterator(dirs)) {
+ for (let [, path] in Iterator(paths)) {
+ let file = File.joinPaths(dir, path);
+
+ liberator.echomsg("Searching for \"" + file.path + "\"", 3);
+
+ if (file.exists() && file.isFile() && file.isReadable()) {
+ io.source(file.path, false);
+ found = true;
+
+ if (!all)
+ break outer;
+ }
+ }
+ }
+
+ if (!found)
+ liberator.echomsg("not found in 'runtimepath': \"" + paths.join(" ") + "\"", 1);
- if (!found)
- {
- liberator.echoerr("E344: Can't find directory \"" + arg + "\" in cdpath\n"
- + "E472: Command failed");
+ return found;
+ },
+
+ /**
+ * Reads Ex commands, JavaScript or CSS from <b>filename</b>.
+ *
+ * @param {string} filename The name of the file to source.
+ * @param {boolean} silent Whether errors should be reported.
+ */
+ source: function (filename, silent) {
+ let wasSourcing = this.sourcing;
+ try {
+ var file = File(filename);
+ this.sourcing = {
+ file: file.path,
+ line: 0
+ };
+
+ if (!file.exists() || !file.isReadable() || file.isDirectory()) {
+ if (!silent) {
+ if (file.exists() && file.isDirectory())
+ liberator.echomsg("Cannot source a directory: \"" + filename + "\"", 0);
+ else
+ liberator.echomsg("could not source: \"" + filename + "\"", 1);
+
+ liberator.echoerr("E484: Can't open file " + filename);
}
+
+ return;
}
- },
- {
- argCount: "?",
- completer: function (context) completion.directory(context, true),
- literal: 0
- });
-
- // NOTE: this command is only used in :source
- commands.add(["fini[sh]"],
- "Stop sourcing a script file",
- function () { liberator.echoerr("E168: :finish used outside of a sourced file"); },
- { argCount: "0" });
-
- commands.add(["pw[d]"],
- "Print the current directory name",
- function () { liberator.echomsg(io.getCurrentDirectory().path); },
- { argCount: "0" });
-
- // "mkv[imperatorrc]" or "mkm[uttatorrc]"
- commands.add([EXTENSION_NAME.replace(/(.)(.*)/, "mk$1[$2rc]")],
- "Write current key mappings and changed options to the config file",
- function (args)
- {
- liberator.assert(args.length <= 1, "E172: Only one file name allowed");
-
- let filename = args[0] || io.getRCFile(null, true).path;
- let file = io.File(filename);
-
- liberator.assert(!file.exists() || args.bang,
- "E189: \"" + filename + "\" exists (add ! to override)");
-
- // TODO: Use a set/specifiable list here:
- let lines = [cmd.serial().map(commands.commandToString) for (cmd in commands) if (cmd.serial)];
- lines = util.Array.flatten(lines);
-
- // source a user .vimperatorrc file
- lines.unshift('"' + liberator.version + "\n");
-
- // For the record, I think that adding this line is absurd. --Kris
- // I can't disagree. --djk
- lines.push(commands.commandToString({
- command: "source",
- bang: true,
- arguments: [filename + ".local"]
- }));
- lines.push("\n\" vim: set ft=" + EXTENSION_NAME + ":");
+ liberator.echomsg("sourcing \"" + filename + "\"", 2);
+
+ let str = file.read();
+ let uri = services.get("io").newFileURI(file);
+
+ // handle pure JavaScript files specially
+ if (/\.js$/.test(filename)) {
+ try {
+ liberator.loadScript(uri.spec, Script(file));
+ if (liberator.initialized)
+ liberator.initHelp();
+ }
+ catch (e) {
+ let err = new Error();
+ for (let [k, v] in Iterator(e))
+ err[k] = v;
+ err.echoerr = <>{file.path}:{e.lineNumber}: {e}</>;
+ throw err;
+ }
+ }
+ else if (/\.css$/.test(filename))
+ storage.styles.registerSheet(uri.spec, false, true);
+ else {
+ let heredoc = "";
+ let heredocEnd = null; // the string which ends the heredoc
+ let lines = str.split(/\r\n|[\r\n]/);
+
+ function execute(args) { command.execute(args, special, count, { setFrom: file }); }
+
+ for (let [i, line] in Iterator(lines)) {
+ if (heredocEnd) { // we already are in a heredoc
+ if (heredocEnd.test(line)) {
+ execute(heredoc);
+ heredoc = "";
+ heredocEnd = null;
+ }
+ else
+ heredoc += line + "\n";
+ }
+ else {
+ this.sourcing.line = i + 1;
+ // skip line comments and blank lines
+ line = line.replace(/\r$/, "");
+
+ if (/^\s*(".*)?$/.test(line))
+ continue;
+
+ var [count, cmd, special, args] = commands.parseCommand(line);
+ var command = commands.get(cmd);
+
+ if (!command) {
+ let lineNumber = i + 1;
+
+ liberator.echoerr("Error detected while processing " + file.path, commandline.FORCE_MULTILINE);
+ commandline.echo("line " + lineNumber + ":", commandline.HL_LINENR, commandline.APPEND_TO_MESSAGES);
+ liberator.echoerr("E492: Not an editor command: " + line);
+ }
+ else {
+ if (command.name == "finish")
+ break;
+ else if (command.hereDoc) {
+ // check for a heredoc
+ let matches = args.match(/(.*)<<\s*(\S+)$/);
+
+ if (matches) {
+ args = matches[1];
+ heredocEnd = RegExp("^" + matches[2] + "$", "m");
+ if (matches[1])
+ heredoc = matches[1] + "\n";
+ continue;
+ }
+ }
+
+ execute(args);
+ }
+ }
+ }
+
+ // if no heredoc-end delimiter is found before EOF then
+ // process the heredoc anyway - Vim compatible ;-)
+ if (heredocEnd)
+ execute(heredoc);
+ }
+
+ if (this._scriptNames.indexOf(file.path) == -1)
+ this._scriptNames.push(file.path);
+
+ liberator.echomsg("finished sourcing \"" + filename + "\"", 2);
+
+ liberator.log("Sourced: " + filename, 3);
+ }
+ catch (e) {
+ liberator.reportError(e);
+ let message = "Sourcing file: " + (e.echoerr || file.path + ": " + e);
+ if (!silent)
+ liberator.echoerr(message);
+ }
+ finally {
+ this.sourcing = wasSourcing;
+ }
+ },
+
+ // TODO: when https://bugzilla.mozilla.org/show_bug.cgi?id=68702 is
+ // fixed use that instead of a tmpfile
+ /**
+ * Runs <b>command</b> in a subshell and returns the output in a
+ * string. The shell used is that specified by the 'shell' option.
+ *
+ * @param {string} command The command to run.
+ * @param {string} input Any input to be provided to the command on stdin.
+ * @returns {string}
+ */
+ system: function (command, input) {
+ liberator.echomsg("Calling shell to execute: " + command, 4);
- try
- {
- file.write(lines.join("\n"));
+ function escape(str) '"' + str.replace(/[\\"$]/g, "\\$&") + '"';
+
+ return this.withTempFiles(function (stdin, stdout, cmd) {
+ if (input)
+ stdin.write(input);
+
+ // TODO: implement 'shellredir'
+ if (liberator.has("Win32")) {
+ command = "cd /D " + this._cwd.path + " && " + command + " > " + stdout.path + " 2>&1" + " < " + stdin.path;
+ var res = this.run(options["shell"], options["shellcmdflag"].split(/\s+/).concat(command), true);
}
- catch (e)
- {
- liberator.echoerr("E190: Cannot open \"" + filename + "\" for writing");
- liberator.log("Could not write to " + file.path + ": " + e.message); // XXX
+ else {
+ cmd.write("cd " + escape(this._cwd.path) + "\n" +
+ ["exec", ">" + escape(stdout.path), "2>&1", "<" + escape(stdin.path),
+ escape(options["shell"]), options["shellcmdflag"], escape(command)].join(" "));
+ res = this.run("/bin/sh", ["-e", cmd.path], true);
}
- },
- {
- argCount: "*", // FIXME: should be "?" but kludged for proper error message
- bang: true,
- completer: function (context) completion.file(context, true)
- });
-
- commands.add(["runt[ime]"],
- "Source the specified file from each directory in 'runtimepath'",
- function (args) { io.sourceFromRuntimePath(args, args.bang); },
- {
- argCount: "+",
- bang: true
+
+ let output = stdout.read();
+ if (res > 0)
+ output += "\nshell returned " + res;
+ // if there is only one \n at the end, chop it off
+ else if (output && output.indexOf("\n") == output.length - 1)
+ output = output.substr(0, output.length - 1);
+
+ return output;
+ }) || "";
+ },
+
+ /**
+ * Creates a temporary file context for executing external commands.
+ * <b>func</b> is called with a temp file, created with
+ * {@link #createTempFile}, for each explicit argument. Ensures that
+ * all files are removed when <b>func</b> returns.
+ *
+ * @param {function} func The function to execute.
+ * @param {Object} self The 'this' object used when executing func.
+ * @returns {boolean} false if temp files couldn't be created,
+ * otherwise, the return value of <b>func</b>.
+ */
+ withTempFiles: function (func, self) {
+ let args = util.map(util.range(0, func.length), this.createTempFile);
+ if (!args.every(util.identity))
+ return false;
+
+ try {
+ return func.apply(self || this, args);
}
- );
-
- commands.add(["scrip[tnames]"],
- "List all sourced script names",
- function ()
- {
- let list = template.tabular(["<SNR>", "Filename"], ["text-align: right; padding-right: 1em;"],
- ([i + 1, file] for ([i, file] in Iterator(scriptNames)))); // TODO: add colon and remove column titles for pedantic Vim compatibility?
-
- commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
- },
- { argCount: "0" });
-
- commands.add(["so[urce]"],
- "Read Ex commands from a file",
- function (args)
- {
- if (args.length > 1)
- liberator.echoerr("E172: Only one file name allowed");
- else
- io.source(args[0], args.bang);
- },
- {
- argCount: "+", // FIXME: should be "1" but kludged for proper error message
- bang: true,
- completer: function (context) completion.file(context, true)
- });
-
- commands.add(["!", "run"],
- "Run a command",
- function (args)
- {
- let arg = args.literalArg;
-
- // :!! needs to be treated specially as the command parser sets the
- // bang flag but removes the ! from arg
- if (args.bang)
- arg = "!" + arg;
-
- // replaceable bang and no previous command?
- liberator.assert(!/((^|[^\\])(\\\\)*)!/.test(arg) || lastRunCommand,
- "E34: No previous command");
-
- // NOTE: Vim doesn't replace ! preceded by 2 or more backslashes and documents it - desirable?
- // pass through a raw bang when escaped or substitute the last command
- arg = arg.replace(/(\\)*!/g,
- function (m) /^\\(\\\\)*!$/.test(m) ? m.replace("\\!", "!") : m.replace("!", lastRunCommand)
- );
-
- lastRunCommand = arg;
-
- let output = io.system(arg);
-
- commandline.command = "!" + arg;
- commandline.echo(template.commandOutput(<span highlight="CmdOutput">{output}</span>));
-
- autocommands.trigger("ShellCmdPost", {});
- },
- {
- argCount: "?", // TODO: "1" - probably not worth supporting weird Vim edge cases. The dream is dead. --djk
- bang: true,
- completer: function (context) completion.shellCommand(context),
- literal: 0
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMPLETIONS /////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- liberator.registerObserver("load_completion", function () {
- completion.setFunctionCompleter([self.File, self.expandPath],
+ finally {
+ args.forEach(function (f) f.remove(false));
+ }
+ }
+}, {
+ /**
+ * @property {string} The value of the $VIMPERATOR_RUNTIME environment
+ * variable.
+ */
+ get runtimePath() {
+ const rtpvar = config.name.toUpperCase() + "_RUNTIME";
+ let rtp = services.get("environment").get(rtpvar);
+ if (!rtp) {
+ rtp = "~/" + (liberator.has("Win32") ? "" : ".") + config.name.toLowerCase();
+ services.get("environment").set(rtpvar, rtp);
+ }
+ return rtp;
+ },
+
+ get PATH_SEP() {
+ delete this.PATH_SEP;
+ let f = services.get("directory").get("CurProcD", Ci.nsIFile);
+ f.append("foo");
+ return this.PATH_SEP = f.path.substr(f.parent.path.length, 1);
+ },
+}, {
+ commands: function () {
+ commands.add(["cd", "chd[ir]"],
+ "Change the current directory",
+ function (args) {
+ let arg = args.literalArg;
+
+ if (!arg)
+ arg = "~";
+ else if (arg == "-") {
+ liberator.assert(io._oldcwd, "E186: No previous directory");
+ arg = io._oldcwd.path;
+ }
+
+ arg = File.expandPath(arg);
+
+ // go directly to an absolute path or look for a relative path
+ // match in 'cdpath'
+ // TODO: handle ../ and ./ paths
+ if (File.isAbsolutePath(arg)) {
+ if (io.setCurrentDirectory(arg))
+ liberator.echomsg(io.getCurrentDirectory().path);
+ }
+ else {
+ let dirs = File.getPathsFromPathList(options["cdpath"]);
+ let found = false;
+
+ for (let [, dir] in Iterator(dirs)) {
+ dir = File.joinPaths(dir, arg);
+
+ if (dir.exists() && dir.isDirectory() && dir.isReadable()) {
+ io.setCurrentDirectory(dir.path);
+ liberator.echomsg(io.getCurrentDirectory().path);
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ liberator.echoerr("E344: Can't find directory " + arg.quote() + " in cdpath\n"
+ + "E472: Command failed");
+ }
+ }
+ }, {
+ argCount: "?",
+ completer: function (context) completion.directory(context, true),
+ literal: 0
+ });
+
+ // NOTE: this command is only used in :source
+ commands.add(["fini[sh]"],
+ "Stop sourcing a script file",
+ function () { liberator.echoerr("E168: :finish used outside of a sourced file"); },
+ { argCount: "0" });
+
+ commands.add(["pw[d]"],
+ "Print the current directory name",
+ function () { liberator.echomsg(io.getCurrentDirectory().path); },
+ { argCount: "0" });
+
+ // "mkv[imperatorrc]" or "mkm[uttatorrc]"
+ commands.add([config.name.toLowerCase().replace(/(.)(.*)/, "mk$1[$2rc]")],
+ "Write current key mappings and changed options to the config file",
+ function (args) {
+ liberator.assert(args.length <= 1, "E172: Only one file name allowed");
+
+ let filename = args[0] || io.getRCFile(null, true).path;
+ let file = io.File(filename);
+
+ liberator.assert(!file.exists() || args.bang,
+ "E189: \"" + filename + "\" exists (add ! to override)");
+
+ // TODO: Use a set/specifiable list here:
+ let lines = [cmd.serial().map(commands.commandToString) for (cmd in commands) if (cmd.serial)];
+ lines = util.Array.flatten(lines);
+
+ // source a user .vimperatorrc file
+ lines.unshift('"' + liberator.version + "\n");
+
+ // For the record, I think that adding this line is absurd. --Kris
+ // I can't disagree. --djk
+ lines.push(commands.commandToString({
+ command: "source",
+ bang: true,
+ arguments: [filename + ".local"]
+ }));
+
+ lines.push("\n\" vim: set ft=" + config.name.toLowerCase() + ":");
+
+ try {
+ file.write(lines.join("\n"));
+ }
+ catch (e) {
+ liberator.echoerr("E190: Cannot open \"" + filename + "\" for writing");
+ liberator.log("Could not write to " + file.path + ": " + e.message); // XXX
+ }
+ }, {
+ argCount: "*", // FIXME: should be "?" but kludged for proper error message
+ bang: true,
+ completer: function (context) completion.file(context, true)
+ });
+
+ commands.add(["runt[ime]"],
+ "Source the specified file from each directory in 'runtimepath'",
+ function (args) { io.sourceFromRuntimePath(args, args.bang); }, {
+ argCount: "+",
+ bang: true
+ }
+ );
+
+ commands.add(["scrip[tnames]"],
+ "List all sourced script names",
+ function () {
+ let list = template.tabular(["<SNR>", "Filename"], ["text-align: right; padding-right: 1em;"],
+ ([i + 1, file] for ([i, file] in Iterator(this._scriptNames)))); // TODO: add colon and remove column titles for pedantic Vim compatibility?
+
+ commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
+ },
+ { argCount: "0" });
+
+ commands.add(["so[urce]"],
+ "Read Ex commands from a file",
+ function (args) {
+ if (args.length > 1)
+ liberator.echoerr("E172: Only one file name allowed");
+ else
+ io.source(args[0], args.bang);
+ }, {
+ argCount: "+", // FIXME: should be "1" but kludged for proper error message
+ bang: true,
+ completer: function (context) completion.file(context, true)
+ });
+
+ commands.add(["!", "run"],
+ "Run a command",
+ function (args) {
+ let arg = args.literalArg;
+
+ // :!! needs to be treated specially as the command parser sets the
+ // bang flag but removes the ! from arg
+ if (args.bang)
+ arg = "!" + arg;
+
+ // replaceable bang and no previous command?
+ liberator.assert(!/((^|[^\\])(\\\\)*)!/.test(arg) || this._lastRunCommand,
+ "E34: No previous command");
+
+ // NOTE: Vim doesn't replace ! preceded by 2 or more backslashes and documents it - desirable?
+ // pass through a raw bang when escaped or substitute the last command
+ arg = arg.replace(/(\\)*!/g,
+ function (m) /^\\(\\\\)*!$/.test(m) ? m.replace("\\!", "!") : m.replace("!", this._lastRunCommand)
+ );
+
+ this._lastRunCommand = arg;
+
+ let output = io.system(arg);
+
+ commandline.command = "!" + arg;
+ commandline.echo(template.commandOutput(<span highlight="CmdOutput">{output}</span>));
+
+ autocommands.trigger("ShellCmdPost", {});
+ }, {
+ argCount: "?", // TODO: "1" - probably not worth supporting weird Vim edge cases. The dream is dead. --djk
+ bang: true,
+ completer: function (context) completion.shellCommand(context),
+ literal: 0
+ });
+ },
+ completion: function () {
+ completion.setFunctionCompleter([this.File, File.expandPath],
[function (context, obj, args) {
context.quote[2] = "";
completion.file(context, true);
@@ -420,18 +1025,15 @@ function IO() //{{{
context.compare = function (a, b)
b.isdir - a.isdir || String.localeCompare(a.text, b.text);
- if (options["wildignore"])
- {
+ if (options["wildignore"]) {
let wigRegexp = RegExp("(^" + options.get("wildignore").values.join("|") + ")$");
context.filters.push(function ({item: f}) f.isDirectory() || !wigRegexp.test(f.leafName));
}
// context.background = true;
context.key = dir;
- context.generate = function generate_file()
- {
- try
- {
+ context.generate = function generate_file() {
+ try {
return File(dir).readDirectory();
}
catch (e) {}
@@ -440,16 +1042,13 @@ function IO() //{{{
completion.shellCommand = function shellCommand(context) {
context.title = ["Shell Command", "Path"];
- context.generate = function ()
- {
+ context.generate = function () {
let dirNames = services.get("environment").get("PATH").split(RegExp(liberator.has("Win32") ? ";" : ":"));
let commands = [];
- for (let [, dirName] in Iterator(dirNames))
- {
+ for (let [, dirName] in Iterator(dirNames)) {
let dir = io.File(dirName);
- if (dir.exists() && dir.isDirectory())
- {
+ if (dir.exists() && dir.isDirectory()) {
commands.push([[file.leafName, dir.path] for (file in dir.iterDirectory())
if (file.isFile() && file.isExecutable())]);
}
@@ -460,768 +1059,45 @@ function IO() //{{{
};
completion.addUrlCompleter("f", "Local files", completion.file);
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// File ////////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- /**
- * @class File A class to wrap nsIFile objects and simplify operations
- * thereon.
- *
- * @param {nsIFile|string} path Expanded according to {@link IO#expandPath}
- * @param {boolean} checkPWD Whether to allow expansion relative to the
- * current directory. @default true
- */
- function File(path, checkPWD)
- {
- if (arguments.length < 2)
- checkPWD = true;
-
- let file = services.create("file");
-
- if (path instanceof Ci.nsIFile)
- file = path;
- else if (/file:\/\//.test(path))
- file = services.create("file:").getFileFromURLSpec(path);
- else
- {
- let expandedPath = io.expandPath(path);
-
- if (!isAbsolutePath(expandedPath) && checkPWD)
- file = joinPaths(io.getCurrentDirectory().path, expandedPath);
- else
- file.initWithPath(expandedPath);
+ },
+ options: function () {
+ var shell, shellcmdflag;
+ if (liberator.has("Win32")) {
+ shell = "cmd.exe";
+ // TODO: setting 'shell' to "something containing sh" updates
+ // 'shellcmdflag' appropriately at startup on Windows in Vim
+ shellcmdflag = "/c";
}
- file = XPCSafeJSObjectWrapper(file);
- file.__proto__ = File.prototype;
- return file;
- }
- File.prototype = {
- /**
- * Iterates over the objects in this directory.
- */
- iterDirectory: function ()
- {
- if (!this.isDirectory())
- throw Error("Not a directory");
- let entries = this.directoryEntries;
- while (entries.hasMoreElements())
- yield File(entries.getNext().QueryInterface(Ci.nsIFile));
- },
- /**
- * Returns the list of files in this directory.
- *
- * @param {boolean} sort Whether to sort the returned directory
- * entries.
- * @returns {nsIFile[]}
- */
- readDirectory: function (sort)
- {
- if (!this.isDirectory())
- throw Error("Not a directory");
-
- let array = [e for (e in this.iterDirectory())];
- if (sort)
- array.sort(function (a, b) b.isDirectory() - a.isDirectory() || String.localeCompare(a.path, b.path));
- return array;
- },
-
- /**
- * Reads this file's entire contents in "text" mode and returns the
- * content as a string.
- *
- * @param {string} encoding The encoding from which to decode the file.
- * @default options["fileencoding"]
- * @returns {string}
- */
- read: function (encoding)
- {
- let ifstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream);
- let icstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream);
-
- if (!encoding)
- encoding = options["fileencoding"];
-
- ifstream.init(this, -1, 0, 0);
- icstream.init(ifstream, encoding, 4096, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); // 4096 bytes buffering
-
- let buffer = [];
- let str = {};
- while (icstream.readString(4096, str) != 0)
- buffer.push(str.value);
-
- icstream.close();
- ifstream.close();
- return buffer.join("");
- },
-
- /**
- * Writes the string <b>buf</b> to this file.
- *
- * @param {string} buf The file content.
- * @param {string|number} mode The file access mode, a bitwise OR of
- * the following flags:
- * {@link #MODE_RDONLY}: 0x01
- * {@link #MODE_WRONLY}: 0x02
- * {@link #MODE_RDWR}: 0x04
- * {@link #MODE_CREATE}: 0x08
- * {@link #MODE_APPEND}: 0x10
- * {@link #MODE_TRUNCATE}: 0x20
- * {@link #MODE_SYNC}: 0x40
- * Alternatively, the following abbreviations may be used:
- * ">" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_TRUNCATE}
- * ">>" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_APPEND}
- * @default ">"
- * @param {number} perms The file mode bits of the created file. This
- * is only used when creating a new file and does not change
- * permissions if the file exists.
- * @default 0644
- * @param {string} encoding The encoding to used to write the file.
- * @default options["fileencoding"]
- */
- write: function (buf, mode, perms, encoding)
- {
- let ofstream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
- function getStream(defaultChar)
- {
- let stream = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream);
- stream.init(ofstream, encoding, 0, defaultChar);
- return stream;
- }
-
- if (!encoding)
- encoding = options["fileencoding"];
-
- if (mode == ">>")
- mode = self.MODE_WRONLY | self.MODE_CREATE | self.MODE_APPEND;
- else if (!mode || mode == ">")
- mode = self.MODE_WRONLY | self.MODE_CREATE | self.MODE_TRUNCATE;
-
- if (!perms)
- perms = 0644;
-
- ofstream.init(this, mode, perms, 0);
- let ocstream = getStream(0);
- try
- {
- ocstream.writeString(buf);
- }
- catch (e)
- {
- liberator.dump(e);
- if (e.result == Cr.NS_ERROR_LOSS_OF_SIGNIFICANT_DATA)
- {
- ocstream = getStream("?".charCodeAt(0));
- ocstream.writeString(buf);
- return false;
- }
- else
- throw e;
- }
- finally
- {
- try
- {
- ocstream.close();
- }
- catch (e) {}
- ofstream.close();
- }
- return true;
- },
- };
-
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// PUBLIC SECTION //////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- const self = {
- /**
- * @property {function} File class.
- * @final
- */
- File: File,
-
- /**
- * @property {number} Open for reading only.
- * @final
- */
- MODE_RDONLY: 0x01,
-
- /**
- * @property {number} Open for writing only.
- * @final
- */
- MODE_WRONLY: 0x02,
-
- /**
- * @property {number} Open for reading and writing.
- * @final
- */
- MODE_RDWR: 0x04,
-
- /**
- * @property {number} If the file does not exist, the file is created.
- * If the file exists, this flag has no effect.
- * @final
- */
- MODE_CREATE: 0x08,
-
- /**
- * @property {number} The file pointer is set to the end of the file
- * prior to each write.
- * @final
- */
- MODE_APPEND: 0x10,
-
- /**
- * @property {number} If the file exists, its length is truncated to 0.
- * @final
- */
- MODE_TRUNCATE: 0x20,
-
- /**
- * @property {number} If set, each write will wait for both the file
- * data and file status to be physically updated.
- * @final
- */
- MODE_SYNC: 0x40,
-
- /**
- * @property {number} With MODE_CREATE, if the file does not exist, the
- * file is created. If the file already exists, no action and NULL
- * is returned.
- * @final
- */
- MODE_EXCL: 0x80,
-
- /**
- * @property {Object} The current file sourcing context. As a file is
- * being sourced the 'file' and 'line' properties of this context
- * object are updated appropriately.
- */
- sourcing: null,
-
- /**
- * Expands "~" and environment variables in <b>path</b>.
- *
- * "~" is expanded to to the value of $HOME. On Windows if this is not
- * set then the following are tried in order:
- * $USERPROFILE
- * ${HOMDRIVE}$HOMEPATH
- *
- * The variable notation is $VAR (terminated by a non-word character)
- * or ${VAR}. %VAR% is also supported on Windows.
- *
- * @param {string} path The unexpanded path string.
- * @param {boolean} relative Whether the path is relative or absolute.
- * @returns {string}
- */
- expandPath: IO.expandPath,
-
- // TODO: there seems to be no way, short of a new component, to change
- // the process's CWD - see https://bugzilla.mozilla.org/show_bug.cgi?id=280953
- /**
- * Returns the current working directory.
- *
- * It's not possible to change the real CWD of the process so this
- * state is maintained internally. External commands run via
- * {@link #system} are executed in this directory.
- *
- * @returns {nsIFile}
- */
- getCurrentDirectory: function ()
- {
- let dir = self.File(cwd.path);
-
- // NOTE: the directory could have been deleted underneath us so
- // fallback to the process's CWD
- if (dir.exists() && dir.isDirectory())
- return dir;
- else
- return processDir;
- },
-
- /**
- * Sets the current working directory.
- *
- * @param {string} newDir The new CWD. This may be a relative or
- * absolute path and is expanded by {@link #expandPath}.
- */
- setCurrentDirectory: function (newDir)
- {
- newDir = newDir || "~";
-
- if (newDir == "-")
- [cwd, oldcwd] = [oldcwd, this.getCurrentDirectory()];
- else
- {
- let dir = self.File(newDir);
-
- if (!dir.exists() || !dir.isDirectory())
- {
- liberator.echoerr("E344: Can't find directory \"" + dir.path + "\" in path");
- return null;
- }
-
- [cwd, oldcwd] = [dir, this.getCurrentDirectory()];
- }
-
- return self.getCurrentDirectory();
- },
-
- /**
- * Returns all directories named <b>name<b/> in 'runtimepath'.
- *
- * @param {string} name
- * @returns {nsIFile[])
- */
- getRuntimeDirectories: function (name)
- {
- let dirs = getPathsFromPathList(options["runtimepath"]);
-
- dirs = dirs.map(function (dir) joinPaths(dir, name))
- .filter(function (dir) dir.exists() && dir.isDirectory() && dir.isReadable());
- return dirs;
- },
-
- /**
- * Returns the first user RC file found in <b>dir</b>.
- *
- * @param {string} dir The directory to search.
- * @param {boolean} always When true, return a path whether
- * the file exists or not.
- * @default $HOME.
- * @returns {nsIFile} The RC file or null if none is found.
- */
- getRCFile: function (dir, always)
- {
- dir = dir || "~";
-
- let rcFile1 = joinPaths(dir, "." + EXTENSION_NAME + "rc");
- let rcFile2 = joinPaths(dir, "_" + EXTENSION_NAME + "rc");
-
- if (WINDOWS)
- [rcFile1, rcFile2] = [rcFile2, rcFile1];
-
- if (rcFile1.exists() && rcFile1.isFile())
- return rcFile1;
- else if (rcFile2.exists() && rcFile2.isFile())
- return rcFile2;
- else if (always)
- return rcFile1;
- return null;
- },
-
- // TODO: make secure
- /**
- * Creates a temporary file.
- *
- * @returns {File}
- */
- createTempFile: function ()
- {
- let file = services.get("directory").get("TmpD", Ci.nsIFile);
-
- file.append(config.tempFile);
- file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
-
- return self.File(file);
- },
-
-
- /**
- * Runs an external program.
- *
- * @param {string} program The program to run.
- * @param {string[]} args An array of arguments to pass to <b>program</b>.
- * @param {boolean} blocking Whether to wait until the process terminates.
- */
- blockingProcesses: [],
- run: function (program, args, blocking)
- {
- args = args || [];
- blocking = !!blocking;
-
- let file;
-
- if (isAbsolutePath(program))
- file = self.File(program, true);
- else
- {
- let dirs = services.get("environment").get("PATH").split(WINDOWS ? ";" : ":");
- // Windows tries the CWD first TODO: desirable?
- if (WINDOWS)
- dirs = [io.getCurrentDirectory().path].concat(dirs);
-
-lookup:
- for (let [, dir] in Iterator(dirs))
- {
- file = joinPaths(dir, program);
- try
- {
- if (file.exists())
- break;
-
- // TODO: couldn't we just palm this off to the start command?
- // automatically try to add the executable path extensions on windows
- if (WINDOWS)
- {
- let extensions = services.get("environment").get("PATHEXT").split(";");
- for (let [, extension] in Iterator(extensions))
- {
- file = joinPaths(dir, program + extension);
- if (file.exists())
- break lookup;
- }
- }
- }
- catch (e) {}
- }
- }
-
- if (!file || !file.exists())
- {
- liberator.echoerr("Command not found: " + program);
- return -1;
- }
-
- let process = services.create("process");
-
- process.init(file);
- process.run(blocking, args.map(String), args.length);
-
- return process.exitValue;
- },
-
- // FIXME: multiple paths?
- /**
- * Sources files found in 'runtimepath'. For each relative path in
- * <b>paths</b> each directory in 'runtimepath' is searched and if a
- * matching file is found it is sourced. Only the first file found (per
- * specified path) is sourced unless <b>all</b> is specified, then
- * all found files are sourced.
- *
- * @param {string[]} paths An array of relative paths to source.
- * @param {boolean} all Whether all found files should be sourced.
- */
- sourceFromRuntimePath: function (paths, all)
- {
- let dirs = getPathsFromPathList(options["runtimepath"]);
- let found = false;
-
- liberator.echomsg("Searching for \"" + paths.join(" ") + "\" in \"" + options["runtimepath"] + "\"", 2);
-
- outer:
- for (let [, dir] in Iterator(dirs))
- {
- for (let [, path] in Iterator(paths))
- {
- let file = joinPaths(dir, path);
-
- liberator.echomsg("Searching for \"" + file.path + "\"", 3);
-
- if (file.exists() && file.isFile() && file.isReadable())
- {
- io.source(file.path, false);
- found = true;
-
- if (!all)
- break outer;
- }
- }
- }
-
- if (!found)
- liberator.echomsg("not found in 'runtimepath': \"" + paths.join(" ") + "\"", 1);
-
- return found;
- },
-
- /**
- * Reads Ex commands, JavaScript or CSS from <b>filename</b>.
- *
- * @param {string} filename The name of the file to source.
- * @param {boolean} silent Whether errors should be reported.
- */
- source: function (filename, silent)
- {
- let wasSourcing = self.sourcing;
- try
- {
- var file = self.File(filename);
- self.sourcing = {
- file: file.path,
- line: 0
- };
-
- if (!file.exists() || !file.isReadable() || file.isDirectory())
- {
- if (!silent)
- {
- if (file.exists() && file.isDirectory())
- liberator.echomsg("Cannot source a directory: \"" + filename + "\"", 0);
- else
- liberator.echomsg("could not source: \"" + filename + "\"", 1);
-
- liberator.echoerr("E484: Can't open file " + filename);
- }
-
- return;
- }
-
- liberator.echomsg("sourcing \"" + filename + "\"", 2);
-
- let str = file.read();
- let uri = services.get("io").newFileURI(file);
-
- // handle pure JavaScript files specially
- if (/\.js$/.test(filename))
- {
- try
- {
- liberator.loadScript(uri.spec, new Script(file));
- if (liberator.initialized)
- liberator.initHelp();
- }
- catch (e)
- {
- let err = new Error();
- for (let [k, v] in Iterator(e))
- err[k] = v;
- err.echoerr = <>{file.path}:{e.lineNumber}: {e}</>;
- throw err;
- }
- }
- else if (/\.css$/.test(filename))
- storage.styles.registerSheet(uri.spec, false, true);
- else
- {
- let heredoc = "";
- let heredocEnd = null; // the string which ends the heredoc
- let lines = str.split(/\r\n|[\r\n]/);
-
- function execute(args) { command.execute(args, special, count, { setFrom: file }); }
-
- for (let [i, line] in Iterator(lines))
- {
- if (heredocEnd) // we already are in a heredoc
- {
- if (heredocEnd.test(line))
- {
- execute(heredoc);
- heredoc = "";
- heredocEnd = null;
- }
- else
- heredoc += line + "\n";
- }
- else
- {
- self.sourcing.line = i + 1;
- // skip line comments and blank lines
- line = line.replace(/\r$/, "");
-
- if (/^\s*(".*)?$/.test(line))
- continue;
-
- var [count, cmd, special, args] = commands.parseCommand(line);
- var command = commands.get(cmd);
-
- if (!command)
- {
- let lineNumber = i + 1;
-
- liberator.echoerr("Error detected while processing " + file.path, commandline.FORCE_MULTILINE);
- commandline.echo("line " + lineNumber + ":", commandline.HL_LINENR, commandline.APPEND_TO_MESSAGES);
- liberator.echoerr("E492: Not an editor command: " + line);
- }
- else
- {
- if (command.name == "finish")
- break;
- else if (command.hereDoc)
- {
- // check for a heredoc
- let matches = args.match(/(.*)<<\s*(\S+)$/);
-
- if (matches)
- {
- args = matches[1];
- heredocEnd = RegExp("^" + matches[2] + "$", "m");
- if (matches[1])
- heredoc = matches[1] + "\n";
- continue;
- }
- }
-
- execute(args);
- }
- }
- }
-
- // if no heredoc-end delimiter is found before EOF then
- // process the heredoc anyway - Vim compatible ;-)
- if (heredocEnd)
- execute(heredoc);
- }
-
- if (scriptNames.indexOf(file.path) == -1)
- scriptNames.push(file.path);
-
- liberator.echomsg("finished sourcing \"" + filename + "\"", 2);
-
- liberator.log("Sourced: " + filename, 3);
- }
- catch (e)
- {
- liberator.reportError(e);
- let message = "Sourcing file: " + (e.echoerr || file.path + ": " + e);
- if (!silent)
- liberator.echoerr(message);
- }
- finally
- {
- self.sourcing = wasSourcing;
- }
- },
-
- // TODO: when https://bugzilla.mozilla.org/show_bug.cgi?id=68702 is
- // fixed use that instead of a tmpfile
- /**
- * Runs <b>command</b> in a subshell and returns the output in a
- * string. The shell used is that specified by the 'shell' option.
- *
- * @param {string} command The command to run.
- * @param {string} input Any input to be provided to the command on stdin.
- * @returns {string}
- */
- system: function (command, input)
- {
- liberator.echomsg("Calling shell to execute: " + command, 4);
-
- function escape(str) '"' + str.replace(/[\\"$]/g, "\\$&") + '"';
-
- return this.withTempFiles(function (stdin, stdout, cmd) {
- if (input)
- stdin.write(input);
-
- // TODO: implement 'shellredir'
- if (WINDOWS)
- {
- command = "cd /D " + cwd.path + " && " + command + " > " + stdout.path + " 2>&1" + " < " + stdin.path;
- var res = this.run(options["shell"], options["shellcmdflag"].split(/\s+/).concat(command), true);
- }
- else
- {
- cmd.write("cd " + escape(cwd.path) + "\n" +
- ["exec", ">" + escape(stdout.path), "2>&1", "<" + escape(stdin.path),
- escape(options["shell"]), options["shellcmdflag"], escape(command)].join(" "));
- res = this.run("/bin/sh", ["-e", cmd.path], true);
- }
-
- let output = stdout.read();
- if (res > 0)
- output += "\nshell returned " + res;
- // if there is only one \n at the end, chop it off
- else if (output && output.indexOf("\n") == output.length - 1)
- output = output.substr(0, output.length - 1);
-
- return output;
- }) || "";
- },
-
- /**
- * Creates a temporary file context for executing external commands.
- * <b>func</b> is called with a temp file, created with
- * {@link #createTempFile}, for each explicit argument. Ensures that
- * all files are removed when <b>func</b> returns.
- *
- * @param {function} func The function to execute.
- * @param {Object} self The 'this' object used when executing func.
- * @returns {boolean} false if temp files couldn't be created,
- * otherwise, the return value of <b>func</b>.
- */
- withTempFiles: function (func, self)
- {
- let args = util.map(util.range(0, func.length), this.createTempFile);
- if (!args.every(util.identity))
- return false;
-
- try
- {
- return func.apply(self || this, args);
- }
- finally
- {
- args.forEach(function (f) f.remove(false));
- }
+ else {
+ shell = services.get("environment").get("SHELL") || "sh";
+ shellcmdflag = "-c";
}
- }; //}}}
-
- return self;
-} //}}}
-
-IO.PATH_SEP = (function () {
- let f = services.get("directory").get("CurProcD", Ci.nsIFile);
- f.append("foo");
- return f.path.substr(f.parent.path.length, 1);
-})();
-
-/**
- * @property {string} The value of the $VIMPERATOR_RUNTIME environment
- * variable.
- */
-IO.__defineGetter__("runtimePath", function () {
- const rtpvar = config.name.toUpperCase() + "_RUNTIME";
- let rtp = services.get("environment").get(rtpvar);
- if (!rtp)
- {
- rtp = "~/" + (liberator.has("Win32") ? "" : ".") + config.name.toLowerCase();
- services.get("environment").set(rtpvar, rtp);
- }
- return rtp;
+ options.add(["fileencoding", "fenc"],
+ "Sets the character encoding of read and written files",
+ "string", "UTF-8", {
+ completer: function (context) completion.charset(context),
+ validator: Option.validateCompleter
+ });
+ options.add(["cdpath", "cd"],
+ "List of directories searched when executing :cd",
+ "stringlist", "," + (services.get("environment").get("CDPATH").replace(/[:;]/g, ",") || ","),
+ { setter: function (value) File.expandPathList(value) });
+
+ options.add(["runtimepath", "rtp"],
+ "List of directories searched for runtime files",
+ "stringlist", IO.runtimePath,
+ { setter: function (value) File.expandPathList(value) });
+
+ options.add(["shell", "sh"],
+ "Shell to use for executing :! and :run commands",
+ "string", shell,
+ { setter: function (value) File.expandPath(value) });
+
+ options.add(["shellcmdflag", "shcf"],
+ "Flag passed to shell when executing :! and :run commands",
+ "string", shellcmdflag);
+ },
});
-IO.expandPath = function (path, relative)
-{
- // TODO: proper pathname separator translation like Vim - this should be done elsewhere
- const WINDOWS = liberator.has("Win32");
-
- // expand any $ENV vars - this is naive but so is Vim and we like to be compatible
- // TODO: Vim does not expand variables set to an empty string (and documents it).
- // Kris reckons we shouldn't replicate this 'bug'. --djk
- // TODO: should we be doing this for all paths?
- function expand(path) path.replace(
- !WINDOWS ? /\$(\w+)\b|\${(\w+)}/g
- : /\$(\w+)\b|\${(\w+)}|%(\w+)%/g,
- function (m, n1, n2, n3) services.get("environment").get(n1 || n2 || n3) || m
- );
- path = expand(path);
-
- // expand ~
- // Yuck.
- if (!relative && RegExp("~(?:$|[/" + util.escapeRegex(IO.PATH_SEP) + "])").test(path))
- {
- // Try $HOME first, on all systems
- let home = services.get("environment").get("HOME");
-
- // Windows has its own idiosyncratic $HOME variables.
- if (!home && WINDOWS)
- home = services.get("environment").get("USERPROFILE") ||
- services.get("environment").get("HOMEDRIVE") + services.get("environment").get("HOMEPATH");
-
- path = home + path.substr(1);
- }
-
- // TODO: Vim expands paths twice, once before checking for ~, once
- // after, but doesn't document it. Is this just a bug? --Kris
- path = expand(path);
- return path.replace("/", IO.PATH_SEP, "g");
-};
-
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/liberator-overlay.js b/common/content/liberator-overlay.js
index 645e062d..b2a310b4 100644
--- a/common/content/liberator-overlay.js
+++ b/common/content/liberator-overlay.js
@@ -11,51 +11,51 @@
const loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
.getService(Components.interfaces.mozIJSSubScriptLoader);
- function load(script)
- {
- for (let [i, base] in Iterator(prefix))
- {
- try
- {
+ function load(script) {
+ for (let [i, base] in Iterator(prefix)) {
+ try {
loader.loadSubScript(base + script, modules);
return;
}
- catch (e)
- {
+ catch (e) {
if (i + 1 < prefix.length)
continue;
if (Components.utils.reportError)
Components.utils.reportError(e);
dump("liberator: Loading script " + script + ": " + e + "\n");
+ dump(e.stack + "\n");
}
}
}
- Components.utils.import("resource://liberator/storage.jsm", modules);
-
let prefix = [BASE];
- ["services.js",
- "liberator.js",
- "configbase.js",
- "config.js"].forEach(load);
- modules.config.__proto__ = modules.configbase;
-
- ["util.js",
- "style.js",
+ ["base.js",
+ "modules.js",
+ "autocommands.js",
"buffer.js",
+ "commandline.js",
"commands.js",
"completion.js",
+ "config.js",
+ "configbase.js",
+ "liberator.js",
"editor.js",
"events.js",
"finder.js",
"hints.js",
"io.js",
"mappings.js",
+ "marks.js",
"modes.js",
"options.js",
+ "services.js",
+ "statusline.js",
+ "style.js",
"template.js",
- "ui.js"].forEach(load);
+ "util.js",
+ ].forEach(load);
+ modules.config.__proto__ = modules.configbase;
prefix.unshift("chrome://" + modules.config.name.toLowerCase() + "/content/");
modules.config.scripts.forEach(load);
diff --git a/common/content/liberator.js b/common/content/liberator.js
index 2ab21379..d9bf4c14 100644
--- a/common/content/liberator.js
+++ b/common/content/liberator.js
@@ -15,97 +15,1033 @@ const EVAL_ERROR = "__liberator_eval_error";
const EVAL_RESULT = "__liberator_eval_result";
const EVAL_STRING = "__liberator_eval_string";
-function FailedAssertion(message) {
- this.message = message;
-}
-FailedAssertion.prototype = {
- __proto__: Error.prototype,
-};
-
-const liberator = (function () //{{{
-{
- ////////////////////////////////////////////////////////////////////////////////
- ////////////////////// PRIVATE SECTION /////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- function Runnable(self, func, args)
- {
- this.self = self;
- this.func = func;
- this.args = args;
- }
- Runnable.prototype = {
+// Move elsewhere?
+const Storage = Module("storage", {
+ init: function () {
+ Components.utils.import("resource://liberator/storage.jsm", this);
+ modules.Timer = this.Timer; // Fix me, please.
+ return this.storage;
+ },
+});
+
+function Runnable(self, func, args) {
+ return {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIRunnable]),
- run: function () { this.func.apply(this.self, this.args); }
- };
+ run: function () { func.apply(self, args); }
+ }
+}
- const observers = {};
+const FailedAssertion = Class("FailedAssertion", Error, {
+ init: function (message) {
+ this.message = message;
+ },
+});
- function registerObserver(type, callback)
- {
- if (!(type in observers))
- observers[type] = [];
- observers[type].push(callback);
- }
+const Liberator = Module("liberator", {
+ requires: ["services"],
+
+ init: function () {
+ window.liberator = this;
+ window.liberator = this;
+ modules.liberator = this;
+ this.observers = {};
+ this.modules = modules;
- let nError = 0;
- function loadModule(name, func)
- {
- let message = "Loading module " + name + "...";
- try
- {
- liberator.log(message, 0);
- liberator.dump(message);
- modules[name] = func();
- liberator.triggerObserver("load_" + name, name);
+ // NOTE: services.get("profile").selectedProfile.name doesn't return
+ // what you might expect. It returns the last _actively_ selected
+ // profile (i.e. via the Profile Manager or -P option) rather than the
+ // current profile. These will differ if the current process was run
+ // without explicitly selecting a profile.
+ /** @property {string} The name of the current user profile. */
+ this.profileName = services.get("directory").get("ProfD", Ci.nsIFile).leafName.replace(/^.+?\./, "");
+ },
+
+ destroy: function () {
+ autocommands.trigger(config.name + "LeavePre", {});
+ storage.saveAll();
+ liberator.triggerObserver("shutdown", null);
+ liberator.dump("All liberator modules destroyed\n");
+ autocommands.trigger(config.name + "Leave", {});
+ },
+
+ /**
+ * @property {number} The current main mode.
+ * @see modes#mainModes
+ */
+ get mode() modes.main,
+ set mode(value) modes.main = value,
+
+ get menuItems() Liberator.getMenuItems(),
+
+ /** @property {Element} The currently focused element. */
+ get focus() document.commandDispatcher.focusedElement,
+
+ get extensions() {
+ const rdf = services.get("rdf");
+ const extensionManager = services.get("extensionManager");
+
+ let extensions = extensionManager.getItemList(Ci.nsIUpdateItem.TYPE_EXTENSION, {});
+
+ function getRdfProperty(item, property) {
+ let resource = rdf.GetResource("urn:mozilla:item:" + item.id);
+ let value = "";
+
+ if (resource) {
+ let target = extensionManager.datasource.GetTarget(resource,
+ rdf.GetResource("http://www.mozilla.org/2004/em-rdf#" + property), true);
+ if (target && target instanceof Ci.nsIRDFLiteral)
+ value = target.Value;
+ }
+
+ return value;
}
- catch (e)
- {
- if (nError++ == 0)
- toOpenWindowByType("global:console", "chrome://global/content/console.xul");
- liberator.reportError(e);
+
+ //const Extension = new Struct("id", "name", "description", "icon", "enabled", "version");
+ return extensions.map(function (e) ({
+ id: e.id,
+ name: e.name,
+ description: getRdfProperty(e, "description"),
+ enabled: getRdfProperty(e, "isDisabled") != "true",
+ icon: e.iconURL,
+ options: getRdfProperty(e, "optionsURL"),
+ version: e.version
+ }));
+ },
+
+ getExtension: function (name) this.extensions.filter(function (e) e.name == name)[0],
+
+ // Global constants
+ CURRENT_TAB: [],
+ NEW_TAB: [],
+ NEW_BACKGROUND_TAB: [],
+ NEW_WINDOW: [],
+
+ forceNewTab: false,
+ forceNewWindow: false,
+
+ /** @property {string} The Liberator version string. */
+ version: "###VERSION### (created: ###DATE###)", // these VERSION and DATE tokens are replaced by the Makefile
+
+ /**
+ * @property {Object} The map of command-line options. These are
+ * specified in the argument to the host application's -liberator
+ * option. E.g. $ firefox -liberator '+u=tempRcFile ++noplugin'
+ * Supported options:
+ * +u=RCFILE Use RCFILE instead of .vimperatorrc.
+ * ++noplugin Don't load plugins.
+ */
+ commandLineOptions: {
+ /** @property Whether plugin loading should be prevented. */
+ noPlugins: false,
+ /** @property An RC file to use rather than the default. */
+ rcFile: null,
+ /** @property An Ex command to run before any initialization is performed. */
+ preCommands: null,
+ /** @property An Ex command to run after all initialization has been performed. */
+ postCommands: null
+ },
+
+ registerObserver: function (type, callback) {
+ if (!(type in this.observers))
+ this.observers[type] = [];
+ this.observers[type].push(callback);
+ },
+
+ unregisterObserver: function (type, callback) {
+ if (type in this.observers)
+ this.observers[type] = this.observers[type].filter(function (c) c != callback);
+ },
+
+ // TODO: "zoom": if the zoom value of the current buffer changed
+ triggerObserver: function (type) {
+ let args = Array.slice(arguments, 1);
+ for (let [, func] in Iterator(this.observers[type] || []))
+ func.apply(null, args);
+ },
+
+ /**
+ * Triggers the application bell to notify the user of an error. The
+ * bell may be either audible or visual depending on the value of the
+ * 'visualbell' option.
+ */
+ beep: function () {
+ // FIXME: popups clear the command line
+ if (options["visualbell"]) {
+ // flash the visual bell
+ let popup = document.getElementById("liberator-visualbell");
+ let win = config.visualbellWindow;
+ let rect = win.getBoundingClientRect();
+ let width = rect.right - rect.left;
+ let height = rect.bottom - rect.top;
+
+ // NOTE: this doesn't seem to work in FF3 with full box dimensions
+ popup.openPopup(win, "overlap", 1, 1, false, false);
+ popup.sizeTo(width - 2, height - 2);
+ setTimeout(function () { popup.hidePopup(); }, 20);
+ }
+ else {
+ let soundService = Cc["@mozilla.org/sound;1"].getService(Ci.nsISound);
+ soundService.beep();
}
+ return false; // so you can do: if (...) return liberator.beep();
+ },
+
+ /**
+ * Creates a new thread.
+ */
+ newThread: function () services.get("threadManager").newThread(0),
+
+ /**
+ * Calls a function asynchronously on a new thread.
+ *
+ * @param {nsIThread} thread The thread to call the function on. If no
+ * thread is specified a new one is created.
+ * @optional
+ * @param {Object} self The 'this' object used when executing the
+ * function.
+ * @param {function} func The function to execute.
+ *
+ */
+ callAsync: function (thread, self, func) {
+ thread = thread || services.get("threadManager").newThread(0);
+ thread.dispatch(Runnable(self, func, Array.slice(arguments, 3)), thread.DISPATCH_NORMAL);
+ },
+
+ /**
+ * Calls a function synchronously on a new thread.
+ *
+ * NOTE: Be sure to call GUI related methods like alert() or dump()
+ * ONLY in the main thread.
+ *
+ * @param {nsIThread} thread The thread to call the function on. If no
+ * thread is specified a new one is created.
+ * @optional
+ * @param {function} func The function to execute.
+ */
+ callFunctionInThread: function (thread, func) {
+ thread = thread || services.get("threadManager").newThread(0);
+
+ // DISPATCH_SYNC is necessary, otherwise strange things will happen
+ thread.dispatch(Runnable(null, func, Array.slice(arguments, 2)), thread.DISPATCH_SYNC);
+ },
+
+ /**
+ * Prints a message to the console. If <b>msg</b> is an object it is
+ * pretty printed.
+ *
+ * NOTE: the "browser.dom.window.dump.enabled" preference needs to be
+ * set.
+ *
+ * @param {string|Object} msg The message to print.
+ */
+ dump: function () {
+ let msg = Array.map(arguments, function (msg) {
+ if (typeof msg == "object")
+ msg = util.objectToString(msg);
+ return msg;
+ }).join(", ");
+ msg = String.replace(msg, /\n?$/, "\n");
+ window.dump(msg.replace(/^./gm, ("config" in modules && config.name.toLowerCase()) + ": $&"));
+ },
+
+ /**
+ * Dumps a stack trace to the console.
+ *
+ * @param {string} msg The trace message.
+ * @param {number} frames The number of frames to print.
+ */
+ dumpStack: function (msg, frames) {
+ let stack = Error().stack.replace(/(?:.*\n){2}/, "");
+ if (frames != null)
+ [stack] = stack.match(RegExp("(?:.*\n){0," + frames + "}"));
+ liberator.dump((msg || "Stack") + "\n" + stack);
+ },
+
+ /**
+ * Outputs a plain message to the command line.
+ *
+ * @param {string} str The message to output.
+ * @param {number} flags These control the multiline message behaviour.
+ * See {@link CommandLine#echo}.
+ */
+ echo: function (str, flags) {
+ commandline.echo(str, commandline.HL_NORMAL, flags);
+ },
+
+ // TODO: Vim replaces unprintable characters in echoerr/echomsg
+ /**
+ * Outputs an error message to the command line.
+ *
+ * @param {string} str The message to output.
+ * @param {number} flags These control the multiline message behaviour.
+ * See {@link CommandLine#echo}.
+ */
+ echoerr: function (str, flags) {
+ flags |= commandline.APPEND_TO_MESSAGES;
+
+ if (typeof str == "object" && "echoerr" in str)
+ str = str.echoerr;
+ else if (str instanceof Error)
+ str = str.fileName + ":" + str.lineNumber + ": " + str;
+
+ if (options["errorbells"])
+ liberator.beep();
+
+ commandline.echo(str, commandline.HL_ERRORMSG, flags);
+ },
+
+ // TODO: add proper level constants
+ /**
+ * Outputs an information message to the command line.
+ *
+ * @param {string} str The message to output.
+ * @param {number} verbosity The messages log level (0 - 15). Only
+ * messages with verbosity less than or equal to the value of the
+ * 'verbosity' option will be output.
+ * @param {number} flags These control the multiline message behaviour.
+ * See {@link CommandLine#echo}.
+ */
+ echomsg: function (str, verbosity, flags) {
+ // TODO: is there a reason for this? --djk
+ // yes, it doesn't show the MOW on startup if you have e.g. some qmarks in your vimperatorrc.
+ // Feel free to add another flag like DONT_OPEN_MULTILINE if really needed --mst
+ //
+ // But it's _supposed_ to show the MOW on startup when there are
+ // messages, surely? As far as I'm concerned it essentially works
+ // exactly as it should with the DISALLOW_MULTILINE flag removed.
+ // Sending N messages to the command line in a row and having them
+ // overwrite each other is completely broken. I also think many of
+ // those messages like "Added quick mark" are plain silly but if
+ // you don't like them you can set verbose=0, or use :silent when
+ // someone adds it. I reckon another flag and 'class' of messages
+ // is just going to unnecessarily complicate things. --djk
+ flags |= commandline.APPEND_TO_MESSAGES | commandline.DISALLOW_MULTILINE;
+
+ if (verbosity == null)
+ verbosity = 0; // verbosity level is exclusionary
+
+ if (options["verbose"] >= verbosity)
+ commandline.echo(str, commandline.HL_INFOMSG, flags);
+ },
+
+ /**
+ * Loads and executes the script referenced by <b>uri</b> in the scope
+ * of the <b>context</b> object.
+ *
+ * @param {string} uri The URI of the script to load. Should be a local
+ * chrome:, file:, or resource: URL.
+ * @param {Object} context The context object into which the script
+ * should be loaded.
+ */
+ loadScript: function (uri, context) {
+ XML.ignoreWhiteSpace = false;
+ XML.prettyPrinting = false;
+ services.get("subscriptLoader").loadSubScript(uri, context);
+ },
+
+ eval: function (str, context) {
+ try {
+ if (!context)
+ context = userContext;
+ context[EVAL_ERROR] = null;
+ context[EVAL_STRING] = str;
+ context[EVAL_RESULT] = null;
+ this.loadScript("chrome://liberator/content/eval.js", context);
+ if (context[EVAL_ERROR]) {
+ try {
+ context[EVAL_ERROR].fileName = io.sourcing.file;
+ context[EVAL_ERROR].lineNumber += io.sourcing.line;
+ }
+ catch (e) {}
+ throw context[EVAL_ERROR];
+ }
+ return context[EVAL_RESULT];
+ }
+ finally {
+ delete context[EVAL_ERROR];
+ delete context[EVAL_RESULT];
+ delete context[EVAL_STRING];
+ }
+ },
+
+ // partial sixth level expression evaluation
+ // TODO: what is that really needed for, and where could it be used?
+ // Or should it be removed? (c) Viktor
+ // Better name? See other liberator.eval()
+ // I agree, the name is confusing, and so is the
+ // description --Kris
+ evalExpression: function (string) {
+ string = string.toString().replace(/^\s*/, "").replace(/\s*$/, "");
+ let matches = string.match(/^&(\w+)/);
+
+ if (matches) {
+ let opt = this.options.get(matches[1]);
+
+ if (!opt)
+ return void this.echoerr("E113: Unknown option: " + matches[1]);
+
+ let type = opt.type;
+ let value = opt.getter();
+
+ if (type != "boolean" && type != "number")
+ value = value.toString();
+
+ return value;
+ }
+ // String
+ else if (matches = string.match(/^(['"])([^\1]*?[^\\]?)\1/)) {
+ if (matches)
+ return matches[2].toString();
+ else
+ return void this.echoerr("E115: Missing quote: " + string);
+ }
+ // Number
+ else if (matches = string.match(/^(\d+)$/))
+ return parseInt(matches[1], 10);
+
+ let reference = this.variableReference(string);
+
+ if (!reference[0])
+ this.echoerr("E121: Undefined variable: " + string);
+ else
+ return reference[0][reference[1]];
+
+ return;
+ },
+
+ /**
+ * Execute an Ex command string. E.g. ":zoom 300".
+ *
+ * @param {string} str The command to execute.
+ * @param {Object} modifiers Any modifiers to be passed to
+ * {@link Command#action}.
+ * @param {boolean} silent Whether the command should be echoed on the
+ * command line.
+ */
+ execute: function (str, modifiers, silent) {
+ // skip comments and blank lines
+ if (/^\s*("|$)/.test(str))
+ return;
+
+ modifiers = modifiers || {};
+
+ let err = null;
+ let [count, cmd, special, args] = commands.parseCommand(str.replace(/^'(.*)'$/, "$1"));
+ let command = commands.get(cmd);
+
+ if (command === null) {
+ err = "E492: Not a " + config.name.toLowerCase() + " command: " + str;
+ liberator.focusContent();
+ }
+ else if (command.action === null)
+ err = "E666: Internal error: command.action === null"; // TODO: need to perform this test? -- djk
+ else if (count != -1 && !command.count)
+ err = "E481: No range allowed";
+ else if (special && !command.bang)
+ err = "E477: No ! allowed";
+
+ liberator.assert(!err, err);
+ if (!silent)
+ commandline.command = str.replace(/^\s*:\s*/, "");
+
+ command.execute(args, special, count, modifiers);
+ },
+
+ /**
+ * Focuses the content window.
+ *
+ * @param {boolean} clearFocusedElement Remove focus from any focused
+ * element.
+ */
+ focusContent: function (clearFocusedElement) {
+ if (window != services.get("windowWatcher").activeWindow)
+ return;
+
+ let elem = config.mainWidget || window.content;
+ // TODO: make more generic
+ try {
+ if (this.has("mail") && !config.isComposeWindow) {
+ let i = gDBView.selection.currentIndex;
+ if (i == -1 && gDBView.rowCount >= 0)
+ i = 0;
+ gDBView.selection.select(i);
+ }
+ else if (this.has("tabs")) {
+ let frame = tabs.localStore.focusedFrame;
+ if (frame && frame.top == window.content)
+ elem = frame;
+ }
+ }
+ catch (e) {}
+
+ if (clearFocusedElement && liberator.focus)
+ liberator.focus.blur();
+ if (elem && elem != liberator.focus)
+ elem.focus();
+ },
+
+ /**
+ * Returns whether this Liberator extension supports <b>feature</b>.
+ *
+ * @param {string} feature The feature name.
+ * @returns {boolean}
+ */
+ has: function (feature) config.features.indexOf(feature) >= 0,
+
+ /**
+ * Returns whether the host application has the specified extension
+ * installed.
+ *
+ * @param {string} name The extension name.
+ * @returns {boolean}
+ */
+ hasExtension: function (name) {
+ let extensions = services.get("extensionManager").getItemList(Ci.nsIUpdateItem.TYPE_EXTENSION, {});
+ return extensions.some(function (e) e.name == name);
+ },
+
+ /**
+ * Returns the URL of the specified help <b>topic</b> if it exists.
+ *
+ * @param {string} topic The help topic to lookup.
+ * @param {boolean} unchunked Whether to search the unchunked help page.
+ * @returns {string}
+ */
+ findHelp: function (topic, unchunked) {
+ if (topic in services.get("liberator:").FILE_MAP)
+ return topic;
+ unchunked = !!unchunked;
+ let items = completion._runCompleter("help", topic, null, unchunked).items;
+ let partialMatch = null;
+
+ function format(item) item.description + "#" + encodeURIComponent(item.text);
+
+ for (let [i, item] in Iterator(items)) {
+ if (item.text == topic)
+ return format(item);
+ else if (!partialMatch && topic)
+ partialMatch = item;
+ }
+
+ if (partialMatch)
+ return format(partialMatch);
+ return null;
+ },
+
+ /**
+ * @private
+ * Initialize the help system.
+ */
+ initHelp: function () {
+ let namespaces = [config.name.toLowerCase(), "liberator"];
+ services.get("liberator:").init({});
+ let tagMap = services.get("liberator:").HELP_TAGS;
+ let fileMap = services.get("liberator:").FILE_MAP;
+ let overlayMap = services.get("liberator:").OVERLAY_MAP;
+ function XSLTProcessor(sheet) {
+ let xslt = Cc["@mozilla.org/document-transformer;1?type=xslt"].createInstance(Ci.nsIXSLTProcessor);
+ xslt.importStylesheet(util.httpGet(sheet).responseXML);
+ return xslt;
+ }
+
+ function findHelpFile(file) {
+ let result = [];
+ for (let [, namespace] in Iterator(namespaces)) {
+ let url = ["chrome://", namespace, "/locale/", file, ".xml"].join("");
+ let res = util.httpGet(url);
+ if (res) {
+ if (res.responseXML.documentElement.localName == "document")
+ fileMap[file] = url;
+ if (res.responseXML.documentElement.localName == "overlay")
+ overlayMap[file] = url;
+ result.push(res.responseXML);
+ }
+ }
+ return result;
+ }
+ function addTags(file, doc) {
+ doc = XSLT.transformToDocument(doc);
+ for (let elem in util.evaluateXPath("//xhtml:a/@id", doc))
+ tagMap[elem.value] = file;
+ }
+
+ const XSLT = XSLTProcessor("chrome://liberator/content/help.xsl");
+
+ tagMap.all = "all";
+ let files = findHelpFile("all").map(function (doc)
+ [f.value for (f in util.evaluateXPath(
+ "//liberator:include/@href", doc))]);
+
+ util.Array.flatten(files).forEach(function (file) {
+ findHelpFile(file).forEach(function (doc) {
+ addTags(file, doc);
+ });
+ });
+
+ XML.ignoreWhiteSpace = false;
+ XML.prettyPrinting = false;
+ XML.prettyPrinting = true; // Should be false, but ignoreWhiteSpace=false doesn't work correctly. This is the lesser evil.
+ XML.prettyIndent = 4;
+ let body = XML();
+ for (let [, context] in Iterator(plugins.contexts))
+ if (context.INFO instanceof XML)
+ body += <h2 xmlns={NS.uri} tag={context.INFO.@name + '-plugin'}>{context.INFO.@summary}</h2> +
+ context.INFO;
+
+ let help = '<?xml version="1.0"?>\n' +
+ '<?xml-stylesheet type="text/xsl" href="chrome://liberator/content/help.xsl"?>\n' +
+ '<!DOCTYPE document SYSTEM "chrome://liberator/content/liberator.dtd">' +
+ <document
+ name="plugins"
+ title={config.name + " Plugins"}
+ xmlns={NS}>
+ <h1 tag="using-plugins">Using Plugins</h1>
+
+ {body}
+ </document>.toXMLString();
+ fileMap["plugins"] = function () ['text/xml;charset=UTF-8', help];
+
+ addTags("plugins", util.httpGet("liberator://help/plugins").responseXML);
+ },
+
+ /**
+ * Opens the help page containing the specified <b>topic</b> if it
+ * exists.
+ *
+ * @param {string} topic The help topic to open.
+ * @param {boolean} unchunked Whether to use the unchunked help page.
+ * @returns {string}
+ */
+ help: function (topic, unchunked) {
+ if (!topic && !unchunked) {
+ let helpFile = options["helpfile"];
+ if (helpFile in services.get("liberator:").FILE_MAP)
+ liberator.open("liberator://help/" + helpFile, { from: "help" });
+ else
+ liberator.echomsg("Sorry, help file " + helpFile.quote() + " not found");
+ return;
+ }
+
+ let page = this.findHelp(topic, unchunked);
+ liberator.assert(page != null, "E149: Sorry, no help for " + topic);
+
+ liberator.open("liberator://help/" + page, { from: "help" });
+ if (options.get("activate").has("all", "help"))
+ content.postMessage("fragmentChange", "*");
+ },
+
+ /**
+ * The map of global variables.
+ *
+ * These are set and accessed with the "g:" prefix.
+ */
+ globalVariables: {},
+
+ loadPlugins: function () {
+ function sourceDirectory(dir) {
+ liberator.assert(dir.isReadable(), "E484: Can't open file " + dir.path);
+
+ liberator.log("Sourcing plugin directory: " + dir.path + "...", 3);
+ dir.readDirectory(true).forEach(function (file) {
+ if (file.isFile() && /\.(js|vimp)$/i.test(file.path) && !(file.path in liberator.pluginFiles)) {
+ try {
+ io.source(file.path, false);
+ liberator.pluginFiles[file.path] = true;
+ }
+ catch (e) {
+ liberator.reportError(e);
+ }
+ }
+ else if (file.isDirectory())
+ sourceDirectory(file);
+ });
+ }
+
+ let dirs = io.getRuntimeDirectories("plugin");
+
+ if (dirs.length == 0) {
+ liberator.log("No user plugin directory found", 3);
+ return;
+ }
+
+ liberator.echomsg('Searching for "plugin/**/*.{js,vimp}" in "'
+ + [dir.path.replace(/.plugin$/, "") for ([, dir] in Iterator(dirs))].join(",") + '"', 2);
+
+ dirs.forEach(function (dir) {
+ liberator.echomsg("Searching for \"" + (dir.path + "/**/*.{js,vimp}") + "\"", 3);
+ sourceDirectory(dir);
+ });
+ },
+
+ // TODO: add proper level constants
+ /**
+ * Logs a message to the JavaScript error console. Each message has an
+ * associated log level. Only messages with a log level less than or
+ * equal to <b>level</b> will be printed. If <b>msg</b> is an object,
+ * it is pretty printed.
+ *
+ * @param {string|Object} msg The message to print.
+ * @param {number} level The logging level 0 - 15.
+ */
+ log: function (msg, level) {
+ let verbose = 0;
+ if (level == undefined)
+ level = 1;
+
+ // options does not exist at the very beginning
+ if (modules.options)
+ verbose = options.getPref("extensions.liberator.loglevel", 0);
+
+ if (level > verbose)
+ return;
+
+ if (typeof msg == "object")
+ msg = util.objectToString(msg, false);
+
+ services.get("console").logStringMessage(config.name.toLowerCase() + ": " + msg);
+ },
+
+ /**
+ * Opens one or more URLs. Returns true when load was initiated, or
+ * false on error.
+ *
+ * @param {string|string[]} urls Either a URL string or an array of URLs.
+ * The array can look like this:
+ * ["url1", "url2", "url3", ...]
+ * or:
+ * [["url1", postdata1], ["url2", postdata2], ...]
+ * @param {number|Object} where If ommited, CURRENT_TAB is assumed but NEW_TAB
+ * is set when liberator.forceNewTab is true.
+ * @param {boolean} force Don't prompt whether to open more than 20
+ * tabs.
+ * @returns {boolean}
+ */
+ open: function (urls, params, force) {
+ // convert the string to an array of converted URLs
+ // -> see util.stringToURLArray for more details
+ if (typeof urls == "string") {
+ // rather switch to the tab instead of opening a new url in case of "12: Tab Title" like "urls"
+ if (liberator.has("tabs")) {
+ let matches = urls.match(/^(\d+):/);
+ if (matches) {
+ tabs.select(parseInt(matches[1], 10) - 1, false); // make it zero-based
+ return;
+ }
+ }
+
+ urls = util.stringToURLArray(urls);
+ }
+
+ if (urls.length > 20 && !force) {
+ commandline.input("This will open " + urls.length + " new tabs. Would you like to continue? (yes/[no]) ",
+ function (resp) {
+ if (resp && resp.match(/^y(es)?$/i))
+ liberator.open(urls, params, true);
+ });
+ return true;
+ }
+
+ let flags = 0;
+ params = params || {};
+ if (params instanceof Array)
+ params = { where: params };
+
+ for (let [opt, flag] in Iterator({ replace: "REPLACE_HISTORY", hide: "BYPASS_HISTORY" }))
+ if (params[opt])
+ flags |= Ci.nsIWebNavigation["LOAD_FLAGS_" + flag];
+
+ let where = params.where || liberator.CURRENT_TAB;
+ if ("from" in params && liberator.has("tabs")) {
+ if (!('where' in params) && options.get("newtab").has("all", params.from))
+ where = liberator.NEW_BACKGROUND_TAB;
+ if (options.get("activate").has("all", params.from)) {
+ if (where == liberator.NEW_TAB)
+ where = liberator.NEW_BACKGROUND_TAB;
+ else if (where == liberator.NEW_BACKGROUND_TAB)
+ where = liberator.NEW_TAB;
+ }
+ }
+
+ if (urls.length == 0)
+ return false;
+
+ let browser = window.getBrowser();
+
+ function open(urls, where) {
+ let url = Array.concat(urls)[0];
+ let postdata = Array.concat(urls)[1];
+
+ // decide where to load the first url
+ switch (where) {
+ case liberator.CURRENT_TAB:
+ browser.loadURIWithFlags(url, flags, null, null, postdata);
+ break;
+
+ case liberator.NEW_BACKGROUND_TAB:
+ case liberator.NEW_TAB:
+ if (!liberator.has("tabs"))
+ return open(urls, liberator.NEW_WINDOW);
+
+ options.withContext(function () {
+ options.setPref("browser.tabs.loadInBackground", true);
+ browser.loadOneTab(url, null, null, postdata, where == liberator.NEW_BACKGROUND_TAB);
+ });
+ break;
+
+ case liberator.NEW_WINDOW:
+ window.open();
+ let win = services.get("windowMediator").getMostRecentWindow("navigator:browser");
+ win.loadURI(url, null, postdata);
+ browser = win.getBrowser();
+ break;
+
+ default:
+ throw Error("Invalid 'where' directive in liberator.open(...)");
+ }
+ }
+
+ if (liberator.forceNewTab)
+ where = liberator.NEW_TAB;
+ else if (liberator.forceNewWindow)
+ where = liberator.NEW_WINDOW;
+ else if (!where)
+ where = liberator.CURRENT_TAB;
+
+ for (let [, url] in Iterator(urls)) {
+ open(url, where);
+ where = liberator.NEW_BACKGROUND_TAB;
+ }
+
+ return true;
+ },
+
+ pluginFiles: {},
+
+ // namespace for plugins/scripts. Actually (only) the active plugin must/can set a
+ // v.plugins.mode = <str> string to show on v.modes.CUSTOM
+ // v.plugins.stop = <func> hooked on a v.modes.reset()
+ // v.plugins.onEvent = <func> function triggered, on keypresses (unless <esc>) (see events.js)
+ plugins: plugins,
+
+ /**
+ * Quit the host application, no matter how many tabs/windows are open.
+ *
+ * @param {boolean} saveSession If true the current session will be
+ * saved and restored when the host application is restarted.
+ * @param {boolean} force Forcibly quit irrespective of whether all
+ * windows could be closed individually.
+ */
+ quit: function (saveSession, force) {
+ // TODO: Use safeSetPref?
+ if (saveSession)
+ options.setPref("browser.startup.page", 3); // start with saved session
+ else
+ options.setPref("browser.startup.page", 1); // start with default homepage session
+
+ if (force)
+ services.get("appStartup").quit(Ci.nsIAppStartup.eForceQuit);
+ else
+ window.goQuitApplication();
+ },
+
+ /*
+ * Tests a condition and throws a FailedAssertion error on
+ * failure.
+ *
+ * @param {boolean} condition The condition to test.
+ * @param {string} message The message to present to the
+ * user on failure.
+ */
+ assert: function (condition, message) {
+ if (!condition)
+ throw new FailedAssertion(message);
+ },
+
+ /**
+ * Traps errors in the called function, possibly reporting them.
+ *
+ * @param {function} func The function to call
+ * @param {object} self The 'this' object for the function.
+ */
+ trapErrors: function (func, self) {
+ try {
+ return func.apply(self || this, Array.slice(arguments, 2));
+ }
+ catch (e) {
+ if (e instanceof FailedAssertion)
+ liberator.echoerr(e.message);
+ }
+ },
+
+ /**
+ * Reports an error to both the console and the host application's
+ * Error Console.
+ *
+ * @param {Object} error The error object.
+ */
+ reportError: function (error) {
+ if (Cu.reportError)
+ Cu.reportError(error);
+
+ try {
+ let obj = {
+ toString: function () String(error),
+ stack: <>{String.replace(error.stack || Error().stack, /^/mg, "\t")}</>
+ };
+ for (let [k, v] in Iterator(error)) {
+ if (!(k in obj))
+ obj[k] = v;
+ }
+ if (liberator.storeErrors) {
+ let errors = storage.newArray("errors", { store: false });
+ errors.toString = function () [String(v[0]) + "\n" + v[1] for ([k, v] in this)].join("\n\n");
+ errors.push([new Date, obj + obj.stack]);
+ }
+ liberator.dump(String(error));
+ liberator.dump(obj);
+ liberator.dump("");
+ }
+ catch (e) { window.dump(e) }
+ },
+
+ /**
+ * Restart the host application.
+ */
+ restart: function () {
+ // notify all windows that an application quit has been requested.
+ var cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
+ services.get("observer").notifyObservers(cancelQuit, "quit-application-requested", null);
+
+ // something aborted the quit process.
+ if (cancelQuit.data)
+ return;
+
+ // notify all windows that an application quit has been granted.
+ services.get("observer").notifyObservers(null, "quit-application-granted", null);
+
+ // enumerate all windows and call shutdown handlers
+ let windows = services.get("windowMediator").getEnumerator(null);
+ while (windows.hasMoreElements()) {
+ let win = windows.getNext();
+ if (("tryToClose" in win) && !win.tryToClose())
+ return;
+ }
+ services.get("appStartup").quit(Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit);
+ },
+
+ /**
+ * Parses a Liberator command-line string i.e. the value of the
+ * -liberator command-line option.
+ *
+ * @param {string} cmdline The string to parse for command-line
+ * options.
+ * @returns {Object}
+ * @see Commands#parseArgs
+ */
+ parseCommandLine: function (cmdline) {
+ const options = [
+ [["+u"], commands.OPTIONS_STRING],
+ [["++noplugin"], commands.OPTIONS_NOARG],
+ [["++cmd"], commands.OPTIONS_STRING, null, null, true],
+ [["+c"], commands.OPTIONS_STRING, null, null, true]
+ ];
+ return commands.parseArgs(cmdline, options, "*");
+ },
+
+ sleep: function (delay) {
+ let mainThread = services.get("threadManager").mainThread;
+
+ let end = Date.now() + delay;
+ while (Date.now() < end)
+ mainThread.processNextEvent(true);
+ return true;
+ },
+
+ callInMainThread: function (callback, self) {
+ let mainThread = services.get("threadManager").mainThread;
+ if (!services.get("threadManager").isMainThread)
+ mainThread.dispatch({ run: callback.call(self) }, mainThread.DISPATCH_NORMAL);
+ else
+ callback.call(self);
+ },
+
+ threadYield: function (flush, interruptable) {
+ let mainThread = services.get("threadManager").mainThread;
+ liberator.interrupted = false;
+ do {
+ mainThread.processNextEvent(!flush);
+ if (liberator.interrupted)
+ throw new Error("Interrupted");
+ }
+ while (flush === true && mainThread.hasPendingEvents());
+ },
+
+ variableReference: function (string) {
+ if (!string)
+ return [null, null, null];
+
+ let matches = string.match(/^([bwtglsv]):(\w+)/);
+ if (matches) { // Variable
+ // Other variables should be implemented
+ if (matches[1] == "g") {
+ if (matches[2] in this.globalVariables)
+ return [this.globalVariables, matches[2], matches[1]];
+ else
+ return [null, matches[2], matches[1]];
+ }
+ }
+ else { // Global variable
+ if (string in this.globalVariables)
+ return [this.globalVariables, string, "g"];
+ else
+ return [null, string, "g"];
+ }
+ },
+
+ /**
+ * @property {Window[]} Returns an array of all the host application's
+ * open windows.
+ */
+ get windows() {
+ let windows = [];
+ let enumerator = services.get("windowMediator").getEnumerator("navigator:browser");
+ while (enumerator.hasMoreElements())
+ windows.push(enumerator.getNext());
+
+ return windows;
}
+}, {
// initially hide all GUI elements, they are later restored unless the user
// has :set go= or something similar in his config
- function hideGUI()
- {
+ hideGUI: function () {
let guioptions = config.guioptions;
- for (let option in guioptions)
- {
+ for (let option in guioptions) {
guioptions[option].forEach(function (elem) {
- try
- {
+ try {
document.getElementById(elem).collapsed = true;
}
catch (e) {}
});
}
- }
+ },
// return the platform normalized to Vim values
- function getPlatformFeature()
- {
+ getPlatformFeature: function () {
let platform = navigator.platform;
return /^Mac/.test(platform) ? "MacUnix" : platform == "Win32" ? "Win32" : "Unix";
- }
+ },
// TODO: move this
- function getMenuItems()
- {
- function addChildren(node, parent)
- {
- for (let [, item] in Iterator(node.childNodes))
- {
+ getMenuItems: function () {
+ function addChildren(node, parent) {
+ for (let [, item] in Iterator(node.childNodes)) {
if (item.childNodes.length == 0 && item.localName == "menuitem"
- && !/rdf:http:/.test(item.getAttribute("label"))) // FIXME
- {
+ && !/rdf:http:/.test(item.getAttribute("label"))) { // FIXME
item.fullMenuPath = parent + item.getAttribute("label");
items.push(item);
}
- else
- {
+ else {
let path = parent;
if (item.localName == "menu")
path += item.getAttribute("label") + ".";
@@ -117,24 +1053,19 @@ const liberator = (function () //{{{
let items = [];
addChildren(document.getElementById(config.guioptions["m"][1]), "");
return items;
- }
+ },
// show a usage index either in the MOW or as a full help page
- function showHelpIndex(tag, items, inMow)
- {
+ showHelpIndex: function (tag, items, inMow) {
if (inMow)
liberator.echo(template.usage(items), commandline.FORCE_MULTILINE);
else
liberator.help(tag);
- }
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// OPTIONS /////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
+ },
+}, {
// Only general options are added here, which are valid for all Liberator extensions
- registerObserver("load_options", function () {
-
+ options: function () {
options.add(["errorbells", "eb"],
"Ring the bell when an error message is displayed",
"boolean", false);
@@ -146,13 +1077,10 @@ const liberator = (function () //{{{
const groups = {
config: {
opts: config.guioptions,
- setter: function (opts)
- {
- for (let [opt, [, ids]] in Iterator(this.opts))
- {
+ setter: function (opts) {
+ for (let [opt, [, ids]] in Iterator(this.opts)) {
ids.map(function (id) document.getElementById(id))
- .forEach(function (elem)
- {
+ .forEach(function (elem) {
if (elem)
elem.collapsed = (opts.indexOf(opt) == -1);
});
@@ -160,10 +1088,16 @@ const liberator = (function () //{{{
}
},
scroll: {
- opts: { r: ["Right Scrollbar", "vertical"], l: ["Left Scrollbar", "vertical"], b: ["Bottom Scrollbar", "horizontal"] },
- setter: function (opts)
- {
- let dir = ["horizontal", "vertical"].filter(function (dir) !Array.some(opts, function (o) this.opts[o] && this.opts[o][1] == dir, this), this);
+ opts: {
+ r: ["Right Scrollbar", "vertical"],
+ l: ["Left Scrollbar", "vertical"],
+ b: ["Bottom Scrollbar", "horizontal"]
+ },
+ setter: function (opts) {
+ let dir = ["horizontal", "vertical"].filter(
+ function (dir) !Array.some(opts,
+ function (o) this.opts[o] && this.opts[o][1] == dir, this),
+ this);
let class = dir.map(function (dir) "html|html > xul|scrollbar[orient=" + dir + "]");
if (class.length)
@@ -180,8 +1114,7 @@ const liberator = (function () //{{{
n: ["Tab number", highlight.selector("TabNumber")],
N: ["Tab number over icon", highlight.selector("TabIconNumber")]
},
- setter: function (opts)
- {
+ setter: function (opts) {
const self = this;
let classes = [v[1] for ([k, v] in Iterator(this.opts)) if (opts.indexOf(k) < 0)];
let css = classes.length ? classes.join(",") + "{ display: none; }" : "";
@@ -194,24 +1127,20 @@ const liberator = (function () //{{{
options.add(["fullscreen", "fs"],
"Show the current window fullscreen",
- "boolean", false,
- {
+ "boolean", false, {
setter: function (value) window.fullScreen = value,
getter: function () window.fullScreen
});
options.add(["guioptions", "go"],
"Show or hide certain GUI elements like the menu or toolbar",
- "charlist", config.defaults.guioptions || "",
- {
- setter: function (value)
- {
+ "charlist", config.defaults.guioptions || "", {
+ setter: function (value) {
for (let [, group] in Iterator(groups))
group.setter(value);
return value;
},
- completer: function (context)
- {
+ completer: function (context) {
let opts = [v.opts for ([k, v] in Iterator(groups))];
opts = opts.map(function (opt) [[k, v[0]] for ([k, v] in Iterator(opt))]);
return util.Array.flatten(opts);
@@ -232,26 +1161,22 @@ const liberator = (function () //{{{
"Change the title of the window",
"string", config.defaults.titlestring || config.hostApplication,
{
- setter: function (value)
- {
+ setter: function (value) {
let win = document.documentElement;
- function updateTitle(old, current)
- {
+ function updateTitle(old, current) {
document.title = document.title.replace(RegExp("(.*)" + util.escapeRegex(old)), "$1" + current);
}
// TODO: remove this FF3.5 test when we no longer support 3.0
// : make this a config feature
- if (services.get("privateBrowsing"))
- {
+ if (services.get("privateBrowsing")) {
let oldValue = win.getAttribute("titlemodifier_normal");
let suffix = win.getAttribute("titlemodifier_privatebrowsing").substr(oldValue.length);
win.setAttribute("titlemodifier_normal", value);
win.setAttribute("titlemodifier_privatebrowsing", value + suffix);
- if (services.get("privateBrowsing").privateBrowsingEnabled)
- {
+ if (services.get("privateBrowsing").privateBrowsingEnabled) {
updateTitle(oldValue + suffix, value + suffix);
return value;
}
@@ -273,27 +1198,20 @@ const liberator = (function () //{{{
"Use visual bell instead of beeping on errors",
"boolean", false,
{
- setter: function (value)
- {
+ setter: function (value) {
options.safeSetPref("accessibility.typeaheadfind.enablesound", !value,
"See 'visualbell' option");
return value;
}
});
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// MAPPINGS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- registerObserver("load_mappings", function () {
+ },
+ mappings: function () {
mappings.add(modes.all, ["<F1>"],
"Open the help page",
function () { liberator.help(); });
- if (liberator.has("session"))
- {
+ if (liberator.has("session")) {
mappings.add([modes.NORMAL], ["ZQ"],
"Quit and don't save the session",
function () { liberator.quit(false); });
@@ -302,20 +1220,12 @@ const liberator = (function () //{{{
mappings.add([modes.NORMAL], ["ZZ"],
"Quit and save the session",
function () { liberator.quit(true); });
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMMANDS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- var toolbox;
-
- registerObserver("load_commands", function () {
+ },
+ commands: function () {
commands.add(["addo[ns]"],
"Manage available Extensions and Themes",
- function ()
- {
+ function () {
liberator.open("chrome://mozapps/content/extensions/extensions.xul",
{ from: "addons" });
},
@@ -328,19 +1238,15 @@ const liberator = (function () //{{{
commands.add(["dia[log]"],
"Open a " + config.name + " dialog",
- function (args)
- {
+ function (args) {
let arg = args[0];
- try
- {
+ try {
// TODO: why are these sorts of properties arrays? --djk
let dialogs = config.dialogs;
- for (let [, dialog] in Iterator(dialogs))
- {
- if (util.compareIgnoreCase(arg, dialog[0]) == 0)
- {
+ for (let [, dialog] in Iterator(dialogs)) {
+ if (util.compareIgnoreCase(arg, dialog[0]) == 0) {
dialog[2]();
return;
}
@@ -348,16 +1254,13 @@ const liberator = (function () //{{{
liberator.echoerr("E475: Invalid argument: " + arg);
}
- catch (e)
- {
+ catch (e) {
liberator.echoerr("Error opening '" + arg + "': " + e);
}
- },
- {
+ }, {
argCount: "1",
bang: true,
- completer: function (context)
- {
+ completer: function (context) {
context.ignoreCase = true;
return completion.dialog(context);
}
@@ -365,21 +1268,18 @@ const liberator = (function () //{{{
commands.add(["em[enu]"],
"Execute the specified menu item from the command line",
- function (args)
- {
+ function (args) {
let arg = args.literalArg;
- let items = getMenuItems();
+ let items = Liberator.getMenuItems();
liberator.assert(items.some(function (i) i.fullMenuPath == arg),
"E334: Menu not found: " + arg);
- for (let [, item] in Iterator(items))
- {
+ for (let [, item] in Iterator(items)) {
if (item.fullMenuPath == arg)
item.doCommand();
}
- },
- {
+ }, {
argCount: "1",
completer: function (context) completion.menuItem(context),
literal: 0
@@ -392,36 +1292,30 @@ const liberator = (function () //{{{
// E.g. :execute "source" io.getRCFile().path
// Need to fix commands.parseArgs which currently strips the quotes
// from quoted args
- function (args)
- {
- try
- {
+ function (args) {
+ try {
let cmd = liberator.eval(args.string);
liberator.execute(cmd, null, true);
}
- catch (e)
- {
+ catch (e) {
liberator.echoerr(e);
}
});
commands.add(["exta[dd]"],
"Install an extension",
- function (args)
- {
+ function (args) {
let file = io.File(args[0]);
if (file.exists() && file.isReadable() && file.isFile())
services.get("extensionManager").installItemFromFile(file, "app-profile");
- else
- {
+ else {
if (file.exists() && file.isDirectory())
liberator.echomsg("Cannot install a directory: \"" + file.path + "\"", 0);
liberator.echoerr("E484: Can't open file " + file.path);
}
- },
- {
+ }, {
argCount: "1",
completer: function (context) {
context.filters.push(function ({ item: f }) f.isDirectory() || /\.xpi$/.test(f.leafName));
@@ -451,15 +1345,13 @@ const liberator = (function () //{{{
].forEach(function (command) {
commands.add([command.name],
command.description,
- function (args)
- {
+ function (args) {
let name = args[0];
function action(e) { services.get("extensionManager")[command.action](e.id); };
if (args.bang)
liberator.extensions.forEach(function (e) { action(e); });
- else
- {
+ else {
liberator.assert(name, "E471: Argument required"); // XXX
let extension = liberator.getExtension(name);
@@ -468,12 +1360,10 @@ const liberator = (function () //{{{
else
liberator.echoerr("E474: Invalid argument");
}
- },
- {
+ }, {
argCount: "?", // FIXME: should be "1"
bang: true,
- completer: function (context)
- {
+ completer: function (context) {
completion.extension(context);
if (command.filter)
context.filters.push(command.filter);
@@ -484,8 +1374,7 @@ const liberator = (function () //{{{
commands.add(["exto[ptions]", "extp[references]"],
"Open an extension's preference dialog",
- function (args)
- {
+ function (args) {
let extension = liberator.getExtension(args[0]);
liberator.assert(extension && extension.options,
"E474: Invalid argument");
@@ -493,12 +1382,10 @@ const liberator = (function () //{{{
window.openDialog(extension.options, "_blank", "chrome");
else
liberator.open(extension.options, { from: "extoptions" });
- },
- {
+ }, {
argCount: "1",
bang: true,
- completer: function (context)
- {
+ completer: function (context) {
completion.extension(context);
context.filters.push(function ({ item: e }) e.options);
},
@@ -508,13 +1395,11 @@ const liberator = (function () //{{{
// TODO: maybe indicate pending status too?
commands.add(["extens[ions]"],
"List available extensions",
- function (args)
- {
+ function (args) {
let filter = args[0] || "";
let extensions = liberator.extensions.filter(function (e) e.name.indexOf(filter) >= 0);
- if (extensions.length > 0)
- {
+ if (extensions.length > 0) {
let list = template.tabular(
["Name", "Version", "Status", "Description"], [],
([template.icon(e, e.name),
@@ -526,8 +1411,7 @@ const liberator = (function () //{{{
commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
}
- else
- {
+ else {
if (filter)
liberator.echoerr("Exxx: No extension matching \"" + filter + "\"");
else
@@ -538,8 +1422,7 @@ const liberator = (function () //{{{
commands.add(["exu[sage]"],
"List all Ex commands with a short description",
- function (args) { showHelpIndex("ex-cmd-index", commands, args.bang); },
- {
+ function (args) { Liberator.showHelpIndex("ex-cmd-index", commands, args.bang); }, {
argCount: "0",
bang: true
});
@@ -548,8 +1431,7 @@ const liberator = (function () //{{{
{
name: "h[elp]",
description: "Open the help page"
- },
- {
+ }, {
name: "helpa[ll]",
description: "Open the single unchunked help page"
}
@@ -558,13 +1440,11 @@ const liberator = (function () //{{{
commands.add([command.name],
command.description,
- function (args)
- {
+ function (args) {
liberator.assert(!args.bang, "E478: Don't panic!");
liberator.help(args.literalArg, unchunked);
- },
- {
+ }, {
argCount: "?",
bang: true,
completer: function (context) completion.help(context, unchunked),
@@ -572,45 +1452,22 @@ const liberator = (function () //{{{
});
});
- commands.add(["exporth[elp]"],
- "Exports " + config.name + "'s help system to the named zip file",
- function (args)
- {
- liberator.echomsg("Exporting help to " + args[0].quote() + ". Please wait...");
- util.exportHelp(args[0]);
- liberator.echomsg("Help exported to " + args[0].quote() + ".");
- },
- {
- argCount: "1",
- completer: function (context) {
- context.filters.push(function ({ item: f }) f.isDirectory() || /\.zip/.test(f.leafName));
- completion.file(context);
- },
- literal: 0
- });
-
commands.add(["javas[cript]", "js"],
"Run a JavaScript command through eval()",
- function (args)
- {
- if (args.bang) // open JavaScript console
- {
+ function (args) {
+ if (args.bang) { // open JavaScript console
liberator.open("chrome://global/content/console.xul",
{ from: "javascript" });
}
- else
- {
- try
- {
+ else {
+ try {
liberator.eval(args.string);
}
- catch (e)
- {
+ catch (e) {
liberator.echoerr(e);
}
}
- },
- {
+ }, {
bang: true,
completer: function (context) completion.javascript(context),
hereDoc: true,
@@ -632,22 +1489,19 @@ const liberator = (function () //{{{
commands.add(["optionu[sage]"],
"List all options with a short description",
- function (args) { showHelpIndex("option-index", options, args.bang); },
- {
+ function (args) { Liberator.showHelpIndex("option-index", options, args.bang); }, {
argCount: "0",
bang: true
});
commands.add(["q[uit]"],
liberator.has("tabs") ? "Quit current tab" : "Quit application",
- function (args)
- {
+ function (args) {
if (liberator.has("tabs"))
tabs.remove(getBrowser().mCurrentTab, 1, false, 1);
else
liberator.quit(false, args.bang);
- },
- {
+ }, {
argCount: "0",
bang: true
});
@@ -657,26 +1511,21 @@ const liberator = (function () //{{{
function () { liberator.restart(); },
{ argCount: "0" });
- toolbox = document.getElementById("navigator-toolbox");
- if (toolbox)
- {
+ var toolbox = document.getElementById("navigator-toolbox");
+ if (toolbox) {
function findToolbar(name) util.evaluateXPath(
"./*[@toolbarname=" + util.escapeString(name, "'") + "]",
document, toolbox).snapshotItem(0);
- let tbcmd = function (names, desc, action, filter)
- {
+ let tbcmd = function (names, desc, action, filter) {
commands.add(names, desc,
- function (args)
- {
+ function (args) {
let toolbar = findToolbar(args[0]);
liberator.assert(toolbar, "E474: Invalid argument");
action(toolbar);
- },
- {
+ }, {
argcount: "1",
- completer: function (context)
- {
+ completer: function (context) {
completion.toolbar(context)
if (filter)
context.filters.push(filter);
@@ -697,8 +1546,7 @@ const liberator = (function () //{{{
commands.add(["time"],
"Profile a piece of code or run a command multiple times",
- function (args)
- {
+ function (args) {
let count = args.count;
let special = args.bang;
args = args.string;
@@ -708,15 +1556,12 @@ const liberator = (function () //{{{
else
method = liberator.eval("(function () {" + args + "})");
- try
- {
- if (count > 1)
- {
+ try {
+ if (count > 1) {
let each, eachUnits, totalUnits;
let total = 0;
- for (let i in util.interruptibleRange(0, count, 500))
- {
+ for (let i in util.interruptibleRange(0, count, 500)) {
let now = Date.now();
method();
total += Date.now() - now;
@@ -725,19 +1570,16 @@ const liberator = (function () //{{{
if (special)
return;
- if (total / count >= 100)
- {
+ if (total / count >= 100) {
each = total / 1000.0 / count;
eachUnits = "sec";
}
- else
- {
+ else {
each = total / count;
eachUnits = "msec";
}
- if (total >= 100)
- {
+ if (total >= 100) {
total = total / 1000.0;
totalUnits = "sec";
}
@@ -755,8 +1597,7 @@ const liberator = (function () //{{{
</table>);
commandline.echo(str, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
}
- else
- {
+ else {
let beforeTime = Date.now();
method();
@@ -771,16 +1612,13 @@ const liberator = (function () //{{{
liberator.echo("Total time: " + (afterTime - beforeTime) + " msec");
}
}
- catch (e)
- {
+ catch (e) {
liberator.echoerr(e);
}
- },
- {
+ }, {
argCount: "+",
bang: true,
- completer: function (context)
- {
+ completer: function (context) {
if (/^:/.test(context.filter))
return completion.ex(context);
else
@@ -792,25 +1630,21 @@ const liberator = (function () //{{{
commands.add(["verb[ose]"],
"Execute a command with 'verbose' set",
- function (args)
- {
+ function (args) {
let vbs = options.get("verbose");
let value = vbs.value;
let setFrom = vbs.setFrom;
- try
- {
+ try {
vbs.set(args.count > -1 ? args.count : 1);
vbs.setFrom = null;
liberator.execute(args[0], null, true);
}
- finally
- {
+ finally {
vbs.set(value);
vbs.setFrom = setFrom;
}
- },
- {
+ }, {
argCount: "+",
completer: function (context) completion.ex(context),
count: true,
@@ -819,33 +1653,26 @@ const liberator = (function () //{{{
commands.add(["ve[rsion]"],
"Show version information",
- function (args)
- {
+ function (args) {
if (args.bang)
liberator.open("about:");
else
liberator.echo(template.commandOutput(<>{config.name} {liberator.version} running on:<br/>{navigator.userAgent}</>));
- },
- {
+ }, {
argCount: "0",
bang: true
});
commands.add(["viu[sage]"],
"List all mappings with a short description",
- function (args) { showHelpIndex("normal-index", mappings, args.bang); },
- {
+ function (args) { Liberator.showHelpIndex("normal-index", mappings, args.bang); }, {
argCount: "0",
bang: true
});
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMPLETIONS /////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
+ },
- registerObserver("load_completion", function () {
+ completion: function () {
completion.setFunctionCompleter(services.get, [function () services.services]);
completion.setFunctionCompleter(services.create, [function () [[c, ""] for (c in services.classes)]]);
@@ -888,1236 +1715,124 @@ const liberator = (function () //{{{
context.keys = { text: function (win) liberator.windows.indexOf(win) + 1, description: function (win) win.document.title };
context.completions = liberator.windows;
};
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// PUBLIC SECTION //////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- return {
-
- modules: modules,
-
- /**
- * @property {number} The current main mode.
- * @see modes#mainModes
- */
- get mode() modes.main,
- set mode(value) modes.main = value,
-
- get menuItems() getMenuItems(),
-
- /** @property {Element} The currently focused element. */
- get focus() document.commandDispatcher.focusedElement,
-
- get extensions()
- {
- const rdf = services.get("rdf");
- const extensionManager = services.get("extensionManager");
-
- let extensions = extensionManager.getItemList(Ci.nsIUpdateItem.TYPE_EXTENSION, {});
-
- function getRdfProperty(item, property)
- {
- let resource = rdf.GetResource("urn:mozilla:item:" + item.id);
- let value = "";
-
- if (resource)
- {
- let target = extensionManager.datasource.GetTarget(resource,
- rdf.GetResource("http://www.mozilla.org/2004/em-rdf#" + property), true);
- if (target && target instanceof Ci.nsIRDFLiteral)
- value = target.Value;
- }
-
- return value;
- }
-
- //const Extension = new Struct("id", "name", "description", "icon", "enabled", "version");
- return extensions.map(function (e) ({
- id: e.id,
- name: e.name,
- description: getRdfProperty(e, "description"),
- enabled: getRdfProperty(e, "isDisabled") != "true",
- icon: e.iconURL,
- options: getRdfProperty(e, "optionsURL"),
- version: e.version
- }));
- },
-
- getExtension: function (name) this.extensions.filter(function (e) e.name == name)[0],
-
- // Global constants
- CURRENT_TAB: [],
- NEW_TAB: [],
- NEW_BACKGROUND_TAB: [],
- NEW_WINDOW: [],
-
- forceNewTab: false,
- forceNewWindow: false,
-
- /** @property {string} The Liberator version string. */
- version: "###VERSION### (created: ###DATE###)", // these VERSION and DATE tokens are replaced by the Makefile
-
- // NOTE: services.get("profile").selectedProfile.name doesn't return
- // what you might expect. It returns the last _actively_ selected
- // profile (i.e. via the Profile Manager or -P option) rather than the
- // current profile. These will differ if the current process was run
- // without explicitly selecting a profile.
- /** @property {string} The name of the current user profile. */
- profileName: services.get("directory").get("ProfD", Ci.nsIFile).leafName.replace(/^.+?\./, ""),
-
- /**
- * @property {Object} The map of command-line options. These are
- * specified in the argument to the host application's -liberator
- * option. E.g. $ firefox -liberator '+u=tempRcFile ++noplugin'
- * Supported options:
- * +u=RCFILE Use RCFILE instead of .vimperatorrc.
- * ++noplugin Don't load plugins.
- */
- commandLineOptions: {
- /** @property Whether plugin loading should be prevented. */
- noPlugins: false,
- /** @property An RC file to use rather than the default. */
- rcFile: null,
- /** @property An Ex command to run before any initialization is performed. */
- preCommands: null,
- /** @property An Ex command to run after all initialization has been performed. */
- postCommands: null
- },
-
- registerObserver: registerObserver,
-
- unregisterObserver: function (type, callback)
- {
- if (type in observers)
- observers[type] = observers[type].filter(function (c) c != callback);
- },
-
- // TODO: "zoom": if the zoom value of the current buffer changed
- triggerObserver: function (type)
- {
- let args = Array.slice(arguments, 1);
- for (let [, func] in Iterator(observers[type] || []))
- func.apply(null, args);
- },
-
- /**
- * Triggers the application bell to notify the user of an error. The
- * bell may be either audible or visual depending on the value of the
- * 'visualbell' option.
- */
- beep: function ()
- {
- // FIXME: popups clear the command line
- if (options["visualbell"])
- {
- // flash the visual bell
- let popup = document.getElementById("liberator-visualbell");
- let win = config.visualbellWindow;
- let rect = win.getBoundingClientRect();
- let width = rect.right - rect.left;
- let height = rect.bottom - rect.top;
-
- // NOTE: this doesn't seem to work in FF3 with full box dimensions
- popup.openPopup(win, "overlap", 1, 1, false, false);
- popup.sizeTo(width - 2, height - 2);
- setTimeout(function () { popup.hidePopup(); }, 20);
- }
- else
- {
- let soundService = Cc["@mozilla.org/sound;1"].getService(Ci.nsISound);
- soundService.beep();
- }
- return false; // so you can do: if (...) return liberator.beep();
- },
-
- /**
- * Creates a new thread.
- */
- newThread: function () services.get("threadManager").newThread(0),
-
- /**
- * Calls a function asynchronously on a new thread.
- *
- * @param {nsIThread} thread The thread to call the function on. If no
- * thread is specified a new one is created.
- * @optional
- * @param {Object} self The 'this' object used when executing the
- * function.
- * @param {function} func The function to execute.
- *
- */
- callAsync: function (thread, self, func)
- {
- thread = thread || services.get("threadManager").newThread(0);
- thread.dispatch(new Runnable(self, func, Array.slice(arguments, 3)), thread.DISPATCH_NORMAL);
- },
-
- /**
- * Calls a function synchronously on a new thread.
- *
- * NOTE: Be sure to call GUI related methods like alert() or dump()
- * ONLY in the main thread.
- *
- * @param {nsIThread} thread The thread to call the function on. If no
- * thread is specified a new one is created.
- * @optional
- * @param {function} func The function to execute.
- */
- callFunctionInThread: function (thread, func)
- {
- thread = thread || services.get("threadManager").newThread(0);
-
- // DISPATCH_SYNC is necessary, otherwise strange things will happen
- thread.dispatch(new Runnable(null, func, Array.slice(arguments, 2)), thread.DISPATCH_SYNC);
- },
-
- /**
- * Prints a message to the console. If <b>msg</b> is an object it is
- * pretty printed.
- *
- * NOTE: the "browser.dom.window.dump.enabled" preference needs to be
- * set.
- *
- * @param {string|Object} msg The message to print.
- */
- dump: function ()
- {
- let msg = Array.map(arguments, function (msg) {
- if (typeof msg == "object")
- msg = util.objectToString(msg);
- return msg;
- }).join(", ");
- msg = String.replace(msg, /\n?$/, "\n");
- window.dump(msg.replace(/^./gm, ("config" in modules && config.name.toLowerCase()) + ": $&"));
- },
-
- /**
- * Dumps a stack trace to the console.
- *
- * @param {string} msg The trace message.
- * @param {number} frames The number of frames to print.
- */
- dumpStack: function (msg, frames)
- {
- let stack = Error().stack.replace(/(?:.*\n){2}/, "");
- if (frames != null)
- [stack] = stack.match(RegExp("(?:.*\n){0," + frames + "}"));
- liberator.dump((msg || "Stack") + "\n" + stack);
- },
-
- /**
- * Tests a condition and throws a FailedAssertion error on
- * failure.
- *
- * @param {boolean} condition The condition to test.
- * @param {string} message The message to present to the
- * user on failure.
- */
- assert: function (condition, message)
- {
- if (!condition)
- throw new FailedAssertion(message);
- },
-
- /**
- * Outputs a plain message to the command line.
- *
- * @param {string} str The message to output.
- * @param {number} flags These control the multiline message behaviour.
- * See {@link CommandLine#echo}.
- */
- echo: function (str, flags)
- {
- commandline.echo(str, commandline.HL_NORMAL, flags);
- },
-
- // TODO: Vim replaces unprintable characters in echoerr/echomsg
- /**
- * Outputs an error message to the command line.
- *
- * @param {string} str The message to output.
- * @param {number} flags These control the multiline message behaviour.
- * See {@link CommandLine#echo}.
- */
- echoerr: function (str, flags)
- {
- flags |= commandline.APPEND_TO_MESSAGES;
-
- if (typeof str == "object" && "echoerr" in str)
- str = str.echoerr;
- else if (str instanceof Error)
- str = str.fileName + ":" + str.lineNumber + ": " + str;
-
- if (options["errorbells"])
- liberator.beep();
-
- commandline.echo(str, commandline.HL_ERRORMSG, flags);
- },
-
- // TODO: add proper level constants
- /**
- * Outputs an information message to the command line.
- *
- * @param {string} str The message to output.
- * @param {number} verbosity The messages log level (0 - 15). Only
- * messages with verbosity less than or equal to the value of the
- * 'verbosity' option will be output.
- * @param {number} flags These control the multiline message behaviour.
- * See {@link CommandLine#echo}.
- */
- echomsg: function (str, verbosity, flags)
- {
- // TODO: is there a reason for this? --djk
- // yes, it doesn't show the MOW on startup if you have e.g. some qmarks in your vimperatorrc.
- // Feel free to add another flag like DONT_OPEN_MULTILINE if really needed --mst
- //
- // But it's _supposed_ to show the MOW on startup when there are
- // messages, surely? As far as I'm concerned it essentially works
- // exactly as it should with the DISALLOW_MULTILINE flag removed.
- // Sending N messages to the command line in a row and having them
- // overwrite each other is completely broken. I also think many of
- // those messages like "Added quick mark" are plain silly but if
- // you don't like them you can set verbose=0, or use :silent when
- // someone adds it. I reckon another flag and 'class' of messages
- // is just going to unnecessarily complicate things. --djk
- flags |= commandline.APPEND_TO_MESSAGES | commandline.DISALLOW_MULTILINE;
-
- if (verbosity == null)
- verbosity = 0; // verbosity level is exclusionary
-
- if (options["verbose"] >= verbosity)
- commandline.echo(str, commandline.HL_INFOMSG, flags);
- },
-
- /**
- * Loads and executes the script referenced by <b>uri</b> in the scope
- * of the <b>context</b> object.
- *
- * @param {string} uri The URI of the script to load. Should be a local
- * chrome:, file:, or resource: URL.
- * @param {Object} context The context object into which the script
- * should be loaded.
- */
- loadScript: function (uri, context)
- {
- XML.ignoreWhiteSpace = false;
- XML.prettyPrinting = false;
- services.get("subscriptLoader").loadSubScript(uri, context);
- },
-
- eval: function (str, context)
- {
- try
- {
- if (!context)
- context = userContext;
- context[EVAL_ERROR] = null;
- context[EVAL_STRING] = str;
- context[EVAL_RESULT] = null;
- this.loadScript("chrome://liberator/content/eval.js", context);
- if (context[EVAL_ERROR])
- {
- try
- {
- context[EVAL_ERROR].fileName = io.sourcing.file;
- context[EVAL_ERROR].lineNumber += io.sourcing.line;
- }
- catch (e) {}
- throw context[EVAL_ERROR];
- }
- return context[EVAL_RESULT];
- }
- finally
- {
- delete context[EVAL_ERROR];
- delete context[EVAL_RESULT];
- delete context[EVAL_STRING];
- }
- },
-
- // partial sixth level expression evaluation
- // TODO: what is that really needed for, and where could it be used?
- // Or should it be removed? (c) Viktor
- // Better name? See other liberator.eval()
- // I agree, the name is confusing, and so is the
- // description --Kris
- evalExpression: function (string)
- {
- string = string.toString().replace(/^\s*/, "").replace(/\s*$/, "");
- let matches = string.match(/^&(\w+)/);
-
- if (matches)
- {
- let opt = this.options.get(matches[1]);
-
- if (!opt)
- return void this.echoerr("E113: Unknown option: " + matches[1]);
-
- let type = opt.type;
- let value = opt.getter();
-
- if (type != "boolean" && type != "number")
- value = value.toString();
-
- return value;
- }
- // String
- else if (matches = string.match(/^(['"])([^\1]*?[^\\]?)\1/))
- {
- if (matches)
- return matches[2].toString();
- else
- return void this.echoerr("E115: Missing quote: " + string);
- }
- // Number
- else if (matches = string.match(/^(\d+)$/))
- return parseInt(matches[1], 10);
-
- let reference = this.variableReference(string);
-
- if (!reference[0])
- this.echoerr("E121: Undefined variable: " + string);
- else
- return reference[0][reference[1]];
+ },
+ load: function () {
+ config.features.push(Liberator.getPlatformFeature());
+
+ try {
+ let infoPath = services.create("file");
+ infoPath.initWithPath(File.expandPath(IO.runtimePath.replace(/,.*/, "")));
+ infoPath.append("info");
+ infoPath.append(liberator.profileName);
+ storage.infoPath = infoPath;
+ }
+ catch (e) {
+ liberator.reportError(e);
+ }
- return;
- },
-
- /**
- * Execute an Ex command string. E.g. ":zoom 300".
- *
- * @param {string} str The command to execute.
- * @param {Object} modifiers Any modifiers to be passed to
- * {@link Command#action}.
- * @param {boolean} silent Whether the command should be echoed on the
- * command line.
- */
- execute: function (str, modifiers, silent)
- {
- // skip comments and blank lines
- if (/^\s*("|$)/.test(str))
- return;
+ config.init();
- modifiers = modifiers || {};
+ liberator.triggerObserver("load");
- let err = null;
- let [count, cmd, special, args] = commands.parseCommand(str.replace(/^'(.*)'$/, "$1"));
- let command = commands.get(cmd);
+ liberator.log("All modules loaded", 3);
- if (command === null)
- {
- err = "E492: Not a " + config.name.toLowerCase() + " command: " + str;
- liberator.focusContent();
- }
- else if (command.action === null)
- err = "E666: Internal error: command.action === null"; // TODO: need to perform this test? -- djk
- else if (count != -1 && !command.count)
- err = "E481: No range allowed";
- else if (special && !command.bang)
- err = "E477: No ! allowed";
-
- liberator.assert(!err, err);
- if (!silent)
- commandline.command = str.replace(/^\s*:\s*/, "");
-
- command.execute(args, special, count, modifiers);
- },
-
- /**
- * Focuses the content window.
- *
- * @param {boolean} clearFocusedElement Remove focus from any focused
- * element.
- */
- focusContent: function (clearFocusedElement)
- {
- if (window != services.get("windowWatcher").activeWindow)
- return;
+ services.add("commandLineHandler", "@mozilla.org/commandlinehandler/general-startup;1?type=" + config.name.toLowerCase(),
+ Ci.nsICommandLineHandler);
- let elem = config.mainWidget || window.content;
- // TODO: make more generic
- try
- {
- if (this.has("mail") && !config.isComposeWindow)
- {
- let i = gDBView.selection.currentIndex;
- if (i == -1 && gDBView.rowCount >= 0)
- i = 0;
- gDBView.selection.select(i);
- }
- else if (this.has("tabs"))
- {
- let frame = tabs.localStore.focusedFrame;
- if (frame && frame.top == window.content)
- elem = frame;
- }
- }
- catch (e) {}
-
- if (clearFocusedElement && liberator.focus)
- liberator.focus.blur();
- if (elem && elem != liberator.focus)
- elem.focus();
- },
-
- /**
- * Returns whether this Liberator extension supports <b>feature</b>.
- *
- * @param {string} feature The feature name.
- * @returns {boolean}
- */
- has: function (feature) config.features.indexOf(feature) >= 0,
-
- /**
- * Returns whether the host application has the specified extension
- * installed.
- *
- * @param {string} name The extension name.
- * @returns {boolean}
- */
- hasExtension: function (name)
- {
- let extensions = services.get("extensionManager").getItemList(Ci.nsIUpdateItem.TYPE_EXTENSION, {});
- return extensions.some(function (e) e.name == name);
- },
-
- /**
- * Returns the URL of the specified help <b>topic</b> if it exists.
- *
- * @param {string} topic The help topic to lookup.
- * @param {boolean} unchunked Whether to search the unchunked help page.
- * @returns {string}
- */
- findHelp: function (topic, unchunked)
- {
- if (topic in services.get("liberator:").FILE_MAP)
- return topic;
- unchunked = !!unchunked;
- let items = completion._runCompleter("help", topic, null, unchunked).items;
- let partialMatch = null;
-
- function format(item) item.description + "#" + encodeURIComponent(item.text);
-
- for (let [i, item] in Iterator(items))
- {
- if (item.text == topic)
- return format(item);
- else if (!partialMatch && topic)
- partialMatch = item;
- }
+ let commandline = services.get("commandLineHandler").optionValue;
+ if (commandline) {
+ let args = liberator.parseCommandLine(commandline);
+ liberator.commandLineOptions.rcFile = args["+u"];
+ liberator.commandLineOptions.noPlugins = "++noplugin" in args;
+ liberator.commandLineOptions.postCommands = args["+c"];
+ liberator.commandLineOptions.preCommands = args["++cmd"];
+ liberator.dump("Processing command-line option: " + commandline);
+ }
- if (partialMatch)
- return format(partialMatch);
- return null;
- },
-
- /**
- * @private
- * Initialize the help system.
- */
- initHelp: function ()
- {
- let namespaces = [config.name.toLowerCase(), "liberator"];
- services.get("liberator:").init({});
- let tagMap = services.get("liberator:").HELP_TAGS;
- let fileMap = services.get("liberator:").FILE_MAP;
- let overlayMap = services.get("liberator:").OVERLAY_MAP;
- function XSLTProcessor(sheet)
- {
- let xslt = Cc["@mozilla.org/document-transformer;1?type=xslt"].createInstance(Ci.nsIXSLTProcessor);
- xslt.importStylesheet(util.httpGet(sheet).responseXML);
- return xslt;
- }
+ liberator.log("Command-line options: " + util.objectToString(liberator.commandLineOptions), 3);
- function findHelpFile(file)
- {
- let result = [];
- for (let [, namespace] in Iterator(namespaces))
- {
- let url = ["chrome://", namespace, "/locale/", file, ".xml"].join("");
- let res = util.httpGet(url);
- if (res)
- {
- if (res.responseXML.documentElement.localName == "document")
- fileMap[file] = url;
- if (res.responseXML.documentElement.localName == "overlay")
- overlayMap[file] = url;
- result.push(res.responseXML);
- }
- }
- return result;
- }
- function addTags(file, doc)
- {
- doc = XSLT.transformToDocument(doc);
- for (let elem in util.evaluateXPath("//xhtml:a/@id", doc))
- tagMap[elem.value] = file;
- }
+ // first time intro message
+ const firstTime = "extensions." + config.name.toLowerCase() + ".firsttime";
+ if (options.getPref(firstTime, true)) {
+ setTimeout(function () {
+ liberator.help();
+ options.setPref(firstTime, false);
+ }, 1000);
+ }
- const XSLT = XSLTProcessor("chrome://liberator/content/help.xsl");
+ // always start in normal mode
+ modes.reset();
- tagMap.all = "all";
- let files = findHelpFile("all").map(function (doc)
- [f.value for (f in util.evaluateXPath(
- "//liberator:include/@href", doc))]);
+ // TODO: we should have some class where all this guioptions stuff fits well
+ Liberator.hideGUI();
- util.Array.flatten(files).map(function (file) {
- findHelpFile(file).forEach(function (doc) {
- addTags(file, doc);
- });
+ if (liberator.commandLineOptions.preCommands)
+ liberator.commandLineOptions.preCommands.forEach(function (cmd) {
+ liberator.execute(cmd);
});
- XML.ignoreWhiteSpace = false;
- XML.prettyPrinting = false;
- XML.prettyPrinting = true; // Should be false, but ignoreWhiteSpace=false doesn't work correctly. This is the lesser evil.
- XML.prettyIndent = 4;
- let body = XML();
- for (let [, context] in Iterator(plugins.contexts))
- if (context.INFO instanceof XML)
- body += <h2 xmlns={NS.uri} tag={context.INFO.@name + '-plugin'}>{context.INFO.@summary}</h2> +
- context.INFO;
-
- let help = '<?xml version="1.0"?>\n' +
- '<?xml-stylesheet type="text/xsl" href="chrome://liberator/content/help.xsl"?>\n' +
- '<!DOCTYPE document SYSTEM "chrome://liberator/content/liberator.dtd">' +
- <document
- name="plugins"
- title={config.name + " Plugins"}
- xmlns="http://vimperator.org/namespaces/liberator">
- <h1 tag="using-plugins">Using Plugins</h1>
-
- {body}
- </document>.toXMLString();
- fileMap["plugins"] = function () ['text/xml;charset=UTF-8', help];
-
- addTags("plugins", util.httpGet("liberator://help/plugins").responseXML);
- },
-
- /**
- * Opens the help page containing the specified <b>topic</b> if it
- * exists.
- *
- * @param {string} topic The help topic to open.
- * @param {boolean} unchunked Whether to use the unchunked help page.
- * @returns {string}
- */
- help: function (topic, unchunked)
- {
- if (!topic && !unchunked)
- {
- let helpFile = options["helpfile"];
- if (helpFile in services.get("liberator:").FILE_MAP)
- liberator.open("liberator://help/" + helpFile, { from: "help" });
- else
- liberator.echomsg("Sorry, help file " + helpFile.quote() + " not found");
- return;
- }
-
- let page = this.findHelp(topic, unchunked);
- liberator.assert(page != null, "E149: Sorry, no help for " + topic);
-
- liberator.open("liberator://help/" + page, { from: "help" });
- if (options.get("activate").has("all", "help"))
- content.postMessage("fragmentChange", "*");
- },
-
- /**
- * The map of global variables.
- *
- * These are set and accessed with the "g:" prefix.
- */
- globalVariables: {},
-
- loadModule: function (name, func) { loadModule(name, func); },
-
- loadPlugins: function ()
- {
- function sourceDirectory(dir)
- {
- liberator.assert(dir.isReadable(), "E484: Can't open file " + dir.path);
-
- liberator.log("Sourcing plugin directory: " + dir.path + "...", 3);
- dir.readDirectory(true).forEach(function (file) {
- if (file.isFile() && /\.(js|vimp)$/i.test(file.path) && !(file.path in liberator.pluginFiles))
- {
- try
- {
- io.source(file.path, false);
- liberator.pluginFiles[file.path] = true;
- }
- catch (e)
- {
- liberator.reportError(e);
- }
- }
- else if (file.isDirectory())
- sourceDirectory(file);
- });
- }
-
- let dirs = io.getRuntimeDirectories("plugin");
-
- if (dirs.length == 0)
- {
- liberator.log("No user plugin directory found", 3);
- return;
+ // finally, read the RC file and source plugins
+ // make sourcing asynchronous, otherwise commands that open new tabs won't work
+ setTimeout(function () {
+ let extensionName = config.name.toUpperCase();
+ let init = services.get("environment").get(extensionName + "_INIT");
+ let rcFile = io.getRCFile("~");
+
+ if (liberator.commandLineOptions.rcFile) {
+ let filename = liberator.commandLineOptions.rcFile;
+ if (!/^(NONE|NORC)$/.test(filename))
+ io.source(io.File(filename).path, false); // let io.source handle any read failure like Vim
}
-
- liberator.echomsg('Searching for "plugin/**/*.{js,vimp}" in "'
- + [dir.path.replace(/.plugin$/, "") for ([, dir] in Iterator(dirs))].join(",") + '"', 2);
-
- dirs.forEach(function (dir) {
- liberator.echomsg("Searching for \"" + (dir.path + "/**/*.{js,vimp}") + "\"", 3);
- sourceDirectory(dir);
- });
- },
-
- // TODO: add proper level constants
- /**
- * Logs a message to the JavaScript error console. Each message has an
- * associated log level. Only messages with a log level less than or
- * equal to <b>level</b> will be printed. If <b>msg</b> is an object,
- * it is pretty printed.
- *
- * @param {string|Object} msg The message to print.
- * @param {number} level The logging level 0 - 15.
- */
- log: function (msg, level)
- {
- let verbose = 0;
- if (level == undefined)
- level = 1;
-
- // options does not exist at the very beginning
- if (modules.options)
- verbose = options.getPref("extensions.liberator.loglevel", 0);
-
- if (level > verbose)
- return;
-
- if (typeof msg == "object")
- msg = util.objectToString(msg, false);
-
- services.get("console").logStringMessage(config.name.toLowerCase() + ": " + msg);
- },
-
- /**
- * Opens one or more URLs. Returns true when load was initiated, or
- * false on error.
- *
- * @param {string|string[]} urls Either a URL string or an array of URLs.
- * The array can look like this:
- * ["url1", "url2", "url3", ...]
- * or:
- * [["url1", postdata1], ["url2", postdata2], ...]
- * @param {number|Object} where If ommited, CURRENT_TAB is assumed but NEW_TAB
- * is set when liberator.forceNewTab is true.
- * @param {boolean} force Don't prompt whether to open more than 20
- * tabs.
- * @returns {boolean}
- */
- open: function (urls, params, force)
- {
- // convert the string to an array of converted URLs
- // -> see util.stringToURLArray for more details
- if (typeof urls == "string")
- {
- // rather switch to the tab instead of opening a new url in case of "12: Tab Title" like "urls"
- if (liberator.has("tabs"))
- {
- let matches = urls.match(/^(\d+):/);
- if (matches)
- {
- tabs.select(parseInt(matches[1], 10) - 1, false); // make it zero-based
- return;
+ else {
+ if (init)
+ liberator.execute(init);
+ else {
+ if (rcFile) {
+ io.source(rcFile.path, true);
+ services.get("environment").set("MY_" + extensionName + "RC", rcFile.path);
}
+ else
+ liberator.log("No user RC file found", 3);
}
- urls = util.stringToURLArray(urls);
- }
-
- if (urls.length > 20 && !force)
- {
- commandline.input("This will open " + urls.length + " new tabs. Would you like to continue? (yes/[no]) ",
- function (resp) {
- if (resp && resp.match(/^y(es)?$/i))
- liberator.open(urls, params, true);
- });
- return true;
- }
-
- let flags = 0;
- params = params || {};
- if (params instanceof Array)
- params = { where: params };
-
- for (let [opt, flag] in Iterator({ replace: "REPLACE_HISTORY", hide: "BYPASS_HISTORY" }))
- if (params[opt])
- flags |= Ci.nsIWebNavigation["LOAD_FLAGS_" + flag];
-
- let where = params.where || liberator.CURRENT_TAB;
- if ("from" in params && liberator.has("tabs"))
- {
- if (!('where' in params) && options.get("newtab").has("all", params.from))
- where = liberator.NEW_BACKGROUND_TAB;
- if (options.get("activate").has("all", params.from))
- {
- if (where == liberator.NEW_TAB)
- where = liberator.NEW_BACKGROUND_TAB;
- else if (where == liberator.NEW_BACKGROUND_TAB)
- where = liberator.NEW_TAB;
- }
- }
-
- if (urls.length == 0)
- return false;
-
- let browser = window.getBrowser();
-
- function open(urls, where)
- {
- let url = Array.concat(urls)[0];
- let postdata = Array.concat(urls)[1];
-
- // decide where to load the first url
- switch (where)
- {
- case liberator.CURRENT_TAB:
- browser.loadURIWithFlags(url, flags, null, null, postdata);
- break;
-
- case liberator.NEW_BACKGROUND_TAB:
- case liberator.NEW_TAB:
- if (!liberator.has("tabs"))
- return open(urls, liberator.NEW_WINDOW);
-
- options.withContext(function () {
- options.setPref("browser.tabs.loadInBackground", true);
- browser.loadOneTab(url, null, null, postdata, where == liberator.NEW_BACKGROUND_TAB);
- });
- break;
-
- case liberator.NEW_WINDOW:
- window.open();
- let win = services.get("windowMediator").getMostRecentWindow("navigator:browser");
- win.loadURI(url, null, postdata);
- browser = win.getBrowser();
- break;
-
- default:
- throw Error("Invalid 'where' directive in liberator.open(...)");
- }
- }
-
- if (liberator.forceNewTab)
- where = liberator.NEW_TAB;
- else if (liberator.forceNewWindow)
- where = liberator.NEW_WINDOW;
- else if (!where)
- where = liberator.CURRENT_TAB;
-
- for (let [, url] in Iterator(urls))
- {
- open(url, where);
- where = liberator.NEW_BACKGROUND_TAB;
- }
-
- return true;
- },
-
- pluginFiles: {},
-
- // namespace for plugins/scripts. Actually (only) the active plugin must/can set a
- // v.plugins.mode = <str> string to show on v.modes.CUSTOM
- // v.plugins.stop = <func> hooked on a v.modes.reset()
- // v.plugins.onEvent = <func> function triggered, on keypresses (unless <esc>) (see events.js)
- plugins: plugins,
-
- /**
- * Quit the host application, no matter how many tabs/windows are open.
- *
- * @param {boolean} saveSession If true the current session will be
- * saved and restored when the host application is restarted.
- * @param {boolean} force Forcibly quit irrespective of whether all
- * windows could be closed individually.
- */
- quit: function (saveSession, force)
- {
- // TODO: Use safeSetPref?
- if (saveSession)
- options.setPref("browser.startup.page", 3); // start with saved session
- else
- options.setPref("browser.startup.page", 1); // start with default homepage session
-
- if (force)
- services.get("appStartup").quit(Ci.nsIAppStartup.eForceQuit);
- else
- window.goQuitApplication();
- },
-
- /**
- * Traps errors in the called function, possibly reporting them.
- *
- * @param {function} func The function to call
- * @param {object} self The 'this' object for the function.
- */
- trapErrors: function (func, self)
- {
- try
- {
- return func.apply(self || this, Array.slice(arguments, 2));
- }
- catch (e)
- {
- if (e instanceof FailedAssertion)
- liberator.echoerr(e.message);
- }
- },
-
- /**
- * Reports an error to both the console and the host application's
- * Error Console.
- *
- * @param {Object} error The error object.
- */
- reportError: function (error)
- {
- if (Cu.reportError)
- Cu.reportError(error);
-
- try
- {
- try
- {
- var string = String(error);
- var stack = error.stack;
- }
- catch (e) {}
-
- let obj = {
- toString: function () string || {}.toString.call(error),
- stack: <>{String.replace(stack || Error().stack, /^/mg, "\t")}</>
- };
- for (let [k, v] in Iterator(error))
- {
- if (!(k in obj))
- obj[k] = v;
- }
- if (liberator.storeErrors)
- {
- let errors = storage.newArray("errors", { store: false });
- errors.toString = function () [String(v[0]) + "\n" + v[1] for ([k, v] in this)].join("\n\n");
- errors.push([new Date, obj + obj.stack]);
+ if (options["exrc"] && !liberator.commandLineOptions.rcFile) {
+ let localRCFile = io.getRCFile(io.getCurrentDirectory().path);
+ if (localRCFile && !localRCFile.equals(rcFile))
+ io.source(localRCFile.path, true);
}
- liberator.dump(string);
- liberator.dump(obj);
- liberator.dump("");
}
- catch (e) { window.dump(e) }
- },
-
- /**
- * Restart the host application.
- */
- restart: function ()
- {
- // notify all windows that an application quit has been requested.
- var cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
- services.get("observer").notifyObservers(cancelQuit, "quit-application-requested", null);
-
- // something aborted the quit process.
- if (cancelQuit.data)
- return;
-
- // notify all windows that an application quit has been granted.
- services.get("observer").notifyObservers(null, "quit-application-granted", null);
-
- // enumerate all windows and call shutdown handlers
- let windows = services.get("windowMediator").getEnumerator(null);
- while (windows.hasMoreElements())
- {
- let win = windows.getNext();
- if (("tryToClose" in win) && !win.tryToClose())
- return;
- }
- services.get("appStartup").quit(Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit);
- },
-
- /**
- * Parses a Liberator command-line string i.e. the value of the
- * -liberator command-line option.
- *
- * @param {string} cmdline The string to parse for command-line
- * options.
- * @returns {Object}
- * @see Commands#parseArgs
- */
- parseCommandLine: function (cmdline)
- {
- const options = [
- [["+u"], commands.OPTIONS_STRING],
- [["++noplugin"], commands.OPTIONS_NOARG],
- [["++cmd"], commands.OPTIONS_STRING, null, null, true],
- [["+c"], commands.OPTIONS_STRING, null, null, true]
- ];
- return commands.parseArgs(cmdline, options, "*");
- },
-
- // this function is called when the chrome is ready
- startup: function ()
- {
- let start = Date.now();
- liberator.log("Initializing liberator object...", 0);
-
- config.features.push(getPlatformFeature());
-
- try
- {
- let infoPath = services.create("file");
- infoPath.initWithPath(IO.expandPath(IO.runtimePath.replace(/,.*/, "")));
- infoPath.append("info");
- infoPath.append(liberator.profileName);
- storage.infoPath = infoPath;
- }
- catch (e)
- {
- liberator.reportError(e);
- }
-
- let img = Image();
- img.src = config.logo || "chrome://" + config.name.toLowerCase() + "/content/logo.png";
- img.onload = function () {
- highlight.set("Logo", String(<>
- display: inline-block;
- background: url({img.src});
- width: {img.width}px;
- height: {img.height}px;
- </>));
- delete img;
- };
-
- // commands must always be the first module to be initialized
- loadModule("commands", Commands);
- loadModule("options", Options);
- loadModule("events", Events);
- loadModule("mappings", Mappings);
- loadModule("buffer", Buffer);
- loadModule("commandline", CommandLine);
- loadModule("statusline", StatusLine);
- loadModule("editor", Editor);
- loadModule("autocommands", AutoCommands);
- loadModule("io", IO);
- loadModule("completion", Completion);
- // add options/mappings/commands which are only valid in this particular extension
- if (config.init)
- config.init();
+ if (liberator.commandLineOptions.rcFile == "NONE" || liberator.commandLineOptions.noPlugins)
+ options["loadplugins"] = false;
- liberator.triggerObserver("load");
+ if (options["loadplugins"])
+ liberator.loadPlugins();
- liberator.log("All modules loaded", 3);
+ liberator.initHelp();
- services.add("commandLineHandler", "@mozilla.org/commandlinehandler/general-startup;1?type=" + config.name.toLowerCase(),
- Ci.nsICommandLineHandler);
-
- let commandline = services.get("commandLineHandler").wrappedJSObject.optionValue;
- if (commandline)
- {
- let args = liberator.parseCommandLine(commandline);
- liberator.commandLineOptions.rcFile = args["+u"];
- liberator.commandLineOptions.noPlugins = "++noplugin" in args;
- liberator.commandLineOptions.postCommands = args["+c"];
- liberator.commandLineOptions.preCommands = args["++cmd"];
- liberator.dump("Processing command-line option: " + commandline);
+ // after sourcing the initialization files, this function will set
+ // all gui options to their default values, if they have not been
+ // set before by any RC file
+ for (let option in options) {
+ // 'encoding' option should not be set at this timing.
+ // Probably a wrong value is set into the option,
+ // if current page's encoging is not UTF-8.
+ if (option.name != "encoding" && option.setter)
+ option.value = option.value;
}
- liberator.log("Command-line options: " + util.objectToString(liberator.commandLineOptions), 3);
-
- // first time intro message
- const firstTime = "extensions." + config.name.toLowerCase() + ".firsttime";
- if (options.getPref(firstTime, true))
- {
- setTimeout(function () {
- liberator.help();
- options.setPref(firstTime, false);
- }, 1000);
- }
-
- // always start in normal mode
- modes.reset();
-
- // TODO: we should have some class where all this guioptions stuff fits well
- hideGUI();
-
- if (liberator.commandLineOptions.preCommands)
- liberator.commandLineOptions.preCommands.forEach(function (cmd) {
+ if (liberator.commandLineOptions.postCommands)
+ liberator.commandLineOptions.postCommands.forEach(function (cmd) {
liberator.execute(cmd);
});
- // finally, read the RC file and source plugins
- // make sourcing asynchronous, otherwise commands that open new tabs won't work
- setTimeout(function () {
-
- let extensionName = config.name.toUpperCase();
- let init = services.get("environment").get(extensionName + "_INIT");
- let rcFile = io.getRCFile("~");
-
- if (liberator.commandLineOptions.rcFile)
- {
- let filename = liberator.commandLineOptions.rcFile;
- if (!/^(NONE|NORC)$/.test(filename))
- io.source(io.File(filename).path, false); // let io.source handle any read failure like Vim
- }
- else
- {
- if (init)
- liberator.execute(init);
- else
- {
- if (rcFile)
- {
- io.source(rcFile.path, true);
- services.get("environment").set("MY_" + extensionName + "RC", rcFile.path);
- }
- else
- liberator.log("No user RC file found", 3);
- }
-
- if (options["exrc"] && !liberator.commandLineOptions.rcFile)
- {
- let localRCFile = io.getRCFile(io.getCurrentDirectory().path);
- if (localRCFile && !localRCFile.equals(rcFile))
- io.source(localRCFile.path, true);
- }
- }
-
- if (liberator.commandLineOptions.rcFile == "NONE" || liberator.commandLineOptions.noPlugins)
- options["loadplugins"] = false;
-
- if (options["loadplugins"])
- liberator.loadPlugins();
-
- liberator.initHelp();
-
- // after sourcing the initialization files, this function will set
- // all gui options to their default values, if they have not been
- // set before by any RC file
- for (let option in options)
- {
- // 'encoding' option should not be set at this timing.
- // Probably a wrong value is set into the option,
- // if current page's encoging is not UTF-8.
- if (option.name != "encoding" && option.setter)
- option.value = option.value;
- }
-
- if (liberator.commandLineOptions.postCommands)
- liberator.commandLineOptions.postCommands.forEach(function (cmd) {
- liberator.execute(cmd);
- });
-
- liberator.triggerObserver("enter", null);
- autocommands.trigger(config.name + "Enter", {});
-
- liberator.initialized = true;
- }, 0);
-
- statusline.update();
-
- liberator.dump("loaded in " + (Date.now() - start) + " ms");
- liberator.log(config.name + " fully initialized", 0);
- },
-
- shutdown: function ()
- {
- autocommands.trigger(config.name + "LeavePre", {});
- storage.saveAll();
- liberator.triggerObserver("shutdown", null);
- liberator.dump("All liberator modules destroyed\n");
- autocommands.trigger(config.name + "Leave", {});
- },
-
- sleep: function (delay)
- {
- let mainThread = services.get("threadManager").mainThread;
-
- let end = Date.now() + delay;
- while (Date.now() < end)
- mainThread.processNextEvent(true);
- return true;
- },
-
- callInMainThread: function (callback, self)
- {
- let mainThread = services.get("threadManager").mainThread;
- if (!services.get("threadManager").isMainThread)
- mainThread.dispatch({ run: callback.call(self) }, mainThread.DISPATCH_NORMAL);
- else
- callback.call(self);
- },
-
- threadYield: function (flush, interruptable)
- {
- let mainThread = services.get("threadManager").mainThread;
- liberator.interrupted = false;
- do
- {
- mainThread.processNextEvent(!flush);
- if (liberator.interrupted)
- throw new Error("Interrupted");
- }
- while (flush === true && mainThread.hasPendingEvents());
- },
-
- variableReference: function (string)
- {
- if (!string)
- return [null, null, null];
-
- let matches = string.match(/^([bwtglsv]):(\w+)/);
- if (matches) // Variable
- {
- // Other variables should be implemented
- if (matches[1] == "g")
- {
- if (matches[2] in this.globalVariables)
- return [this.globalVariables, matches[2], matches[1]];
- else
- return [null, matches[2], matches[1]];
- }
- }
- else // Global variable
- {
- if (string in this.globalVariables)
- return [this.globalVariables, string, "g"];
- else
- return [null, string, "g"];
- }
- },
-
- /**
- * @property {Window[]} Returns an array of all the host application's
- * open windows.
- */
- get windows()
- {
- let windows = [];
- let enumerator = services.get("windowMediator").getEnumerator("navigator:browser");
- while (enumerator.hasMoreElements())
- windows.push(enumerator.getNext());
-
- return windows;
- }
- };
- //}}}
-})(); //}}}
-
-window.liberator = liberator;
+ liberator.triggerObserver("enter", null);
+ autocommands.trigger(config.name + "Enter", {});
+ }, 0);
-// called when the chrome is fully loaded and before the main window is shown
-window.addEventListener("load", liberator.startup, false);
-window.addEventListener("unload", liberator.shutdown, false);
+ statusline.update();
+ liberator.log(config.name + " fully initialized", 0);
+ },
+});
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/liberator.xul b/common/content/liberator.xul
index 92800cce..c1acf2bb 100644
--- a/common/content/liberator.xul
+++ b/common/content/liberator.xul
@@ -10,6 +10,7 @@
<?xml-stylesheet href="chrome://liberator/skin/liberator.css" type="text/css"?>
<!DOCTYPE overlay SYSTEM "liberator.dtd" [
<!ENTITY liberator.content "chrome://liberator/content/">
+ <!ENTITY and "&amp;&amp;">
]>
<overlay id="liberator"
@@ -28,8 +29,8 @@
</stringbundleset>
<keyset id="mainKeyset">
- <key id="key_open_vimbar" key=":" oncommand="liberator.modules.commandline.open(':', '', liberator.modules.modes.EX);" modifiers=""/>
- <key id="key_stop" keycode="VK_ESCAPE" oncommand="liberator.modules.events.onEscape();"/>
+ <key id="key_open_vimbar" key=":" oncommand="window.liberator &and; liberator.modules.commandline.open(':', '', liberator.modules.modes.EX);" modifiers=""/>
+ <key id="key_stop" keycode="VK_ESCAPE" oncommand="window.liberator &and; liberator.modules.events.onEscape();"/>
<!-- other keys are handled inside the event loop in events.js -->
</keyset>
@@ -42,24 +43,24 @@
<commandset id="onVimperatorFocus"
commandupdater="true"
events="focus"
- oncommandupdate="if (liberator.modules.events != undefined) liberator.modules.events.onFocusChange(event);"/>
+ oncommandupdate="if (window.liberator &and; liberator.modules.events != undefined) liberator.modules.events.onFocusChange(event);"/>
<commandset id="onVimperatorSelect"
commandupdater="true"
events="select"
- oncommandupdate="if (liberator.modules.events != undefined) liberator.modules.events.onSelectionChange(event);"/>
+ oncommandupdate="if (window.liberator &and; liberator.modules.events != undefined) liberator.modules.events.onSelectionChange(event);"/>
<!-- As of Firefox 3.1pre, <iframe>.height changes do not seem to have immediate effect,
therefore we need to put them into a <vbox> for which that works just fine -->
<vbox class="liberator-container" hidden="false" collapsed="true">
<iframe id="liberator-multiline-output" src="chrome://liberator/content/buffer.xhtml"
flex="1" hidden="false" collapsed="false"
- onclick="liberator.modules.commandline.onMultilineOutputEvent(event)"/>
+ onclick="window.liberator &and; liberator.modules.commandline.onMultilineOutputEvent(event)"/>
</vbox>
<vbox class="liberator-container" hidden="false" collapsed="true">
<iframe id="liberator-completions" src="chrome://liberator/content/buffer.xhtml"
flex="1" hidden="false" collapsed="false"
- onclick="liberator.modules.commandline.onMultilineOutputEvent(event)"/>
+ onclick="window.liberator &and; liberator.modules.commandline.onMultilineOutputEvent(event)"/>
</vbox>
<stack orient="horizontal" align="stretch" class="liberator-container" liberator:highlight="CmdLine">
@@ -67,18 +68,18 @@
<hbox id="liberator-commandline" hidden="false" collapsed="true" class="liberator-container" liberator:highlight="Normal">
<label class="plain" id="liberator-commandline-prompt" flex="0" crop="end" value="" collapsed="true"/>
<textbox class="plain" id="liberator-commandline-command" flex="1" type="timed" timeout="100"
- oninput="liberator.modules.commandline.onEvent(event);"
- onkeyup="liberator.modules.commandline.onEvent(event);"
- onfocus="liberator.modules.commandline.onEvent(event);"
- onblur="liberator.modules.commandline.onEvent(event);"/>
+ oninput="window.liberator &and; liberator.modules.commandline.onEvent(event);"
+ onkeyup="window.liberator &and; liberator.modules.commandline.onEvent(event);"
+ onfocus="window.liberator &and; liberator.modules.commandline.onEvent(event);"
+ onblur="window.liberator &and; liberator.modules.commandline.onEvent(event);"/>
</hbox>
</stack>
<vbox class="liberator-container" hidden="false" collapsed="false" liberator:highlight="CmdLine">
<textbox id="liberator-multiline-input" class="plain" flex="1" rows="1" hidden="false" collapsed="true" multiline="true" liberator:highlight="Normal"
- onkeypress="liberator.modules.commandline.onMultilineInputEvent(event);"
- oninput="liberator.modules.commandline.onMultilineInputEvent(event);"
- onblur="liberator.modules.commandline.onMultilineInputEvent(event);"/>
+ onkeypress="window.liberator &and; liberator.modules.commandline.onMultilineInputEvent(event);"
+ oninput="window.liberator &and; liberator.modules.commandline.onMultilineInputEvent(event);"
+ onblur="window.liberator &and; liberator.modules.commandline.onMultilineInputEvent(event);"/>
</vbox>
</window>
diff --git a/common/content/mappings.js b/common/content/mappings.js
index f73c32b1..21ade1b3 100644
--- a/common/content/mappings.js
+++ b/common/content/mappings.js
@@ -27,52 +27,50 @@
* @optional
* @private
*/
-function Map(modes, keys, description, action, extraInfo) //{{{
-{
- modes = Array.concat(modes);
-
- if (!extraInfo)
- extraInfo = {};
-
- /** @property {number[]} All of the modes for which this mapping applies. */
- this.modes = modes;
- /** @property {string[]} All of this mapping's names (key sequences). */
- this.names = keys.map(events.canonicalKeys);
- /** @property {function (number)} The function called to execute this mapping. */
- this.action = action;
- /** @property {string} This mapping's description, as shown in :viusage. */
- this.description = description || "";
-
- /** @property {boolean} Whether this mapping accepts an argument. */
- this.arg = extraInfo.arg || false;
- /** @property {boolean} Whether this mapping accepts a count. */
- this.count = extraInfo.count || false;
- /**
- * @property {boolean} Whether the mapping accepts a motion mapping
- * as an argument.
- */
- this.motion = extraInfo.motion || false;
- /**
- * @property {boolean} Whether the mapping's key events should be
- * propagated to the host application.
- */
- // TODO: I'm not sure this is the best name but it reflects that which it replaced. --djk
- this.route = extraInfo.route || false;
- /** @property {boolean} Whether the RHS of the mapping should expand mappings recursively. */
- this.noremap = extraInfo.noremap || false;
- /** @property {boolean} Whether any output from the mapping should be echoed on the command line. */
- this.silent = extraInfo.silent || false;
- /** @property {string} The literal RHS expansion of this mapping. */
- this.rhs = extraInfo.rhs || null;
- /**
- * @property {boolean} Specifies whether this is a user mapping. User
- * mappings may be created by plugins, or directly by users. Users and
- * plugin authors should create only user mappings.
- */
- this.user = extraInfo.user || false;
-}
-
-Map.prototype = {
+const Map = Class("Map", {
+ init: function (modes, keys, description, action, extraInfo) {
+ modes = Array.concat(modes);
+
+ if (!extraInfo)
+ extraInfo = {};
+
+ /** @property {number[]} All of the modes for which this mapping applies. */
+ this.modes = modes;
+ /** @property {string[]} All of this mapping's names (key sequences). */
+ this.names = keys.map(events.canonicalKeys);
+ /** @property {function (number)} The function called to execute this mapping. */
+ this.action = action;
+ /** @property {string} This mapping's description, as shown in :viusage. */
+ this.description = description || "";
+
+ /** @property {boolean} Whether this mapping accepts an argument. */
+ this.arg = extraInfo.arg || false;
+ /** @property {boolean} Whether this mapping accepts a count. */
+ this.count = extraInfo.count || false;
+ /**
+ * @property {boolean} Whether the mapping accepts a motion mapping
+ * as an argument.
+ */
+ this.motion = extraInfo.motion || false;
+ /**
+ * @property {boolean} Whether the mapping's key events should be
+ * propagated to the host application.
+ */
+ // TODO: I'm not sure this is the best name but it reflects that which it replaced. --djk
+ this.route = extraInfo.route || false;
+ /** @property {boolean} Whether the RHS of the mapping should expand mappings recursively. */
+ this.noremap = extraInfo.noremap || false;
+ /** @property {boolean} Whether any output from the mapping should be echoed on the command line. */
+ this.silent = extraInfo.silent || false;
+ /** @property {string} The literal RHS expansion of this mapping. */
+ this.rhs = extraInfo.rhs || null;
+ /**
+ * @property {boolean} Specifies whether this is a user mapping. User
+ * mappings may be created by plugins, or directly by users. Users and
+ * plugin authors should create only user mappings.
+ */
+ this.user = extraInfo.user || false;
+ },
/**
* Returns whether this mapping can be invoked by a key sequence matching
@@ -93,8 +91,7 @@ Map.prototype = {
* @param {string} argument The normal argument if accepted by this
* mapping. E.g. "a" for "ma"
*/
- execute: function (motion, count, argument)
- {
+ execute: function (motion, count, argument) {
let args = [];
if (this.motion)
@@ -112,60 +109,46 @@ Map.prototype = {
return liberator.trapErrors(repeat);
}
-}; //}}}
+});
/**
* @instance mappings
*/
-function Mappings() //{{{
-{
- ////////////////////////////////////////////////////////////////////////////////
- ////////////////////// PRIVATE SECTION /////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- var main = []; // default mappings
- var user = []; // user created mappings
-
- for (let mode in modes)
- {
- main[mode] = [];
- user[mode] = [];
- }
+const Mappings = Module("mappings", {
+ requires: ["modes"],
+
+ init: function () {
+ this._main = []; // default mappings
+ this._user = []; // user created mappings
+ },
- function addMap(map)
- {
- let where = map.user ? user : main;
+ _addMap: function (map) {
+ let where = map.user ? this._user : this._main;
map.modes.forEach(function (mode) {
if (!(mode in where))
where[mode] = [];
where[mode].push(map);
});
- }
+ },
- function getMap(mode, cmd, stack)
- {
+ _getMap: function (mode, cmd, stack) {
let maps = stack[mode] || [];
- for (let [, map] in Iterator(maps))
- {
+ for (let [, map] in Iterator(maps)) {
if (map.hasName(cmd))
return map;
}
return null;
- }
+ },
- function removeMap(mode, cmd)
- {
- let maps = user[mode] || [];
+ _removeMap: function (mode, cmd) {
+ let maps = this._user[mode] || [];
let names;
- for (let [i, map] in Iterator(maps))
- {
- for (let [j, name] in Iterator(map.names))
- {
- if (name == cmd)
- {
+ for (let [i, map] in Iterator(maps)) {
+ for (let [j, name] in Iterator(map.names)) {
+ if (name == cmd) {
map.names.splice(j, 1);
if (map.names.length == 0)
maps.splice(i, 1);
@@ -173,152 +156,329 @@ function Mappings() //{{{
}
}
}
- }
+ },
- function expandLeader(keyString) keyString.replace(/<Leader>/i, mappings.getMapLeader())
+ _expandLeader: function (keyString) keyString.replace(/<Leader>/i, mappings.getMapLeader()),
// Return all mappings present in all @modes
- function mappingsIterator(modes, stack)
- {
+ _mappingsIterator: function (modes, stack) {
modes = modes.slice();
return (map for ([i, map] in Iterator(stack[modes.shift()]))
if (modes.every(function (mode) stack[mode].some(
function (m) m.rhs == map.rhs && m.names[0] == map.names[0]))))
- }
+ },
- function addMapCommands(ch, modes, modeDescription)
- {
- // 0 args -> list all maps
- // 1 arg -> list the maps starting with args
- // 2 args -> map arg1 to arg*
- function map(args, modes, noremap)
- {
- if (!args.length)
- {
- mappings.list(modes);
- return;
- }
+ // NOTE: just normal mode for now
+ /** @property {Iterator(Map)} @private */
+ __iterator__: function () this._mappingsIterator([modes.NORMAL], this._main),
- let [lhs, rhs] = args;
-
- if (!rhs) // list the mapping
- mappings.list(modes, expandLeader(lhs));
- else
- {
- // this matches Vim's behaviour
- if (/^<Nop>$/i.test(rhs))
- noremap = true;
-
- mappings.addUserMap(modes, [lhs],
- "User defined mapping",
- function (count) { events.feedkeys((count > -1 ? count : "") + this.rhs, this.noremap, this.silent); },
- {
- count: true,
- rhs: events.canonicalKeys(rhs),
- noremap: !!noremap,
- silent: "<silent>" in args
- });
- }
+ // used by :mkvimperatorrc to save mappings
+ /**
+ * Returns a user-defined mappings iterator for the specified
+ * <b>mode</b>.
+ *
+ * @param {number} mode The mode to return mappings from.
+ * @returns {Iterator(Map)}
+ */
+ getUserIterator: function (mode) this._mappingsIterator(mode, this._user),
+
+ addMode: function (mode) {
+ if (!(mode in this._user || mode in this._main)) {
+ this._main[mode] = [];
+ this._user[mode] = [];
}
+ },
- modeDescription = modeDescription ? " in " + modeDescription + " mode" : "";
+ /**
+ * Adds a new default key mapping.
+ *
+ * @param {number[]} modes The modes that this mapping applies to.
+ * @param {string[]} keys The key sequences which are bound to
+ * <b>action</b>.
+ * @param {string} description A description of the key mapping.
+ * @param {function} action The action invoked by each key sequence.
+ * @param {Object} extra An optional extra configuration hash.
+ * @optional
+ */
+ add: function (modes, keys, description, action, extra) {
+ this._addMap(new Map(modes, keys, description, action, extra));
+ },
- // :map, :noremap => NORMAL + VISUAL modes
- function isMultiMode(map, cmd)
- {
- return map.modes.indexOf(modules.modes.NORMAL) >= 0
- && map.modes.indexOf(modules.modes.VISUAL) >= 0
- && /^[nv](nore)?map$/.test(cmd);
+ /**
+ * Adds a new user-defined key mapping.
+ *
+ * @param {number[]} modes The modes that this mapping applies to.
+ * @param {string[]} keys The key sequences which are bound to
+ * <b>action</b>.
+ * @param {string} description A description of the key mapping.
+ * @param {function} action The action invoked by each key sequence.
+ * @param {Object} extra An optional extra configuration hash (see
+ * {@link Map#extraInfo}).
+ * @optional
+ */
+ addUserMap: function (modes, keys, description, action, extra) {
+ keys = keys.map(this._expandLeader);
+ extra = extra || {};
+ extra.user = true;
+ let map = new Map(modes, keys, description || "User defined mapping", action, extra);
+
+ // remove all old mappings to this key sequence
+ for (let [, name] in Iterator(map.names)) {
+ for (let [, mode] in Iterator(map.modes))
+ this._removeMap(mode, name);
}
- const opts = {
- completer: function (context, args) completion.userMapping(context, args, modes),
- options: [
- [["<silent>", "<Silent>"], commands.OPTION_NOARG]
- ],
- literal: 1,
- serial: function ()
- {
- let noremap = this.name.indexOf("noremap") > -1;
- return [
- {
- command: this.name,
- options: map.silent ? { "<silent>": null } : {},
- arguments: [map.names[0]],
- literalArg: map.rhs
- }
- for (map in mappingsIterator(modes, user))
- if (map.rhs && map.noremap == noremap && !isMultiMode(map, this.name))
- ];
+ this._addMap(map);
+ },
+
+ /**
+ * Returns the map from <b>mode</b> named <b>cmd</b>.
+ *
+ * @param {number} mode The mode to search.
+ * @param {string} cmd The map name to match.
+ * @returns {Map}
+ */
+ get: function (mode, cmd) {
+ mode = mode || modes.NORMAL;
+ return this._getMap(mode, cmd, this._user) || this._getMap(mode, cmd, this._main);
+ },
+
+ /**
+ * Returns the default map from <b>mode</b> named <b>cmd</b>.
+ *
+ * @param {number} mode The mode to search.
+ * @param {string} cmd The map name to match.
+ * @returns {Map}
+ */
+ getDefault: function (mode, cmd) {
+ mode = mode || modes.NORMAL;
+ return this._getMap(mode, cmd, this._main);
+ },
+
+ /**
+ * Returns an array of maps with names starting with but not equal to
+ * <b>prefix</b>.
+ *
+ * @param {number} mode The mode to search.
+ * @param {string} prefix The map prefix string to match.
+ * @returns {Map[]}
+ */
+ getCandidates: function (mode, prefix) {
+ let mappings = this._user[mode].concat(this._main[mode]);
+ let matches = [];
+
+ for (let [, map] in Iterator(mappings)) {
+ for (let [, name] in Iterator(map.names)) {
+ if (name.indexOf(prefix) == 0 && name.length > prefix.length) {
+ // for < only return a candidate if it doesn't look like a <c-x> mapping
+ if (prefix != "<" || !/^<.+>/.test(name))
+ matches.push(map);
}
- };
+ }
+ }
+
+ return matches;
+ },
+
+ /*
+ * Returns the map leader string used to replace the special token
+ * "<Leader>" when user mappings are defined.
+ *
+ * @returns {string}
+ */
+ // FIXME: property
+ getMapLeader: function () {
+ let leaderRef = liberator.variableReference("mapleader");
+ return leaderRef[0] ? leaderRef[0][leaderRef[1]] : "\\";
+ },
+
+ /**
+ * Returns whether there is a user-defined mapping <b>cmd</b> for the
+ * specified <b>mode</b>.
+ *
+ * @param {number} mode The mode to search.
+ * @param {string} cmd The candidate key mapping.
+ * @returns {boolean}
+ */
+ hasMap: function (mode, cmd) this._user[mode].some(function (map) map.hasName(cmd)),
+
+ /**
+ * Remove the user-defined mapping named <b>cmd</b> for <b>mode</b>.
+ *
+ * @param {number} mode The mode to search.
+ * @param {string} cmd The map name to match.
+ */
+ remove: function (mode, cmd) {
+ this._removeMap(mode, cmd);
+ },
+
+ /**
+ * Remove all user-defined mappings for <b>mode</b>.
+ *
+ * @param {number} mode The mode to remove all mappings from.
+ */
+ removeAll: function (mode) {
+ this._user[mode] = [];
+ },
- commands.add([ch ? ch + "m[ap]" : "map"],
- "Map a key sequence" + modeDescription,
- function (args) { map(args, modes, false); },
- opts);
-
- commands.add([ch + "no[remap]"],
- "Map a key sequence without remapping keys" + modeDescription,
- function (args) { map(args, modes, true); },
- opts);
-
- commands.add([ch + "mapc[lear]"],
- "Remove all mappings" + modeDescription,
- function () { modes.forEach(function (mode) { mappings.removeAll(mode); }); },
- { argCount: "0" });
-
- commands.add([ch + "unm[ap]"],
- "Remove a mapping" + modeDescription,
- function (args)
- {
- args = args[0];
-
- let found = false;
- for (let [, mode] in Iterator(modes))
+ /**
+ * Lists all user-defined mappings matching <b>filter</b> for the
+ * specified <b>modes</b>.
+ *
+ * @param {number[]} modes An array of modes to search.
+ * @param {string} filter The filter string to match.
+ */
+ list: function (modes, filter) {
+ let modeSign = "";
+
+ // TODO: Vim hides "nv" in a :map and "v" and "n" in :vmap and
+ // :nmap respectively if the map is not exclusive.
+ modes.forEach(function (mode) {
+ for (let m in modules.modes.mainModes)
+ if (mode == m.mask && modeSign.indexOf(m.char) == -1)
+ modeSign += m.char;
+ });
+
+ let maps = this._mappingsIterator(modes, this._user);
+ if (filter)
+ maps = [map for (map in maps) if (map.names[0] == filter)];
+
+ let list = <table>
{
- if (mappings.hasMap(mode, args))
- {
- mappings.remove(mode, args);
- found = true;
- }
+ template.map(maps, function (map)
+ template.map(map.names, function (name)
+ <tr>
+ <td>{modeSign} {name}</td>
+ <td>{map.noremap ? "*" : " "}</td>
+ <td>{map.rhs || "function () { ... }"}</td>
+ </tr>))
}
- if (!found)
- liberator.echoerr("E31: No such mapping");
- },
- {
- argCount: "1",
- completer: function (context, args) completion.userMapping(context, args, modes)
- });
+ </table>;
+
+ // TODO: Move this to an ItemList to show this automatically
+ if (list.*.length() == list.text().length()) {
+ liberator.echomsg("No mapping found");
+ return;
+ }
+ commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
}
+}, {
+}, {
+ commands: function () {
+ function addMapCommands(ch, modes, modeDescription) {
+ // 0 args -> list all maps
+ // 1 arg -> list the maps starting with args
+ // 2 args -> map arg1 to arg*
+ function map(args, modes, noremap) {
+ if (!args.length) {
+ mappings.list(modes);
+ return;
+ }
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMMANDS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
+ let [lhs, rhs] = args;
+
+ if (!rhs) // list the mapping
+ mappings.list(modes, this._expandLeader(lhs));
+ else {
+ // this matches Vim's behaviour
+ if (/^<Nop>$/i.test(rhs))
+ noremap = true;
+
+ mappings.addUserMap(modes, [lhs],
+ "User defined mapping",
+ function (count) { events.feedkeys((count > -1 ? count : "") + this.rhs, this.noremap, this.silent); }, {
+ count: true,
+ rhs: events.canonicalKeys(rhs),
+ noremap: !!noremap,
+ silent: "<silent>" in args
+ });
+ }
+ }
- addMapCommands("", [modes.NORMAL, modes.VISUAL], "");
+ modeDescription = modeDescription ? " in " + modeDescription + " mode" : "";
- for (let mode in modes.mainModes)
- if (mode.char && !commands.get(mode.char + "map"))
- addMapCommands(mode.char,
- [m.mask for (m in modes.mainModes) if (m.char == mode.char)],
- [mode.disp.toLowerCase()]);
+ // :map, :noremap => NORMAL + VISUAL modes
+ function isMultiMode(map, cmd) {
+ return map.modes.indexOf(modules.modes.NORMAL) >= 0
+ && map.modes.indexOf(modules.modes.VISUAL) >= 0
+ && /^[nv](nore)?map$/.test(cmd);
+ }
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMPLETIONS /////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
+ const opts = {
+ completer: function (context, args) completion.userMapping(context, args, modes),
+ options: [
+ [["<silent>", "<Silent>"], commands.OPTION_NOARG]
+ ],
+ literal: 1,
+ serial: function () {
+ let noremap = this.name.indexOf("noremap") > -1;
+ return [
+ {
+ command: this.name,
+ options: map.silent ? { "<silent>": null } : {},
+ arguments: [map.names[0]],
+ literalArg: map.rhs
+ }
+ for (map in this._mappingsIterator(modes, this._user))
+ if (map.rhs && map.noremap == noremap && !isMultiMode(map, this.name))
+ ];
+ }
+ };
+
+ commands.add([ch ? ch + "m[ap]" : "map"],
+ "Map a key sequence" + modeDescription,
+ function (args) { map(args, modes, false); },
+ opts);
+
+ commands.add([ch + "no[remap]"],
+ "Map a key sequence without remapping keys" + modeDescription,
+ function (args) { map(args, modes, true); },
+ opts);
+
+ commands.add([ch + "mapc[lear]"],
+ "Remove all mappings" + modeDescription,
+ function () { modes.forEach(function (mode) { mappings.removeAll(mode); }); },
+ { argCount: "0" });
+
+ commands.add([ch + "unm[ap]"],
+ "Remove a mapping" + modeDescription,
+ function (args) {
+ args = args[0];
+
+ let found = false;
+ for (let [, mode] in Iterator(modes)) {
+ if (mappings.hasMap(mode, args)) {
+ mappings.remove(mode, args);
+ found = true;
+ }
+ }
+ if (!found)
+ liberator.echoerr("E31: No such mapping");
+ },
+ {
+ argCount: "1",
+ completer: function (context, args) completion.userMapping(context, args, modes)
+ });
+ }
- liberator.registerObserver("load_completion", function () {
+ addMapCommands("", [modes.NORMAL, modes.VISUAL], "");
+
+ for (let mode in modes.mainModes)
+ if (mode.char && !commands.get(mode.char + "map"))
+ addMapCommands(mode.char,
+ [m.mask for (m in modes.mainModes) if (m.char == mode.char)],
+ [mode.disp.toLowerCase()]);
+ },
+ completion: function () {
completion.setFunctionCompleter(mappings.get,
[
null,
- function (context, obj, args)
- {
+ function (context, obj, args) {
let mode = args[0];
return util.Array.flatten(
[
[[name, map.description] for ([i, name] in Iterator(map.names))]
- for ([i, map] in Iterator(user[mode].concat(main[mode])))
+ for ([i, map] in Iterator(this._user[mode].concat(this._main[mode])))
]);
}
]);
@@ -327,232 +487,18 @@ function Mappings() //{{{
// FIXME: have we decided on a 'standard' way to handle this clash? --djk
modes = modes || [modules.modes.NORMAL];
- if (args.completeArg == 0)
- {
+ if (args.completeArg == 0) {
let maps = [[m.names[0], ""] for (m in mappings.getUserIterator(modes))];
context.completions = maps;
}
};
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// PUBLIC SECTION //////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- return {
-
- // NOTE: just normal mode for now
- /** @property {Iterator(Map)} @private */
- __iterator__: function () mappingsIterator([modes.NORMAL], main),
-
- // used by :mkvimperatorrc to save mappings
- /**
- * Returns a user-defined mappings iterator for the specified
- * <b>mode</b>.
- *
- * @param {number} mode The mode to return mappings from.
- * @returns {Iterator(Map)}
- */
- getUserIterator: function (mode) mappingsIterator(mode, user),
-
- addMode: function (mode)
- {
- if (!(mode in user || mode in main))
- {
- main[mode] = [];
- user[mode] = [];
- }
- },
-
- /**
- * Adds a new default key mapping.
- *
- * @param {number[]} modes The modes that this mapping applies to.
- * @param {string[]} keys The key sequences which are bound to
- * <b>action</b>.
- * @param {string} description A description of the key mapping.
- * @param {function} action The action invoked by each key sequence.
- * @param {Object} extra An optional extra configuration hash.
- * @optional
- */
- add: function (modes, keys, description, action, extra)
- {
- addMap(new Map(modes, keys, description, action, extra));
- },
-
- /**
- * Adds a new user-defined key mapping.
- *
- * @param {number[]} modes The modes that this mapping applies to.
- * @param {string[]} keys The key sequences which are bound to
- * <b>action</b>.
- * @param {string} description A description of the key mapping.
- * @param {function} action The action invoked by each key sequence.
- * @param {Object} extra An optional extra configuration hash (see
- * {@link Map#extraInfo}).
- * @optional
- */
- addUserMap: function (modes, keys, description, action, extra)
- {
- keys = keys.map(expandLeader);
- extra = extra || {};
- extra.user = true;
- let map = new Map(modes, keys, description || "User defined mapping", action, extra);
-
- // remove all old mappings to this key sequence
- for (let [, name] in Iterator(map.names))
- {
- for (let [, mode] in Iterator(map.modes))
- removeMap(mode, name);
- }
-
- addMap(map);
- },
-
- /**
- * Returns the map from <b>mode</b> named <b>cmd</b>.
- *
- * @param {number} mode The mode to search.
- * @param {string} cmd The map name to match.
- * @returns {Map}
- */
- get: function (mode, cmd)
- {
- mode = mode || modes.NORMAL;
- return getMap(mode, cmd, user) || getMap(mode, cmd, main);
- },
-
- /**
- * Returns the default map from <b>mode</b> named <b>cmd</b>.
- *
- * @param {number} mode The mode to search.
- * @param {string} cmd The map name to match.
- * @returns {Map}
- */
- getDefault: function (mode, cmd)
- {
- mode = mode || modes.NORMAL;
- return getMap(mode, cmd, main);
- },
-
- /**
- * Returns an array of maps with names starting with but not equal to
- * <b>prefix</b>.
- *
- * @param {number} mode The mode to search.
- * @param {string} prefix The map prefix string to match.
- * @returns {Map[]}
- */
- getCandidates: function (mode, prefix)
- {
- let mappings = user[mode].concat(main[mode]);
- let matches = [];
-
- for (let [, map] in Iterator(mappings))
- {
- for (let [, name] in Iterator(map.names))
- {
- if (name.indexOf(prefix) == 0 && name.length > prefix.length)
- {
- // for < only return a candidate if it doesn't look like a <c-x> mapping
- if (prefix != "<" || !/^<.+>/.test(name))
- matches.push(map);
- }
- }
- }
-
- return matches;
- },
-
- /*
- * Returns the map leader string used to replace the special token
- * "<Leader>" when user mappings are defined.
- *
- * @returns {string}
- */
- // FIXME: property
- getMapLeader: function ()
- {
- let leaderRef = liberator.variableReference("mapleader");
- return leaderRef[0] ? leaderRef[0][leaderRef[1]] : "\\";
- },
-
- /**
- * Returns whether there is a user-defined mapping <b>cmd</b> for the
- * specified <b>mode</b>.
- *
- * @param {number} mode The mode to search.
- * @param {string} cmd The candidate key mapping.
- * @returns {boolean}
- */
- hasMap: function (mode, cmd) user[mode].some(function (map) map.hasName(cmd)),
-
- /**
- * Remove the user-defined mapping named <b>cmd</b> for <b>mode</b>.
- *
- * @param {number} mode The mode to search.
- * @param {string} cmd The map name to match.
- */
- remove: function (mode, cmd)
- {
- removeMap(mode, cmd);
- },
-
- /**
- * Remove all user-defined mappings for <b>mode</b>.
- *
- * @param {number} mode The mode to remove all mappings from.
- */
- removeAll: function (mode)
- {
- user[mode] = [];
- },
-
- /**
- * Lists all user-defined mappings matching <b>filter</b> for the
- * specified <b>modes</b>.
- *
- * @param {number[]} modes An array of modes to search.
- * @param {string} filter The filter string to match.
- */
- list: function (modes, filter)
- {
- let modeSign = "";
-
- // TODO: Vim hides "nv" in a :map and "v" and "n" in :vmap and
- // :nmap respectively if the map is not exclusive.
- modes.forEach(function (mode) {
- for (let m in modules.modes.mainModes)
- if (mode == m.mask && modeSign.indexOf(m.char) == -1)
- modeSign += m.char;
- });
-
- let maps = mappingsIterator(modes, user);
- if (filter)
- maps = [map for (map in maps) if (map.names[0] == filter)];
-
- let list = <table>
- {
- template.map(maps, function (map)
- template.map(map.names, function (name)
- <tr>
- <td>{modeSign} {name}</td>
- <td>{map.noremap ? "*" : " "}</td>
- <td>{map.rhs || "function () { ... }"}</td>
- </tr>))
- }
- </table>;
-
- // TODO: Move this to an ItemList to show this automatically
- if (list.*.length() == list.text().length())
- {
- liberator.echomsg("No mapping found");
- return;
- }
- commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
+ },
+ modes: function () {
+ for (let mode in modes) {
+ this._main[mode] = [];
+ this._user[mode] = [];
}
- };
- //}}}
-} //}}}
+ },
+});
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/marks.js b/common/content/marks.js
new file mode 100644
index 00000000..7213d8dd
--- /dev/null
+++ b/common/content/marks.js
@@ -0,0 +1,343 @@
+// Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
+//
+// This work is licensed for reuse under an MIT license. Details are
+// given in the LICENSE.txt file included with this file.
+
+/**
+ * @scope modules
+ * @instance marks
+ */
+const Marks = Module("marks", {
+ requires: ["storage"],
+
+ init: function init() {
+ this._localMarks = storage.newMap("local-marks", { store: true, privateData: true });
+ this._urlMarks = storage.newMap("url-marks", { store: true, privateData: true });
+
+ this._pendingJumps = [];
+
+ var appContent = document.getElementById("appcontent");
+ if (appContent)
+ appContent.addEventListener("load", this.closure._onPageLoad, true);
+ },
+
+ /**
+ * @property {Array} Returns all marks, both local and URL, in a sorted
+ * array.
+ */
+ get all() {
+ // local marks
+ let location = window.content.location.href;
+ let lmarks = [i for (i in this._localMarkIter()) if (i[1].location == location)];
+ lmarks.sort();
+
+ // URL marks
+ // FIXME: why does umarks.sort() cause a "Component is not available =
+ // NS_ERROR_NOT_AVAILABLE" exception when used here?
+ let umarks = [i for (i in this._urlMarks)];
+ umarks.sort(function (a, b) a[0].localeCompare(b[0]));
+
+ return lmarks.concat(umarks);
+ },
+
+
+ /**
+ * Add a named mark for the current buffer, at its current position.
+ * If mark matches [A-Z], it's considered a URL mark, and will jump to
+ * the same position at the same URL no matter what buffer it's
+ * selected from. If it matches [a-z'"], it's a local mark, and can
+ * only be recalled from a buffer with a matching URL.
+ *
+ * @param {string} mark The mark name.
+ * @param {boolean} silent Whether to output error messages.
+ */
+ // TODO: add support for frameset pages
+ add: function (mark, silent) {
+ let win = window.content;
+ let doc = win.document;
+
+ if (!doc.body)
+ return;
+ if (doc.body instanceof HTMLFrameSetElement) {
+ if (!silent)
+ liberator.echoerr("Marks support for frameset pages not implemented yet");
+ return;
+ }
+
+ let x = win.scrollMaxX ? win.pageXOffset / win.scrollMaxX : 0;
+ let y = win.scrollMaxY ? win.pageYOffset / win.scrollMaxY : 0;
+ let position = { x: x, y: y };
+
+ if (Marks.isURLMark(mark)) {
+ this._urlMarks.set(mark, { location: win.location.href, position: position, tab: tabs.getTab() });
+ if (!silent)
+ liberator.log("Adding URL mark: " + Marks.markToString(mark, this._urlMarks.get(mark)), 5);
+ }
+ else if (Marks.isLocalMark(mark)) {
+ // remove any previous mark of the same name for this location
+ this._removeLocalMark(mark);
+ if (!this._localMarks.get(mark))
+ this._localMarks.set(mark, []);
+ let vals = { location: win.location.href, position: position };
+ this._localMarks.get(mark).push(vals);
+ if (!silent)
+ liberator.log("Adding local mark: " + Marks.markToString(mark, vals), 5);
+ }
+ },
+
+ /**
+ * Remove all marks matching <b>filter</b>. If <b>special</b> is
+ * given, removes all local marks.
+ *
+ * @param {string} filter A string containing one character for each
+ * mark to be removed.
+ * @param {boolean} special Whether to delete all local marks.
+ */
+ // FIXME: Shouldn't special be replaced with a null filter?
+ remove: function (filter, special) {
+ if (special) {
+ // :delmarks! only deletes a-z marks
+ for (let [mark, ] in this._localMarks)
+ this._removeLocalMark(mark);
+ }
+ else {
+ for (let [mark, ] in this._urlMarks) {
+ if (filter.indexOf(mark) >= 0)
+ this._removeURLMark(mark);
+ }
+ for (let [mark, ] in this._localMarks) {
+ if (filter.indexOf(mark) >= 0)
+ this._removeLocalMark(mark);
+ }
+ }
+ },
+
+ /**
+ * Jumps to the named mark. See {@link #add}
+ *
+ * @param {string} mark The mark to jump to.
+ */
+ jumpTo: function (mark) {
+ let ok = false;
+
+ if (Marks.isURLMark(mark)) {
+ let slice = this._urlMarks.get(mark);
+ if (slice && slice.tab && slice.tab.linkedBrowser) {
+ if (slice.tab.parentNode != getBrowser().tabContainer) {
+ this._pendingJumps.push(slice);
+ // NOTE: this obviously won't work on generated pages using
+ // non-unique URLs :(
+ liberator.open(slice.location, liberator.NEW_TAB);
+ return;
+ }
+ let index = tabs.index(slice.tab);
+ if (index != -1) {
+ tabs.select(index);
+ let win = slice.tab.linkedBrowser.contentWindow;
+ if (win.location.href != slice.location) {
+ this._pendingJumps.push(slice);
+ win.location.href = slice.location;
+ return;
+ }
+ liberator.log("Jumping to URL mark: " + Marks.markToString(mark, slice), 5);
+ buffer.scrollToPercent(slice.position.x * 100, slice.position.y * 100);
+ ok = true;
+ }
+ }
+ }
+ else if (Marks.isLocalMark(mark)) {
+ let win = window.content;
+ let slice = this._localMarks.get(mark) || [];
+
+ for (let [, lmark] in Iterator(slice)) {
+ if (win.location.href == lmark.location) {
+ liberator.log("Jumping to local mark: " + Marks.markToString(mark, lmark), 5);
+ buffer.scrollToPercent(lmark.position.x * 100, lmark.position.y * 100);
+ ok = true;
+ break;
+ }
+ }
+ }
+
+ if (!ok)
+ liberator.echoerr("E20: Mark not set");
+ },
+
+ /**
+ * List all marks matching <b>filter</b>.
+ *
+ * @param {string} filter
+ */
+ list: function (filter) {
+ let marks = this.all;
+
+ liberator.assert(marks.length > 0, "No marks set");
+
+ if (filter.length > 0) {
+ marks = marks.filter(function (mark) filter.indexOf(mark[0]) >= 0);
+ liberator.assert(marks.length > 0, "E283: No marks matching " + filter.quote());
+ }
+
+ let list = template.tabular(
+ ["Mark", "Line", "Column", "File"],
+ ["", "text-align: right", "text-align: right", "color: green"],
+ ([mark[0],
+ Math.round(mark[1].position.x * 100) + "%",
+ Math.round(mark[1].position.y * 100) + "%",
+ mark[1].location]
+ for ([, mark] in Iterator(marks))));
+ commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
+ },
+
+ _onPageLoad: function _onPageLoad(event) {
+ let win = event.originalTarget.defaultView;
+ for (let [i, mark] in Iterator(this._pendingJumps)) {
+ if (win && win.location.href == mark.location) {
+ buffer.scrollToPercent(mark.position.x * 100, mark.position.y * 100);
+ this._pendingJumps.splice(i, 1);
+ return;
+ }
+ }
+ },
+
+ _removeLocalMark: function _removeLocalMark(mark) {
+ let localmark = this._localMarks.get(mark);
+ if (localmark) {
+ let win = window.content;
+ for (let [i, ] in Iterator(localmark)) {
+ if (localmark[i].location == win.location.href) {
+ liberator.log("Deleting local mark: " + Marks.markToString(mark, localmark[i]), 5);
+ localmark.splice(i, 1);
+ if (localmark.length == 0)
+ this._localMarks.remove(mark);
+ break;
+ }
+ }
+ }
+ },
+
+ _removeURLMark: function _removeURLMark(mark) {
+ let urlmark = this._urlMarks.get(mark);
+ if (urlmark) {
+ liberator.log("Deleting URL mark: " + Marks.markToString(mark, urlmark), 5);
+ this._urlMarks.remove(mark);
+ }
+ },
+
+ _localMarkIter: function _localMarkIter() {
+ for (let [mark, value] in this._localMarks)
+ for (let [, val] in Iterator(value))
+ yield [mark, val];
+ },
+
+}, {
+ markToString: function markToString(name, mark) {
+ return name + ", " + mark.location +
+ ", (" + Math.round(mark.position.x * 100) +
+ "%, " + Math.round(mark.position.y * 100) + "%)" +
+ (("tab" in mark) ? ", tab: " + tabs.index(mark.tab) : "");
+ },
+
+ isLocalMark: function isLocalMark(mark) /^['`a-z]$/.test(mark),
+ isURLMark: function isURLMark(mark) /^[A-Z0-9]$/.test(mark),
+}, {
+ mappings: function () {
+ var myModes = config.browserModes;
+
+ mappings.add(myModes,
+ ["m"], "Set mark at the cursor position",
+ function (arg) {
+ if (/[^a-zA-Z]/.test(arg))
+ return void liberator.beep();
+
+ marks.add(arg);
+ },
+ { arg: true });
+
+ mappings.add(myModes,
+ ["'", "`"], "Jump to the mark in the current buffer",
+ function (arg) { marks.jumpTo(arg); },
+ { arg: true });
+ },
+
+ commands: function () {
+ commands.add(["delm[arks]"],
+ "Delete the specified marks",
+ function (args) {
+ let special = args.bang;
+ args = args.string;
+
+ // assert(special ^ args)
+ liberator.assert( special || args, "E471: Argument required");
+ liberator.assert(!special || !args, "E474: Invalid argument");
+
+ let matches;
+ if (matches = args.match(/(?:(?:^|[^a-zA-Z0-9])-|-(?:$|[^a-zA-Z0-9])|[^a-zA-Z0-9 -]).*/)) {
+ // NOTE: this currently differs from Vim's behavior which
+ // deletes any valid marks in the arg list, up to the first
+ // invalid arg, as well as giving the error message.
+ liberator.echoerr("E475: Invalid argument: " + matches[0]);
+ return;
+ }
+ // check for illegal ranges - only allow a-z A-Z 0-9
+ if (matches = args.match(/[a-zA-Z0-9]-[a-zA-Z0-9]/g)) {
+ for (let i = 0; i < matches.length; i++) {
+ let start = matches[i][0];
+ let end = matches[i][2];
+ if (/[a-z]/.test(start) != /[a-z]/.test(end) ||
+ /[A-Z]/.test(start) != /[A-Z]/.test(end) ||
+ /[0-9]/.test(start) != /[0-9]/.test(end) ||
+ start > end)
+ {
+ liberator.echoerr("E475: Invalid argument: " + args.match(matches[i] + ".*")[0]);
+ return;
+ }
+ }
+ }
+
+ marks.remove(args, special);
+ },
+ {
+ bang: true,
+ completer: function (context) completion.mark(context)
+ });
+
+ commands.add(["ma[rk]"],
+ "Mark current location within the web page",
+ function (args) {
+ let mark = args[0];
+ liberator.assert(mark.length <= 1, "E488: Trailing characters");
+ liberator.assert(/[a-zA-Z]/.test(mark),
+ "E191: Argument must be a letter or forward/backward quote");
+
+ marks.add(mark);
+ },
+ { argCount: "1" });
+
+ commands.add(["marks"],
+ "Show all location marks of current web page",
+ function (args) {
+ args = args.string;
+
+ // ignore invalid mark characters unless there are no valid mark chars
+ liberator.assert(!args || /[a-zA-Z]/.test(args),
+ "E283: No marks matching " + args.quote());
+
+ let filter = args.replace(/[^a-zA-Z]/g, "");
+ marks.list(filter);
+ });
+ },
+
+ completion: function () {
+ completion.mark = function mark(context) {
+ function percent(i) Math.round(i * 100);
+
+ // FIXME: Line/Column doesn't make sense with %
+ context.title = ["Mark", "Line Column File"];
+ context.keys.description = function ([, m]) percent(m.position.y) + "% " + percent(m.position.x) + "% " + m.location;
+ context.completions = marks.all;
+ };
+ },
+});
+
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/modes.js b/common/content/modes.js
index ebfc7e65..c47ea658 100644
--- a/common/content/modes.js
+++ b/common/content/modes.js
@@ -5,29 +5,55 @@
/** @scope modules */
-const modes = (function () //{{{
-{
- ////////////////////////////////////////////////////////////////////////////////
- ////////////////////// PRIVATE SECTION /////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- var main = 1; // NORMAL
- var extended = 0; // NONE
-
- var lastShown = null;
-
- var passNextKey = false;
- var passAllKeys = false;
- var isRecording = false;
- var isReplaying = false; // playing a macro
-
- var modeStack = [];
-
- function getModeMessage()
- {
- if (passNextKey && !passAllKeys)
+const Modes = Module("modes", {
+ requires: ["util"],
+
+ init: function () {
+ this._main = 1; // NORMAL
+ this._extended = 0; // NONE
+
+ this._lastShown = null;
+
+ this._passNextKey = false;
+ this._passAllKeys = false;
+ this._isRecording = false;
+ this._isReplaying = false; // playing a macro
+
+ this._modeStack = [];
+
+ this._mainModes = [self.NONE];
+ this._lastMode = 0;
+ this._modeMap = {};
+
+ // main modes, only one should ever be active
+ this.addMode("NORMAL", { char: "n", display: -1 });
+ this.addMode("INSERT", { char: "i", input: true });
+ this.addMode("VISUAL", { char: "v", display: function () "VISUAL" + (this._extended & modes.LINE ? " LINE" : "") });
+ this.addMode("COMMAND_LINE", { char: "c", input: true });
+ this.addMode("CARET"); // text cursor is visible
+ this.addMode("TEXTAREA", { char: "i" });
+ this.addMode("EMBED", { input: true });
+ this.addMode("CUSTOM", { display: function () plugins.mode });
+ // this._extended modes, can include multiple modes, and even main modes
+ this.addMode("EX", true);
+ this.addMode("HINTS", true);
+ this.addMode("INPUT_MULTILINE", true);
+ this.addMode("OUTPUT_MULTILINE", true);
+ this.addMode("SEARCH_FORWARD", true);
+ this.addMode("SEARCH_BACKWARD", true);
+ this.addMode("SEARCH_VIEW_FORWARD", true);
+ this.addMode("SEARCH_VIEW_BACKWARD", true);
+ this.addMode("MENU", true); // a popupmenu is active
+ this.addMode("LINE", true); // linewise visual mode
+ this.addMode("PROMPT", true);
+
+ config.modes.forEach(function (mode) { this.addMode.apply(this, mode); }, this);
+ },
+
+ _getModeMessage: function () {
+ if (this._passNextKey && !this._passAllKeys)
return "-- PASS THROUGH (next) --";
- else if (passAllKeys && !passNextKey)
+ else if (this._passAllKeys && !this._passNextKey)
return "-- PASS THROUGH --";
// when recording a macro
@@ -38,57 +64,52 @@ const modes = (function () //{{{
macromode = "replaying";
let ext = "";
- if (extended & modes.MENU) // TODO: desirable?
+ if (this._extended & modes.MENU) // TODO: desirable?
ext += " (menu)";
ext += " --" + macromode;
- if (main in modeMap && typeof modeMap[main].display == "function")
- return "-- " + modeMap[main].display() + ext;
+ if (this._main in this._modeMap && typeof this._modeMap[this._main].display == "function")
+ return "-- " + this._modeMap[this._main].display() + ext;
return macromode;
- }
+ },
// NOTE: Pay attention that you don't run into endless loops
// Usually you should only indicate to leave a special mode like HINTS
// by calling modes.reset() and adding the stuff which is needed
// for its cleanup here
- function handleModeChange(oldMode, newMode, oldExtended)
- {
-
- switch (oldMode)
- {
- case modes.TEXTAREA:
- case modes.INSERT:
- editor.unselectText();
- break;
-
- case modes.VISUAL:
- if (newMode == modes.CARET)
- {
- try
- { // clear any selection made; a simple if (selection) does not work
- let selection = window.content.getSelection();
- selection.collapseToStart();
- }
- catch (e) {}
+ _handleModeChange: function (oldMode, newMode, oldExtended) {
+
+ switch (oldMode) {
+ case modes.TEXTAREA:
+ case modes.INSERT:
+ editor.unselectText();
+ break;
+
+ case modes.VISUAL:
+ if (newMode == modes.CARET) {
+ try { // clear any selection made; a simple if (selection) does not work
+ let selection = window.content.getSelection();
+ selection.collapseToStart();
}
- else
- editor.unselectText();
- break;
-
- case modes.CUSTOM:
- plugins.stop();
- break;
-
- case modes.COMMAND_LINE:
- // clean up for HINT mode
- if (oldExtended & modes.HINTS)
- hints.hide();
- commandline.close();
- break;
+ catch (e) {}
+ }
+ else
+ editor.unselectText();
+ break;
+
+ case modes.CUSTOM:
+ plugins.stop();
+ break;
+
+ case modes.COMMAND_LINE:
+ // clean up for HINT mode
+ if (oldExtended & modes.HINTS)
+ hints.hide();
+ commandline.close();
+ break;
}
- if (newMode == modes.NORMAL)
- {
+ if (newMode == modes.NORMAL) {
// disable caret mode when we want to switch to normal mode
if (options.getPref("accessibility.browsewithcaret"))
options.setPref("accessibility.browsewithcaret", false);
@@ -96,185 +117,134 @@ const modes = (function () //{{{
statusline.updateUrl();
liberator.focusContent(true);
}
- }
+ },
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// PUBLIC SECTION //////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
+ NONE: 0,
- const self = {
+ __iterator__: function () util.Array.itervalues(this.all),
- NONE: 0,
+ get all() this._mainModes.slice(),
- __iterator__: function () util.Array.itervalues(this.all),
+ get mainModes() (mode for ([k, mode] in Iterator(modes._modeMap)) if (!mode.extended && mode.name == k)),
- get all() mainModes.slice(),
+ get mainMode() this._modeMap[this._main],
- get mainModes() (mode for ([k, mode] in Iterator(modeMap)) if (!mode.extended && mode.name == k)),
-
- get mainMode() modeMap[main],
+ addMode: function (name, extended, options) {
+ let disp = name.replace("_", " ", "g");
+ this[name] = 1 << this._lastMode++;
+ if (typeof extended == "object") {
+ options = extended;
+ extended = false;
+ }
+ this._modeMap[name] = this._modeMap[this[name]] = util.extend({
+ extended: extended,
+ count: true,
+ input: false,
+ mask: this[name],
+ name: name,
+ disp: disp
+ }, options);
+ this._modeMap[name].display = this._modeMap[name].display || function () disp;
+ if (!extended)
+ this._mainModes.push(this[name]);
+ if ("mappings" in modules)
+ mappings.addMode(this[name]);
+ },
+
+ getMode: function (name) this._modeMap[name],
+
+ // show the current mode string in the command line
+ show: function () {
+ let msg = "";
+ if (options["showmode"])
+ msg = this._getModeMessage();
+
+ commandline.echo(msg, "ModeMsg", commandline.FORCE_SINGLELINE);
+ },
+
+ // add/remove always work on the this._extended mode only
+ add: function (mode) {
+ this._extended |= mode;
+ this.show();
+ },
+
+ // helper function to set both modes in one go
+ // if silent == true, you also need to take care of the mode handling changes yourself
+ set: function (mainMode, extendedMode, silent, stack) {
+ silent = (silent || this._main == mainMode && this._extended == extendedMode);
+ // if a this._main mode is set, the this._extended is always cleared
+ let oldMain = this._main, oldExtended = this._extended;
+ if (typeof extendedMode === "number")
+ this._extended = extendedMode;
+ if (typeof mainMode === "number") {
+ this._main = mainMode;
+ if (!extendedMode)
+ this._extended = modes.NONE;
+
+ if (this._main != oldMain)
+ this._handleModeChange(oldMain, mainMode, oldExtended);
+ }
+ liberator.triggerObserver("modeChange", [oldMain, oldExtended], [this._main, this._extended], stack);
- addMode: function (name, extended, options)
- {
- let disp = name.replace("_", " ", "g");
- this[name] = 1 << lastMode++;
- if (typeof extended == "object")
- {
- options = extended;
- extended = false;
- }
- modeMap[name] = modeMap[this[name]] = util.extend({
- extended: extended,
- count: true,
- input: false,
- mask: this[name],
- name: name,
- disp: disp
- }, options);
- modeMap[name].display = modeMap[name].display || function () disp;
- if (!extended)
- mainModes.push(this[name]);
- if ("mappings" in modules)
- mappings.addMode(this[name]);
- },
-
- getMode: function (name) modeMap[name],
-
- // show the current mode string in the command line
- show: function ()
- {
- let msg = "";
- if (options["showmode"])
- msg = getModeMessage();
-
- commandline.echo(msg, "ModeMsg", commandline.FORCE_SINGLELINE);
- },
-
- // add/remove always work on the extended mode only
- add: function (mode)
- {
- extended |= mode;
+ if (!silent)
this.show();
- },
-
- // helper function to set both modes in one go
- // if silent == true, you also need to take care of the mode handling changes yourself
- set: function (mainMode, extendedMode, silent, stack)
- {
- silent = (silent || main == mainMode && extended == extendedMode);
- // if a main mode is set, the extended is always cleared
- let oldMain = main, oldExtended = extended;
- if (typeof extendedMode === "number")
- extended = extendedMode;
- if (typeof mainMode === "number")
- {
- main = mainMode;
- if (!extendedMode)
- extended = modes.NONE;
-
- if (main != oldMain)
- handleModeChange(oldMain, mainMode, oldExtended);
- }
- liberator.triggerObserver("modeChange", [oldMain, oldExtended], [main, extended], stack);
-
- if (!silent)
- this.show();
- },
-
- push: function (mainMode, extendedMode, silent)
- {
- modeStack.push([main, extended]);
- this.set(mainMode, extendedMode, silent, { push: modeStack[modeStack.length - 1] });
- },
-
- pop: function (silent)
- {
- let a = modeStack.pop();
- if (a)
- this.set(a[0], a[1], silent, { pop: a });
- else
- this.reset(silent);
- },
-
- // TODO: Deprecate this in favor of addMode? --Kris
- // Ya --djk
- setCustomMode: function (modestr, oneventfunc, stopfunc)
- {
- // TODO this.plugin[id]... ('id' maybe submode or what..)
- plugins.mode = modestr;
- plugins.onEvent = oneventfunc;
- plugins.stop = stopfunc;
- },
-
- // keeps recording state
- reset: function (silent)
- {
- modeStack = [];
- if (config.isComposeWindow)
- this.set(modes.COMPOSE, modes.NONE, silent);
- else
- this.set(modes.NORMAL, modes.NONE, silent);
- },
-
- remove: function (mode)
- {
- if (extended & mode)
- {
- extended &= ~mode;
- this.show();
- }
- },
-
- get passNextKey() passNextKey,
- set passNextKey(value) { passNextKey = value; this.show(); },
-
- get passAllKeys() passAllKeys,
- set passAllKeys(value) { passAllKeys = value; this.show(); },
-
- get isRecording() isRecording,
- set isRecording(value) { isRecording = value; this.show(); },
-
- get isReplaying() isReplaying,
- set isReplaying(value) { isReplaying = value; this.show(); },
-
- get main() main,
- set main(value) { this.set(value); },
-
- get extended() extended,
- set extended(value) { this.set(null, value); }
-
- };
-
- var mainModes = [self.NONE];
- var lastMode = 0;
- var modeMap = {};
-
- // main modes, only one should ever be active
- self.addMode("NORMAL", { char: "n", display: -1 });
- self.addMode("INSERT", { char: "i", input: true });
- self.addMode("VISUAL", { char: "v", display: function () "VISUAL" + (extended & modes.LINE ? " LINE" : "") });
- self.addMode("COMMAND_LINE", { char: "c", input: true });
- self.addMode("CARET"); // text cursor is visible
- self.addMode("TEXTAREA", { char: "i" });
- self.addMode("EMBED", { input: true });
- self.addMode("CUSTOM", { display: function () plugins.mode });
- // extended modes, can include multiple modes, and even main modes
- self.addMode("EX", true);
- self.addMode("HINTS", true);
- self.addMode("INPUT_MULTILINE", true);
- self.addMode("OUTPUT_MULTILINE", true);
- self.addMode("SEARCH_FORWARD", true);
- self.addMode("SEARCH_BACKWARD", true);
- self.addMode("SEARCH_VIEW_FORWARD", true);
- self.addMode("SEARCH_VIEW_BACKWARD", true);
- self.addMode("MENU", true); // a popupmenu is active
- self.addMode("LINE", true); // linewise visual mode
- self.addMode("PROMPT", true);
-
- config.modes.forEach(function (mode) { self.addMode.apply(self, mode); });
-
- return self;
- //}}}
-})(); //}}}
+ },
+
+ push: function (mainMode, extendedMode, silent) {
+ this._modeStack.push([this._main, this._extended]);
+ this.set(mainMode, extendedMode, silent, { push: this._modeStack[this._modeStack.length - 1] });
+ },
+
+ pop: function (silent) {
+ let a = this._modeStack.pop();
+ if (a)
+ this.set(a[0], a[1], silent, { pop: a });
+ else
+ this.reset(silent);
+ },
+
+ // TODO: Deprecate this in favor of addMode? --Kris
+ // Ya --djk
+ setCustomMode: function (modestr, oneventfunc, stopfunc) {
+ // TODO this.plugin[id]... ('id' maybe submode or what..)
+ plugins.mode = modestr;
+ plugins.onEvent = oneventfunc;
+ plugins.stop = stopfunc;
+ },
+
+ // keeps recording state
+ reset: function (silent) {
+ this._modeStack = [];
+ if (config.isComposeWindow)
+ this.set(modes.COMPOSE, modes.NONE, silent);
+ else
+ this.set(modes.NORMAL, modes.NONE, silent);
+ },
+
+ remove: function (mode) {
+ if (this._extended & mode) {
+ this._extended &= ~mode;
+ this.show();
+ }
+ },
+
+ get passNextKey() this._passNextKey,
+ set passNextKey(value) { this._passNextKey = value; this.show(); },
+
+ get passAllKeys() this._passAllKeys,
+ set passAllKeys(value) { this._passAllKeys = value; this.show(); },
+
+ get isRecording() this._isRecording,
+ set isRecording(value) { this._isRecording = value; this.show(); },
+
+ get isReplaying() this._isReplaying,
+ set isReplaying(value) { this._isReplaying = value; this.show(); },
+
+ get main() this._main,
+ set main(value) { this.set(value); },
+
+ get extended() this._extended,
+ set extended(value) { this.set(null, value); }
+});
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/modules.js b/common/content/modules.js
new file mode 100644
index 00000000..7e1a0b1b
--- /dev/null
+++ b/common/content/modules.js
@@ -0,0 +1,73 @@
+
+const ModuleBase = Class("ModuleBase", { requires: [] });
+function Module(name, inst, clas, moduleInit) {
+ const module = Class(name, ModuleBase, inst, clas);
+ module.INIT = moduleInit || {};
+ module.requires = inst.requires || [];
+ Module.list.push(module);
+ Module.constructors[name] = module;
+ return module;
+}
+Module.list = [];
+Module.constructors = {};
+
+window.addEventListener("load", function () {
+ function dump(str) window.dump(String.replace(str, /\n?$/, "\n").replace(/^/m, config.name.toLowerCase() + ": "));
+ const start = Date.now();
+ const deferredInit = { load: [] };
+ const seen = set();
+
+ function load(module, prereq) {
+ try {
+ if (module.name in modules)
+ return;
+ if (module.name in seen)
+ throw Error("Module dependency loop.");
+ set.add(seen, module.name);
+
+ for (let dep in values(module.requires))
+ load(Module.constructors[dep], module.name);
+
+ dump("Load" + (isstring(prereq) ? " " + prereq + " dependency: " : ": ") + module.name);
+ modules[module.name] = module();
+
+ function init(mod, module)
+ function () module.INIT[mod].call(modules[module.name], modules[mod]);
+ for (let [mod, ] in iter(module.INIT))
+ try {
+ if (mod in modules)
+ init(mod, module)();
+ else {
+ deferredInit[mod] = deferredInit[mod] || [];
+ deferredInit[mod].push(init(mod, module));
+ }
+ }
+ catch(e) {
+ if (modules.liberator)
+ liberator.reportError(e);
+ }
+ for (let [, fn] in iter(deferredInit[module.name] || []))
+ fn();
+ }
+ catch (e) {
+ dump("Loading " + (module && module.name) + ": " + e);
+ if (e.stack)
+ dump(e.stack);
+ }
+ }
+ Module.list.forEach(load);
+ deferredInit['load'].forEach(call)
+
+ for (let module in values(Module.list))
+ delete module.INIT;
+
+ dump("Loaded in " + (Date.now() - start) + "ms\n");
+}, false);
+
+window.addEventListener("unload", function () {
+ for (let [, mod] in iter(modules))
+ if (mod instanceof ModuleBase && 'destroy' in mod)
+ mod.destroy();
+}, false);
+
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/options.js b/common/content/options.js
index a908deae..32e63ee4 100644
--- a/common/content/options.js
+++ b/common/content/options.js
@@ -26,97 +26,93 @@
* @optional
* @private
*/
-function Option(names, description, type, defaultValue, extraInfo) //{{{
-{
- if (!extraInfo)
- extraInfo = {};
-
- /** @property {string} The option's canonical name. */
- this.name = names[0];
- /** @property {string[]} All names by which this option is identified. */
- this.names = names;
- /**
- * @property {string} The option's data type. One of:
- * "boolean" - Boolean E.g. true
- * "number" - Integer E.g. 1
- * "string" - String E.g. "Vimperator"
- * "charlist" - Character list E.g. "rb"
- * "stringlist" - String list E.g. "homepage,quickmark,tabopen,paste"
- */
- this.type = type;
- /**
- * @property {number} The scope of the option. This can be local, global,
- * or both.
- * @see Options#OPTION_SCOPE_LOCAL
- * @see Options#OPTION_SCOPE_GLOBAL
- * @see Options#OPTION_SCOPE_BOTH
- */
- this.scope = (extraInfo.scope & options.OPTION_SCOPE_BOTH) || options.OPTION_SCOPE_GLOBAL; // XXX set to BOTH by default someday? - kstep
- /**
- * @property {string} This option's description, as shown in :optionusage.
- */
- this.description = description || "";
+const Option = Class("Option", {
+ init: function (names, description, type, defaultValue, extraInfo) {
+ if (!extraInfo)
+ extraInfo = {};
+
+ /** @property {string} The option's canonical name. */
+ this.name = names[0];
+ /** @property {string[]} All names by which this option is identified. */
+ this.names = names;
+ /**
+ * @property {string} The option's data type. One of:
+ * "boolean" - Boolean E.g. true
+ * "number" - Integer E.g. 1
+ * "string" - String E.g. "Vimperator"
+ * "charlist" - Character list E.g. "rb"
+ * "stringlist" - String list E.g. "homepage,quickmark,tabopen,paste"
+ */
+ this.type = type;
+ /**
+ * @property {number} The scope of the option. This can be local, global,
+ * or both.
+ * @see Options#OPTION_SCOPE_LOCAL
+ * @see Options#OPTION_SCOPE_GLOBAL
+ * @see Options#OPTION_SCOPE_BOTH
+ */
+ this.scope = (extraInfo.scope & options.OPTION_SCOPE_BOTH) || options.OPTION_SCOPE_GLOBAL; // XXX set to BOTH by default someday? - kstep
+ /**
+ * @property {string} This option's description, as shown in :optionusage.
+ */
+ this.description = description || "";
- /**
- * @property {value} The option's default value. This value will be used
- * unless the option is explicitly set either interactively or in an RC
- * file or plugin.
- */
- this.defaultValue = (defaultValue === undefined) ? null : defaultValue; // "", 0 are valid default values
+ /**
+ * @property {value} The option's default value. This value will be used
+ * unless the option is explicitly set either interactively or in an RC
+ * file or plugin.
+ */
+ this.defaultValue = (defaultValue === undefined) ? null : defaultValue; // "", 0 are valid default values
- /**
- * @property {function} The function called when the option value is set.
- */
- this.setter = extraInfo.setter || null;
- /**
- * @property {function} The function called when the option value is read.
- */
- this.getter = extraInfo.getter || null;
- /**
- * @property {function(CompletionContext, Args)} This option's completer.
- * @see CompletionContext
- */
- this.completer = extraInfo.completer || null;
- /**
- * @property {function} The function called to validate the option's value
- * when set.
- */
- this.validator = extraInfo.validator || null;
- /**
- * @property The function called to determine whether the option already
- * contains a specified value.
- * @see #has
- */
- this.checkHas = extraInfo.checkHas || null;
+ /**
+ * @property {function} The function called when the option value is set.
+ */
+ this.setter = extraInfo.setter || null;
+ /**
+ * @property {function} The function called when the option value is read.
+ */
+ this.getter = extraInfo.getter || null;
+ /**
+ * @property {function(CompletionContext, Args)} This option's completer.
+ * @see CompletionContext
+ */
+ this.completer = extraInfo.completer || null;
+ /**
+ * @property {function} The function called to validate the option's value
+ * when set.
+ */
+ this.validator = extraInfo.validator || null;
+ /**
+ * @property The function called to determine whether the option already
+ * contains a specified value.
+ * @see #has
+ */
+ this.checkHas = extraInfo.checkHas || null;
- /**
- * @property {boolean} Set to true whenever the option is first set. This
- * is useful to see whether it was changed from its default value
- * interactively or by some RC file.
- */
- this.hasChanged = false;
- /**
- * @property {nsIFile} The script in which this option was last set. null
- * implies an interactive command.
- */
- this.setFrom = null;
-
- // add no{option} variant of boolean {option} to this.names
- if (this.type == "boolean")
- {
- this.names = []; // reset since order is important
- for (let [, name] in Iterator(names))
- {
- this.names.push(name);
- this.names.push("no" + name);
+ /**
+ * @property {boolean} Set to true whenever the option is first set. This
+ * is useful to see whether it was changed from its default value
+ * interactively or by some RC file.
+ */
+ this.hasChanged = false;
+ /**
+ * @property {nsIFile} The script in which this option was last set. null
+ * implies an interactive command.
+ */
+ this.setFrom = null;
+
+ // add no{option} variant of boolean {option} to this.names
+ if (this.type == "boolean") {
+ this.names = []; // reset since order is important
+ for (let [, name] in Iterator(names)) {
+ this.names.push(name);
+ this.names.push("no" + name);
+ }
}
- }
-
- if (this.globalValue == undefined)
- this.globalValue = this.defaultValue;
-}
-Option.prototype = {
+ if (this.globalValue == undefined)
+ this.globalValue = this.defaultValue;
+ },
/** @property {value} The option's global value. @see #scope */
get globalValue() options.store.get(this.name),
@@ -129,8 +125,7 @@ Option.prototype = {
* @param {value} value The option value.
* @returns {value|string[]}
*/
- parseValues: function (value)
- {
+ parseValues: function (value) {
if (this.type == "stringlist")
return (value === "") ? [] : value.split(",");
if (this.type == "charlist")
@@ -145,8 +140,7 @@ Option.prototype = {
* @param {value|string[]} values The option value.
* @returns {value}
*/
- joinValues: function (values)
- {
+ joinValues: function (values) {
if (this.type == "stringlist")
return values.join(",");
if (this.type == "charlist")
@@ -175,8 +169,7 @@ Option.prototype = {
* @param {number} scope The scope to apply these values to (see
* {@link Option#scope}).
*/
- setValues: function (values, scope)
- {
+ setValues: function (values, scope) {
this.set(this.joinValues(values), scope || this.scope);
},
@@ -189,10 +182,8 @@ Option.prototype = {
* {@link Option#scope}).
* @returns {value}
*/
- get: function (scope)
- {
- if (scope)
- {
+ get: function (scope) {
+ if (scope) {
if ((scope & this.scope) == 0) // option doesn't exist in this scope
return null;
}
@@ -200,6 +191,7 @@ Option.prototype = {
scope = this.scope;
let aValue;
+
if (liberator.has("tabs") && (scope & options.OPTION_SCOPE_LOCAL))
aValue = tabs.options[this.name];
if ((scope & options.OPTION_SCOPE_GLOBAL) && (aValue == undefined))
@@ -220,8 +212,7 @@ Option.prototype = {
* @param {number} scope The scope to apply this value to (see
* {@link Option#scope}).
*/
- set: function (newValue, scope)
- {
+ set: function (newValue, scope) {
scope = scope || this.scope;
if ((scope & this.scope) == 0) // option doesn't exist in this scope
return null;
@@ -250,8 +241,7 @@ Option.prototype = {
*
* @returns {boolean}
*/
- has: function ()
- {
+ has: function () {
let self = this;
let test = function (val) values.indexOf(val) >= 0;
if (this.checkHas)
@@ -273,8 +263,7 @@ Option.prototype = {
* Returns whether the specified <b>values</b> are valid for this option.
* @see Option#validator
*/
- isValidValue: function (values)
- {
+ isValidValue: function (values) {
if (this.validator)
return this.validator(values);
else
@@ -284,8 +273,7 @@ Option.prototype = {
/**
* Resets the option to its default value.
*/
- reset: function ()
- {
+ reset: function () {
this.value = this.defaultValue;
},
@@ -298,97 +286,91 @@ Option.prototype = {
* {@link #scope}).
* @param {boolean} invert Whether this is an invert boolean operation.
*/
- op: function (operator, values, scope, invert)
- {
+ op: function (operator, values, scope, invert) {
let newValue = null;
let self = this;
- switch (this.type)
- {
- case "boolean":
- if (operator != "=")
- break;
-
- if (invert)
- newValue = !this.value;
- else
- newValue = values;
+ switch (this.type) {
+ case "boolean":
+ if (operator != "=")
break;
- case "number":
- // TODO: support floats? Validators need updating.
- if (!/^[+-]?(?:0x[0-9a-f]+|0[0-7]+|0|[1-9]\d*)$/i.test(values))
- return "E521: Number required after := " + this.name + "=" + values;
-
- let value = parseInt(values/* deduce radix */);
-
- switch (operator)
- {
- case "+":
- newValue = this.value + value;
- break;
- case "-":
- newValue = this.value - value;
- break;
- case "^":
- newValue = this.value * value;
- break;
- case "=":
- newValue = value;
- break;
- }
+ if (invert)
+ newValue = !this.value;
+ else
+ newValue = values;
+ break;
- break;
+ case "number":
+ // TODO: support floats? Validators need updating.
+ if (!/^[+-]?(?:0x[0-9a-f]+|0[0-7]+|0|[1-9]\d*)$/i.test(values))
+ return "E521: Number required after := " + this.name + "=" + values;
- case "charlist":
- case "stringlist":
- values = Array.concat(values);
- switch (operator)
- {
- case "+":
- newValue = util.Array.uniq(Array.concat(this.values, values), true);
- break;
- case "^":
- // NOTE: Vim doesn't prepend if there's a match in the current value
- newValue = util.Array.uniq(Array.concat(values, this.values), true);
- break;
- case "-":
- newValue = this.values.filter(function (item) values.indexOf(item) == -1);
- break;
- case "=":
- newValue = values;
- if (invert)
- {
- let keepValues = this.values.filter(function (item) values.indexOf(item) == -1);
- let addValues = values.filter(function (item) self.values.indexOf(item) == -1);
- newValue = addValues.concat(keepValues);
- }
- break;
- }
+ let value = parseInt(values/* deduce radix */);
+ switch (operator) {
+ case "+":
+ newValue = this.value + value;
+ break;
+ case "-":
+ newValue = this.value - value;
+ break;
+ case "^":
+ newValue = this.value * value;
+ break;
+ case "=":
+ newValue = value;
break;
+ }
+
+ break;
- case "string":
- switch (operator)
- {
- case "+":
- newValue = this.value + values;
- break;
- case "-":
- newValue = this.value.replace(values, "");
- break;
- case "^":
- newValue = values + this.value;
- break;
- case "=":
- newValue = values;
- break;
+ case "charlist":
+ case "stringlist":
+ values = Array.concat(values);
+ switch (operator) {
+ case "+":
+ newValue = util.Array.uniq(Array.concat(this.values, values), true);
+ break;
+ case "^":
+ // NOTE: Vim doesn't prepend if there's a match in the current value
+ newValue = util.Array.uniq(Array.concat(values, this.values), true);
+ break;
+ case "-":
+ newValue = this.values.filter(function (item) values.indexOf(item) == -1);
+ break;
+ case "=":
+ newValue = values;
+ if (invert) {
+ let keepValues = this.values.filter(function (item) values.indexOf(item) == -1);
+ let addValues = values.filter(function (item) self.values.indexOf(item) == -1);
+ newValue = addValues.concat(keepValues);
}
+ break;
+ }
+ break;
+
+ case "string":
+ switch (operator) {
+ case "+":
+ newValue = this.value + values;
+ break;
+ case "-":
+ newValue = this.value.replace(values, "");
break;
+ case "^":
+ newValue = values + this.value;
+ break;
+ case "=":
+ newValue = values;
+ break;
+ }
- default:
- return "E685: Internal error: option type `" + this.type + "' not supported";
+ break;
+
+ default:
+ return "E685: Internal error: option type `" + this.type + "' not supported";
}
if (newValue == null)
@@ -397,168 +379,571 @@ Option.prototype = {
return "E474: Invalid argument: " + values;
this.setValues(newValue, scope);
}
-};
-
-// TODO: Run this by default?
-/**
- * Validates the specified <b>values</b> against values generated by the
- * option's completer function.
- *
- * @param {value|string[]} values The value or array of values to validate.
- * @returns {boolean}
- */
-Option.validateCompleter = function (values)
-{
- let context = CompletionContext("");
- let res = context.fork("", 0, this, this.completer);
- if (!res)
- res = context.allItems.items.map(function (item) [item.text]);
- return Array.concat(values).every(function (value) res.some(function (item) item[0] == value));
-}; //}}}
+}, {
+ // TODO: Run this by default?
+ /**
+ * Validates the specified <b>values</b> against values generated by the
+ * option's completer function.
+ *
+ * @param {value|string[]} values The value or array of values to validate.
+ * @returns {boolean}
+ */
+ validateCompleter: function (values) {
+ let context = CompletionContext("");
+ let res = context.fork("", 0, this, this.completer);
+ if (!res)
+ res = context.allItems.items.map(function (item) [item.text]);
+ return Array.concat(values).every(function (value) res.some(function (item) item[0] == value));
+ },
+});
/**
* @instance options
*/
-function Options() //{{{
-{
- ////////////////////////////////////////////////////////////////////////////////
- ////////////////////// PRIVATE SECTION /////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
+const Options = Module("options", {
+ requires: ["highlight", "storage"],
+
+ init: function () {
+ for (let [, pref] in Iterator(this.allPrefs(Options.OLD_SAVED))) {
+ let saved = Options.SAVED + pref.substr(Options.OLD_SAVED.length)
+ if (!this.getPref(saved))
+ this.setPref(saved, this.getPref(pref));
+ this.resetPref(pref);
+ }
+
+ // Host application preferences which need to be changed to work well with
+ //
+
+ // Work around the popup blocker
+ // TODO: Make this work like safeSetPref
+ var popupAllowedEvents = this._loadPreference("dom.popup_allowed_events", "change click dblclick mouseup reset submit");
+ if (!/keypress/.test(popupAllowedEvents)) {
+ this._storePreference("dom.popup_allowed_events", popupAllowedEvents + " keypress");
+ liberator.registerObserver("shutdown", function () {
+ if (this._loadPreference("dom.popup_allowed_events", "") == popupAllowedEvents + " keypress")
+ this._storePreference("dom.popup_allowed_events", popupAllowedEvents);
+ });
+ }
- const SAVED = "extensions.liberator.saved.";
- const OLD_SAVED = "liberator.saved.";
- const optionHash = {};
+ function optionObserver(key, event, option) {
+ // Trigger any setters.
+ let opt = options.get(option);
+ if (event == "change" && opt)
+ opt.set(opt.value, options.OPTION_SCOPE_GLOBAL);
+ }
- const prefContexts = [];
+ storage.newMap("options", { store: false });
+ storage.addObserver("options", optionObserver, window);
- function optionObserver(key, event, option)
- {
- // Trigger any setters.
- let opt = options.get(option);
- if (event == "change" && opt)
- opt.set(opt.value, options.OPTION_SCOPE_GLOBAL);
- }
+ this._optionHash = {};
+ this._prefContexts = [];
- storage.newMap("options", { store: false });
- storage.addObserver("options", optionObserver, window);
+ this.prefObserver.register();
+ },
- function storePreference(name, value)
- {
- if (prefContexts.length)
- {
- let val = loadPreference(name, null);
- if (val != null)
- prefContexts[prefContexts.length - 1][name] = val;
+ destroy: function () {
+ this.prefObserver.unregister();
+ },
+
+ /**
+ * @property {number} Global option scope.
+ * @final
+ */
+ OPTION_SCOPE_GLOBAL: 1,
+
+ /**
+ * @property {number} Local option scope. Options in this scope only
+ * apply to the current tab/buffer.
+ * @final
+ */
+ OPTION_SCOPE_LOCAL: 2,
+
+ /**
+ * @property {number} Both local and global option scope.
+ * @final
+ */
+ OPTION_SCOPE_BOTH: 3,
+
+ /** @property {Iterator(Option)} @private */
+ __iterator__: function () {
+ let sorted = [o for ([i, o] in Iterator(this._optionHash))].sort(function (a, b) String.localeCompare(a.name, b.name));
+ return (v for ([k, v] in Iterator(sorted)));
+ },
+
+ /** @property {Object} Observes preference value changes. */
+ prefObserver: {
+ register: function () {
+ // better way to monitor all changes?
+ this._branch = services.get("pref").getBranch("").QueryInterface(Ci.nsIPrefBranch2);
+ this._branch.addObserver("", this, false);
+ },
+
+ unregister: function () {
+ if (this._branch)
+ this._branch.removeObserver("", this);
+ },
+
+ observe: function (subject, topic, data) {
+ if (topic != "nsPref:changed")
+ return;
+
+ // subject is the nsIPrefBranch we're observing (after appropriate QI)
+ // data is the name of the pref that's been changed (relative to subject)
+ switch (data) {
+ case "accessibility.browsewithcaret":
+ let value = options.getPref("accessibility.browsewithcaret", false);
+ liberator.mode = value ? modes.CARET : modes.NORMAL;
+ break;
+ }
+ }
+ },
+
+ /**
+ * Adds a new option.
+ *
+ * @param {string[]} names All names for the option.
+ * @param {string} description A description of the option.
+ * @param {string} type The option type (see {@link Option#type}).
+ * @param {value} defaultValue The option's default value.
+ * @param {Object} extra An optional extra configuration hash (see
+ * {@link Map#extraInfo}).
+ * @optional
+ * @returns {boolean} Whether the option was created.
+ */
+ add: function (names, description, type, defaultValue, extraInfo) {
+ if (!extraInfo)
+ extraInfo = {};
+
+ let option = new Option(names, description, type, defaultValue, extraInfo);
+
+ if (!option)
+ return false;
+
+ if (option.name in this._optionHash) {
+ // never replace for now
+ liberator.log("Warning: '" + names[0] + "' already exists, NOT replacing existing option.", 1);
+ return false;
}
- let type = services.get("pref").getPrefType(name);
- switch (typeof value)
- {
- case "string":
- if (type == Ci.nsIPrefBranch.PREF_INVALID || type == Ci.nsIPrefBranch.PREF_STRING)
- {
- let supportString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
- supportString.data = value;
- services.get("pref").setComplexValue(name, Ci.nsISupportsString, supportString);
+ // quickly access options with options["wildmode"]:
+ this.__defineGetter__(option.name, function () option.value);
+ this.__defineSetter__(option.name, function (value) { option.value = value; });
+
+ this._optionHash[option.name] = option;
+ return true;
+ },
+
+ /**
+ * Returns the names of all preferences.
+ *
+ * @param {string} branch The branch in which to search preferences.
+ * @default ""
+ */
+ allPrefs: function (branch) services.get("pref").getChildList(branch || "", { value: 0 }),
+
+ /**
+ * Returns the option with <b>name</b> in the specified <b>scope</b>.
+ *
+ * @param {string} name The option's name.
+ * @param {number} scope The option's scope (see {@link Option#scope}).
+ * @optional
+ * @returns {Option} The matching option.
+ */
+ get: function (name, scope) {
+ if (!scope)
+ scope = options.OPTION_SCOPE_BOTH;
+
+ if (name in this._optionHash)
+ return (this._optionHash[name].scope & scope) && this._optionHash[name];
+
+ for (let opt in Iterator(options)) {
+try {
+ if (opt.hasName(name))
+ return (opt.scope & scope) && opt;
+}catch(e) {
+ liberator.dump(options.__iterator__);
+ liberator.dump(opt);
+ liberator.reportError(e);
+ throw e
+}
+ }
+
+ return null;
+ },
+
+ /**
+ * Lists all options in <b>scope</b> or only those with changed values
+ * if <b>onlyNonDefault</b> is specified.
+ *
+ * @param {boolean} onlyNonDefault Limit the list to prefs with a
+ * non-default value.
+ * @param {number} scope Only list options in this scope (see
+ * {@link Option#scope}).
+ */
+ list: function (onlyNonDefault, scope) {
+ if (!scope)
+ scope = options.OPTION_SCOPE_BOTH;
+
+ function opts(opt) {
+ for (let opt in Iterator(options)) {
+ let option = {
+ isDefault: opt.value == opt.defaultValue,
+ name: opt.name,
+ default: opt.defaultValue,
+ pre: "\u00a0\u00a0", // Unicode nonbreaking space.
+ value: <></>
+ };
+
+ if (onlyNonDefault && option.isDefault)
+ continue;
+ if (!(opt.scope & scope))
+ continue;
+
+ if (opt.type == "boolean") {
+ if (!opt.value)
+ option.pre = "no";
+ option.default = (option.default ? "" : "no") + opt.name;
}
- else if (type == Ci.nsIPrefBranch.PREF_INT)
- liberator.echoerr("E521: Number required after =: " + name + "=" + value);
else
- liberator.echoerr("E474: Invalid argument: " + name + "=" + value);
- break;
- case "number":
- if (type == Ci.nsIPrefBranch.PREF_INVALID || type == Ci.nsIPrefBranch.PREF_INT)
- services.get("pref").setIntPref(name, value);
- else
- liberator.echoerr("E474: Invalid argument: " + name + "=" + value);
- break;
- case "boolean":
- if (type == Ci.nsIPrefBranch.PREF_INVALID || type == Ci.nsIPrefBranch.PREF_BOOL)
- services.get("pref").setBoolPref(name, value);
- else if (type == Ci.nsIPrefBranch.PREF_INT)
- liberator.echoerr("E521: Number required after =: " + name + "=" + value);
- else
- liberator.echoerr("E474: Invalid argument: " + name + "=" + value);
- break;
- default:
- liberator.echoerr("Unknown preference type: " + typeof value + " (" + name + "=" + value + ")");
+ option.value = <>={template.highlight(opt.value)}</>;
+ yield option;
+ }
+ };
+
+ let list = template.options("Options", opts());
+ commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
+ },
+
+ /**
+ * Lists all preferences matching <b>filter</b> or only those with
+ * changed values if <b>onlyNonDefault</b> is specified.
+ *
+ * @param {boolean} onlyNonDefault Limit the list to prefs with a
+ * non-default value.
+ * @param {string} filter The list filter. A null filter lists all
+ * prefs.
+ * @optional
+ */
+ listPrefs: function (onlyNonDefault, filter) {
+ if (!filter)
+ filter = "";
+
+ let prefArray = options.allPrefs();
+ prefArray.sort();
+ function prefs() {
+ for (let [, pref] in Iterator(prefArray)) {
+ let userValue = services.get("pref").prefHasUserValue(pref);
+ if (onlyNonDefault && !userValue || pref.indexOf(filter) == -1)
+ continue;
+
+ value = options.getPref(pref);
+
+ let option = {
+ isDefault: !userValue,
+ default: options._loadPreference(pref, null, true),
+ value: <>={template.highlight(value, true, 100)}</>,
+ name: pref,
+ pre: "\u00a0\u00a0" // Unicode nonbreaking space.
+ };
+
+ yield option;
+ }
+ };
+
+ let list = template.options(config.hostApplication + " Options", prefs());
+ commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
+ },
+
+ /**
+ * Parses a :set command's argument string.
+ *
+ * @param {string} args The :set command's argument string.
+ * @param {Object} modifiers A hash of parsing modifiers. These are:
+ * scope - see {@link Option#scope}
+ * @optional
+ * @returns {Object} The parsed command object.
+ */
+ parseOpt: function parseOpt(args, modifiers) {
+ let ret = {};
+ let matches, prefix, postfix, valueGiven;
+
+ [matches, prefix, ret.name, postfix, valueGiven, ret.operator, ret.value] =
+ args.match(/^\s*(no|inv)?([a-z_]*)([?&!])?\s*(([-+^]?)=(.*))?\s*$/) || [];
+
+ ret.args = args;
+ ret.onlyNonDefault = false; // used for :set to print non-default options
+ if (!args) {
+ ret.name = "all";
+ ret.onlyNonDefault = true;
}
- }
- function loadPreference(name, forcedDefault, defaultBranch)
- {
+ if (matches)
+ ret.option = options.get(ret.name, ret.scope);
+
+ ret.prefix = prefix;
+ ret.postfix = postfix;
+
+ ret.all = (ret.name == "all");
+ ret.get = (ret.all || postfix == "?" || (ret.option && ret.option.type != "boolean" && !valueGiven));
+ ret.invert = (prefix == "inv" || postfix == "!");
+ ret.reset = (postfix == "&");
+ ret.unsetBoolean = (prefix == "no");
+
+ ret.scope = modifiers && modifiers.scope;
+
+ if (!ret.option)
+ return ret;
+
+ if (ret.value === undefined)
+ ret.value = "";
+
+ ret.optionValue = ret.option.get(ret.scope);
+ ret.optionValues = ret.option.getValues(ret.scope);
+
+ ret.values = ret.option.parseValues(ret.value);
+
+ return ret;
+ },
+
+ /**
+ * Remove the option with matching <b>name</b>.
+ *
+ * @param {string} name The name of the option to remove. This can be
+ * any of the options's names.
+ */
+ remove: function (name) {
+ for each (let option in this._optionHash) {
+ if (option.hasName(name))
+ delete this._optionHash[option.name];
+ }
+ },
+
+ /** @property {Object} The options store. */
+ get store() storage.options,
+
+ /**
+ * Returns the value of the preference <b>name</b>.
+ *
+ * @param {string} name The preference name.
+ * @param {value} forcedDefault The the default value for this
+ * preference. Used for internal liberator preferences.
+ */
+ getPref: function (name, forcedDefault) {
+ return this._loadPreference(name, forcedDefault);
+ },
+
+ /**
+ * Sets the preference <b>name</b> to </b>value</b> but warns the user
+ * if the value is changed from its default.
+ *
+ * @param {string} name The preference name.
+ * @param {value} value The new preference value.
+ */
+ // FIXME: Well it used to. I'm looking at you mst! --djk
+ safeSetPref: function (name, value, message) {
+ let val = this._loadPreference(name, null, false);
+ let def = this._loadPreference(name, null, true);
+ let lib = this._loadPreference(Options.SAVED + name);
+ if (lib == null && val != def || val != lib) {
+ let msg = "Warning: setting preference " + name + ", but it's changed from its default value.";
+ if (message)
+ msg += " " + message;
+ liberator.echomsg(msg);
+ }
+ this._storePreference(name, value);
+ this._storePreference(Options.SAVED + name, value);
+ },
+
+ /**
+ * Sets the preference <b>name</b> to </b>value</b>.
+ *
+ * @param {string} name The preference name.
+ * @param {value} value The new preference value.
+ */
+ setPref: function (name, value) {
+ this._storePreference(name, value);
+ },
+
+ /**
+ * Resets the preference <b>name</b> to its default value.
+ *
+ * @param {string} name The preference name.
+ */
+ resetPref: function (name) {
+ try {
+ services.get("pref").clearUserPref(name);
+ }
+ catch (e) {
+ // ignore - thrown if not a user set value
+ }
+ },
+
+ /**
+ * Toggles the value of the boolean preference <b>name</b>.
+ *
+ * @param {string} name The preference name.
+ */
+ invertPref: function (name) {
+ if (services.get("pref").getPrefType(name) == Ci.nsIPrefBranch.PREF_BOOL)
+ this.setPref(name, !this.getPref(name));
+ else
+ liberator.echoerr("E488: Trailing characters: " + name + "!");
+ },
+
+ /**
+ * Pushes a new preference context onto the context stack.
+ *
+ * @see #withContext
+ */
+ pushContext: function () {
+ this._prefContexts.push({});
+ },
+
+ /**
+ * Pops the top preference context from the stack.
+ *
+ * @see #withContext
+ */
+ popContext: function () {
+ for (let [k, v] in Iterator(this._prefContexts.pop()))
+ this._storePreference(k, v);
+ },
+
+ /**
+ * Executes <b>func</b> with a new preference context. When <b>func</b>
+ * returns, the context is popped and any preferences set via
+ * {@link #setPref} or {@link #invertPref} are restored to their
+ * previous values.
+ *
+ * @param {function} func The function to call.
+ * @param {Object} func The 'this' object with which to call <b>func</b>
+ * @see #pushContext
+ * @see #popContext
+ */
+ withContext: function (func, self) {
+ try {
+ this.pushContext();
+ return func.call(self);
+ }
+ finally {
+ this.popContext();
+ }
+ },
+
+ _storePreference: function (name, value) {
+ if (this._prefContexts.length) {
+ let val = this._loadPreference(name, null);
+ if (val != null)
+ this._prefContexts[this._prefContexts.length - 1][name] = val;
+ }
+
+ let type = services.get("pref").getPrefType(name);
+ switch (typeof value) {
+ case "string":
+ if (type == Ci.nsIPrefBranch.PREF_INVALID || type == Ci.nsIPrefBranch.PREF_STRING) {
+ let supportString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+ supportString.data = value;
+ services.get("pref").setComplexValue(name, Ci.nsISupportsString, supportString);
+ }
+ else if (type == Ci.nsIPrefBranch.PREF_INT)
+ liberator.echoerr("E521: Number required after =: " + name + "=" + value);
+ else
+ liberator.echoerr("E474: Invalid argument: " + name + "=" + value);
+ break;
+ case "number":
+ if (type == Ci.nsIPrefBranch.PREF_INVALID || type == Ci.nsIPrefBranch.PREF_INT)
+ services.get("pref").setIntPref(name, value);
+ else
+ liberator.echoerr("E474: Invalid argument: " + name + "=" + value);
+ break;
+ case "boolean":
+ if (type == Ci.nsIPrefBranch.PREF_INVALID || type == Ci.nsIPrefBranch.PREF_BOOL)
+ services.get("pref").setBoolPref(name, value);
+ else if (type == Ci.nsIPrefBranch.PREF_INT)
+ liberator.echoerr("E521: Number required after =: " + name + "=" + value);
+ else
+ liberator.echoerr("E474: Invalid argument: " + name + "=" + value);
+ break;
+ default:
+ liberator.echoerr("Unknown preference type: " + typeof value + " (" + name + "=" + value + ")");
+ }
+ },
+
+ _loadPreference: function (name, forcedDefault, defaultBranch) {
let defaultValue = null; // XXX
if (forcedDefault != null) // this argument sets defaults for non-user settable options (like extensions.history.comp_history)
defaultValue = forcedDefault;
let branch = defaultBranch ? services.get("pref").getDefaultBranch("") : services.get("pref");
let type = services.get("pref").getPrefType(name);
- try
- {
- switch (type)
- {
- case Ci.nsIPrefBranch.PREF_STRING:
- let value = branch.getComplexValue(name, Ci.nsISupportsString).data;
- // try in case it's a localized string (will throw an exception if not)
- if (!services.get("pref").prefIsLocked(name) && !services.get("pref").prefHasUserValue(name) &&
- RegExp("chrome://.+/locale/.+\\.properties").test(value))
- value = branch.getComplexValue(name, Ci.nsIPrefLocalizedString).data;
- return value;
- case Ci.nsIPrefBranch.PREF_INT:
- return branch.getIntPref(name);
- case Ci.nsIPrefBranch.PREF_BOOL:
- return branch.getBoolPref(name);
- default:
- return defaultValue;
+ try {
+ switch (type) {
+ case Ci.nsIPrefBranch.PREF_STRING:
+ let value = branch.getComplexValue(name, Ci.nsISupportsString).data;
+ // try in case it's a localized string (will throw an exception if not)
+ if (!services.get("pref").prefIsLocked(name) && !services.get("pref").prefHasUserValue(name) &&
+ RegExp("chrome://.+/locale/.+\\.properties").test(value))
+ value = branch.getComplexValue(name, Ci.nsIPrefLocalizedString).data;
+ return value;
+ case Ci.nsIPrefBranch.PREF_INT:
+ return branch.getIntPref(name);
+ case Ci.nsIPrefBranch.PREF_BOOL:
+ return branch.getBoolPref(name);
+ default:
+ return defaultValue;
}
}
- catch (e)
- {
+ catch (e) {
return defaultValue;
}
- }
-
- function setAction(args, modifiers)
- {
- let bang = args.bang;
- if (!args.length)
- args[0] = "";
-
- for (let [, arg] in args)
- {
- if (bang)
- {
- let onlyNonDefault = false;
- let reset = false;
- let invertBoolean = false;
-
- if (args[0] == "")
- {
- var name = "all";
- onlyNonDefault = true;
- }
- else
- {
- var [matches, name, postfix, valueGiven, operator, value] =
- arg.match(/^\s*?([a-zA-Z0-9\.\-_{}]+)([?&!])?\s*(([-+^]?)=(.*))?\s*$/);
- reset = (postfix == "&");
- invertBoolean = (postfix == "!");
- }
+ },
+}, {
+ SAVED: "extensions.liberator.saved.",
+ OLD_SAVED: "liberator.saved.",
+}, {
+ commandline: function () {
+ // TODO: maybe reset in .destroy()?
+ // TODO: move to buffer.js
+ // we have our own typeahead find implementation
+ // See: https://bugzilla.mozilla.org/show_bug.cgi?id=348187
+ options.safeSetPref("accessibility.typeaheadfind.autostart", false);
+ options.safeSetPref("accessibility.typeaheadfind", false); // actually the above setting should do it, but has no effect in Firefox
+ },
+ commands: function () {
+ function setAction(args, modifiers) {
+ let bang = args.bang;
+ if (!args.length)
+ args[0] = "";
+
+ for (let [, arg] in args) {
+ if (bang) {
+ let onlyNonDefault = false;
+ let reset = false;
+ let invertBoolean = false;
+
+ if (args[0] == "") {
+ var name = "all";
+ onlyNonDefault = true;
+ }
+ else {
+ var [matches, name, postfix, valueGiven, operator, value] =
+ arg.match(/^\s*?([a-zA-Z0-9\.\-_{}]+)([?&!])?\s*(([-+^]?)=(.*))?\s*$/);
+ reset = (postfix == "&");
+ invertBoolean = (postfix == "!");
+ }
- if (name == "all" && reset)
- // TODO: Why? --djk
- liberator.echoerr("You can't reset all options, it could make " + config.hostApplication + " unusable.");
- else if (name == "all")
- options.listPrefs(onlyNonDefault, "");
- else if (reset)
- options.resetPref(name);
- else if (invertBoolean)
- options.invertPref(name);
- else if (valueGiven)
- {
- switch (value)
- {
+ if (name == "all" && reset)
+ // TODO: Why? --djk
+ liberator.echoerr("You can't reset all options, it could make " + config.hostApplication + " unusable.");
+ else if (name == "all")
+ options.listPrefs(onlyNonDefault, "");
+ else if (reset)
+ options.resetPref(name);
+ else if (invertBoolean)
+ options.invertPref(name);
+ else if (valueGiven) {
+ switch (value) {
case undefined:
value = "";
break;
@@ -571,329 +956,283 @@ function Options() //{{{
default:
if (/^\d+$/.test(value))
value = parseInt(value, 10);
+ }
+ options.setPref(name, value);
}
- options.setPref(name, value);
+ else
+ options.listPrefs(onlyNonDefault, name);
+ return;
}
- else
- options.listPrefs(onlyNonDefault, name);
- return;
- }
- let opt = options.parseOpt(arg, modifiers);
- liberator.assert(opt, "Error parsing :set command: " + arg);
+ let opt = options.parseOpt(arg, modifiers);
+ liberator.assert(opt, "Error parsing :set command: " + arg);
- let option = opt.option;
- liberator.assert(option != null || opt.all,
- "E518: Unknown option: " + arg);
+ let option = opt.option;
+ liberator.assert(option != null || opt.all,
+ "E518: Unknown option: " + opt.name);
- // reset a variable to its default value
- if (opt.reset)
- {
- if (opt.all)
- {
- for (let option in options)
+ // reset a variable to its default value
+ if (opt.reset) {
+ if (opt.all) {
+ for (let option in options)
+ option.reset();
+ }
+ else {
+ option.setFrom = modifiers.setFrom || null;
option.reset();
+ }
}
- else
- {
- option.setFrom = modifiers.setFrom || null;
- option.reset();
- }
- }
- // read access
- else if (opt.get)
- {
- if (opt.all)
- options.list(opt.onlyNonDefault, opt.scope);
- else
- {
- if (option.type == "boolean")
- var msg = (opt.optionValue ? " " : "no") + option.name;
- else
- msg = " " + option.name + "=" + opt.optionValue;
+ // read access
+ else if (opt.get) {
+ if (opt.all)
+ options.list(opt.onlyNonDefault, opt.scope);
+ else {
+ if (option.type == "boolean")
+ var msg = (opt.optionValue ? " " : "no") + option.name;
+ else
+ msg = " " + option.name + "=" + opt.optionValue;
- if (options["verbose"] > 0 && option.setFrom)
- msg += "\n Last set from " + option.setFrom.path;
+ if (options["verbose"] > 0 && option.setFrom)
+ msg += "\n Last set from " + option.setFrom.path;
- // FIXME: Message highlight group wrapping messes up the indent up for multi-arg verbose :set queries
- liberator.echo(<span highlight="CmdOutput">{msg}</span>);
+ // FIXME: Message highlight group wrapping messes up the indent up for multi-arg verbose :set queries
+ liberator.echo(<span highlight="CmdOutput">{msg}</span>);
+ }
}
- }
- // write access
- // NOTE: the behavior is generally Vim compatible but could be
- // improved. i.e. Vim's behavior is pretty sloppy to no real benefit
- else
- {
- option.setFrom = modifiers.setFrom || null;
+ // write access
+ // NOTE: the behavior is generally Vim compatible but could be
+ // improved. i.e. Vim's behavior is pretty sloppy to no real benefit
+ else {
+ option.setFrom = modifiers.setFrom || null;
- if (opt.option.type == "boolean")
- {
- liberator.assert(!opt.valueGiven, "E474: Invalid argument: " + arg);
- opt.values = !opt.unsetBoolean;
+ if (opt.option.type == "boolean") {
+ liberator.assert(!opt.valueGiven, "E474: Invalid argument: " + arg);
+ opt.values = !opt.unsetBoolean;
+ }
+ let res = opt.option.op(opt.operator || "=", opt.values, opt.scope, opt.invert);
+ if (res)
+ liberator.echoerr(res);
}
- let res = opt.option.op(opt.operator || "=", opt.values, opt.scope, opt.invert);
- if (res)
- liberator.echoerr(res);
}
}
- }
- function setCompleter(context, args, modifiers)
- {
- let filter = context.filter;
+ function setCompleter(context, args, modifiers) {
+ let filter = context.filter;
+
+ if (args.bang) { // list completions for about:config entries
+ if (filter[filter.length - 1] == "=") {
+ context.advance(filter.length);
+ filter = filter.substr(0, filter.length - 1);
+ context.completions = [
+ [this._loadPreference(filter, null, false), "Current Value"],
+ [this._loadPreference(filter, null, true), "Default Value"]
+ ].filter(function ([k]) k != null);
+ return;
+ }
- if (args.bang) // list completions for about:config entries
- {
- if (filter[filter.length - 1] == "=")
- {
- context.advance(filter.length);
- filter = filter.substr(0, filter.length - 1);
- context.completions = [
- [loadPreference(filter, null, false), "Current Value"],
- [loadPreference(filter, null, true), "Default Value"]
- ].filter(function ([k]) k != null);
- return;
+ return completion.preference(context);
}
- return completion.preference(context);
- }
+ let opt = options.parseOpt(filter, modifiers);
+ let prefix = opt.prefix;
- let opt = options.parseOpt(filter, modifiers);
- let prefix = opt.prefix;
+ if (context.filter.indexOf("=") == -1) {
+ if (prefix)
+ context.filters.push(function ({ item: opt }) opt.type == "boolean" || prefix == "inv" && opt.values instanceof Array);
+ return completion.option(context, opt.scope);
+ }
+ else if (prefix == "no")
+ return;
- if (context.filter.indexOf("=") == -1)
- {
if (prefix)
- context.filters.push(function ({ item: opt }) opt.type == "boolean" || prefix == "inv" && opt.values instanceof Array);
- return completion.option(context, opt.scope);
- }
- else if (prefix == "no")
- return;
+ context.advance(prefix.length);
- if (prefix)
- context.advance(prefix.length);
-
- let option = opt.option;
- context.advance(context.filter.indexOf("=") + 1);
+ let option = opt.option;
+ context.advance(context.filter.indexOf("=") + 1);
- if (!option)
- {
- context.message = "No such option: " + opt.name;
- context.highlight(0, name.length, "SPELLCHECK");
- }
+ if (!option) {
+ context.message = "No such option: " + opt.name;
+ context.highlight(0, name.length, "SPELLCHECK");
+ }
- if (opt.get || opt.reset || !option || prefix)
- return;
-
- if (!opt.value)
- {
- context.fork("default", 0, this, function (context) {
- context.title = ["Extra Completions"];
- context.completions = [
- [option.value, "Current value"],
- [option.defaultValue, "Default value"]
- ].filter(function (f) f[0] != "");
- });
- }
+ if (opt.get || opt.reset || !option || prefix)
+ return;
- context.fork("values", 0, completion, "optionValue", opt.name, opt.operator);
- }
+ if (!opt.value) {
+ context.fork("default", 0, this, function (context) {
+ context.title = ["Extra Completions"];
+ context.completions = [
+ [option.value, "Current value"],
+ [option.defaultValue, "Default value"]
+ ].filter(function (f) f[0] != "");
+ });
+ }
- //
- // Host application preferences which need to be changed to work well with
- // Liberator
- //
-
- // work around the popup blocker
- // TODO: Make this work like safeSetPref
- var popupAllowedEvents = loadPreference("dom.popup_allowed_events", "change click dblclick mouseup reset submit");
- if (!/keypress/.test(popupAllowedEvents))
- {
- storePreference("dom.popup_allowed_events", popupAllowedEvents + " keypress");
- liberator.registerObserver("shutdown", function () {
- if (loadPreference("dom.popup_allowed_events", "") == popupAllowedEvents + " keypress")
- storePreference("dom.popup_allowed_events", popupAllowedEvents);
- });
- }
+ context.fork("values", 0, completion, "optionValue", opt.name, opt.operator);
+ }
- // safeSetPref might try to echomsg. Need commandline.
- liberator.registerObserver("load_commandline", function () {
- // TODO: maybe reset in .destroy()?
- // TODO: move to buffer.js
- // we have our own typeahead find implementation
- // See: https://bugzilla.mozilla.org/show_bug.cgi?id=348187
- options.safeSetPref("accessibility.typeaheadfind.autostart", false);
- options.safeSetPref("accessibility.typeaheadfind", false); // actually the above setting should do it, but has no effect in Firefox
- });
+ commands.add(["let"],
+ "Set or list a variable",
+ function (args) {
+ args = args.string;
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMMANDS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
+ if (!args) {
+ let str =
+ <table>
+ {
+ template.map(liberator.globalVariables, function ([i, value]) {
+ let prefix = typeof value == "number" ? "#" :
+ typeof value == "function" ? "*" :
+ " ";
+ return <tr>
+ <td style="width: 200px;">{i}</td>
+ <td>{prefix}{value}</td>
+ </tr>;
+ })
+ }
+ </table>;
+ if (str.*.length())
+ liberator.echo(str, commandline.FORCE_MULTILINE);
+ else
+ liberator.echomsg("No variables found");
+ return;
+ }
- commands.add(["let"],
- "Set or list a variable",
- function (args)
- {
- args = args.string;
+ let matches;
+ // 1 - type, 2 - name, 3 - +-., 4 - expr
+ if (matches = args.match(/([$@&])?([\w:]+)\s*([-+.])?=\s*(.+)/)) {
+ if (!matches[1]) {
+ let reference = liberator.variableReference(matches[2]);
+ liberator.assert(reference[0] || !matches[3],
+ "E121: Undefined variable: " + matches[2]);
+
+ let expr = liberator.evalExpression(matches[4]);
+ liberator.assert(expr !== undefined, "E15: Invalid expression: " + matches[4]);
+
+ if (!reference[0]) {
+ if (reference[2] == "g")
+ reference[0] = liberator.globalVariables;
+ else
+ return; // for now
+ }
- if (!args)
- {
- let str =
- <table>
- {
- template.map(liberator.globalVariables, function ([i, value]) {
- let prefix = typeof value == "number" ? "#" :
- typeof value == "function" ? "*" :
- " ";
- return <tr>
- <td style="width: 200px;">{i}</td>
- <td>{prefix}{value}</td>
- </tr>;
- })
+ if (matches[3]) {
+ if (matches[3] == "+")
+ reference[0][reference[1]] += expr;
+ else if (matches[3] == "-")
+ reference[0][reference[1]] -= expr;
+ else if (matches[3] == ".")
+ reference[0][reference[1]] += expr.toString();
+ }
+ else {
+ if (!reference[0]) {
+ if (reference[2] == "g")
+ reference[0] = liberator.globalVariables;
+ else
+ return; // for now
+ }
+
+ if (matches[3]) {
+ if (matches[3] == "+")
+ reference[0][reference[1]] += expr;
+ else if (matches[3] == "-")
+ reference[0][reference[1]] -= expr;
+ else if (matches[3] == ".")
+ reference[0][reference[1]] += expr.toString();
+ }
+ else
+ reference[0][reference[1]] = expr;
+ }
}
- </table>;
- if (str.*.length())
- liberator.echo(str, commandline.FORCE_MULTILINE);
- else
- liberator.echomsg("No variables found");
- return;
+ }
+ // 1 - name
+ else if (matches = args.match(/^\s*([\w:]+)\s*$/)) {
+ let reference = liberator.variableReference(matches[1]);
+ liberator.assert(reference[0], "E121: Undefined variable: " + matches[1]);
+
+ let value = reference[0][reference[1]];
+ let prefix = typeof value == "number" ? "#" :
+ typeof value == "function" ? "*" :
+ " ";
+ liberator.echo(reference[1] + "\t\t" + prefix + value);
+ }
+ },
+ {
+ literal: 0
}
+ );
- let matches;
- // 1 - type, 2 - name, 3 - +-., 4 - expr
- if (matches = args.match(/([$@&])?([\w:]+)\s*([-+.])?=\s*(.+)/))
+ commands.add(["setl[ocal]"],
+ "Set local option",
+ function (args, modifiers) {
+ modifiers.scope = options.OPTION_SCOPE_LOCAL;
+ setAction(args, modifiers);
+ },
{
- if (!matches[1])
- {
- let reference = liberator.variableReference(matches[2]);
- liberator.assert(reference[0] || !matches[3],
- "E121: Undefined variable: " + matches[2]);
+ bang: true,
+ count: true,
+ completer: function (context, args) {
+ return setCompleter(context, args, { scope: options.OPTION_SCOPE_LOCAL });
+ },
+ literal: 0
+ }
+ );
- let expr = liberator.evalExpression(matches[4]);
- liberator.assert(expr !== undefined, "E15: Invalid expression: " + matches[4]);
+ commands.add(["setg[lobal]"],
+ "Set global option",
+ function (args, modifiers) {
+ modifiers.scope = options.OPTION_SCOPE_GLOBAL;
+ setAction(args, modifiers);
+ },
+ {
+ bang: true,
+ count: true,
+ completer: function (context, args) {
+ return setCompleter(context, args, { scope: options.OPTION_SCOPE_GLOBAL });
+ },
+ literal: 0
+ }
+ );
- if (!reference[0])
+ commands.add(["se[t]"],
+ "Set an option",
+ function (args, modifiers) { setAction(args, modifiers); },
+ {
+ bang: true,
+ completer: function (context, args) {
+ return setCompleter(context, args);
+ },
+ serial: function () [
{
- if (reference[2] == "g")
- reference[0] = liberator.globalVariables;
- else
- return; // for now
+ command: this.name,
+ arguments: [opt.type == "boolean" ? (opt.value ? "" : "no") + opt.name
+ : opt.name + "=" + opt.value]
}
+ for (opt in options)
+ if (!opt.getter && opt.value != opt.defaultValue && (opt.scope & options.OPTION_SCOPE_GLOBAL))
+ ]
+ });
- if (matches[3])
- {
- if (matches[3] == "+")
- reference[0][reference[1]] += expr;
- else if (matches[3] == "-")
- reference[0][reference[1]] -= expr;
- else if (matches[3] == ".")
- reference[0][reference[1]] += expr.toString();
+ commands.add(["unl[et]"],
+ "Delete a variable",
+ function (args) {
+ for (let [, name] in args) {
+ let reference = liberator.variableReference(name);
+ if (!reference[0]) {
+ if (!args.bang)
+ liberator.echoerr("E108: No such variable: " + name);
+ return;
}
- else
- reference[0][reference[1]] = expr;
+
+ delete reference[0][reference[1]];
}
- }
- // 1 - name
- else if (matches = args.match(/^\s*([\w:]+)\s*$/))
- {
- let reference = liberator.variableReference(matches[1]);
- liberator.assert(reference[0], "E121: Undefined variable: " + matches[1]);
-
- let value = reference[0][reference[1]];
- let prefix = typeof value == "number" ? "#" :
- typeof value == "function" ? "*" :
- " ";
- liberator.echo(reference[1] + "\t\t" + prefix + value);
- }
- },
- {
- literal: 0
- }
- );
-
- commands.add(["setl[ocal]"],
- "Set local option",
- function (args, modifiers)
- {
- modifiers.scope = options.OPTION_SCOPE_LOCAL;
- setAction(args, modifiers);
- },
- {
- bang: true,
- count: true,
- completer: function (context, args)
- {
- return setCompleter(context, args, { scope: options.OPTION_SCOPE_LOCAL });
- },
- literal: 0
- }
- );
-
- commands.add(["setg[lobal]"],
- "Set global option",
- function (args, modifiers)
- {
- modifiers.scope = options.OPTION_SCOPE_GLOBAL;
- setAction(args, modifiers);
- },
- {
- bang: true,
- count: true,
- completer: function (context, args)
- {
- return setCompleter(context, args, { scope: options.OPTION_SCOPE_GLOBAL });
},
- literal: 0
- }
- );
-
- commands.add(["se[t]"],
- "Set an option",
- function (args, modifiers) { setAction(args, modifiers); },
- {
- bang: true,
- completer: function (context, args)
{
- return setCompleter(context, args);
- },
- serial: function () [
- {
- command: this.name,
- arguments: [opt.type == "boolean" ? (opt.value ? "" : "no") + opt.name
- : opt.name + "=" + opt.value]
- }
- for (opt in options)
- if (!opt.getter && opt.value != opt.defaultValue && (opt.scope & options.OPTION_SCOPE_GLOBAL))
- ]
- });
-
- commands.add(["unl[et]"],
- "Delete a variable",
- function (args)
- {
- for (let [, name] in args)
- {
- let reference = liberator.variableReference(name);
- if (!reference[0])
- {
- if (!args.bang)
- liberator.echoerr("E108: No such variable: " + name);
- return;
- }
-
- delete reference[0][reference[1]];
- }
- },
- {
- argCount: "+",
- bang: true
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMPLETIONS /////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- liberator.registerObserver("load_completion", function () {
+ argCount: "+",
+ bang: true
+ });
+ },
+ completion: function () {
completion.setFunctionCompleter(options.get, [function () ([o.name, o.description] for (o in options))]);
completion.setFunctionCompleter([options.getPref, options.safeSetPref, options.setPref, options.resetPref, options.invertPref],
[function () options.allPrefs().map(function (pref) [pref, ""])]);
@@ -916,19 +1255,18 @@ function Options() //{{{
let newValues = opt.parseValues(context.filter);
let len = context.filter.length;
- switch (opt.type)
- {
- case "boolean":
- if (!completer)
- completer = function () [["true", ""], ["false", ""]];
- break;
- case "stringlist":
- let target = newValues.pop();
- len = target ? target.length : 0;
- break;
- case "charlist":
- len = 0;
- break;
+ switch (opt.type) {
+ case "boolean":
+ if (!completer)
+ completer = function () [["true", ""], ["false", ""]];
+ break;
+ case "stringlist":
+ let target = newValues.pop();
+ len = target ? target.length : 0;
+ break;
+ case "charlist":
+ len = 0;
+ break;
}
// TODO: Highlight when invalid
context.advance(context.filter.length - len);
@@ -939,17 +1277,15 @@ function Options() //{{{
return;
// Not Vim compatible, but is a significant enough improvement
// that it's worth breaking compatibility.
- if (newValues instanceof Array)
- {
+ if (newValues instanceof Array) {
completions = completions.filter(function (val) newValues.indexOf(val[0]) == -1);
- switch (op)
- {
- case "+":
- completions = completions.filter(function (val) curValues.indexOf(val[0]) == -1);
- break;
- case "-":
- completions = completions.filter(function (val) curValues.indexOf(val[0]) > -1);
- break;
+ switch (op) {
+ case "+":
+ completions = completions.filter(function (val) curValues.indexOf(val[0]) == -1);
+ break;
+ case "-":
+ completions = completions.filter(function (val) curValues.indexOf(val[0]) > -1);
+ break;
}
}
context.completions = completions;
@@ -961,439 +1297,7 @@ function Options() //{{{
context.keys = { text: function (item) item, description: function (item) options.getPref(item) };
context.completions = options.allPrefs();
};
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// PUBLIC SECTION //////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- const self = {
-
- /**
- * @property {number} Global option scope.
- * @final
- */
- OPTION_SCOPE_GLOBAL: 1,
-
- /**
- * @property {number} Local option scope. Options in this scope only
- * apply to the current tab/buffer.
- * @final
- */
- OPTION_SCOPE_LOCAL: 2,
-
- /**
- * @property {number} Both local and global option scope.
- * @final
- */
- OPTION_SCOPE_BOTH: 3,
-
- /** @property {Iterator(Option)} @private */
- __iterator__: function ()
- {
- let sorted = [o for ([i, o] in Iterator(optionHash))].sort(function (a, b) String.localeCompare(a.name, b.name));
- return (v for ([k, v] in Iterator(sorted)));
- },
-
- /** @property {Object} Observes preference value changes. */
- prefObserver: {
- register: function ()
- {
- // better way to monitor all changes?
- this._branch = services.get("pref").getBranch("").QueryInterface(Ci.nsIPrefBranch2);
- this._branch.addObserver("", this, false);
- },
-
- unregister: function ()
- {
- if (this._branch)
- this._branch.removeObserver("", this);
- },
-
- observe: function (subject, topic, data)
- {
- if (topic != "nsPref:changed")
- return;
-
- // subject is the nsIPrefBranch we're observing (after appropriate QI)
- // data is the name of the pref that's been changed (relative to subject)
- switch (data)
- {
- case "accessibility.browsewithcaret":
- let value = options.getPref("accessibility.browsewithcaret", false);
- liberator.mode = value ? modes.CARET : modes.NORMAL;
- break;
- }
- }
- },
-
- /**
- * Adds a new option.
- *
- * @param {string[]} names All names for the option.
- * @param {string} description A description of the option.
- * @param {string} type The option type (see {@link Option#type}).
- * @param {value} defaultValue The option's default value.
- * @param {Object} extra An optional extra configuration hash (see
- * {@link Map#extraInfo}).
- * @optional
- * @returns {boolean} Whether the option was created.
- */
- add: function (names, description, type, defaultValue, extraInfo)
- {
- if (!extraInfo)
- extraInfo = {};
-
- let option = new Option(names, description, type, defaultValue, extraInfo);
-
- if (!option)
- return false;
-
- if (option.name in optionHash)
- {
- // never replace for now
- liberator.log("Warning: '" + names[0] + "' already exists, NOT replacing existing option.", 1);
- return false;
- }
-
- // quickly access options with options["wildmode"]:
- this.__defineGetter__(option.name, function () option.value);
- this.__defineSetter__(option.name, function (value) { option.value = value; });
-
- optionHash[option.name] = option;
- return true;
- },
-
- /**
- * Returns the names of all preferences.
- *
- * @param {string} branch The branch in which to search preferences.
- * @default ""
- */
- allPrefs: function (branch) services.get("pref").getChildList(branch || "", { value: 0 }),
-
- /**
- * Returns the option with <b>name</b> in the specified <b>scope</b>.
- *
- * @param {string} name The option's name.
- * @param {number} scope The option's scope (see {@link Option#scope}).
- * @optional
- * @returns {Option} The matching option.
- */
- get: function (name, scope)
- {
- if (!scope)
- scope = options.OPTION_SCOPE_BOTH;
-
- if (name in optionHash)
- return (optionHash[name].scope & scope) && optionHash[name];
-
- for (let opt in Iterator(options))
- {
- if (opt.hasName(name))
- return (opt.scope & scope) && opt;
- }
-
- return null;
- },
-
- /**
- * Lists all options in <b>scope</b> or only those with changed values
- * if <b>onlyNonDefault</b> is specified.
- *
- * @param {boolean} onlyNonDefault Limit the list to prefs with a
- * non-default value.
- * @param {number} scope Only list options in this scope (see
- * {@link Option#scope}).
- */
- list: function (onlyNonDefault, scope)
- {
- if (!scope)
- scope = options.OPTION_SCOPE_BOTH;
-
- function opts(opt) {
- for (let opt in Iterator(options))
- {
- let option = {
- isDefault: opt.value == opt.defaultValue,
- name: opt.name,
- default: opt.defaultValue,
- pre: "\u00a0\u00a0", // Unicode nonbreaking space.
- value: <></>
- };
-
- if (onlyNonDefault && option.isDefault)
- continue;
- if (!(opt.scope & scope))
- continue;
-
- if (opt.type == "boolean")
- {
- if (!opt.value)
- option.pre = "no";
- option.default = (option.default ? "" : "no") + opt.name;
- }
- else
- option.value = <>={template.highlight(opt.value)}</>;
- yield option;
- }
- };
-
- let list = template.options("Options", opts());
- commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
- },
-
- /**
- * Lists all preferences matching <b>filter</b> or only those with
- * changed values if <b>onlyNonDefault</b> is specified.
- *
- * @param {boolean} onlyNonDefault Limit the list to prefs with a
- * non-default value.
- * @param {string} filter The list filter. A null filter lists all
- * prefs.
- * @optional
- */
- listPrefs: function (onlyNonDefault, filter)
- {
- if (!filter)
- filter = "";
-
- let prefArray = options.allPrefs();
- prefArray.sort();
- function prefs() {
- for (let [, pref] in Iterator(prefArray))
- {
- let userValue = services.get("pref").prefHasUserValue(pref);
- if (onlyNonDefault && !userValue || pref.indexOf(filter) == -1)
- continue;
-
- value = options.getPref(pref);
-
- let option = {
- isDefault: !userValue,
- default: loadPreference(pref, null, true),
- value: <>={template.highlight(value, true, 100)}</>,
- name: pref,
- pre: "\u00a0\u00a0" // Unicode nonbreaking space.
- };
-
- yield option;
- }
- };
-
- let list = template.options(config.hostApplication + " Options", prefs());
- commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
- },
-
- /**
- * Parses a :set command's argument string.
- *
- * @param {string} args The :set command's argument string.
- * @param {Object} modifiers A hash of parsing modifiers. These are:
- * scope - see {@link Option#scope}
- * @optional
- * @returns {Object} The parsed command object.
- */
- parseOpt: function parseOpt(args, modifiers)
- {
- let ret = {};
- let matches, prefix, postfix, valueGiven;
-
- [matches, prefix, ret.name, postfix, valueGiven, ret.operator, ret.value] =
- args.match(/^\s*(no|inv)?([a-z_]*)([?&!])?\s*(([-+^]?)=(.*))?\s*$/) || [];
-
- ret.args = args;
- ret.onlyNonDefault = false; // used for :set to print non-default options
- if (!args)
- {
- ret.name = "all";
- ret.onlyNonDefault = true;
- }
-
- if (matches)
- ret.option = options.get(ret.name, ret.scope);
-
- ret.prefix = prefix;
- ret.postfix = postfix;
-
- ret.all = (ret.name == "all");
- ret.get = (ret.all || postfix == "?" || (ret.option && ret.option.type != "boolean" && !valueGiven));
- ret.invert = (prefix == "inv" || postfix == "!");
- ret.reset = (postfix == "&");
- ret.unsetBoolean = (prefix == "no");
-
- ret.scope = modifiers && modifiers.scope;
-
- if (!ret.option)
- return ret;
-
- if (ret.value === undefined)
- ret.value = "";
-
- ret.optionValue = ret.option.get(ret.scope);
- ret.optionValues = ret.option.getValues(ret.scope);
-
- ret.values = ret.option.parseValues(ret.value);
-
- return ret;
- },
-
- /**
- * Remove the option with matching <b>name</b>.
- *
- * @param {string} name The name of the option to remove. This can be
- * any of the options's names.
- */
- remove: function (name)
- {
- for each (let option in optionHash)
- {
- if (option.hasName(name))
- delete optionHash[option.name];
- }
- },
-
- /** @property {Object} The options store. */
- get store() storage.options,
-
- /**
- * Returns the value of the preference <b>name</b>.
- *
- * @param {string} name The preference name.
- * @param {value} forcedDefault The the default value for this
- * preference. Used for internal liberator preferences.
- */
- getPref: function (name, forcedDefault)
- {
- return loadPreference(name, forcedDefault);
- },
-
- /**
- * Sets the preference <b>name</b> to </b>value</b> but warns the user
- * if the value is changed from its default.
- *
- * @param {string} name The preference name.
- * @param {value} value The new preference value.
- */
- // FIXME: Well it used to. I'm looking at you mst! --djk
- safeSetPref: function (name, value, message)
- {
- let val = loadPreference(name, null, false);
- let def = loadPreference(name, null, true);
- let lib = loadPreference(SAVED + name);
- if (lib == null && val != def || val != lib)
- {
- let msg = "Warning: setting preference " + name + ", but it's changed from its default value.";
- if (message)
- msg += " " + message;
- liberator.echomsg(msg);
- }
- storePreference(name, value);
- storePreference(SAVED + name, value);
- },
-
- /**
- * Sets the preference <b>name</b> to </b>value</b>.
- *
- * @param {string} name The preference name.
- * @param {value} value The new preference value.
- */
- setPref: function (name, value)
- {
- storePreference(name, value);
- },
-
- /**
- * Resets the preference <b>name</b> to its default value.
- *
- * @param {string} name The preference name.
- */
- resetPref: function (name)
- {
- try
- {
- services.get("pref").clearUserPref(name);
- }
- catch (e)
- {
- // ignore - thrown if not a user set value
- }
- },
-
- /**
- * Toggles the value of the boolean preference <b>name</b>.
- *
- * @param {string} name The preference name.
- */
- invertPref: function (name)
- {
- if (services.get("pref").getPrefType(name) == Ci.nsIPrefBranch.PREF_BOOL)
- this.setPref(name, !this.getPref(name));
- else
- liberator.echoerr("E488: Trailing characters: " + name + "!");
- },
-
- /**
- * Pushes a new preference context onto the context stack.
- *
- * @see #withContext
- */
- pushContext: function ()
- {
- prefContexts.push({});
- },
-
- /**
- * Pops the top preference context from the stack.
- *
- * @see #withContext
- */
- popContext: function ()
- {
- for (let [k, v] in Iterator(prefContexts.pop()))
- storePreference(k, v);
- },
-
- /**
- * Executes <b>func</b> with a new preference context. When <b>func</b>
- * returns, the context is popped and any preferences set via
- * {@link #setPref} or {@link #invertPref} are restored to their
- * previous values.
- *
- * @param {function} func The function to call.
- * @param {Object} func The 'this' object with which to call <b>func</b>
- * @see #pushContext
- * @see #popContext
- */
- withContext: function (func, self)
- {
- try
- {
- this.pushContext();
- return func.call(self);
- }
- finally
- {
- this.popContext();
- }
- }
- }; //}}}
-
- for (let [, pref] in Iterator(self.allPrefs(OLD_SAVED)))
- {
- let saved = SAVED + pref.substr(OLD_SAVED.length)
- if (!self.getPref(saved))
- self.setPref(saved, self.getPref(pref));
- self.resetPref(pref);
- }
-
- self.prefObserver.register();
- liberator.registerObserver("shutdown", function () {
- self.prefObserver.unregister();
- });
-
- return self;
-
-} //}}}
+ },
+});
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/quickmarks.js b/common/content/quickmarks.js
new file mode 100644
index 00000000..ed0a8e4c
--- /dev/null
+++ b/common/content/quickmarks.js
@@ -0,0 +1,173 @@
+// Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
+//
+// This work is licensed for reuse under an MIT license. Details are
+// given in the LICENSE.txt file included with this file.
+
+/** @scope modules */
+
+/**
+* @instance quickmarks
+*/
+const QuickMarks = Module("quickmarks", {
+ requires: ["storage"],
+
+ init: function () {
+ this._qmarks = storage.newMap("quickmarks", { store: true });
+ },
+
+ /**
+ * Adds a new quickmark with name <b>qmark</b> referencing
+ * the URL <b>location</b>. Any existing quickmark with the same name
+ * will be replaced.
+ *
+ * @param {string} qmark The name of the quickmark {A-Z}.
+ * @param {string} location The URL accessed by this quickmark.
+ */
+ add: function add(qmark, location) {
+ this._qmarks.set(qmark, location);
+ liberator.echomsg("Added Quick Mark '" + qmark + "': " + location, 1);
+ },
+
+ /**
+ * Deletes the specified quickmarks. The <b>filter</b> is a list of
+ * quickmarks and ranges are supported. Eg. "ab c d e-k".
+ *
+ * @param {string} filter The list of quickmarks to delete.
+ *
+ */
+ remove: function remove(filter) {
+ let pattern = RegExp("[" + filter.replace(/\s+/g, "") + "]");
+
+ for (let [qmark, ] in this._qmarks) {
+ if (pattern.test(qmark))
+ this._qmarks.remove(qmark);
+ }
+ },
+
+ /**
+ * Removes all quickmarks.
+ */
+ removeAll: function removeAll() {
+ this._qmarks.clear();
+ },
+
+ /**
+ * Opens the URL referenced by the specified <b>qmark</b>.
+ *
+ * @param {string} qmark The quickmark to open.
+ * @param {number} where A constant describing where to open the page.
+ * See {@link Liberator#open}.
+ */
+ jumpTo: function jumpTo(qmark, where) {
+ let url = this._qmarks.get(qmark);
+
+ if (url)
+ liberator.open(url, where);
+ else
+ liberator.echoerr("E20: QuickMark not set");
+ },
+
+ /**
+ * Lists all quickmarks matching <b>filter</b> in the message window.
+ *
+ * @param {string} filter The list of quickmarks to display. Eg. "abc"
+ * Ranges are not supported.
+ */
+ // FIXME: filter should match that of quickmarks.remove or vice versa
+ list: function list(filter) {
+ let marks = [k for ([k, v] in this._qmarks)];
+ let lowercaseMarks = marks.filter(function (x) /[a-z]/.test(x)).sort();
+ let uppercaseMarks = marks.filter(function (x) /[A-Z]/.test(x)).sort();
+ let numberMarks = marks.filter(function (x) /[0-9]/.test(x)).sort();
+
+ marks = Array.concat(lowercaseMarks, uppercaseMarks, numberMarks);
+
+ liberator.assert(marks.length > 0, "No QuickMarks set");
+
+ if (filter.length > 0) {
+ marks = marks.filter(function (qmark) filter.indexOf(qmark) >= 0);
+ liberator.assert(marks.length >= 0, "E283: No QuickMarks matching \"" + filter + "\"");
+ }
+
+ let items = [[mark, this._qmarks.get(mark)] for ([k, mark] in Iterator(marks))];
+ template.genericTable(items, { title: ["QuickMark", "URL"] });
+ }
+}, {
+}, {
+ commands: function () {
+ commands.add(["delqm[arks]"],
+ "Delete the specified QuickMarks",
+ function (args) {
+ // TODO: finish arg parsing - we really need a proper way to do this. :)
+ // assert(args.bang ^ args.string)
+ liberator.assert( args.bang || args.string, "E471: Argument required");
+ liberator.assert(!args.bang || !args.string, "E474: Invalid argument");
+
+ if (args.bang)
+ quickmarks.removeAll();
+ else
+ quickmarks.remove(args.string);
+ },
+ {
+ bang: true,
+ completer: function (context) {
+ context.title = ["QuickMark", "URL"];
+ context.completions = this._qmarks;
+ }
+ });
+
+ commands.add(["qma[rk]"],
+ "Mark a URL with a letter for quick access",
+ function (args) {
+ let matches = args.string.match(/^([a-zA-Z0-9])(?:\s+(.+))?$/);
+ if (!matches)
+ liberator.echoerr("E488: Trailing characters");
+ else if (!matches[2])
+ quickmarks.add(matches[1], buffer.URL);
+ else
+ quickmarks.add(matches[1], matches[2]);
+ },
+ { argCount: "+" });
+
+ commands.add(["qmarks"],
+ "Show all QuickMarks",
+ function (args) {
+ args = args.string;
+
+ // ignore invalid qmark characters unless there are no valid qmark chars
+ liberator.assert(!args || /[a-zA-Z0-9]/.test(args), "E283: No QuickMarks matching \"" + args + "\"");
+
+ let filter = args.replace(/[^a-zA-Z0-9]/g, "");
+ quickmarks.list(filter);
+ });
+ },
+ mappings: function () {
+ var myModes = config.browserModes;
+
+ mappings.add(myModes,
+ ["go"], "Jump to a QuickMark",
+ function (arg) { quickmarks.jumpTo(arg, liberator.CURRENT_TAB); },
+ { arg: true });
+
+ mappings.add(myModes,
+ ["gn"], "Jump to a QuickMark in a new tab",
+ function (arg) {
+ quickmarks.jumpTo(arg,
+ /\bquickmark\b/.test(options["activate"]) ?
+ liberator.NEW_TAB : liberator.NEW_BACKGROUND_TAB);
+ },
+ { arg: true });
+
+ mappings.add(myModes,
+ ["M"], "Add new QuickMark for current URL",
+ function (arg) {
+ if (/[^a-zA-Z0-9]/.test(arg))
+ return void liberator.beep();
+
+ quickmarks.add(arg, buffer.URL);
+ },
+ { arg: true });
+ },
+});
+
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/sanitizer.js b/common/content/sanitizer.js
index c1e57c2e..cffaeca7 100644
--- a/common/content/sanitizer.js
+++ b/common/content/sanitizer.js
@@ -13,254 +13,36 @@
// FIXME:
// - finish 1.9.0 support if we're going to support sanitizing in Xulmus
-function Sanitizer() //{{{
-{
- ////////////////////////////////////////////////////////////////////////////////
- ////////////////////// PRIVATE SECTION /////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- const local = {}; // XXX: is there some reason liberator.loadModule doesn't create modules with new?
- services.get("subscriptLoader").loadSubScript("chrome://browser/content/sanitize.js", local);
- const Sanitizer = local.Sanitizer;
-
- var prefArgList = [["commandLine", "commandline"],
- ["offlineApps", "offlineapps"],
- ["siteSettings", "sitesettings"]];
-
- function prefToArg(pref)
- {
- let pref = pref.replace(/.*\./, "");
- return util.Array.toObject(prefArgList)[pref] || pref;
- }
-
- function argToPref(arg) [k for ([, [k, v]] in Iterator(prefArgList)) if (v == arg)][0] || arg
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// OPTIONS /////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- options.add(["sanitizeitems", "si"],
- "The default list of private items to sanitize",
- "stringlist", "cache,commandline,cookies,formdata,history,marks,sessions",
- {
- setter: function (values)
- {
- for (let [, pref] in Iterator(sanitizer.prefNames))
- {
- options.setPref(pref, false);
-
- for (let [, value] in Iterator(this.parseValues(values)))
- {
- if (prefToArg(pref) == value)
- {
- options.setPref(pref, true);
- break;
- }
- }
- }
-
- return values;
- },
- getter: function () sanitizer.prefNames.filter(function (pref) options.getPref(pref)).map(prefToArg).join(","),
- completer: function (value) [
- ["cache", "Cache"],
- ["commandline", "Command-line history"],
- ["cookies", "Cookies"],
- ["downloads", "Download history"],
- ["formdata", "Saved form and search history"],
- ["history", "Browsing history"],
- ["macros", "Saved macros"],
- ["marks", "Local and URL marks"],
- ["offlineapps", "Offline website data"],
- ["passwords", "Saved passwords"],
- ["sessions", "Authenticated sessions"],
- ["sitesettings", "Site preferences"],
- ],
- validator: Option.validateCompleter
- });
-
- options.add(["sanitizetimespan", "sts"],
- "The default sanitizer time span",
- "number", 1,
- {
- setter: function (value)
- {
- options.setPref("privacy.sanitize.timeSpan", value);
- return value;
- },
- getter: function () options.getPref("privacy.sanitize.timeSpan", this.defaultValue),
- completer: function (value) [
- ["0", "Everything"],
- ["1", "Last hour"],
- ["2", "Last two hours"],
- ["3", "Last four hours"],
- ["4", "Today"]
- ],
- validator: Option.validateCompleter
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMMANDS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- commands.add(["sa[nitize]"],
- "Clear private data",
- function (args)
- {
- if (options['private'])
- return void liberator.echomsg("Cannot sanitize items in private mode");
-
- let timespan = args["-timespan"] || options["sanitizetimespan"];
-
- sanitizer.range = Sanitizer.getClearRange(timespan);
- sanitizer.ignoreTimespan = !sanitizer.range;
-
- if (args.bang)
- {
- liberator.assert(args.length == 0, "E488: Trailing characters");
-
- liberator.log("Sanitizing all items in 'sanitizeitems'...");
-
- let errors = sanitizer.sanitize();
-
- if (errors)
- {
- for (let item in errors)
- liberator.echoerr("Error sanitizing " + item + ": " + errors[item]);
- }
- }
- else
- {
- liberator.assert(args.length > 0, "E471: Argument required");
-
- for (let [, item] in Iterator(args.map(argToPref)))
- {
- liberator.log("Sanitizing " + item + " items...");
-
- if (sanitizer.canClearItem(item))
- {
- try
- {
- sanitizer.items[item].range = sanitizer.range;
- sanitizer.clearItem(item);
- }
- catch (e)
- {
- liberator.echoerr("Error sanitizing " + item + ": " + e);
- }
- }
- else
- liberator.echomsg("Cannot sanitize " + item);
- }
- }
- },
- {
- argCount: "*", // FIXME: should be + and 0
- bang: true,
- completer: function (context) {
- context.title = ["Privacy Item", "Description"];
- context.completions = options.get("sanitizeitems").completer();
- },
- options: [
- [["-timespan", "-t"],
- commands.OPTION_INT,
- function (arg) /^[0-4]$/.test(arg),
- function () options.get("sanitizetimespan").completer()]
- ]
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// PUBLIC SECTION //////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- var self = new Sanitizer();
-
- // TODO: remove this version test
- if (/^1.9.1/.test(services.get("xulAppInfo").platformVersion))
- self.prefDomain = "privacy.cpd.";
- else
- self.prefDomain = "privacy.item.";
-
- self.prefDomain2 = "extensions.liberator.privacy.cpd.";
-
- // add liberator-specific private items
- [
- {
- name: "commandLine",
- action: function ()
- {
- let stores = ["command", "search"];
-
- if (self.range)
- {
- stores.forEach(function (store) {
- storage["history-" + store].mutate("filter", function (item) {
- let timestamp = item.timestamp * 1000;
- return timestamp < self.range[0] || timestamp > self.range[1];
- });
- });
- }
- else
- stores.forEach(function (store) { storage["history-" + store].truncate(0); });
- }
- },
- {
- name: "macros",
- action: function () { storage["macros"].clear(); }
- },
- {
- name: "marks",
- action: function ()
- {
- storage["local-marks"].clear();
- storage["url-marks"].clear();
- }
- }
- ].forEach(function (item) {
- let pref = self.prefDomain2 + item.name;
+const Sanitizer = Module("sanitizer", {
+ requires: ["liberator"],
- if (options.getPref(pref) == null)
- options.setPref(pref, false);
+ init: function () {
+ const self = this;
+ liberator.loadScript("chrome://browser/content/sanitize.js", Sanitizer);
+ this.__proto__.__proto__ = new Sanitizer.Sanitizer; // Good enough.
- self.items[item.name] = {
- canClear: true,
- clear: item.action
- }
- });
-
- // call Sanitize autocommand
- for (let [name, item] in Iterator(self.items))
- {
- let arg = prefToArg(name);
-
- if (item.clear)
- {
- let func = item.clear;
- item.clear = function () {
- autocommands.trigger("Sanitize", { name: arg })
- func.call(item);
- }
- }
- }
+ // TODO: remove this version test
+ if (/^1.9.1/.test(services.get("xulAppInfo").platformVersion))
+ self.prefDomain = "privacy.cpd.";
+ else
+ self.prefDomain = "privacy.item.";
- self.getClearRange = Sanitizer.getClearRange;
+ self.prefDomain2 = "extensions.liberator.privacy.cpd.";
+ },
// Largely ripped from from browser/base/content/sanitize.js so we can override
// the pref strategy without stepping on the global prefs namespace.
- self.sanitize = function () {
+ sanitize: function () {
const prefService = services.get("pref");
let branch = prefService.getBranch(this.prefDomain);
let branch2 = prefService.getBranch(this.prefDomain2);
let errors = null;
- function prefSet(name)
- {
- try
- {
+ function prefSet(name) {
+ try {
return branch.getBoolPref(name);
}
- catch (e)
- {
+ catch (e) {
return branch2.getBoolPref(name);
}
}
@@ -271,25 +53,21 @@ function Sanitizer() //{{{
else
range = this.range || Sanitizer.getClearRange();
- for (let itemName in this.items)
- {
+ for (let itemName in this.items) {
let item = this.items[itemName];
item.range = range;
- if ("clear" in item && item.canClear && prefSet(itemName))
- {
+ if ("clear" in item && item.canClear && prefSet(itemName)) {
liberator.log("Sanitizing " + itemName + " items...");
// Some of these clear() may raise exceptions (see bug #265028)
// to sanitize as much as possible, we catch and store them,
// rather than fail fast.
// Callers should check returned errors and give user feedback
// about items that could not be sanitized
- try
- {
+ try {
item.clear();
}
- catch (e)
- {
+ catch (e) {
if (!errors)
errors = {};
errors[itemName] = e;
@@ -299,14 +77,193 @@ function Sanitizer() //{{{
}
return errors;
- };
+ },
+
+ get prefNames() util.Array.flatten([this.prefDomain, this.prefDomain2].map(options.allPrefs)),
+}, {
+ prefArgList: [["commandLine", "commandline"],
+ ["offlineApps", "offlineapps"],
+ ["siteSettings", "sitesettings"]],
+ prefToArg: function (pref) {
+ let pref = pref.replace(/.*\./, "");
+ return util.Array.toObject(Sanitizer.prefArgList)[pref] || pref;
+ },
+
+ argToPref: function (arg) [k for ([k, v] in values(Sanitizer.prefArgList)) if (v == arg)][0] || arg,
+}, {
+ commands: function () {
+ commands.add(["sa[nitize]"],
+ "Clear private data",
+ function (args) {
+ if (options['private'])
+ return void liberator.echomsg("Cannot sanitize items in private mode");
+
+ let timespan = args["-timespan"] || options["sanitizetimespan"];
+
+ sanitizer.range = Sanitizer.getClearRange(timespan);
+ sanitizer.ignoreTimespan = !sanitizer.range;
+
+ if (args.bang) {
+ liberator.assert(args.length == 0, "E488: Trailing characters");
+
+ liberator.log("Sanitizing all items in 'sanitizeitems'...");
+
+ let errors = sanitizer.sanitize();
+
+ if (errors) {
+ for (let item in errors)
+ liberator.echoerr("Error sanitizing " + item + ": " + errors[item]);
+ }
+ }
+ else {
+ liberator.assert(args.length > 0, "E471: Argument required");
+
+ for (let [, item] in Iterator(args.map(Sanitizer.argToPref))) {
+ liberator.log("Sanitizing " + item + " items...");
+
+ if (sanitizer.canClearItem(item)) {
+ try {
+ sanitizer.items[item].range = sanitizer.range;
+ sanitizer.clearItem(item);
+ }
+ catch (e) {
+ liberator.echoerr("Error sanitizing " + item + ": " + e);
+ }
+ }
+ else
+ liberator.echomsg("Cannot sanitize " + item);
+ }
+ }
+ },
+ {
+ argCount: "*", // FIXME: should be + and 0
+ bang: true,
+ completer: function (context) {
+ context.title = ["Privacy Item", "Description"];
+ context.completions = options.get("sanitizeitems").completer();
+ },
+ options: [
+ [["-timespan", "-t"],
+ commands.OPTION_INT,
+ function (arg) /^[0-4]$/.test(arg),
+ function () options.get("sanitizetimespan").completer()]
+ ]
+ });
+ },
+ options: function () {
+ const self = this;
+
+ // add liberator-specific private items
+ [
+ {
+ name: "commandLine",
+ action: function () {
+ let stores = ["command", "search"];
+
+ if (self.range) {
+ stores.forEach(function (store) {
+ storage["history-" + store].mutate("filter", function (item) {
+ let timestamp = item.timestamp * 1000;
+ return timestamp < self.range[0] || timestamp > self.range[1];
+ });
+ });
+ }
+ else
+ stores.forEach(function (store) { storage["history-" + store].truncate(0); });
+ }
+ },
+ {
+ name: "macros",
+ action: function () { storage["macros"].clear(); }
+ },
+ {
+ name: "marks",
+ action: function () {
+ storage["local-marks"].clear();
+ storage["url-marks"].clear();
+ }
+ }
+ ].forEach(function (item) {
+ let pref = self.prefDomain2 + item.name;
- self.__defineGetter__("prefNames",
- function () util.Array.flatten([self.prefDomain, self.prefDomain2].map(options.allPrefs)));
- //}}}
+ if (options.getPref(pref) == null)
+ options.setPref(pref, false);
- return self;
+ self.items[item.name] = {
+ canClear: true,
+ clear: item.action
+ }
+ });
+
+ // call Sanitize autocommand
+ for (let [name, item] in Iterator(self.items)) {
+ let arg = Sanitizer.prefToArg(name);
+
+ if (item.clear) {
+ let func = item.clear;
+ item.clear = function () {
+ autocommands.trigger("Sanitize", { name: arg })
+ func.call(item);
+ }
+ }
+ }
-} //}}}
+ options.add(["sanitizeitems", "si"],
+ "The default list of private items to sanitize",
+ "stringlist", "cache,commandline,cookies,formdata,history,marks,sessions",
+ {
+ setter: function (values) {
+ for (let [, pref] in Iterator(sanitizer.prefNames)) {
+ continue;
+ options.setPref(pref, false);
+
+ for (let [, value] in Iterator(this.parseValues(values))) {
+ if (Sanitizer.prefToArg(pref) == value) {
+ options.setPref(pref, true);
+ break;
+ }
+ }
+ }
+
+ return values;
+ },
+ getter: function () sanitizer.prefNames.filter(function (pref) options.getPref(pref)).map(Sanitizer.prefToArg).join(","),
+ completer: function (value) [
+ ["cache", "Cache"],
+ ["commandline", "Command-line history"],
+ ["cookies", "Cookies"],
+ ["downloads", "Download history"],
+ ["formdata", "Saved form and search history"],
+ ["history", "Browsing history"],
+ ["macros", "Saved macros"],
+ ["marks", "Local and URL marks"],
+ ["offlineapps", "Offline website data"],
+ ["passwords", "Saved passwords"],
+ ["sessions", "Authenticated sessions"],
+ ["sitesettings", "Site preferences"],
+ ],
+ validator: Option.validateCompleter
+ });
+
+ options.add(["sanitizetimespan", "sts"],
+ "The default sanitizer time span",
+ "number", 1,
+ {
+ setter: function (value) {
+ options.setPref("privacy.sanitize.timeSpan", value);
+ return value;
+ },
+ getter: function () options.getPref("privacy.sanitize.timeSpan", this.defaultValue),
+ completer: function (value) [
+ ["0", "Everything"],
+ ["1", "Last hour"],
+ ["2", "Last two hours"],
+ ["3", "Last four hours"],
+ ["4", "Today"]
+ ],
+ validator: Option.validateCompleter
+ });
+ },
+});
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/services.js b/common/content/services.js
index 715277f7..2d3f6d7b 100644
--- a/common/content/services.js
+++ b/common/content/services.js
@@ -15,15 +15,46 @@ const Cu = Components.utils;
*
* @constructor
*/
-function Services()
-{
- const classes = {};
- const services = {};
+const Services = Module("services", {
+ init: function () {
+ this.classes = {};
+ this.services = {};
- function create(classes, ifaces, meth)
- {
- try
- {
+ this.add("appStartup", "@mozilla.org/toolkit/app-startup;1", Ci.nsIAppStartup);
+ this.add("autoCompleteSearch", "@mozilla.org/autocomplete/search;1?name=history", Ci.nsIAutoCompleteSearch);
+ this.add("bookmarks", "@mozilla.org/browser/nav-bookmarks-service;1", Ci.nsINavBookmarksService);
+ this.add("browserSearch", "@mozilla.org/browser/search-service;1", Ci.nsIBrowserSearchService);
+ this.add("cache", "@mozilla.org/network/cache-service;1", Ci.nsICacheService);
+ this.add("console", "@mozilla.org/consoleservice;1", Ci.nsIConsoleService);
+ this.add("liberator:", "@mozilla.org/network/protocol;1?name=liberator");
+ this.add("directory", "@mozilla.org/file/directory_service;1", Ci.nsIProperties);
+ this.add("downloadManager", "@mozilla.org/download-manager;1", Ci.nsIDownloadManager);
+ this.add("environment", "@mozilla.org/process/environment;1", Ci.nsIEnvironment);
+ this.add("extensionManager", "@mozilla.org/extensions/manager;1", Ci.nsIExtensionManager);
+ this.add("favicon", "@mozilla.org/browser/favicon-service;1", Ci.nsIFaviconService);
+ this.add("history", "@mozilla.org/browser/global-history;2", [Ci.nsIGlobalHistory3, Ci.nsINavHistoryService, Ci.nsIBrowserHistory]);
+ this.add("io", "@mozilla.org/network/io-service;1", Ci.nsIIOService);
+ this.add("json", "@mozilla.org/dom/json;1", Ci.nsIJSON, "createInstance");
+ this.add("livemark", "@mozilla.org/browser/livemark-service;2", Ci.nsILivemarkService);
+ this.add("observer", "@mozilla.org/observer-service;1", Ci.nsIObserverService);
+ this.add("pref", "@mozilla.org/preferences-service;1", [Ci.nsIPrefService, Ci.nsIPrefBranch, Ci.nsIPrefBranch2]);
+ this.add("profile", "@mozilla.org/toolkit/profile-service;1", Ci.nsIToolkitProfileService);
+ this.add("rdf", "@mozilla.org/rdf/rdf-service;1", Ci.nsIRDFService);
+ this.add("sessionStore", "@mozilla.org/browser/sessionstore;1", Ci.nsISessionStore);
+ this.add("subscriptLoader", "@mozilla.org/moz/jssubscript-loader;1", Ci.mozIJSSubScriptLoader);
+ this.add("threadManager", "@mozilla.org/thread-manager;1", Ci.nsIThreadManager);
+ this.add("windowMediator", "@mozilla.org/appshell/window-mediator;1", Ci.nsIWindowMediator);
+ this.add("windowWatcher", "@mozilla.org/embedcomp/window-watcher;1", Ci.nsIWindowWatcher);
+ this.add("xulAppInfo", "@mozilla.org/xre/app-info;1", Ci.nsIXULAppInfo);
+
+ this.addClass("file", "@mozilla.org/file/local;1", Ci.nsILocalFile);
+ this.addClass("file:", "@mozilla.org/network/protocol;1?name=file", Ci.nsIFileProtocolHandler);
+ this.addClass("find", "@mozilla.org/embedcomp/rangefind;1", Ci.nsIFind);
+ this.addClass("process", "@mozilla.org/process/util;1", Ci.nsIProcess);
+ },
+
+ _create: function (classes, ifaces, meth) {
+ try {
let res = Cc[classes][meth || "getService"]();
if (!ifaces)
return res.wrappedJSObject;
@@ -31,97 +62,52 @@ function Services()
ifaces.forEach(function (iface) res.QueryInterface(iface));
return res;
}
- catch (e)
- {
+ catch (e) {
// liberator.log() is not defined at this time, so just dump any error
dump("Service creation failed for '" + classes + "': " + e + "\n");
}
- }
-
- const self = {
- /* @property {Object} A map of all cached services. */
- get services() services,
-
- /* @property {Object} A map of all cached classes. */
- get classes() classes,
-
- /**
- * Adds a new XPCOM service to the cache.
- *
- * @param {string} name The service's cache key.
- * @param {string} class The class's contract ID.
- * @param {nsISupports|nsISupports[]} ifaces The interface or array of
- * interfaces implemented by this service.
- * @param {string} meth The name of the function used to instanciate
- * the service.
- */
- add: function (name, class, ifaces, meth)
- {
- return services[name] = create(class, ifaces, meth);
- },
-
- /**
- * Adds a new XPCOM class to the cache.
- *
- * @param {string} name The class's cache key.
- * @param {string} class The class's contract ID.
- * @param {nsISupports|nsISupports[]} ifaces The interface or array of
- * interfaces implemented by this class.
- */
- addClass: function (name, class, ifaces)
- {
- return classes[name] = function () create(class, ifaces, "createInstance");
- },
-
- /**
- * Returns the cached service with the specified name.
- *
- * @param {string} name The service's cache key.
- */
- get: function (name) services[name],
-
- /**
- * Returns a new instance of the cached class with the specified name.
- *
- * @param {string} name The class's cache key.
- */
- create: function (name) classes[name]()
- };
+ },
- self.add("appStartup", "@mozilla.org/toolkit/app-startup;1", Ci.nsIAppStartup);
- self.add("autoCompleteSearch", "@mozilla.org/autocomplete/search;1?name=history", Ci.nsIAutoCompleteSearch);
- self.add("bookmarks", "@mozilla.org/browser/nav-bookmarks-service;1", Ci.nsINavBookmarksService);
- self.add("browserSearch", "@mozilla.org/browser/search-service;1", Ci.nsIBrowserSearchService);
- self.add("cache", "@mozilla.org/network/cache-service;1", Ci.nsICacheService);
- self.add("console", "@mozilla.org/consoleservice;1", Ci.nsIConsoleService);
- self.add("directory", "@mozilla.org/file/directory_service;1", Ci.nsIProperties);
- self.add("environment", "@mozilla.org/process/environment;1", Ci.nsIEnvironment);
- self.add("extensionManager", "@mozilla.org/extensions/manager;1", Ci.nsIExtensionManager);
- self.add("favicon", "@mozilla.org/browser/favicon-service;1", Ci.nsIFaviconService);
- self.add("json", "@mozilla.org/dom/json;1", Ci.nsIJSON, "createInstance");
- self.add("liberator:", "@mozilla.org/network/protocol;1?name=liberator");
- self.add("livemark", "@mozilla.org/browser/livemark-service;2", Ci.nsILivemarkService);
- self.add("observer", "@mozilla.org/observer-service;1", Ci.nsIObserverService);
- self.add("io", "@mozilla.org/network/io-service;1", Ci.nsIIOService);
- self.add("pref", "@mozilla.org/preferences-service;1", [Ci.nsIPrefService, Ci.nsIPrefBranch, Ci.nsIPrefBranch2]);
- self.add("profile", "@mozilla.org/toolkit/profile-service;1", Ci.nsIToolkitProfileService);
- self.add("rdf", "@mozilla.org/rdf/rdf-service;1", Ci.nsIRDFService);
- self.add("sessionStore", "@mozilla.org/browser/sessionstore;1", Ci.nsISessionStore);
- self.add("subscriptLoader", "@mozilla.org/moz/jssubscript-loader;1", Ci.mozIJSSubScriptLoader);
- self.add("threadManager", "@mozilla.org/thread-manager;1", Ci.nsIThreadManager);
- self.add("windowMediator", "@mozilla.org/appshell/window-mediator;1", Ci.nsIWindowMediator);
- self.add("windowWatcher", "@mozilla.org/embedcomp/window-watcher;1", Ci.nsIWindowWatcher);
- self.add("xulAppInfo", "@mozilla.org/xre/app-info;1", Ci.nsIXULAppInfo);
+ /**
+ * Adds a new XPCOM service to the cache.
+ *
+ * @param {string} name The service's cache key.
+ * @param {string} class The class's contract ID.
+ * @param {nsISupports|nsISupports[]} ifaces The interface or array of
+ * interfaces implemented by this service.
+ * @param {string} meth The name of the function used to instanciate
+ * the service.
+ */
+ add: function (name, class, ifaces, meth) {
+ return this.services[name] = this._create(class, ifaces, meth);
+ },
- self.addClass("file", "@mozilla.org/file/local;1", Ci.nsILocalFile);
- self.addClass("file:", "@mozilla.org/network/protocol;1?name=file", Ci.nsIFileProtocolHandler);
- self.addClass("find", "@mozilla.org/embedcomp/rangefind;1", Ci.nsIFind);
- self.addClass("process", "@mozilla.org/process/util;1", Ci.nsIProcess);
- self.addClass("zipWriter", "@mozilla.org/zipwriter;1", Ci.nsIZipWriter);
+ /**
+ * Adds a new XPCOM class to the cache.
+ *
+ * @param {string} name The class's cache key.
+ * @param {string} class The class's contract ID.
+ * @param {nsISupports|nsISupports[]} ifaces The interface or array of
+ * interfaces implemented by this class.
+ */
+ addClass: function (name, class, ifaces) {
+ const self = this;
+ return this.classes[name] = function () self._create(class, ifaces, "createInstance");
+ },
- return self;
-}
+ /**
+ * Returns the cached service with the specified name.
+ *
+ * @param {string} name The service's cache key.
+ */
+ get: function (name) this.services[name],
-var services = Services();
+ /**
+ * Returns a new instance of the cached class with the specified name.
+ *
+ * @param {string} name The class's cache key.
+ */
+ create: function (name) this.classes[name]()
+});
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/statusline.js b/common/content/statusline.js
new file mode 100644
index 00000000..c0408cbe
--- /dev/null
+++ b/common/content/statusline.js
@@ -0,0 +1,245 @@
+// Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
+//
+// This work is licensed for reuse under an MIT license. Details are
+// given in the LICENSE.txt file included with this file.
+
+/** @scope modules */
+
+const StatusLine = Module("statusline", {
+ init: function () {
+ this._statusBar = document.getElementById("status-bar");
+ this._statusBar.collapsed = true; // it is later restored unless the user sets laststatus=0
+
+ // our status bar fields
+ this._statuslineWidget = document.getElementById("liberator-statusline");
+ this._urlWidget = document.getElementById("liberator-statusline-field-url");
+ this._inputBufferWidget = document.getElementById("liberator-statusline-field-inputbuffer");
+ this._progressWidget = document.getElementById("liberator-statusline-field-progress");
+ this._tabCountWidget = document.getElementById("liberator-statusline-field-tabcount");
+ this._bufferPositionWidget = document.getElementById("liberator-statusline-field-bufferposition");
+ },
+
+ /**
+ * Update the status bar to indicate how secure the website is:
+ * extended - Secure connection with Extended Validation(EV) certificate.
+ * secure - Secure connection with valid certificate.
+ * broken - Secure connection with invalid certificate, or
+ * mixed content.
+ * insecure - Insecure connection.
+ *
+ * @param {'extended'|'secure'|'broken'|'insecure'} type
+ */
+ setClass: function setClass(type) {
+ const highlightGroup = {
+ extended: "StatusLineExtended",
+ secure: "StatusLineSecure",
+ broken: "StatusLineBroken",
+ insecure: "StatusLine"
+ };
+
+ this._statusBar.setAttributeNS(NS.uri, "highlight", highlightGroup[type]);
+ },
+
+ // update all fields of the statusline
+ update: function update() {
+ this.updateUrl();
+ this.updateInputBuffer();
+ this.updateProgress();
+ this.updateTabCount();
+ this.updateBufferPosition();
+ },
+
+ /**
+ * Update the URL displayed in the status line. Also displays status
+ * icons, [+-♥], when there are next and previous pages in the
+ * current tab's history, and when the current URL is bookmarked,
+ * respectively.
+ *
+ * @param {string} url The URL to display.
+ * @default buffer.URL
+ */
+ updateUrl: function updateUrl(url) {
+ // ripped from Firefox; modified
+ function losslessDecodeURI(url) {
+ // 1. decodeURI decodes %25 to %, which creates unintended
+ // encoding sequences.
+ url = url.split("%25").map(decodeURI).join("%25");
+ // 2. Re-encode whitespace so that it doesn't get eaten away
+ // by the location bar (bug 410726).
+ url = url.replace(/[\r\n\t]/g, encodeURIComponent);
+
+ // Encode invisible characters (soft hyphen, zero-width space, BOM,
+ // line and paragraph separator, word joiner, invisible times,
+ // invisible separator, object replacement character) (bug 452979)
+ url = url.replace(/[\v\x0c\x1c\x1d\x1e\x1f\u00ad\u200b\ufeff\u2028\u2029\u2060\u2062\u2063\ufffc]/g,
+ encodeURIComponent);
+
+ // Encode bidirectional formatting characters.
+ // (RFC 3987 sections 3.2 and 4.1 paragraph 6)
+ url = url.replace(/[\u200e\u200f\u202a\u202b\u202c\u202d\u202e]/g,
+ encodeURIComponent);
+ return url;
+ };
+
+ if (url == null)
+ // TODO: this probably needs a more general solution.
+ url = losslessDecodeURI(buffer.URL);
+
+ // make it even more Vim-like
+ if (url == "about:blank") {
+ if (!buffer.title)
+ url = "[No Name]";
+ }
+ else {
+ url = url.replace(RegExp("^liberator://help/(\\S+)#(.*)"), function (m, n1, n2) n1 + " " + decodeURIComponent(n2) + " [Help]")
+ .replace(RegExp("^liberator://help/(\\S+)"), "$1 [Help]");
+ }
+
+ // when session information is available, add [+] when we can go
+ // backwards, [-] when we can go forwards
+ let modified = "";
+ if (window.getWebNavigation) {
+ let sh = window.getWebNavigation().sessionHistory;
+ if (sh && sh.index > 0)
+ modified += "+";
+ if (sh && sh.index < sh.count -1)
+ modified += "-";
+ }
+ if (modules.bookmarks) {
+ if (bookmarks.isBookmarked(buffer.URL))
+ modified += "\u2764"; // a heart symbol: ❤
+ //modified += "\u2665"; // a heart symbol: ♥
+ }
+
+ if (modified)
+ url += " [" + modified + "]";
+
+ this._urlWidget.value = url;
+ },
+
+ /**
+ * Set the contents of the status line's input buffer to the given
+ * string. Used primarily when a key press requires further input
+ * before being processed, including mapping counts and arguments,
+ * along with multi-key mappings.
+ *
+ * @param {string} buffer
+ */
+ updateInputBuffer: function updateInputBuffer(buffer) {
+ if (!buffer || typeof buffer != "string")
+ buffer = "";
+
+ this._inputBufferWidget.value = buffer;
+ },
+
+ /**
+ * Update the page load progress bar.
+ *
+ * @param {string|number} progress The current progress, as follows:
+ * A string - Displayed literally.
+ * A ratio 0 < n < 1 - Displayed as a progress bar.
+ * A number n <= 0 - Displayed as a "Loading" message.
+ * Any other number - The progress is cleared.
+ */
+ updateProgress: function updateProgress(progress) {
+ if (!progress)
+ progress = "";
+
+ if (typeof progress == "string")
+ this._progressWidget.value = progress;
+ else if (typeof progress == "number") {
+ let progressStr = "";
+ if (progress <= 0)
+ progressStr = "[ Loading... ]";
+ else if (progress < 1) {
+ progress = Math.floor(progress * 20);
+ progressStr = "["
+ + "====================".substr(0, progress)
+ + ">"
+ + " ".substr(0, 19 - progress)
+ + "]";
+ }
+ this._progressWidget.value = progressStr;
+ }
+ },
+
+ /**
+ * Display the correct tabcount (e.g., [1/5]) on the status bar.
+ *
+ * @param {bool} delayed When true, update count after a
+ * brief timeout. Useful in the many cases when an
+ * event that triggers an update is broadcast before
+ * the tab state is fully updated.
+ */
+ updateTabCount: function updateTabCount(delayed) {
+ if (liberator.has("tabs")) {
+ if (delayed)
+ return void setTimeout(function () statusline.updateTabCount(false), 0);
+
+ // update the ordinal which is used for numbered tabs
+ if (options.get("guioptions").has("n", "N"))
+ for (let [i, tab] in util.Array.iteritems(getBrowser().mTabs))
+ tab.setAttribute("ordinal", i + 1);
+
+ this._tabCountWidget.value = "[" + (tabs.index() + 1) + "/" + tabs.count + "]";
+ }
+ },
+
+ /**
+ * Display the main content's vertical scroll position in the status
+ * bar.
+ *
+ * @param {number} percent The position, as a percentage. @optional
+ */
+ updateBufferPosition: function updateBufferPosition(percent) {
+ if (!percent || typeof percent != "number") {
+ let win = document.commandDispatcher.focusedWindow;
+ if (!win)
+ return;
+ percent = win.scrollMaxY == 0 ? -1 : win.scrollY / win.scrollMaxY;
+ }
+
+ let bufferPositionStr = "";
+ percent = Math.round(percent * 100);
+ if (percent < 0)
+ bufferPositionStr = "All";
+ else if (percent == 0)
+ bufferPositionStr = "Top";
+ else if (percent < 10)
+ bufferPositionStr = " " + percent + "%";
+ else if (percent >= 100)
+ bufferPositionStr = "Bot";
+ else
+ bufferPositionStr = percent + "%";
+
+ this._bufferPositionWidget.value = bufferPositionStr;
+ }
+
+}, {
+}, {
+ options: function () {
+ options.add(["laststatus", "ls"],
+ "Show the status line",
+ "number", 2,
+ {
+ setter: function setter(value) {
+ if (value == 0)
+ document.getElementById("status-bar").collapsed = true;
+ else if (value == 1)
+ liberator.echoerr("show status line only with > 1 window not implemented yet");
+ else
+ document.getElementById("status-bar").collapsed = false;
+
+ return value;
+ },
+ completer: function completer(context) [
+ ["0", "Never display status line"],
+ ["1", "Display status line only if there are multiple windows"],
+ ["2", "Always display status line"]
+ ],
+ validator: Option.validateCompleter
+ });
+ },
+});
+
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/style.js b/common/content/style.js
index 1cccecf7..121c15ef 100644
--- a/common/content/style.js
+++ b/common/content/style.js
@@ -231,14 +231,13 @@ Highlights.prototype.CSS = <![CDATA[
*
* @author Kris Maglione <maglione.k@gmail.com>
*/
-function Highlights(name, store)
-{
+function Highlights(name, store) {
let self = this;
let highlight = {};
let styles = storage.styles;
const Highlight = Struct("class", "selector", "filter", "default", "value", "base");
- Highlight.defaultValue("filter", function ()
+ Highlight.defaultValue("filter", function ()
this.base ? this.base.filter :
["chrome://liberator/*",
"liberator:*",
@@ -256,8 +255,7 @@ function Highlights(name, store)
this.__iterator__ = function () (highlight[v] for ([k, v] in Iterator(keys())));
this.get = function (k) highlight[k];
- this.set = function (key, newStyle, force, append)
- {
+ this.set = function (key, newStyle, force, append) {
let [, class, selectors] = key.match(/^([a-zA-Z_-]+)(.*)/);
if (!(class in highlight))
@@ -270,10 +268,8 @@ function Highlights(name, store)
newStyle = (style.value || "").replace(/;?\s*$/, "; " + newStyle);
if (/^\s*$/.test(newStyle))
newStyle = null;
- if (newStyle == null)
- {
- if (style.default == null)
- {
+ if (newStyle == null) {
+ if (style.default == null) {
delete highlight[style.class];
styles.removeSheet(true, style.selector);
return null;
@@ -284,8 +280,7 @@ function Highlights(name, store)
let css = newStyle.replace(/(?:!\s*important\s*)?(?:;?\s*$|;)/g, "!important;")
.replace(";!important;", ";", "g"); // Seeming Spidermonkey bug
- if (!/^\s*(?:!\s*important\s*)?;*\s*$/.test(css))
- {
+ if (!/^\s*(?:!\s*important\s*)?;*\s*$/.test(css)) {
css = style.selector + " { " + css + " }";
let error = styles.addSheet(true, "highlight:" + style.class, style.filter, css, true);
@@ -301,8 +296,7 @@ function Highlights(name, store)
*
* @param {string} class
*/
- this.selector = function (class)
- {
+ this.selector = function (class) {
let [, hl, rest] = class.match(/^(\w*)(.*)/);
let pattern = "[liberator|highlight~=" + hl + "]"
if (highlight[hl] && highlight[hl].class != class)
@@ -314,8 +308,7 @@ function Highlights(name, store)
* Clears all highlighting rules. Rules with default values are
* reset.
*/
- this.clear = function ()
- {
+ this.clear = function () {
for (let [k, v] in Iterator(highlight))
this.set(k, null, true);
};
@@ -325,8 +318,7 @@ function Highlights(name, store)
*
* @param {string} css The rules to load. See {@link Highlights#css}.
*/
- this.loadCSS = function (css)
- {
+ this.loadCSS = function (css) {
css.replace(/^(\s*\S*\s+)\{((?:.|\n)*?)\}\s*$/gm, function (_, _1, _2) _1 + " " + _2.replace(/\n\s*/g, " "))
.split("\n").filter(function (s) /\S/.test(s))
.forEach(function (style)
@@ -340,8 +332,7 @@ function Highlights(name, store)
if (old && old.value != old.default)
style.value = old.value;
});
- for (let [class, hl] in Iterator(highlight))
- {
+ for (let [class, hl] in Iterator(highlight)) {
if (hl.value == hl.default)
this.set(class);
}
@@ -356,8 +347,7 @@ function Highlights(name, store)
*
* @author Kris Maglione <maglione.k@gmail.com>
*/
-function Styles(name, store)
-{
+function Styles(name, store) {
// Can't reference liberator or Components inside Styles --
// they're members of the window object, which disappear
// with this window.
@@ -387,14 +377,12 @@ function Styles(name, store)
Sheet.prototype.__defineGetter__("enabled", function () this._enabled);
Sheet.prototype.__defineSetter__("enabled", function (on) {
this._enabled = Boolean(on);
- if (on)
- {
+ if (on) {
self.registerSheet(cssUri(this.fullCSS));
if (this.agent)
self.registerSheet(cssUri(this.fullCSS), true);
}
- else
- {
+ else {
self.unregisterSheet(cssUri(this.fullCSS));
self.unregisterSheet(cssUri(this.fullCSS), true);
}
@@ -428,8 +416,7 @@ function Styles(name, store)
* "*" is matched as a prefix.
* @param {string} css The CSS to be applied.
*/
- this.addSheet = function (system, name, filter, css, agent)
- {
+ this.addSheet = function (system, name, filter, css, agent) {
let sheets = system ? systemSheets : userSheets;
let names = system ? systemNames : userNames;
if (name && name in names)
@@ -437,12 +424,10 @@ function Styles(name, store)
let sheet = Sheet(name, id++, filter.split(",").filter(util.identity), String(css), null, system, agent);
- try
- {
+ try {
sheet.enabled = true;
}
- catch (e)
- {
+ catch (e) {
return e.echoerr || e;
}
sheets.push(sheet);
@@ -459,8 +444,7 @@ function Styles(name, store)
* @param {string or number} sheet The sheet to retrieve. Strings indicate
* sheet names, while numbers indicate indices.
*/
- this.get = function get(system, sheet)
- {
+ this.get = function get(system, sheet) {
let sheets = system ? systemSheets : userSheets;
let names = system ? systemNames : userNames;
if (typeof sheet === "number")
@@ -478,8 +462,7 @@ function Styles(name, store)
* @param {string} css
* @param {number} index
*/
- this.findSheets = function (system, name, filter, css, index)
- {
+ this.findSheets = function (system, name, filter, css, index) {
let sheets = system ? systemSheets : userSheets;
let names = system ? systemNames : userNames;
@@ -508,11 +491,9 @@ function Styles(name, store)
* @param {string} css
* @param {number} index
*/
- this.removeSheet = function (system, name, filter, css, index)
- {
+ this.removeSheet = function (system, name, filter, css, index) {
let self = this;
- if (arguments.length == 0)
- {
+ if (arguments.length == 0) {
var matches = [system];
system = sheet.system;
}
@@ -531,8 +512,7 @@ function Styles(name, store)
if (matches.length == 0)
return;
- for (let [, sheet] in Iterator(matches.reverse()))
- {
+ for (let [, sheet] in Iterator(matches.reverse())) {
sheet.enabled = false;
if (name)
delete names[name];
@@ -540,8 +520,7 @@ function Styles(name, store)
sheets.splice(sheets.indexOf(sheet), 1);
/* Re-add if we're only changing the site filter. */
- if (filter)
- {
+ if (filter) {
let sites = sheet.sites.filter(function (f) f != filter);
if (sites.length)
this.addSheet(system, name, sites.join(","), css, sheet.agent);
@@ -558,8 +537,7 @@ function Styles(name, store)
* @param {boolean} reload Whether to reload any sheets that are
* already registered.
*/
- this.registerSheet = function (uri, agent, reload)
- {
+ this.registerSheet = function (uri, agent, reload) {
if (reload)
this.unregisterSheet(uri, agent);
uri = ios.newURI(uri, null, null);
@@ -572,298 +550,279 @@ function Styles(name, store)
*
* @param {string} uri The URI of the sheet to unregister.
*/
- this.unregisterSheet = function (uri, agent)
- {
+ this.unregisterSheet = function (uri, agent) {
uri = ios.newURI(uri, null, null);
if (sss.sheetRegistered(uri, agent ? sss.AGENT_SHEET : sss.USER_SHEET))
sss.unregisterSheet(uri, agent ? sss.AGENT_SHEET : sss.USER_SHEET);
};
}
-let (array = util.Array)
-{
- util.extend(Styles.prototype, {
- get sites() array([v.sites for ([k, v] in this.userSheets)]).flatten().uniq().__proto__,
- completeSite: function (context, content)
- {
- context.anchored = false;
- try
- {
- context.fork("current", 0, this, function (context) {
- context.title = ["Current Site"];
- context.completions = [
- [content.location.host, "Current Host"],
- [content.location.href, "Current URL"]
- ];
- });
- }
- catch (e) {}
- context.fork("others", 0, this, function (context) {
- context.title = ["Site"];
- context.completions = [[s, ""] for ([, s] in Iterator(styles.sites))];
- });
- }
- });
-}
-/**
- * @property {Styles}
- */
-const styles = storage.newObject("styles", Styles, { store: false });
-
-/**
- * @property {Highlights}
- */
-const highlight = storage.newObject("highlight", Highlights, { store: false });
-
-if (highlight.CSS != Highlights.prototype.CSS)
-{
- highlight.CSS = Highlights.prototype.CSS;
- highlight.loadCSS(highlight.CSS);
-}
-
-liberator.triggerObserver("load_styles", "styles");
-liberator.triggerObserver("load_highlight", "highlight");
-
-/////////////////////////////////////////////////////////////////////////////}}}
-////////////////////// COMMANDS ////////////////////////////////////////////////
-/////////////////////////////////////////////////////////////////////////////{{{
-
-liberator.registerObserver("load_commands", function () {
-
- commands.add(["colo[rscheme]"],
- "Load a color scheme",
- function (args)
- {
- let scheme = args[0];
-
- if (scheme == "default")
- highlight.clear();
- else
- liberator.assert(!io.sourceFromRuntimePath(["colors/" + scheme + ".vimp"]),
- "E185: Cannot find color scheme " + scheme);
- autocommands.trigger("ColorScheme", { name: scheme });
- },
- {
- argCount: "1",
- completer: function (context) completion.colorScheme(context)
- });
-
- commands.add(["sty[le]"],
- "Add or list user styles",
- function (args)
- {
- let [filter, css] = args;
- let name = args["-name"];
-
- if (!css)
- {
- let list = Array.concat([i for (i in styles.userNames)],
- [i for (i in styles.userSheets) if (!i[1].name)]);
- let str = template.tabular(["", "Name", "Filter", "CSS"],
- ["min-width: 1em; text-align: center; color: red; font-weight: bold;",
- "padding: 0 1em 0 1ex; vertical-align: top;",
- "padding: 0 1em 0 0; vertical-align: top;"],
- ([sheet.enabled ? "" : "\u00d7",
- key,
- sheet.sites.join(","),
- sheet.css]
- for ([i, [key, sheet]] in Iterator(list))
- if ((!filter || sheet.sites.indexOf(filter) >= 0) && (!name || sheet.name == name))));
- commandline.echo(str, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
- }
- else
- {
- if ("-append" in args)
- {
- let sheet = styles.get(false, name);
- if (sheet)
- {
- filter = sheet.sites.concat(filter).join(",");
- css = sheet.css + " " + css;
+Module("styles", {
+ requires: ["liberator", "storage", "util"],
+
+ init: function () {
+ let (array = util.Array) {
+ update(Styles.prototype, {
+ get sites() array([v.sites for ([k, v] in this.userSheets)]).flatten().uniq().__proto__,
+ completeSite: function (context, content) {
+ context.anchored = false;
+ try {
+ context.fork("current", 0, this, function (context) {
+ context.title = ["Current Site"];
+ context.completions = [
+ [content.location.host, "Current Host"],
+ [content.location.href, "Current URL"]
+ ];
+ });
}
+ catch (e) {}
+ context.fork("others", 0, this, function (context) {
+ context.title = ["Site"];
+ context.completions = [[s, ""] for ([, s] in Iterator(styles.sites))];
+ });
}
- let err = styles.addSheet(false, name, filter, css);
- if (err)
- liberator.echoerr(err);
- }
- },
- {
- bang: true,
- completer: function (context, args)
- {
- let compl = [];
- if (args.completeArg == 0)
- styles.completeSite(context, content);
- else if (args.completeArg == 1)
- {
- let sheet = styles.get(false, args["-name"]);
- if (sheet)
- context.completions = [[sheet.css, "Current Value"]];
+ });
+ }
+ return storage.newObject("styles", Styles, { store: false });
+ },
+}, {
+}, {
+ commands: function () {
+ commands.add(["sty[le]"],
+ "Add or list user styles",
+ function (args) {
+ let [filter, css] = args;
+ let name = args["-name"];
+
+ if (!css) {
+ let list = Array.concat([i for (i in styles.userNames)],
+ [i for (i in styles.userSheets) if (!i[1].name)]);
+ let str = template.tabular(["", "Name", "Filter", "CSS"],
+ ["min-width: 1em; text-align: center; color: red; font-weight: bold;",
+ "padding: 0 1em 0 1ex; vertical-align: top;",
+ "padding: 0 1em 0 0; vertical-align: top;"],
+ ([sheet.enabled ? "" : "\u00d7",
+ key,
+ sheet.sites.join(","),
+ sheet.css]
+ for ([i, [key, sheet]] in Iterator(list))
+ if ((!filter || sheet.sites.indexOf(filter) >= 0) && (!name || sheet.name == name))));
+ commandline.echo(str, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
+ }
+ else {
+ if ("-append" in args) {
+ let sheet = styles.get(false, name);
+ if (sheet) {
+ filter = sheet.sites.concat(filter).join(",");
+ css = sheet.css + " " + css;
+ }
+ }
+ let err = styles.addSheet(false, name, filter, css);
+ if (err)
+ liberator.echoerr(err);
}
},
- hereDoc: true,
- literal: 1,
- options: [[["-name", "-n"], commands.OPTION_STRING, null, function () [[k, v.css] for ([k, v] in Iterator(styles.userNames))]],
- [["-append", "-a"], commands.OPTION_NOARG]],
- serial: function () [
- {
- command: this.name,
- bang: true,
- options: sty.name ? { "-name": sty.name } : {},
- arguments: [sty.sites.join(",")],
- literalArg: sty.css
- } for ([k, sty] in styles.userSheets)
- ]
- });
+ {
+ bang: true,
+ completer: function (context, args) {
+ let compl = [];
+ if (args.completeArg == 0)
+ styles.completeSite(context, content);
+ else if (args.completeArg == 1) {
+ let sheet = styles.get(false, args["-name"]);
+ if (sheet)
+ context.completions = [[sheet.css, "Current Value"]];
+ }
+ },
+ hereDoc: true,
+ literal: 1,
+ options: [[["-name", "-n"], commands.OPTION_STRING, null, function () [[k, v.css] for ([k, v] in Iterator(styles.userNames))]],
+ [["-append", "-a"], commands.OPTION_NOARG]],
+ serial: function () [
+ {
+ command: this.name,
+ bang: true,
+ options: sty.name ? { "-name": sty.name } : {},
+ arguments: [sty.sites.join(",")],
+ literalArg: sty.css
+ } for ([k, sty] in styles.userSheets)
+ ]
+ });
- [
- {
- name: ["stylee[nable]", "stye[nable]"],
- desc: "Enable a user style sheet",
- action: function (sheet) sheet.enabled = true,
- filter: function (sheet) !sheet.enabled
- },
- {
- name: ["styled[isable]", "styd[isable]"],
- desc: "Disable a user style sheet",
- action: function (sheet) sheet.enabled = false,
- filter: function (sheet) sheet.enabled
- },
- {
- name: ["stylet[oggle]", "styt[oggle]"],
- desc: "Toggle a user style sheet",
- action: function (sheet) sheet.enabled = !sheet.enabled
- },
- {
- name: ["dels[tyle]"],
- desc: "Remove a user style sheet",
- action: function (sheet) styles.removeSheet(sheet)
- }
- ].forEach(function (cmd) {
- commands.add(cmd.name, cmd.desc,
- function (args)
+ [
{
- styles.findSheets(false, args["-name"], args[0], args.literalArg, args["-index"])
- .forEach(cmd.action);
+ name: ["stylee[nable]", "stye[nable]"],
+ desc: "Enable a user style sheet",
+ action: function (sheet) sheet.enabled = true,
+ filter: function (sheet) !sheet.enabled
},
- {
- completer: function (context) { context.completions = styles.sites.map(function (site) [site, ""]); },
- literal: 1,
- options: [[["-index", "-i"], commands.OPTION_INT, null,
- function (context) {
- context.compare = CompletionContext.Sort.number;
- return [[i, <>{sheet.sites.join(",")}: {sheet.css.replace("\n", "\\n")}</>]
- for ([i, sheet] in styles.userSheets)
- if (!cmd.filter || cmd.filter(sheet))]
- }],
- [["-name", "-n"], commands.OPTION_STRING, null,
- function () [[name, sheet.css]
- for ([name, sheet] in Iterator(styles.userNames))
- if (!cmd.filter || cmd.filter(sheet))]]]
- });
- });
-
- commands.add(["hi[ghlight]"],
- "Set the style of certain display elements",
- function (args)
- {
- let style = <![CDATA[
- ;
- display: inline-block !important;
- position: static !important;
- margin: 0px !important; padding: 0px !important;
- width: 3em !important; min-width: 3em !important; max-width: 3em !important;
- height: 1em !important; min-height: 1em !important; max-height: 1em !important;
- overflow: hidden !important;
- ]]>;
- let clear = args[0] == "clear";
- if (clear)
- args.shift();
-
- let [key, css] = args;
- if (clear && css)
- return liberator.echo("E488: Trailing characters");
-
- if (!css && !clear)
{
- // List matching keys
- let str = template.tabular(["Key", "Sample", "CSS"],
- ["padding: 0 1em 0 0; vertical-align: top",
- "text-align: center"],
- ([h.class,
- <span style={"text-align: center; line-height: 1em;" + h.value + style}>XXX</span>,
- template.highlightRegexp(h.value, /\b[-\w]+(?=:)/g)]
- for (h in highlight)
- if (!key || h.class.indexOf(key) > -1)));
- commandline.echo(str, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
- return;
- }
- if (!key && clear)
- return highlight.clear();
- let error = highlight.set(key, css, clear, "-append" in args);
- if (error)
- liberator.echoerr(error);
- },
- {
- // TODO: add this as a standard highlight completion function?
- completer: function (context, args)
+ name: ["styled[isable]", "styd[isable]"],
+ desc: "Disable a user style sheet",
+ action: function (sheet) sheet.enabled = false,
+ filter: function (sheet) sheet.enabled
+ },
{
- // Complete a highlight group on :hi clear ...
- if (args.completeArg > 0 && args[0] == "clear")
- args.completeArg = args.completeArg > 1 ? -1 : 0;
-
- if (args.completeArg == 0)
- context.completions = [[v.class, v.value] for (v in highlight)];
- else if (args.completeArg == 1)
- {
- let hl = highlight.get(args[0]);
- if (hl)
- context.completions = [[hl.value, "Current Value"], [hl.default || "", "Default Value"]];
- }
+ name: ["stylet[oggle]", "styt[oggle]"],
+ desc: "Toggle a user style sheet",
+ action: function (sheet) sheet.enabled = !sheet.enabled
},
- hereDoc: true,
- literal: 1,
- options: [[["-append", "-a"], commands.OPTION_NOARG]],
- serial: function () [
- {
- command: this.name,
- arguments: [k],
- literalArg: v
- }
- for ([k, v] in Iterator(highlight))
- if (v.value != v.default)
- ]
+ {
+ name: ["dels[tyle]"],
+ desc: "Remove a user style sheet",
+ action: function (sheet) styles.removeSheet(sheet)
+ }
+ ].forEach(function (cmd) {
+ commands.add(cmd.name, cmd.desc,
+ function (args) {
+ styles.findSheets(false, args["-name"], args[0], args.literalArg, args["-index"])
+ .forEach(cmd.action);
+ },
+ {
+ completer: function (context) { context.completions = styles.sites.map(function (site) [site, ""]); },
+ literal: 1,
+ options: [[["-index", "-i"], commands.OPTION_INT, null,
+ function (context) {
+ context.compare = CompletionContext.Sort.number;
+ return [[i, <>{sheet.sites.join(",")}: {sheet.css.replace("\n", "\\n")}</>]
+ for ([i, sheet] in styles.userSheets)
+ if (!cmd.filter || cmd.filter(sheet))]
+ }],
+ [["-name", "-n"], commands.OPTION_STRING, null,
+ function () [[name, sheet.css]
+ for ([name, sheet] in Iterator(styles.userNames))
+ if (!cmd.filter || cmd.filter(sheet))]]]
+ });
});
+ },
+ completion: function () {
+ completion.setFunctionCompleter(["get", "addSheet", "removeSheet", "findSheets"].map(function (m) styles[m]),
+ [ // Prototype: (system, name, filter, css, index)
+ null,
+ function (context, obj, args) args[0] ? styles.systemNames : styles.userNames,
+ function (context, obj, args) styles.completeSite(context, content),
+ null,
+ function (context, obj, args) args[0] ? styles.systemSheets : styles.userSheets
+ ]);
+ },
});
-/////////////////////////////////////////////////////////////////////////////}}}
-////////////////////// COMPLETIONS /////////////////////////////////////////////
-/////////////////////////////////////////////////////////////////////////////{{{
-
-liberator.registerObserver("load_completion", function () {
- completion.setFunctionCompleter(["get", "addSheet", "removeSheet", "findSheets"].map(function (m) styles[m]),
- [ // Prototype: (system, name, filter, css, index)
- null,
- function (context, obj, args) args[0] ? styles.systemNames : styles.userNames,
- function (context, obj, args) styles.completeSite(context, content),
- null,
- function (context, obj, args) args[0] ? styles.systemSheets : styles.userSheets
- ]);
-
- completion.colorScheme = function colorScheme(context) {
- context.title = ["Color Scheme", "Runtime Path"];
- context.keys = { text: function (f) f.leafName.replace(/\.vimp$/, ""), description: ".parent.path" };
- context.completions = util.Array.flatten(
- io.getRuntimeDirectories("colors").map(
- function (dir) dir.readDirectory().filter(
- function (file) /\.vimp$/.test(file.leafName))))
- };
+Module("highlight", {
+ requires: ["styles"],
- completion.highlightGroup = function highlightGroup(context) {
- context.title = ["Highlight Group", "Value"];
- context.completions = [[v.class, v.value] for (v in highlight)];
- };
+ init: function () {
+ const self = storage.newObject("highlight", Highlights, { store: false });
+
+ if (self.CSS != Highlights.prototype.CSS) {
+ self.CSS = Highlights.prototype.CSS;
+ self.loadCSS(self.CSS);
+ }
+ return self;
+ },
+}, {
+}, {
+ commands: function () {
+ commands.add(["colo[rscheme]"],
+ "Load a color scheme",
+ function (args) {
+ let scheme = args[0];
+
+ if (scheme == "default")
+ highlight.clear();
+ else
+ liberator.assert(!io.sourceFromRuntimePath(["colors/" + scheme + ".vimp"]),
+ "E185: Cannot find color scheme " + scheme);
+ autocommands.trigger("ColorScheme", { name: scheme });
+ },
+ {
+ argCount: "1",
+ completer: function (context) completion.colorScheme(context)
+ });
+
+ commands.add(["hi[ghlight]"],
+ "Set the style of certain display elements",
+ function (args) {
+ let style = <![CDATA[
+ ;
+ display: inline-block !important;
+ position: static !important;
+ margin: 0px !important; padding: 0px !important;
+ width: 3em !important; min-width: 3em !important; max-width: 3em !important;
+ height: 1em !important; min-height: 1em !important; max-height: 1em !important;
+ overflow: hidden !important;
+ ]]>;
+ let clear = args[0] == "clear";
+ if (clear)
+ args.shift();
+
+ let [key, css] = args;
+ if (clear && css)
+ return liberator.echo("E488: Trailing characters");
+
+ if (!css && !clear) {
+ // List matching keys
+ let str = template.tabular(["Key", "Sample", "CSS"],
+ ["padding: 0 1em 0 0; vertical-align: top",
+ "text-align: center"],
+ ([h.class,
+ <span style={"text-align: center; line-height: 1em;" + h.value + style}>XXX</span>,
+ template.highlightRegexp(h.value, /\b[-\w]+(?=:)/g)]
+ for (h in highlight)
+ if (!key || h.class.indexOf(key) > -1)));
+ commandline.echo(str, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
+ return;
+ }
+ if (!key && clear)
+ return highlight.clear();
+ let error = highlight.set(key, css, clear, "-append" in args);
+ if (error)
+ liberator.echoerr(error);
+ },
+ {
+ // TODO: add this as a standard highlight completion function?
+ completer: function (context, args) {
+ // Complete a highlight group on :hi clear ...
+ if (args.completeArg > 0 && args[0] == "clear")
+ args.completeArg = args.completeArg > 1 ? -1 : 0;
+
+ if (args.completeArg == 0)
+ context.completions = [[v.class, v.value] for (v in highlight)];
+ else if (args.completeArg == 1) {
+ let hl = highlight.get(args[0]);
+ if (hl)
+ context.completions = [[hl.value, "Current Value"], [hl.default || "", "Default Value"]];
+ }
+ },
+ hereDoc: true,
+ literal: 1,
+ options: [[["-append", "-a"], commands.OPTION_NOARG]],
+ serial: function () [
+ {
+ command: this.name,
+ arguments: [k],
+ literalArg: v
+ }
+ for ([k, v] in Iterator(highlight))
+ if (v.value != v.default)
+ ]
+ });
+ },
+ completion: function () {
+ completion.colorScheme = function colorScheme(context) {
+ context.title = ["Color Scheme", "Runtime Path"];
+ context.keys = { text: function (f) f.leafName.replace(/\.vimp$/, ""), description: ".parent.path" };
+ context.completions = util.Array.flatten(
+ io.getRuntimeDirectories("colors").map(
+ function (dir) dir.readDirectory().filter(
+ function (file) /\.vimp$/.test(file.leafName))))
+
+ };
+
+ completion.highlightGroup = function highlightGroup(context) {
+ context.title = ["Highlight Group", "Value"];
+ context.completions = [[v.class, v.value] for (v in highlight)];
+ };
+ },
});
-//}}}
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/tabs.js b/common/content/tabs.js
index 396e30d2..c81701df 100644
--- a/common/content/tabs.js
+++ b/common/content/tabs.js
@@ -11,28 +11,508 @@
/**
* @instance tabs
*/
-function Tabs() //{{{
-{
- ////////////////////////////////////////////////////////////////////////////////
- ////////////////////// PRIVATE SECTION /////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- var getBrowser = config.getBrowser || window.getBrowser;
-
- var alternates = [getBrowser().mCurrentTab, null];
-
- // used for the "gb" and "gB" mappings to remember the last :buffer[!] command
- var lastBufferSwitchArgs = "";
- var lastBufferSwitchSpecial = true;
-
- // @param spec can either be:
- // - an absolute integer
- // - "" for the current tab
- // - "+1" for the next tab
- // - "-3" for the tab, which is 3 positions left of the current
- // - "$" for the last tab
- function indexFromSpec(spec, wrap)
- {
+const Tabs = Module("tabs", {
+ init: function () {
+ this._alternates = [getBrowser().mCurrentTab, null];
+
+ // used for the "gb" and "gB" mappings to remember the last :buffer[!] command
+ this._lastBufferSwitchArgs = "";
+ this._lastBufferSwitchSpecial = true;
+
+ // hide tabs initially to prevent flickering when 'stal' would hide them
+ // on startup
+ if (config.hasTabbrowser)
+ getBrowser().mTabContainer.collapsed = true; // FIXME: see 'stal' comment
+ },
+
+ /**
+ * @property {Object} The previously accessed tab or null if no tab
+ * other than the current one has been accessed.
+ */
+ get alternate() this._alternates[1],
+
+ /**
+ * @property {Iterator(Object)} A genenerator that returns all browsers
+ * in the current window.
+ */
+ get browsers() {
+ let browsers = getBrowser().browsers;
+ for (let i = 0; i < browsers.length; i++)
+ yield [i, browsers[i]];
+ },
+
+ /**
+ * @property {boolean} Whether the tab numbering XBL binding has been
+ * applied.
+ */
+ get tabsBound() Boolean(styles.get(true, "tab-binding")),
+ set tabsBound(val) {
+ let fragment = liberator.has("MacUnix") ? "tab-mac" : "tab";
+ if (!val)
+ styles.removeSheet(true, "tab-binding");
+ else if (!this.tabsBound)
+ styles.addSheet(true, "tab-binding", "chrome://browser/content/browser.xul",
+ ".tabbrowser-tab { -moz-binding: url(chrome://liberator/content/bindings.xml#" + fragment + ") !important; }" +
+ // FIXME: better solution for themes?
+ ".tabbrowser-tab[busy] > .tab-icon > .tab-icon-image { list-style-image: url('chrome://global/skin/icons/loading_16.png') !important; }");
+ },
+
+ /**
+ * @property {number} The number of tabs in the current window.
+ */
+ get count() getBrowser().mTabs.length,
+
+ /**
+ * @property {Object} The local options store for the current tab.
+ */
+ get options() {
+ let store = this.localStore;
+ if (!("options" in store))
+ store.options = {};
+ return store.options;
+ },
+
+ getBrowser: getBrowser,
+
+ /**
+ * Returns the local state store for the tab at the specified
+ * <b>tabIndex</b>. If <b>tabIndex</b> is not specified then the
+ * current tab is used.
+ *
+ * @param {number} tabIndex
+ * @returns {Object}
+ */
+ // FIXME: why not a tab arg? Why this and the property?
+ getLocalStore: function (tabIndex) {
+ let tab = this.getTab(tabIndex);
+ if (!tab.liberatorStore)
+ tab.liberatorStore = {};
+ return tab.liberatorStore;
+ },
+
+ /**
+ * @property {Object} The local state store for the currently selected
+ * tab.
+ */
+ get localStore() this.getLocalStore(),
+
+ /**
+ * @property {Object[]} The array of closed tabs for the current
+ * session.
+ */
+ get closedTabs() services.get("json").decode(services.get("sessionStore").getClosedTabData(window)),
+
+ /**
+ * Returns the index of <b>tab</b> or the index of the currently
+ * selected tab if <b>tab</b> is not specified. This is a 0-based
+ * index.
+ *
+ * @param {Object} tab A tab from the current tab list.
+ * @returns {number}
+ */
+ index: function (tab) {
+ if (tab)
+ return Array.indexOf(getBrowser().mTabs, tab);
+ else
+ return getBrowser().mTabContainer.selectedIndex;
+ },
+
+ // TODO: implement filter
+ /**
+ * Returns an array of all tabs in the tab list.
+ *
+ * @returns {Object[]}
+ */
+ // FIXME: why not return the tab element?
+ // : unused? Remove me.
+ get: function () {
+ let buffers = [];
+ for (let [i, browser] in this.browsers) {
+ let title = browser.contentTitle || "(Untitled)";
+ let uri = browser.currentURI.spec;
+ let number = i + 1;
+ buffers.push([number, title, uri]);
+ }
+ return buffers;
+ },
+
+ /**
+ * Returns the index of the tab containing <b>content</b>.
+ *
+ * @param {Object} content Either a content window or a content
+ * document.
+ */
+ // FIXME: Only called once...necessary?
+ getContentIndex: function (content) {
+ for (let [i, browser] in this.browsers) {
+ if (browser.contentWindow == content || browser.contentDocument == content)
+ return i;
+ }
+ return -1;
+ },
+
+ /**
+ * Returns the tab at the specified <b>index</b> or the currently
+ * selected tab if <b>index</b> is not specified. This is a 0-based
+ * index.
+ *
+ * @param {number} index The index of the tab required.
+ * @returns {Object}
+ */
+ getTab: function (index) {
+ if (index != undefined)
+ return getBrowser().mTabs[index];
+ else
+ return getBrowser().mCurrentTab;
+ },
+
+ /**
+ * Lists all tabs matching <b>filter</b>.
+ *
+ * @param {string} filter A filter matching a substring of the tab's
+ * document title or URL.
+ */
+ list: function (filter) {
+ completion.listCompleter("buffer", filter);
+ },
+
+ /**
+ * Moves a tab to a new position in the tab list.
+ *
+ * @param {Object} tab The tab to move.
+ * @param {string} spec See {@link Tabs.indexFromSpec}.
+ * @param {boolean} wrap Whether an out of bounds <b>spec</b> causes
+ * the destination position to wrap around the start/end of the tab
+ * list.
+ */
+ move: function (tab, spec, wrap) {
+ let index = Tabs.indexFromSpec(spec, wrap);
+ getBrowser().moveTabTo(tab, index);
+ },
+
+ /**
+ * Removes the specified <b>tab</b> from the tab list.
+ *
+ * @param {Object} tab
+ * @param {number} count
+ * @param {boolean} focusLeftTab Focus the tab to the left of the removed tab.
+ * @param {number} quitOnLastTab Whether to quit if the tab being
+ * deleted is the only tab in the tab list:
+ * 1 - quit without saving session
+ * 2 - quit and save session
+ */
+ // FIXME: what is quitOnLastTab {1,2} all about then, eh? --djk
+ remove: function (tab, count, focusLeftTab, quitOnLastTab) {
+ let removeOrBlankTab = {
+ Firefox: function (tab) {
+ if (getBrowser().mTabs.length > 1)
+ getBrowser().removeTab(tab);
+ else {
+ if (buffer.URL != "about:blank" ||
+ window.getWebNavigation().sessionHistory.count > 0) {
+ liberator.open("about:blank", liberator.NEW_BACKGROUND_TAB);
+ getBrowser().removeTab(tab);
+ }
+ else
+ liberator.beep();
+ }
+ },
+ Thunderbird: function (tab) {
+ if (getBrowser().mTabs.length > 1)
+ getBrowser().removeTab(tab);
+ else
+ liberator.beep();
+ },
+ Songbird: function (tab) {
+ if (getBrowser().mTabs.length > 1)
+ getBrowser().removeTab(tab);
+ else {
+ if (buffer.URL != "about:blank" ||
+ window.getWebNavigation().sessionHistory.count > 0)
+ {
+ liberator.open("about:blank", liberator.NEW_BACKGROUND_TAB);
+ getBrowser().removeTab(tab);
+ }
+ else
+ liberator.beep();
+ }
+ }
+ }[config.hostApplication] || function () {};
+
+ if (typeof count != "number" || count < 1)
+ count = 1;
+
+ if (quitOnLastTab >= 1 && getBrowser().mTabs.length <= count) {
+ if (liberator.windows.length > 1)
+ window.close();
+ else
+ liberator.quit(quitOnLastTab == 2);
+
+ return;
+ }
+
+ let index = this.index(tab);
+ if (focusLeftTab) {
+ let lastRemovedTab = 0;
+ for (let i = index; i > index - count && i >= 0; i--) {
+ removeOrBlankTab(this.getTab(i));
+ lastRemovedTab = i > 0 ? i : 1;
+ }
+ getBrowser().mTabContainer.selectedIndex = lastRemovedTab - 1;
+ }
+ else {
+ let i = index + count - 1;
+ if (i >= this.count)
+ i = this.count - 1;
+
+ for (; i >= index; i--)
+ removeOrBlankTab(this.getTab(i));
+ getBrowser().mTabContainer.selectedIndex = index;
+ }
+ },
+
+ /**
+ * Removes all tabs from the tab list except the specified <b>tab</b>.
+ *
+ * @param {Object} tab The tab to keep.
+ */
+ keepOnly: function (tab) {
+ getBrowser().removeAllTabsBut(tab);
+ },
+
+ /**
+ * Selects the tab at the position specified by <b>spec</b>.
+ *
+ * @param {string} spec See {@link Tabs.indexFromSpec}
+ * @param {boolean} wrap Whether an out of bounds <b>spec</b> causes
+ * the selection position to wrap around the start/end of the tab
+ * list.
+ */
+ select: function (spec, wrap) {
+ let index = Tabs.indexFromSpec(spec, wrap);
+ // FIXME:
+ if (index == -1)
+ return void liberator.beep();
+ getBrowser().mTabContainer.selectedIndex = index;
+ },
+
+ /**
+ * Reloads the specified tab.
+ *
+ * @param {Object} tab The tab to reload.
+ * @param {boolean} bypassCache Whether to bypass the cache when
+ * reloading.
+ */
+ reload: function (tab, bypassCache) {
+ if (bypassCache) {
+ const flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
+ getBrowser().getBrowserForTab(tab).reloadWithFlags(flags);
+ }
+ else
+ getBrowser().reloadTab(tab);
+ },
+
+ /**
+ * Reloads all tabs.
+ *
+ * @param {boolean} bypassCache Whether to bypass the cache when
+ * reloading.
+ */
+ reloadAll: function (bypassCache) {
+ if (bypassCache) {
+ for (let i = 0; i < getBrowser().mTabs.length; i++) {
+ try {
+ this.reload(getBrowser().mTabs[i], bypassCache);
+ }
+ catch (e) {
+ // FIXME: can we do anything useful here without stopping the
+ // other tabs from reloading?
+ }
+ }
+ }
+ else
+ getBrowser().reloadAllTabs();
+ },
+
+ /**
+ * Stops loading the specified tab.
+ *
+ * @param {Object} tab The tab to stop loading.
+ */
+ stop: function (tab) {
+ if (config.stop)
+ config.stop(tab);
+ else
+ tab.linkedBrowser.stop();
+ },
+
+ /**
+ * Stops loading all tabs.
+ */
+ stopAll: function () {
+ for (let [, browser] in this.browsers)
+ browser.stop();
+ },
+
+ /**
+ * Selects the tab containing the specified <b>buffer</b>.
+ *
+ * @param {string} buffer A string which matches the URL or title of a
+ * buffer, if it is null, the last used string is used again.
+ * @param {boolean} allowNonUnique Whether to select the first of
+ * multiple matches.
+ * @param {number} count If there are multiple matches select the
+ * count'th match.
+ * @param {boolean} reverse Whether to search the buffer list in
+ * reverse order.
+ *
+ */
+ // FIXME: help!
+ switchTo: function (buffer, allowNonUnique, count, reverse) {
+ if (buffer == "")
+ return;
+
+ if (buffer != null) {
+ // store this command, so it can be repeated with "B"
+ this._lastBufferSwitchArgs = buffer;
+ this._lastBufferSwitchSpecial = allowNonUnique;
+ }
+ else {
+ buffer = this._lastBufferSwitchArgs;
+ if (allowNonUnique === undefined || allowNonUnique == null) // XXX
+ allowNonUnique = this._lastBufferSwitchSpecial;
+ }
+
+ if (buffer == "#") {
+ tabs.selectAlternateTab();
+ return;
+ }
+
+ if (!count || count < 1)
+ count = 1;
+ if (typeof reverse != "boolean")
+ reverse = false;
+
+ let matches = buffer.match(/^(\d+):?/);
+ if (matches) {
+ tabs.select(parseInt(matches[1], 10) - 1, false); // make it zero-based
+ return;
+ }
+
+ matches = [];
+ let lowerBuffer = buffer.toLowerCase();
+ let first = tabs.index() + (reverse ? 0 : 1);
+ let nbrowsers = getBrowser().browsers.length;
+ for (let [i, ] in tabs.browsers) {
+ let index = (i + first) % nbrowsers;
+ let url = getBrowser().getBrowserAtIndex(index).contentDocument.location.href;
+ let title = getBrowser().getBrowserAtIndex(index).contentDocument.title.toLowerCase();
+ if (url == buffer) {
+ tabs.select(index, false);
+ return;
+ }
+
+ if (url.indexOf(buffer) >= 0 || title.indexOf(lowerBuffer) >= 0)
+ matches.push(index);
+ }
+ if (matches.length == 0)
+ liberator.echoerr("E94: No matching buffer for " + buffer);
+ else if (matches.length > 1 && !allowNonUnique)
+ liberator.echoerr("E93: More than one match for " + buffer);
+ else {
+ if (reverse) {
+ index = matches.length - count;
+ while (index < 0)
+ index += matches.length;
+ }
+ else
+ index = (count - 1) % matches.length;
+
+ tabs.select(matches[index], false);
+ }
+ },
+
+ /**
+ * Clones the specified <b>tab</b> and append it to the tab list.
+ *
+ * @param {Object} tab The tab to clone.
+ * @param {boolean} activate Whether to select the newly cloned tab.
+ */
+ cloneTab: function (tab, activate) {
+ let newTab = getBrowser().addTab();
+ Tabs.copyTab(newTab, tab);
+
+ if (activate)
+ getBrowser().mTabContainer.selectedItem = newTab;
+
+ return newTab;
+ },
+
+ /**
+ * Detaches the specified <b>tab</b> and open it in a new window. If no
+ * tab is specified the currently selected tab is detached.
+ *
+ * @param {Object} tab The tab to detach.
+ */
+ detachTab: function (tab) {
+ if (!tab)
+ tab = getBrowser().mTabContainer.selectedItem;
+
+ services.get("windowWatcher")
+ .openWindow(window, window.getBrowserURL(), null, "chrome,dialog=no,all", tab);
+ },
+
+ /**
+ * Selects the alternate tab.
+ */
+ selectAlternateTab: function () {
+ liberator.assert(tabs.alternate != null && tabs.getTab() != tabs.alternate,
+ "E23: No alternate page");
+
+ // NOTE: this currently relies on v.tabs.index() returning the
+ // currently selected tab index when passed null
+ let index = tabs.index(tabs.alternate);
+
+ // TODO: since a tab close is more like a bdelete for us we
+ // should probably reopen the closed tab when a 'deleted'
+ // alternate is selected
+ liberator.assert(index >= 0, "E86: Buffer does not exist"); // TODO: This should read "Buffer N does not exist"
+ tabs.select(index);
+ },
+
+ // NOTE: when restarting a session FF selects the first tab and then the
+ // tab that was selected when the session was created. As a result the
+ // alternate after a restart is often incorrectly tab 1 when there
+ // shouldn't be one yet.
+ /**
+ * Sets the current and alternate tabs, updating the tab selection
+ * history.
+ *
+ * @param {Array(Object)} tabs The current and alternate tab.
+ * @see tabs#alternate
+ */
+ updateSelectionHistory: function (tabs) {
+ this._alternates = tabs || [this.getTab(), this._alternates[0]];
+ }
+}, {
+ copyTab: function (to, from) {
+ if (!from)
+ from = getBrowser().mTabContainer.selectedItem;
+
+ let tabState = services.get("sessionStore").getTabState(from);
+ services.get("sessionStore").setTabState(to, tabState);
+ },
+
+
+ /**
+ * @param spec can either be:
+ * - an absolute integer
+ * - "" for the current tab
+ * - "+1" for the next tab
+ * - "-3" for the tab, which is 3 positions left of the current
+ * - "$" for the last tab
+ */
+ indexFromSpec: function (spec, wrap) {
let position = getBrowser().mTabContainer.selectedIndex;
let length = getBrowser().mTabs.length;
let last = length - 1;
@@ -57,430 +537,57 @@ function Tabs() //{{{
position = wrap ? (position % length) + length : 0;
return position;
- }
-
- function copyTab(to, from)
- {
- if (!from)
- from = getBrowser().mTabContainer.selectedItem;
-
- let tabState = services.get("sessionStore").getTabState(from);
- services.get("sessionStore").setTabState(to, tabState);
- }
-
- // hide tabs initially to prevent flickering when 'stal' would hide them
- // on startup
- if (config.hasTabbrowser)
- getBrowser().mTabContainer.collapsed = true; // FIXME: see 'stal' comment
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// OPTIONS /////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- options.add(["showtabline", "stal"],
- "Control when to show the tab bar of opened web pages",
- "number", config.defaults["showtabline"],
- {
- setter: function (value)
- {
- // FIXME: we manipulate mTabContainer underneath mStrip so we
- // don't have to fight against the host app's attempts to keep
- // it open - hack! Adding a filter watch to mStrip is probably
- // the cleanest solution.
- let tabStrip = getBrowser().mTabContainer;
-
- if (value == 0)
- tabStrip.collapsed = true;
- else
- {
- // FIXME: Why are we preferring our own created preference
- // here? --djk
- let pref = "browser.tabStrip.autoHide";
- if (options.getPref(pref) == null) // Try for FF 3.0 & 3.1
- pref = "browser.tabs.autoHide";
- options.safeSetPref(pref, value == 1);
- tabStrip.collapsed = false;
- }
-
- return value;
- },
- completer: function (context) [
- ["0", "Never show tab bar"],
- ["1", "Show tab bar only if more than one tab is open"],
- ["2", "Always show tab bar"]
- ],
- validator: Option.validateCompleter
- });
-
- if (config.hasTabbrowser)
- {
- options.add(["activate", "act"],
- "Define when tabs are automatically activated",
- "stringlist", "homepage,quickmark,tabopen,paste",
- {
- completer: function (context) [
- ["homepage", "gH mapping"],
- ["quickmark", "go and gn mappings"],
- ["tabopen", ":tabopen[!] command"],
- ["paste", "P and gP mappings"]
- ],
- validator: Option.validateCompleter
- });
-
- options.add(["newtab"],
- "Define which commands should output in a new tab by default",
- "stringlist", "",
- {
- completer: function (context) [
- ["all", "All commands"],
- ["addons", ":addo[ns] command"],
- ["downloads", ":downl[oads] command"],
- ["extoptions", ":exto[ptions] command"],
- ["help", ":h[elp] command"],
- ["javascript", ":javascript! or :js! command"],
- ["prefs", ":pref[erences]! or :prefs! command"]
- ],
- validator: Option.validateCompleter
- });
-
- // TODO: Is this really applicable to Xulmus?
- options.add(["popups", "pps"],
- "Where to show requested popup windows",
- "stringlist", "tab",
- {
- setter: function (value)
- {
- let [open, restriction] = [1, 0];
- for (let [, opt] in Iterator(this.parseValues(value)))
- {
- if (opt == "tab")
- open = 3;
- else if (opt == "window")
- open = 2;
- else if (opt == "resized")
- restriction = 2;
- }
-
- options.safeSetPref("browser.link.open_newwindow", open, "See 'popups' option.");
- options.safeSetPref("browser.link.open_newwindow.restriction", restriction, "See 'popups' option.");
- return value;
- },
- completer: function (context) [
- ["tab", "Open popups in a new tab"],
- ["window", "Open popups in a new window"],
- ["resized", "Open resized popups in a new window"]
- ],
- validator: Option.validateCompleter
- });
- }
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// MAPPINGS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- mappings.add([modes.NORMAL], ["g0", "g^"],
- "Go to the first tab",
- function (count) { tabs.select(0); });
-
- mappings.add([modes.NORMAL], ["g$"],
- "Go to the last tab",
- function (count) { tabs.select("$"); });
-
- mappings.add([modes.NORMAL], ["gt"],
- "Go to the next tab",
- function (count)
- {
- if (count > 0)
- tabs.select(count - 1, false);
- else
- tabs.select("+1", true);
- },
- { count: true });
-
- mappings.add([modes.NORMAL], ["<C-n>", "<C-Tab>", "<C-PageDown>"],
- "Go to the next tab",
- function (count) { tabs.select("+" + (count < 1 ? 1 : count), true); },
- { count: true });
-
- mappings.add([modes.NORMAL], ["gT", "<C-p>", "<C-S-Tab>", "<C-PageUp>"],
- "Go to previous tab",
- function (count) { tabs.select("-" + (count < 1 ? 1 : count), true); },
- { count: true });
-
- if (config.hasTabbrowser)
- {
- mappings.add([modes.NORMAL], ["b"],
- "Open a prompt to switch buffers",
- function (count)
- {
- if (count != -1)
- tabs.switchTo(String(count));
- else
- commandline.open(":", "buffer! ", modes.EX);
- },
- { count: true });
-
- mappings.add([modes.NORMAL], ["B"],
- "Show buffer list",
- function () { tabs.list(false); });
-
- mappings.add([modes.NORMAL], ["d"],
+ },
+}, {
+ commands: function () {
+ commands.add(["bd[elete]", "bw[ipeout]", "bun[load]", "tabc[lose]"],
"Delete current buffer",
- function (count) { tabs.remove(tabs.getTab(), count, false, 0); },
- { count: true });
-
- mappings.add([modes.NORMAL], ["D"],
- "Delete current buffer, focus tab to the left",
- function (count) { tabs.remove(tabs.getTab(), count, true, 0); },
- { count: true });
-
- mappings.add([modes.NORMAL], ["gb"],
- "Repeat last :buffer[!] command",
- function (count) { tabs.switchTo(null, null, count, false); },
- { count: true });
-
- mappings.add([modes.NORMAL], ["gB"],
- "Repeat last :buffer[!] command in reverse direction",
- function (count) { tabs.switchTo(null, null, count, true); },
- { count: true });
-
- // TODO: feature dependencies - implies "session"?
- if (liberator.has("tabs_undo"))
- {
- mappings.add([modes.NORMAL], ["u"],
- "Undo closing of a tab",
- function (count) { commands.get("undo").execute("", false, count); },
- { count: true });
- }
-
- mappings.add([modes.NORMAL], ["<C-^>", "<C-6>"],
- "Select the alternate tab or the [count]th tab",
- function (count)
- {
- if (count < 1)
- tabs.selectAlternateTab();
- else
- tabs.switchTo(count.toString(), false);
- },
- { count: true });
- }
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMMANDS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- commands.add(["bd[elete]", "bw[ipeout]", "bun[load]", "tabc[lose]"],
- "Delete current buffer",
- function (args)
- {
- let special = args.bang;
- let count = args.count;
- let arg = args.literalArg;
-
- if (arg)
- {
- let removed = 0;
- let matches = arg.match(/^(\d+):?/);
+ function (args) {
+ let special = args.bang;
+ let count = args.count;
+ let arg = args.literalArg;
- if (matches)
- {
- tabs.remove(tabs.getTab(parseInt(matches[1], 10) - 1));
- removed = 1;
- }
- else
- {
- let str = arg.toLowerCase();
- let browsers = getBrowser().browsers;
+ if (arg) {
+ let removed = 0;
+ let matches = arg.match(/^(\d+):?/);
- for (let i = browsers.length - 1; i >= 0; i--)
- {
- let host, title, uri = browsers[i].currentURI.spec;
- if (browsers[i].currentURI.schemeIs("about"))
- {
- host = "";
- title = "(Untitled)";
- }
- else
- {
- host = browsers[i].currentURI.host;
- title = browsers[i].contentTitle;
- }
+ if (matches) {
+ tabs.remove(tabs.getTab(parseInt(matches[1], 10) - 1));
+ removed = 1;
+ }
+ else {
+ let str = arg.toLowerCase();
+ let browsers = getBrowser().browsers;
+
+ for (let i = browsers.length - 1; i >= 0; i--) {
+ let host, title, uri = browsers[i].currentURI.spec;
+ if (browsers[i].currentURI.schemeIs("about")) {
+ host = "";
+ title = "(Untitled)";
+ }
+ else {
+ host = browsers[i].currentURI.host;
+ title = browsers[i].contentTitle;
+ }
- [host, title, uri] = [host, title, uri].map(String.toLowerCase);
+ [host, title, uri] = [host, title, uri].map(String.toLowerCase);
- if (host.indexOf(str) >= 0 || uri == str ||
- (special && (title.indexOf(str) >= 0 || uri.indexOf(str) >= 0)))
- {
- tabs.remove(tabs.getTab(i));
- removed++;
+ if (host.indexOf(str) >= 0 || uri == str ||
+ (special && (title.indexOf(str) >= 0 || uri.indexOf(str) >= 0))) {
+ tabs.remove(tabs.getTab(i));
+ removed++;
+ }
}
}
- }
-
- if (removed > 0)
- liberator.echomsg(removed + " fewer tab(s)", 9);
- else
- liberator.echoerr("E94: No matching tab for " + arg);
- }
- else // just remove the current tab
- tabs.remove(tabs.getTab(), Math.max(count, 1), special, 0);
- },
- {
- argCount: "?",
- bang: true,
- count: true,
- completer: function (context) completion.buffer(context),
- literal: 0
- });
-
- commands.add(["keepa[lt]"],
- "Execute a command without changing the current alternate buffer",
- function (args)
- {
- let alternate = tabs.alternate;
-
- try
- {
- liberator.execute(args[0], null, true);
- }
- finally
- {
- tabs.updateSelectionHistory([tabs.getTab(), alternate]);
- }
- },
- {
- argCount: "+",
- completer: function (context) completion.ex(context),
- literal: 0
- });
-
- // TODO: this should open in a new tab positioned directly after the current one, not at the end
- commands.add(["tab"],
- "Execute a command and tell it to output in a new tab",
- function (args)
- {
- liberator.forceNewTab = true;
- liberator.execute(args.string, null, true);
- liberator.forceNewTab = false;
- },
- {
- argCount: "+",
- completer: function (context) completion.ex(context),
- literal: 0
- });
-
- commands.add(["tabd[o]", "bufd[o]"],
- "Execute a command in each tab",
- function (args)
- {
- for (let i = 0; i < tabs.count; i++)
- {
- tabs.select(i);
- liberator.execute(args.string, null, true);
- }
- },
- {
- argCount: "1",
- completer: function (context) completion.ex(context),
- literal: 0
- });
-
- commands.add(["tabl[ast]", "bl[ast]"],
- "Switch to the last tab",
- function () tabs.select("$", false),
- { argCount: "0" });
-
- // TODO: "Zero count" if 0 specified as arg
- commands.add(["tabp[revious]", "tp[revious]", "tabN[ext]", "tN[ext]", "bp[revious]", "bN[ext]"],
- "Switch to the previous tab or go [count] tabs back",
- function (args)
- {
- let count = args.count;
- let arg = args[0];
-
- // count is ignored if an arg is specified, as per Vim
- if (arg)
- {
- if (/^\d+$/.test(arg))
- tabs.select("-" + arg, true);
- else
- liberator.echoerr("E488: Trailing characters");
- }
- else if (count > 0)
- tabs.select("-" + count, true);
- else
- tabs.select("-1", true);
- },
- {
- argCount: "?",
- count: true
- });
-
- // TODO: "Zero count" if 0 specified as arg
- commands.add(["tabn[ext]", "tn[ext]", "bn[ext]"],
- "Switch to the next or [count]th tab",
- function (args)
- {
- let count = args.count;
- let arg = args[0];
-
- if (arg || count > 0)
- {
- let index;
-
- // count is ignored if an arg is specified, as per Vim
- if (arg)
- {
- liberator.assert(/^\d+$/.test(arg), "E488: Trailing characters");
- index = arg - 1;
- }
- else
- index = count - 1;
- if (index < tabs.count)
- tabs.select(index, true);
- else
- liberator.beep();
- }
- else
- tabs.select("+1", true);
- },
- {
- argCount: "?",
- count: true
- });
-
- commands.add(["tabr[ewind]", "tabfir[st]", "br[ewind]", "bf[irst]"],
- "Switch to the first tab",
- function () { tabs.select(0, false); },
- { argCount: "0" });
-
- if (config.hasTabbrowser)
- {
- // TODO: "Zero count" if 0 specified as arg, multiple args and count ranges?
- commands.add(["b[uffer]"],
- "Switch to a buffer",
- function (args)
- {
- let special = args.bang;
- let count = args.count;
- let arg = args.literalArg;
-
- // if a numeric arg is specified any count is ignored; if a
- // count and non-numeric arg are both specified then E488
- if (arg && count > 0)
- {
- if (/^\d+$/.test(arg))
- tabs.switchTo(arg, special);
+ if (removed > 0)
+ liberator.echomsg(removed + " fewer tab(s)", 9);
else
- liberator.echoerr("E488: Trailing characters");
+ liberator.echoerr("E94: No matching tab for " + arg);
}
- else if (count > 0)
- tabs.switchTo(count.toString(), special);
- else
- tabs.switchTo(arg, special);
- },
- {
+ else // just remove the current tab
+ tabs.remove(tabs.getTab(), Math.max(count, 1), special, 0);
+ }, {
argCount: "?",
bang: true,
count: true,
@@ -488,736 +595,501 @@ function Tabs() //{{{
literal: 0
});
- commands.add(["buffers", "files", "ls", "tabs"],
- "Show a list of all buffers",
- function (args) { tabs.list(args.literalArg); },
- {
- argCount: "?",
- literal: 0
- });
+ commands.add(["keepa[lt]"],
+ "Execute a command without changing the current alternate buffer",
+ function (args) {
+ let alternate = tabs.alternate;
- commands.add(["quita[ll]", "qa[ll]"],
- "Quit " + config.name,
- function (args) { liberator.quit(false, args.bang); },
- {
- argCount: "0",
- bang: true
+ try {
+ liberator.execute(args[0], null, true);
+ }
+ finally {
+ tabs.updateSelectionHistory([tabs.getTab(), alternate]);
+ }
+ }, {
+ argCount: "+",
+ completer: function (context) completion.ex(context),
+ literal: 0
});
- commands.add(["reloada[ll]"],
- "Reload all tab pages",
- function (args) { tabs.reloadAll(args.bang); },
- {
- argCount: "0",
- bang: true
+ // TODO: this should open in a new tab positioned directly after the current one, not at the end
+ commands.add(["tab"],
+ "Execute a command and tell it to output in a new tab",
+ function (args) {
+ liberator.forceNewTab = true;
+ liberator.execute(args.string, null, true);
+ liberator.forceNewTab = false;
+ }, {
+ argCount: "+",
+ completer: function (context) completion.ex(context),
+ literal: 0
});
- commands.add(["stopa[ll]"],
- "Stop loading all tab pages",
- function () { tabs.stopAll(); },
- { argCount: "0" });
-
- // TODO: add count support
- commands.add(["tabm[ove]"],
- "Move the current tab after tab N",
- function (args)
- {
- let arg = args[0];
-
- // FIXME: tabmove! N should probably produce an error
- liberator.assert(!arg || /^([+-]?\d+)$/.test(arg),
- "E488: Trailing characters");
-
- // if not specified, move to after the last tab
- tabs.move(getBrowser().mCurrentTab, arg || "$", args.bang);
- },
- {
- argCount: "?",
- bang: true
+ commands.add(["tabd[o]", "bufd[o]"],
+ "Execute a command in each tab",
+ function (args) {
+ for (let i = 0; i < tabs.count; i++) {
+ tabs.select(i);
+ liberator.execute(args.string, null, true);
+ }
+ }, {
+ argCount: "1",
+ completer: function (context) completion.ex(context),
+ literal: 0
});
- commands.add(["tabo[nly]"],
- "Close all other tabs",
- function () { tabs.keepOnly(getBrowser().mCurrentTab); },
+ commands.add(["tabl[ast]", "bl[ast]"],
+ "Switch to the last tab",
+ function () tabs.select("$", false),
{ argCount: "0" });
- commands.add(["tabopen", "t[open]", "tabnew", "tabe[dit]"],
- "Open one or more URLs in a new tab",
- function (args)
- {
- let special = args.bang;
- args = args.string;
+ // TODO: "Zero count" if 0 specified as arg
+ commands.add(["tabp[revious]", "tp[revious]", "tabN[ext]", "tN[ext]", "bp[revious]", "bN[ext]"],
+ "Switch to the previous tab or go [count] tabs back",
+ function (args) {
+ let count = args.count;
+ let arg = args[0];
- let where = special ? liberator.NEW_TAB : liberator.NEW_BACKGROUND_TAB;
- if (args)
- liberator.open(args, { from: "tabopen", where: where });
+ // count is ignored if an arg is specified, as per Vim
+ if (arg) {
+ if (/^\d+$/.test(arg))
+ tabs.select("-" + arg, true);
+ else
+ liberator.echoerr("E488: Trailing characters");
+ }
+ else if (count > 0)
+ tabs.select("-" + count, true);
else
- liberator.open("about:blank", { from: "tabopen", where: where });
- },
- {
- bang: true,
- completer: function (context) completion.url(context),
- literal: 0,
- privateData: true
+ tabs.select("-1", true);
+ }, {
+ argCount: "?",
+ count: true
});
- commands.add(["tabde[tach]"],
- "Detach current tab to its own window",
- function ()
- {
- liberator.assert(tabs.count > 1, "Can't detach the last tab");
+ // TODO: "Zero count" if 0 specified as arg
+ commands.add(["tabn[ext]", "tn[ext]", "bn[ext]"],
+ "Switch to the next or [count]th tab",
+ function (args) {
+ let count = args.count;
+ let arg = args[0];
- tabs.detachTab(null);
- },
- { argCount: "0" });
+ if (arg || count > 0) {
+ let index;
- commands.add(["tabdu[plicate]"],
- "Duplicate current tab",
- function (args)
- {
- let tab = tabs.getTab();
-
- let activate = args.bang ? true : false;
- if (/\btabopen\b/.test(options["activate"]))
- activate = !activate;
+ // count is ignored if an arg is specified, as per Vim
+ if (arg) {
+ liberator.assert(/^\d+$/.test(arg), "E488: Trailing characters");
+ index = arg - 1;
+ }
+ else
+ index = count - 1;
- for (let i in util.range(0, Math.max(1, args.count)))
- tabs.cloneTab(tab, activate);
- },
- {
- argCount: "0",
- bang: true,
+ if (index < tabs.count)
+ tabs.select(index, true);
+ else
+ liberator.beep();
+ }
+ else
+ tabs.select("+1", true);
+ }, {
+ argCount: "?",
count: true
});
- // TODO: match window by title too?
- // : accept the full :tabmove arg spec for the tab index arg?
- // : better name or merge with :tabmove?
- commands.add(["taba[ttach]"],
- "Attach the current tab to another window",
- function (args)
- {
- liberator.assert(args.length <= 2 && !args.some(function (i) i && !/^\d+$/.test(i)),
- "E488: Trailing characters");
-
- let [winIndex, tabIndex] = args.map(parseInt);
- let win = liberator.windows[winIndex - 1];
+ commands.add(["tabr[ewind]", "tabfir[st]", "br[ewind]", "bf[irst]"],
+ "Switch to the first tab",
+ function () { tabs.select(0, false); },
+ { argCount: "0" });
- liberator.assert(win, "Window " + winIndex + " does not exist");
- liberator.assert(win != window, "Can't reattach to the same window");
+ if (config.hasTabbrowser) {
+ // TODO: "Zero count" if 0 specified as arg, multiple args and count ranges?
+ commands.add(["b[uffer]"],
+ "Switch to a buffer",
+ function (args) {
+ let special = args.bang;
+ let count = args.count;
+ let arg = args.literalArg;
+
+ // if a numeric arg is specified any count is ignored; if a
+ // count and non-numeric arg are both specified then E488
+ if (arg && count > 0) {
+ liberator.assert(/^\d+$/.test(arg), "E488: Trailing characters");
+ tabs.switchTo(arg, special);
+ }
+ else if (count > 0)
+ tabs.switchTo(count.toString(), special);
+ else
+ tabs.switchTo(arg, special);
+ }, {
+ argCount: "?",
+ bang: true,
+ count: true,
+ completer: function (context) completion.buffer(context),
+ literal: 0
+ });
+
+ commands.add(["buffers", "files", "ls", "tabs"],
+ "Show a list of all buffers",
+ function (args) { tabs.list(args.literalArg); }, {
+ argCount: "?",
+ literal: 0
+ });
+
+ commands.add(["quita[ll]", "qa[ll]"],
+ "Quit " + config.name,
+ function (args) { liberator.quit(false, args.bang); }, {
+ argCount: "0",
+ bang: true
+ });
+
+ commands.add(["reloada[ll]"],
+ "Reload all tab pages",
+ function (args) { tabs.reloadAll(args.bang); }, {
+ argCount: "0",
+ bang: true
+ });
+
+ commands.add(["stopa[ll]"],
+ "Stop loading all tab pages",
+ function () { tabs.stopAll(); },
+ { argCount: "0" });
+
+ // TODO: add count support
+ commands.add(["tabm[ove]"],
+ "Move the current tab after tab N",
+ function (args) {
+ let arg = args[0];
+
+ // FIXME: tabmove! N should probably produce an error
+ liberator.assert(!arg || /^([+-]?\d+)$/.test(arg),
+ "E488: Trailing characters");
+
+ // if not specified, move to after the last tab
+ tabs.move(getBrowser().mCurrentTab, arg || "$", args.bang);
+ }, {
+ argCount: "?",
+ bang: true
+ });
+
+ commands.add(["tabo[nly]"],
+ "Close all other tabs",
+ function () { tabs.keepOnly(getBrowser().mCurrentTab); },
+ { argCount: "0" });
+
+ commands.add(["tabopen", "t[open]", "tabnew", "tabe[dit]"],
+ "Open one or more URLs in a new tab",
+ function (args) {
+ let special = args.bang;
+ args = args.string;
+
+ let where = special ? liberator.NEW_TAB : liberator.NEW_BACKGROUND_TAB;
+ if (args)
+ liberator.open(args, { from: "tabopen", where: where });
+ else
+ liberator.open("about:blank", { from: "tabopen", where: where });
+ }, {
+ bang: true,
+ completer: function (context) completion.url(context),
+ literal: 0,
+ privateData: true
+ });
+
+ commands.add(["tabde[tach]"],
+ "Detach current tab to its own window",
+ function () {
+ liberator.assert(tabs.count > 1, "Can't detach the last tab");
+
+ tabs.detachTab(null);
+ },
+ { argCount: "0" });
+
+ commands.add(["tabdu[plicate]"],
+ "Duplicate current tab",
+ function (args) {
+ let tab = tabs.getTab();
+
+ let activate = args.bang ? true : false;
+ if (/\btabopen\b/.test(options["activate"]))
+ activate = !activate;
+
+ for (let i in util.range(0, Math.max(1, args.count)))
+ tabs.cloneTab(tab, activate);
+ }, {
+ argCount: "0",
+ bang: true,
+ count: true
+ });
+
+ // TODO: match window by title too?
+ // : accept the full :tabmove arg spec for the tab index arg?
+ // : better name or merge with :tabmove?
+ commands.add(["taba[ttach]"],
+ "Attach the current tab to another window",
+ function (args) {
+ liberator.assert(args.length <= 2 && !args.some(function (i) !/^\d+$/.test(i)),
+ "E488: Trailing characters");
+
+ let [winIndex, tabIndex] = args.map(parseInt);
+ let win = liberator.windows[winIndex - 1];
+
+ liberator.assert(win, "Window " + winIndex + " does not exist");
+ liberator.assert(win != window, "Can't reattach to the same window");
+
+ let browser = win.getBrowser();
+ let dummy = browser.addTab("about:blank");
+ browser.stop();
+ // XXX: the implementation of DnD in tabbrowser.xml suggests
+ // that we may not be guaranteed of having a docshell here
+ // without this reference?
+ browser.docShell;
+
+ let last = browser.mTabs.length - 1;
+
+ browser.moveTabTo(dummy, util.Math.constrain(tabIndex || last, 0, last));
+ browser.selectedTab = dummy; // required
+ browser.swapBrowsersAndCloseOther(dummy, getBrowser().mCurrentTab);
+ }, {
+ argCount: "+",
+ completer: function (context, args) {
+ if (args.completeArg == 0) {
+ context.filters.push(function ({ item: win }) win != window);
+ completion.window(context);
+ }
+ }
+ });
+ }
- let browser = win.getBrowser();
- let dummy = browser.addTab("about:blank");
- browser.stop();
- // XXX: the implementation of DnD in tabbrowser.xml suggests
- // that we may not be guaranteed of having a docshell here
- // without this reference?
- browser.docShell;
+ if (liberator.has("tabs_undo")) {
+ commands.add(["u[ndo]"],
+ "Undo closing of a tab",
+ function (args) {
+ if (args.length)
+ args = args[0];
+ else
+ args = Math.max(args.count, 0);
- let last = browser.mTabs.length - 1;
+ let m;
+ if (m = /^(\d+)(:|$)/.exec(args || '1'))
+ window.undoCloseTab(Number(m[1]) - 1);
+ else if (args) {
+ for (let [i, item] in Iterator(tabs.closedTabs))
+ if (item.state.entries[item.state.index - 1].url == args)
+ return void window.undoCloseTab(i);
- browser.moveTabTo(dummy, util.Math.constrain(tabIndex || last, 0, last));
- browser.selectedTab = dummy; // required
- browser.swapBrowsersAndCloseOther(dummy, getBrowser().mCurrentTab);
- },
- {
- argCount: "+",
- completer: function (context, args)
- {
- if (args.completeArg == 0)
- {
- context.filters.push(function ({ item: win }) win != window);
- completion.window(context);
+ liberator.echoerr("Exxx: No matching closed tab");
}
- }
- });
- }
-
- if (liberator.has("tabs_undo"))
- {
- commands.add(["u[ndo]"],
- "Undo closing of a tab",
- function (args)
- {
- if (args.length)
- args = args[0];
- else
- args = Math.max(args.count, 0);
+ }, {
+ argCount: "?",
+ completer: function (context) {
+ context.anchored = false;
+ context.compare = CompletionContext.Sort.unsorted;
+ context.filters = [CompletionContext.Filter.textDescription];
+ context.keys = { text: function ([i, { state: s }]) (i + 1) + ": " + s.entries[s.index - 1].url, description: "[1].title", icon: "[1].image" };
+ context.completions = Iterator(tabs.closedTabs);
+ },
+ count: true,
+ literal: 0
+ });
- let m;
- if (m = /^(\d+)(:|$)/.exec(args || '1'))
- window.undoCloseTab(Number(m[1]) - 1);
- else if (args)
- {
- for (let [i, item] in Iterator(tabs.closedTabs))
- if (item.state.entries[item.state.index - 1].url == args)
- return void window.undoCloseTab(i);
+ commands.add(["undoa[ll]"],
+ "Undo closing of all closed tabs",
+ function (args) {
+ for (let i in Iterator(tabs.closedTabs))
+ window.undoCloseTab(0);
- liberator.echoerr("Exxx: No matching closed tab");
- }
- },
- {
- argCount: "?",
- completer: function (context)
- {
- context.anchored = false;
- context.compare = CompletionContext.Sort.unsorted;
- context.filters = [CompletionContext.Filter.textDescription];
- context.keys = { text: function ([i, { state: s }]) (i + 1) + ": " + s.entries[s.index - 1].url, description: "[1].title", icon: "[1].image" };
- context.completions = Iterator(tabs.closedTabs);
},
- count: true,
- literal: 0
- });
+ { argCount: "0" });
- commands.add(["undoa[ll]"],
- "Undo closing of all closed tabs",
- function (args)
- {
- for (let i in Iterator(tabs.closedTabs))
- window.undoCloseTab(0);
+ }
+ if (liberator.has("session")) {
+ commands.add(["wqa[ll]", "wq", "xa[ll]"],
+ "Save the session and quit",
+ function () { liberator.quit(true); },
+ { argCount: "0" });
+ }
+ },
+ completion: function () {
+ completion.addUrlCompleter("t",
+ "Open tabs",
+ completion.buffer);
+ },
+ mappings: function () {
+ mappings.add([modes.NORMAL], ["g0", "g^"],
+ "Go to the first tab",
+ function (count) { tabs.select(0); });
+
+ mappings.add([modes.NORMAL], ["g$"],
+ "Go to the last tab",
+ function (count) { tabs.select("$"); });
+
+ mappings.add([modes.NORMAL], ["gt"],
+ "Go to the next tab",
+ function (count) {
+ if (count > 0)
+ tabs.select(count - 1, false);
+ else
+ tabs.select("+1", true);
},
- { argCount: "0" });
+ { count: true });
- }
+ mappings.add([modes.NORMAL], ["<C-n>", "<C-Tab>", "<C-PageDown>"],
+ "Go to the next tab",
+ function (count) { tabs.select("+" + (count < 1 ? 1 : count), true); },
+ { count: true });
- if (liberator.has("session"))
- {
- commands.add(["wqa[ll]", "wq", "xa[ll]"],
- "Save the session and quit",
- function () { liberator.quit(true); },
- { argCount: "0" });
- }
+ mappings.add([modes.NORMAL], ["gT", "<C-p>", "<C-S-Tab>", "<C-PageUp>"],
+ "Go to previous tab",
+ function (count) { tabs.select("-" + (count < 1 ? 1 : count), true); },
+ { count: true });
- completion.addUrlCompleter("t",
- "Open tabs",
- completion.buffer);
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// PUBLIC SECTION //////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- return {
-
- /**
- * @property {Object} The previously accessed tab or null if no tab
- * other than the current one has been accessed.
- */
- get alternate() alternates[1],
-
- /**
- * @property {Iterator(Object)} A genenerator that returns all browsers
- * in the current window.
- */
- get browsers()
- {
- let browsers = getBrowser().browsers;
- for (let i = 0; i < browsers.length; i++)
- yield [i, browsers[i]];
- },
-
- /**
- * @property {boolean} Whether the tab numbering XBL binding has been
- * applied.
- */
- get tabsBound() Boolean(styles.get(true, "tab-binding")),
- set tabsBound(val)
- {
- let fragment = liberator.has("MacUnix") ? "tab-mac" : "tab";
- if (!val)
- styles.removeSheet(true, "tab-binding");
- else if (!this.tabsBound)
- styles.addSheet(true, "tab-binding", "chrome://browser/content/browser.xul",
- ".tabbrowser-tab { -moz-binding: url(chrome://liberator/content/bindings.xml#" + fragment + ") !important; }" +
- // FIXME: better solution for themes?
- ".tabbrowser-tab[busy] > .tab-icon > .tab-icon-image { list-style-image: url('chrome://global/skin/icons/loading_16.png') !important; }");
- },
-
- /**
- * @property {number} The number of tabs in the current window.
- */
- get count() getBrowser().mTabs.length,
-
- /**
- * @property {Object} The local options store for the current tab.
- */
- get options()
- {
- let store = this.localStore;
- if (!("options" in store))
- store.options = {};
- return store.options;
- },
-
- getBrowser: getBrowser,
-
- /**
- * Returns the local state store for the tab at the specified
- * <b>tabIndex</b>. If <b>tabIndex</b> is not specified then the
- * current tab is used.
- *
- * @param {number} tabIndex
- * @returns {Object}
- */
- // FIXME: why not a tab arg? Why this and the property?
- getLocalStore: function (tabIndex)
- {
- let tab = this.getTab(tabIndex);
- if (!tab.liberatorStore)
- tab.liberatorStore = {};
- return tab.liberatorStore;
- },
-
- /**
- * @property {Object} The local state store for the currently selected
- * tab.
- */
- get localStore() this.getLocalStore(),
-
- /**
- * @property {Object[]} The array of closed tabs for the current
- * session.
- */
- get closedTabs() services.get("json").decode(services.get("sessionStore").getClosedTabData(window)),
-
- /**
- * Returns the index of <b>tab</b> or the index of the currently
- * selected tab if <b>tab</b> is not specified. This is a 0-based
- * index.
- *
- * @param {Object} tab A tab from the current tab list.
- * @returns {number}
- */
- index: function (tab)
- {
- if (tab)
- return Array.indexOf(getBrowser().mTabs, tab);
- else
- return getBrowser().mTabContainer.selectedIndex;
- },
-
- // TODO: implement filter
- /**
- * Returns an array of all tabs in the tab list.
- *
- * @returns {Object[]}
- */
- // FIXME: why not return the tab element?
- // : unused? Remove me.
- get: function ()
- {
- let buffers = [];
- for (let [i, browser] in this.browsers)
- {
- let title = browser.contentTitle || "(Untitled)";
- let uri = browser.currentURI.spec;
- let number = i + 1;
- buffers.push([number, title, uri]);
- }
- return buffers;
- },
-
- /**
- * Returns the index of the tab containing <b>content</b>.
- *
- * @param {Object} content Either a content window or a content
- * document.
- */
- // FIXME: Only called once...necessary?
- getContentIndex: function (content)
- {
- for (let [i, browser] in this.browsers)
- {
- if (browser.contentWindow == content || browser.contentDocument == content)
- return i;
- }
- return -1;
- },
-
- /**
- * Returns the tab at the specified <b>index</b> or the currently
- * selected tab if <b>index</b> is not specified. This is a 0-based
- * index.
- *
- * @param {number} index The index of the tab required.
- * @returns {Object}
- */
- getTab: function (index)
- {
- if (index != undefined)
- return getBrowser().mTabs[index];
- else
- return getBrowser().mCurrentTab;
- },
-
- /**
- * Lists all tabs matching <b>filter</b>.
- *
- * @param {string} filter A filter matching a substring of the tab's
- * document title or URL.
- */
- list: function (filter)
- {
- completion.listCompleter("buffer", filter);
- },
-
- /**
- * Moves a tab to a new position in the tab list.
- *
- * @param {Object} tab The tab to move.
- * @param {string} spec See {@link indexFromSpec}.
- * @param {boolean} wrap Whether an out of bounds <b>spec</b> causes
- * the destination position to wrap around the start/end of the tab
- * list.
- */
- move: function (tab, spec, wrap)
- {
- let index = indexFromSpec(spec, wrap);
- getBrowser().moveTabTo(tab, index);
- },
-
- /**
- * Removes the specified <b>tab</b> from the tab list.
- *
- * @param {Object} tab
- * @param {number} count
- * @param {boolean} focusLeftTab Focus the tab to the left of the removed tab.
- * @param {number} quitOnLastTab Whether to quit if the tab being
- * deleted is the only tab in the tab list:
- * 1 - quit without saving session
- * 2 - quit and save session
- */
- // FIXME: what is quitOnLastTab {1,2} all about then, eh? --djk
- remove: function (tab, count, focusLeftTab, quitOnLastTab)
- {
- let removeOrBlankTab = {
- Firefox: function (tab)
- {
- if (getBrowser().mTabs.length > 1)
- getBrowser().removeTab(tab);
- else
- {
- if (buffer.URL != "about:blank" ||
- window.getWebNavigation().sessionHistory.count > 0)
- {
- liberator.open("about:blank", liberator.NEW_BACKGROUND_TAB);
- getBrowser().removeTab(tab);
- }
- else
- liberator.beep();
- }
- },
- Thunderbird: function (tab)
- {
- if (getBrowser().mTabs.length > 1)
- getBrowser().removeTab(tab);
- else
- liberator.beep();
- },
- Songbird: function (tab)
- {
- if (getBrowser().mTabs.length > 1)
- getBrowser().removeTab(tab);
- else
- {
- if (buffer.URL != "about:blank" ||
- window.getWebNavigation().sessionHistory.count > 0)
- {
- liberator.open("about:blank", liberator.NEW_BACKGROUND_TAB);
- getBrowser().removeTab(tab);
- }
- else
- liberator.beep();
- }
- }
- }[config.hostApplication] || function () {};
+ if (config.hasTabbrowser) {
+ mappings.add([modes.NORMAL], ["b"],
+ "Open a prompt to switch buffers",
+ function (count) {
+ if (count != -1)
+ tabs.switchTo(String(count));
+ else
+ commandline.open(":", "buffer! ", modes.EX);
+ },
+ { count: true });
- if (typeof count != "number" || count < 1)
- count = 1;
+ mappings.add([modes.NORMAL], ["B"],
+ "Show buffer list",
+ function () { tabs.list(false); });
- if (quitOnLastTab >= 1 && getBrowser().mTabs.length <= count)
- {
- if (liberator.windows.length > 1)
- window.close();
- else
- liberator.quit(quitOnLastTab == 2);
+ mappings.add([modes.NORMAL], ["d"],
+ "Delete current buffer",
+ function (count) { tabs.remove(tabs.getTab(), count, false, 0); },
+ { count: true });
- return;
- }
+ mappings.add([modes.NORMAL], ["D"],
+ "Delete current buffer, focus tab to the left",
+ function (count) { tabs.remove(tabs.getTab(), count, true, 0); },
+ { count: true });
- let index = this.index(tab);
- if (focusLeftTab)
- {
- let lastRemovedTab = 0;
- for (let i = index; i > index - count && i >= 0; i--)
- {
- removeOrBlankTab(this.getTab(i));
- lastRemovedTab = i > 0 ? i : 1;
- }
- getBrowser().mTabContainer.selectedIndex = lastRemovedTab - 1;
- }
- else
- {
- let i = index + count - 1;
- if (i >= this.count)
- i = this.count - 1;
+ mappings.add([modes.NORMAL], ["gb"],
+ "Repeat last :buffer[!] command",
+ function (count) { tabs.switchTo(null, null, count, false); },
+ { count: true });
- for (; i >= index; i--)
- removeOrBlankTab(this.getTab(i));
- getBrowser().mTabContainer.selectedIndex = index;
- }
- },
-
- /**
- * Removes all tabs from the tab list except the specified <b>tab</b>.
- *
- * @param {Object} tab The tab to keep.
- */
- keepOnly: function (tab)
- {
- getBrowser().removeAllTabsBut(tab);
- },
-
- /**
- * Selects the tab at the position specified by <b>spec</b>.
- *
- * @param {string} spec See {@link indexFromSpec}
- * @param {boolean} wrap Whether an out of bounds <b>spec</b> causes
- * the selection position to wrap around the start/end of the tab
- * list.
- */
- select: function (spec, wrap)
- {
- let index = indexFromSpec(spec, wrap);
- // FIXME:
- if (index == -1)
- return void liberator.beep();
- getBrowser().mTabContainer.selectedIndex = index;
- },
-
- /**
- * Reloads the specified tab.
- *
- * @param {Object} tab The tab to reload.
- * @param {boolean} bypassCache Whether to bypass the cache when
- * reloading.
- */
- reload: function (tab, bypassCache)
- {
- if (bypassCache)
- {
- const flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
- getBrowser().getBrowserForTab(tab).reloadWithFlags(flags);
- }
- else
- getBrowser().reloadTab(tab);
- },
-
- /**
- * Reloads all tabs.
- *
- * @param {boolean} bypassCache Whether to bypass the cache when
- * reloading.
- */
- reloadAll: function (bypassCache)
- {
- if (bypassCache)
- {
- for (let i = 0; i < getBrowser().mTabs.length; i++)
- {
- try
- {
- this.reload(getBrowser().mTabs[i], bypassCache);
- }
- catch (e)
- {
- // FIXME: can we do anything useful here without stopping the
- // other tabs from reloading?
- }
- }
- }
- else
- getBrowser().reloadAllTabs();
- },
-
- /**
- * Stops loading the specified tab.
- *
- * @param {Object} tab The tab to stop loading.
- */
- stop: function (tab)
- {
- if (config.stop)
- config.stop(tab);
- else
- tab.linkedBrowser.stop();
- },
-
- /**
- * Stops loading all tabs.
- */
- stopAll: function ()
- {
- for (let [, browser] in this.browsers)
- browser.stop();
- },
-
- /**
- * Selects the tab containing the specified <b>buffer</b>.
- *
- * @param {string} buffer A string which matches the URL or title of a
- * buffer, if it is null, the last used string is used again.
- * @param {boolean} allowNonUnique Whether to select the first of
- * multiple matches.
- * @param {number} count If there are multiple matches select the
- * count'th match.
- * @param {boolean} reverse Whether to search the buffer list in
- * reverse order.
- *
- */
- // FIXME: help!
- switchTo: function (buffer, allowNonUnique, count, reverse)
- {
- if (buffer == "")
- return;
+ mappings.add([modes.NORMAL], ["gB"],
+ "Repeat last :buffer[!] command in reverse direction",
+ function (count) { tabs.switchTo(null, null, count, true); },
+ { count: true });
- if (buffer != null)
- {
- // store this command, so it can be repeated with "B"
- lastBufferSwitchArgs = buffer;
- lastBufferSwitchSpecial = allowNonUnique;
- }
- else
- {
- buffer = lastBufferSwitchArgs;
- if (allowNonUnique === undefined || allowNonUnique == null) // XXX
- allowNonUnique = lastBufferSwitchSpecial;
+ // TODO: feature dependencies - implies "session"?
+ if (liberator.has("tabs_undo")) {
+ mappings.add([modes.NORMAL], ["u"],
+ "Undo closing of a tab",
+ function (count) { commands.get("undo").execute("", false, count); },
+ { count: true });
}
- if (buffer == "#")
+ mappings.add([modes.NORMAL], ["<C-^>", "<C-6>"],
+ "Select the alternate tab or the [count]th tab",
+ function (count) {
+ if (count < 1)
+ tabs.selectAlternateTab();
+ else
+ tabs.switchTo(count.toString(), false);
+ },
+ { count: true });
+ }
+ },
+ options: function () {
+ options.add(["showtabline", "stal"],
+ "Control when to show the tab bar of opened web pages",
+ "number", config.defaults["showtabline"],
{
- tabs.selectAlternateTab();
- return;
- }
-
- if (!count || count < 1)
- count = 1;
- if (typeof reverse != "boolean")
- reverse = false;
+ setter: function (value) {
+ // FIXME: we manipulate mTabContainer underneath mStrip so we
+ // don't have to fight against the host app's attempts to keep
+ // it open - hack! Adding a filter watch to mStrip is probably
+ // the cleanest solution.
+ let tabStrip = getBrowser().mTabContainer;
+
+ if (value == 0)
+ tabStrip.collapsed = true;
+ else {
+ // FIXME: Why are we preferring our own created preference
+ // here? --djk
+ let pref = "browser.tabStrip.autoHide";
+ if (options.getPref(pref) == null) // Try for FF 3.0 & 3.1
+ pref = "browser.tabs.autoHide";
+ options.safeSetPref(pref, value == 1);
+ tabStrip.collapsed = false;
+ }
- let matches = buffer.match(/^(\d+):?/);
- if (matches)
- {
- tabs.select(parseInt(matches[1], 10) - 1, false); // make it zero-based
- return;
- }
+ return value;
+ },
+ completer: function (context) [
+ ["0", "Never show tab bar"],
+ ["1", "Show tab bar only if more than one tab is open"],
+ ["2", "Always show tab bar"]
+ ],
+ validator: Option.validateCompleter
+ });
- matches = [];
- let lowerBuffer = buffer.toLowerCase();
- let first = tabs.index() + (reverse ? 0 : 1);
- let nbrowsers = getBrowser().browsers.length;
- for (let [i, ] in tabs.browsers)
- {
- let index = (i + first) % nbrowsers;
- let url = getBrowser().getBrowserAtIndex(index).contentDocument.location.href;
- let title = getBrowser().getBrowserAtIndex(index).contentDocument.title.toLowerCase();
- if (url == buffer)
+ if (config.hasTabbrowser) {
+ options.add(["activate", "act"],
+ "Define when tabs are automatically activated",
+ "stringlist", "homepage,quickmark,tabopen,paste",
{
- tabs.select(index, false);
- return;
- }
-
- if (url.indexOf(buffer) >= 0 || title.indexOf(lowerBuffer) >= 0)
- matches.push(index);
- }
- if (matches.length == 0)
- liberator.echoerr("E94: No matching buffer for " + buffer);
- else if (matches.length > 1 && !allowNonUnique)
- liberator.echoerr("E93: More than one match for " + buffer);
- else
- {
- if (reverse)
+ completer: function (context) [
+ ["homepage", "gH mapping"],
+ ["quickmark", "go and gn mappings"],
+ ["tabopen", ":tabopen[!] command"],
+ ["paste", "P and gP mappings"]
+ ],
+ validator: Option.validateCompleter
+ });
+
+ options.add(["newtab"],
+ "Define which commands should output in a new tab by default",
+ "stringlist", "",
{
- index = matches.length - count;
- while (index < 0)
- index += matches.length;
- }
- else
- index = (count - 1) % matches.length;
+ completer: function (context) [
+ ["all", "All commands"],
+ ["addons", ":addo[ns] command"],
+ ["downloads", ":downl[oads] command"],
+ ["extoptions", ":exto[ptions] command"],
+ ["help", ":h[elp] command"],
+ ["javascript", ":javascript! or :js! command"],
+ ["prefs", ":pref[erences]! or :prefs! command"]
+ ],
+ validator: Option.validateCompleter
+ });
+
+ // TODO: Is this really applicable to Xulmus?
+ options.add(["popups", "pps"],
+ "Where to show requested popup windows",
+ "stringlist", "tab",
+ {
+ setter: function (value) {
+ let [open, restriction] = [1, 0];
+ for (let [, opt] in Iterator(this.parseValues(value))) {
+ if (opt == "tab")
+ open = 3;
+ else if (opt == "window")
+ open = 2;
+ else if (opt == "resized")
+ restriction = 2;
+ }
- tabs.select(matches[index], false);
- }
- },
-
- /**
- * Clones the specified <b>tab</b> and append it to the tab list.
- *
- * @param {Object} tab The tab to clone.
- * @param {boolean} activate Whether to select the newly cloned tab.
- */
- cloneTab: function (tab, activate)
- {
- let newTab = getBrowser().addTab();
- copyTab(newTab, tab);
-
- if (activate)
- getBrowser().mTabContainer.selectedItem = newTab;
-
- return newTab;
- },
-
- /**
- * Detaches the specified <b>tab</b> and open it in a new window. If no
- * tab is specified the currently selected tab is detached.
- *
- * @param {Object} tab The tab to detach.
- */
- detachTab: function (tab)
- {
- if (!tab)
- tab = getBrowser().mTabContainer.selectedItem;
-
- services.get("windowWatcher")
- .openWindow(window, window.getBrowserURL(), null, "chrome,dialog=no,all", tab);
- },
-
- /**
- * Selects the alternate tab.
- */
- selectAlternateTab: function ()
- {
- liberator.assert(tabs.alternate != null && tabs.getTab() != tabs.alternate,
- "E23: No alternate page");
-
- // NOTE: this currently relies on v.tabs.index() returning the
- // currently selected tab index when passed null
- let index = tabs.index(tabs.alternate);
-
- // TODO: since a tab close is more like a bdelete for us we
- // should probably reopen the closed tab when a 'deleted'
- // alternate is selected
- liberator.assert(index >= 0, "E86: Buffer does not exist"); // TODO: This should read "Buffer N does not exist"
- tabs.select(index);
- },
-
- // NOTE: when restarting a session FF selects the first tab and then the
- // tab that was selected when the session was created. As a result the
- // alternate after a restart is often incorrectly tab 1 when there
- // shouldn't be one yet.
- /**
- * Sets the current and alternate tabs, updating the tab selection
- * history.
- *
- * @param {Array(Object)} tabs The current and alternate tab.
- * @see tabs#alternate
- */
- updateSelectionHistory: function (tabs)
- {
- alternates = tabs || [this.getTab(), alternates[0]];
+ options.safeSetPref("browser.link.open_newwindow", open, "See 'popups' option.");
+ options.safeSetPref("browser.link.open_newwindow.restriction", restriction, "See 'popups' option.");
+ return value;
+ },
+ completer: function (context) [
+ ["tab", "Open popups in a new tab"],
+ ["window", "Open popups in a new window"],
+ ["resized", "Open resized popups in a new window"]
+ ],
+ validator: Option.validateCompleter
+ });
}
- };
- //}}}
-} //}}}
+ },
+});
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/template.js b/common/content/template.js
index 8a494366..116f0d54 100644
--- a/common/content/template.js
+++ b/common/content/template.js
@@ -6,18 +6,16 @@
/** @scope modules */
-const template = { //{{{
+const Template = Module("template", {
add: function add(a, b) a + b,
join: function join(c) function (a, b) a + c + b,
- map: function map(iter, func, sep, interruptable)
- {
+ map: function map(iter, func, sep, interruptable) {
if (iter.length) // FIXME: Kludge?
iter = util.Array.itervalues(iter);
let ret = <></>;
let n = 0;
- for each (let i in Iterator(iter))
- {
+ for each (let i in Iterator(iter)) {
let val = func(i);
if (val == undefined)
continue;
@@ -30,30 +28,25 @@ const template = { //{{{
return ret;
},
- maybeXML: function maybeXML(xml)
- {
+ maybeXML: function maybeXML(xml) {
if (typeof xml == "xml")
return xml;
- try
- {
+ try {
return new XMLList(xml);
}
catch (e) {}
return <>{xml}</>;
},
- completionRow: function completionRow(item, highlightGroup)
- {
+ completionRow: function completionRow(item, highlightGroup) {
if (typeof icon == "function")
icon = icon();
- if (highlightGroup)
- {
+ if (highlightGroup) {
var text = item[0] || "";
var desc = item[1] || "";
}
- else
- {
+ else {
var text = this.process[0].call(this, item, item.text);
var desc = this.process[1].call(this, item, item.description);
}
@@ -85,8 +78,7 @@ const template = { //{{{
}
</>,
- icon: function (item, text)
- {
+ icon: function (item, text) {
return <><span highlight="CompIcon">{item.icon ? <img src={item.icon}/> : <></>}</span><span class="td-strut"/>{text}</>
},
@@ -108,79 +100,69 @@ const template = { //{{{
// if "processStrings" is true, any passed strings will be surrounded by " and
// any line breaks are displayed as \n
- highlight: function highlight(arg, processStrings, clip)
- {
+ highlight: function highlight(arg, processStrings, clip) {
// some objects like window.JSON or getBrowsers()._browsers need the try/catch
- try
- {
+ try {
let str = clip ? util.clip(String(arg), clip) : String(arg);
- switch (arg == null ? "undefined" : typeof arg)
- {
- case "number":
- return <span highlight="Number">{str}</span>;
- case "string":
- if (processStrings)
- str = str.quote();
- return <span highlight="String">{str}</span>;
- case "boolean":
- return <span highlight="Boolean">{str}</span>;
- case "function":
- // Vim generally doesn't like /foo*/, because */ looks like a comment terminator.
- // Using /foo*(:?)/ instead.
- if (processStrings)
- return <span highlight="Function">{str.replace(/\{(.|\n)*(?:)/g, "{ ... }")}</span>;
- return <>{arg}</>;
- case "undefined":
- return <span highlight="Null">{arg}</span>;
- case "object":
- // for java packages value.toString() would crash so badly
- // that we cannot even try/catch it
- if (/^\[JavaPackage.*\]$/.test(arg))
- return <>[JavaPackage]</>;
- if (processStrings && false)
- str = template.highlightFilter(str, "\n", function () <span highlight="NonText">^J</span>);
- return <span highlight="Object">{str}</span>;
- case "xml":
- return arg;
- default:
- return <![CDATA[<unknown type>]]>;
+ switch (arg == null ? "undefined" : typeof arg) {
+ case "number":
+ return <span highlight="Number">{str}</span>;
+ case "string":
+ if (processStrings)
+ str = str.quote();
+ return <span highlight="String">{str}</span>;
+ case "boolean":
+ return <span highlight="Boolean">{str}</span>;
+ case "function":
+ // Vim generally doesn't like /foo*/, because */ looks like a comment terminator.
+ // Using /foo*(:?)/ instead.
+ if (processStrings)
+ return <span highlight="Function">{str.replace(/\{(.|\n)*(?:)/g, "{ ... }")}</span>;
+ return <>{arg}</>;
+ case "undefined":
+ return <span highlight="Null">{arg}</span>;
+ case "object":
+ // for java packages value.toString() would crash so badly
+ // that we cannot even try/catch it
+ if (/^\[JavaPackage.*\]$/.test(arg))
+ return <>[JavaPackage]</>;
+ if (processStrings && false)
+ str = template.highlightFilter(str, "\n", function () <span highlight="NonText">^J</span>);
+ return <span highlight="Object">{str}</span>;
+ case "xml":
+ return arg;
+ default:
+ return <![CDATA[<unknown type>]]>;
}
}
- catch (e)
- {
+ catch (e) {
return<![CDATA[<unknown>]]>;
}
},
- highlightFilter: function highlightFilter(str, filter, highlight)
- {
- return this.highlightSubstrings(str, (function ()
- {
+ highlightFilter: function highlightFilter(str, filter, highlight) {
+ return this.highlightSubstrings(str, (function () {
if (filter.length == 0)
return;
let lcstr = String.toLowerCase(str);
let lcfilter = filter.toLowerCase();
let start = 0;
- while ((start = lcstr.indexOf(lcfilter, start)) > -1)
- {
+ while ((start = lcstr.indexOf(lcfilter, start)) > -1) {
yield [start, filter.length];
start += filter.length;
}
})(), highlight || template.filter);
},
- highlightRegexp: function highlightRegexp(str, re, highlight)
- {
- return this.highlightSubstrings(str, (function ()
- {
+ highlightRegexp: function highlightRegexp(str, re, highlight) {
+ return this.highlightSubstrings(str, (function () {
let res;
while ((res = re.exec(str)) && res[0].length)
yield [res.index, res[0].length];
})(), highlight || template.filter);
},
- highlightSubstrings: function highlightSubstrings(str, iter, highlight)
- {
+ highlightSubstrings: function highlightSubstrings(str, iter, highlight) {
if (typeof str == "xml")
return str;
if (str == "")
@@ -190,8 +172,7 @@ const template = { //{{{
let s = <></>;
let start = 0;
let n = 0;
- for (let [i, length] in iter)
- {
+ for (let [i, length] in iter) {
if (n++ > 50) // Prevent infinite loops.
return s + <>{str.substr(start)}</>;
XML.ignoreWhitespace = false;
@@ -202,23 +183,20 @@ const template = { //{{{
return s + <>{str.substr(start)}</>;
},
- highlightURL: function highlightURL(str, force)
- {
+ highlightURL: function highlightURL(str, force) {
if (force || /^[a-zA-Z]+:\/\//.test(str))
return <a highlight="URL" href={str}>{str}</a>;
else
return str;
},
- commandOutput: function generic(xml)
- {
+ commandOutput: function generic(xml) {
return <>:{commandline.command}<br/>{xml}</>;
},
// every item must have a .xml property which defines how to draw itself
// @param headers is an array of strings, the text for the header columns
- genericTable: function genericTable(items, format)
- {
+ genericTable: function genericTable(items, format) {
completion.listCompleter(function (context) {
context.filterFunc = null;
if (format)
@@ -227,8 +205,7 @@ const template = { //{{{
});
},
- jumps: function jumps(index, elems)
- {
+ jumps: function jumps(index, elems) {
// <e4x>
return this.commandOutput(
<table>
@@ -248,8 +225,7 @@ const template = { //{{{
// </e4x>
},
- options: function options(title, opts)
- {
+ options: function options(title, opts) {
// <e4x>
return this.commandOutput(
<table>
@@ -269,8 +245,7 @@ const template = { //{{{
// </e4x>
},
- table: function table(title, data, indent)
- {
+ table: function table(title, data, indent) {
let table =
// <e4x>
<table>
@@ -290,8 +265,7 @@ const template = { //{{{
return table;
},
- tabular: function tabular(headings, style, iter)
- {
+ tabular: function tabular(headings, style, iter) {
// TODO: This might be mind-bogglingly slow. We'll see.
// <e4x>
return this.commandOutput(
@@ -315,8 +289,7 @@ const template = { //{{{
// </e4x>
},
- usage: function usage(iter)
- {
+ usage: function usage(iter) {
// <e4x>
return this.commandOutput(
<table>
@@ -330,6 +303,6 @@ const template = { //{{{
</table>);
// </e4x>
}
-}; //}}}
+});
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/ui.js b/common/content/ui.js
deleted file mode 100644
index 61782568..00000000
--- a/common/content/ui.js
+++ /dev/null
@@ -1,2312 +0,0 @@
-// Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
-//
-// This work is licensed for reuse under an MIT license. Details are
-// given in the LICENSE.txt file included with this file.
-
-/** @scope modules */
-
-let fontSize = util.computedStyle(document.getElementById(config.mainWindowId)).fontSize;
-
-styles.registerSheet("chrome://liberator/skin/liberator.css");
-let error = styles.addSheet(true, "font-size", "chrome://liberator/content/buffer.xhtml",
- "body { font-size: " + fontSize + "; }");
-
-
-/**
- * This class is used for prompting of user input and echoing of messages.
- *
- * It consists of a prompt and command field be sure to only create objects of
- * this class when the chrome is ready.
- */
-function CommandLine() //{{{
-{
- ////////////////////////////////////////////////////////////////////////////////
- ////////////////////// PRIVATE SECTION /////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- const callbacks = {};
-
- storage.newArray("history-search", { store: true, privateData: true });
- storage.newArray("history-command", { store: true, privateData: true });
-
- // Really inideal.
- let services = modules.services; // Storage objects are global to all windows, 'modules' isn't.
- storage.newObject("sanitize", function () {
- ({
- CLEAR: "browser:purge-session-history",
- QUIT: "quit-application",
- init: function ()
- {
- services.get("observer").addObserver(this, this.CLEAR, false);
- services.get("observer").addObserver(this, this.QUIT, false);
- },
- observe: function (subject, topic, data)
- {
- switch (topic)
- {
- case this.CLEAR:
- ["search", "command"].forEach(function (mode) {
- History(null, mode).sanitize();
- });
- break;
- case this.QUIT:
- services.get("observer").removeObserver(this, this.CLEAR);
- services.get("observer").removeObserver(this, this.QUIT);
- break;
- }
- }
- }).init();
- }, { store: false });
- storage.addObserver("sanitize",
- function (key, event, value) {
- autocommands.trigger("Sanitize", {});
- }, window);
-
- var messageHistory = { //{{{
- _messages: [],
- get messages()
- {
- let max = options["messages"];
-
- // resize if 'messages' has changed
- if (this._messages.length > max)
- this._messages = this._messages.splice(this._messages.length - max);
-
- return this._messages;
- },
-
- get length() this._messages.length,
-
- clear: function clear()
- {
- this._messages = [];
- },
-
- add: function add(message)
- {
- if (!message)
- return;
-
- if (this._messages.length >= options["messages"])
- this._messages.shift();
-
- this._messages.push(message);
- }
- }; //}}}
- var lastMowOutput = null;
-
- var silent = false;
- var quiet = false;
- var keepCommand = false;
- var lastEcho = null;
-
- /**
- * A class for managing the history of an inputField.
- *
- * @param {HTMLInputElement} inputField
- * @param {string} mode The mode for which we need history.
- */
- function History(inputField, mode) //{{{
- {
- if (!(this instanceof arguments.callee))
- return new arguments.callee(inputField, mode);
-
- this.mode = mode;
- this.input = inputField;
- this.store = storage["history-" + mode];
- this.reset();
- }
- History.prototype = {
- /**
- * Reset the history index to the first entry.
- */
- reset: function ()
- {
- this.index = null;
- },
- /**
- * Save the last entry to the permanent store. All duplicate entries
- * are removed and the list is truncated, if necessary.
- */
- save: function ()
- {
- if (events.feedingKeys)
- return;
- let str = this.input.value;
- if (/^\s*$/.test(str))
- return;
- this.store.mutate("filter", function (line) (line.value || line) != str);
- this.store.push({ value: str, timestamp: Date.now(), privateData: this.checkPrivate(str) });
- this.store.truncate(options["history"], true);
- },
- /**
- * @property {function} Returns whether a data item should be
- * considered private.
- */
- checkPrivate: function (str)
- {
- // Not really the ideal place for this check.
- if (this.mode == "command")
- return (commands.get(commands.parseCommand(str)[1]) || {}).privateData;
- return false;
- },
- /**
- * Removes any private data from this history.
- */
- sanitize: function (timespan)
- {
- let range = [0, Number.MAX_VALUE];
- if (liberator.has("sanitizer") && (timespan || options["sanitizetimespan"]))
- range = sanitizer.getClearRange(timespan || options["sanitizetimespan"]);
-
- this.store.mutate("filter", function (item) {
- let timestamp = (item.timestamp || Date.now()/1000) * 1000;
- return !line.privateData || timestamp < self.range[0] || timestamp > self.range[1];
- });
- },
- /**
- * Replace the current input field value.
- *
- * @param {string} val The new value.
- */
- replace: function (val)
- {
- this.input.value = val;
- commandline.triggerCallback("change", currentExtendedMode, val);
- },
-
- /**
- * Move forward or backward in history.
- *
- * @param {boolean} backward Direction to move.
- * @param {boolean} matchCurrent Search for matches starting
- * with the current input value.
- */
- select: function (backward, matchCurrent)
- {
- // always reset the tab completion if we use up/down keys
- completions.reset();
-
- let diff = backward ? -1 : 1;
-
- if (this.index == null)
- {
- this.original = this.input.value;
- this.index = this.store.length;
- }
-
- // search the history for the first item matching the current
- // commandline string
- while (true)
- {
- this.index += diff;
- if (this.index < 0 || this.index > this.store.length)
- {
- this.index = util.Math.constrain(this.index, 0, this.store.length);
- liberator.beep();
- // I don't know why this kludge is needed. It
- // prevents the caret from moving to the end of
- // the input field.
- if (this.input.value == "")
- {
- this.input.value = " ";
- this.input.value = "";
- }
- break;
- }
-
- let hist = this.store.get(this.index);
- // user pressed DOWN when there is no newer history item
- if (!hist)
- hist = this.original;
- else
- hist = (hist.value || hist);
-
- if (!matchCurrent || hist.substr(0, this.original.length) == this.original)
- {
- this.replace(hist);
- break;
- }
- }
- }
- }; //}}}
-
- /**
- * A class for tab completions on an input field.
- *
- * @param {Object} input
- */
- function Completions(input) //{{{
- {
- if (!(this instanceof arguments.callee))
- return new arguments.callee(input);
-
- let self = this;
- this.context = CompletionContext(input.editor);
- this.context.onUpdate = function ()
- {
- self._reset();
- };
- this.editor = input.editor;
- this.selected = null;
- this.wildmode = options.get("wildmode");
- this.itemList = completionList;
- this.itemList.setItems(this.context);
- this.reset();
- }
- Completions.prototype = {
- UP: {},
- DOWN: {},
- PAGE_UP: {},
- PAGE_DOWN: {},
- RESET: null,
-
- get completion()
- {
- let str = commandline.command;
- return str.substring(this.prefix.length, str.length - this.suffix.length);
- },
- set completion set_completion(completion)
- {
- this.previewClear();
-
- // Change the completion text.
- // The second line is a hack to deal with some substring
- // preview corner cases.
- commandWidget.value = this.prefix + completion + this.suffix;
- this.editor.selection.focusNode.textContent = commandWidget.value;
-
- // Reset the caret to one position after the completion.
- this.caret = this.prefix.length + completion.length;
- },
-
- get caret() this.editor.selection.focusOffset,
- set caret(offset)
- {
- commandWidget.selectionStart = offset;
- commandWidget.selectionEnd = offset;
- },
-
- get start() this.context.allItems.start,
-
- get items() this.context.allItems.items,
-
- get substring() this.context.longestAllSubstring,
-
- get wildtype() this.wildtypes[this.wildIndex] || "",
-
- get type() ({
- list: this.wildmode.checkHas(this.wildtype, "list"),
- longest: this.wildmode.checkHas(this.wildtype, "longest"),
- first: this.wildmode.checkHas(this.wildtype, ""),
- full: this.wildmode.checkHas(this.wildtype, "full")
- }),
-
- complete: function complete(show, tabPressed)
- {
- this.context.reset();
- this.context.tabPressed = tabPressed;
- commandline.triggerCallback("complete", currentExtendedMode, this.context);
- this.context.updateAsync = true;
- this.reset(show, tabPressed);
- this.wildIndex = 0;
- },
-
- preview: function preview()
- {
- this.previewClear();
- if (this.wildIndex < 0 || this.suffix || !this.items.length)
- return;
-
- let substring = "";
- switch (this.wildtype.replace(/.*:/, ""))
- {
- case "":
- substring = this.items[0].text;
- break;
- case "longest":
- if (this.items.length > 1)
- {
- substring = this.substring;
- break;
- }
- // Fallthrough
- case "full":
- let item = this.items[this.selected != null ? this.selected + 1 : 0];
- if (item)
- substring = item.text;
- break;
- }
-
- // Don't show 1-character substrings unless we've just hit backspace
- if (substring.length < 2 && (!this.lastSubstring || this.lastSubstring.indexOf(substring) != 0))
- return;
- this.lastSubstring = substring;
-
- let value = this.completion;
- if (util.compareIgnoreCase(value, substring.substr(0, value.length)))
- return;
- substring = substring.substr(value.length);
- this.removeSubstring = substring;
-
- let node = util.xmlToDom(<span highlight="Preview">{substring}</span>,
- document);
- let start = this.caret;
- this.editor.insertNode(node, this.editor.rootElement, 1);
- this.caret = start;
- },
-
- previewClear: function previewClear()
- {
- let node = this.editor.rootElement.firstChild;
- if (node && node.nextSibling)
- {
- try
- {
- this.editor.deleteNode(node.nextSibling);
- }
- catch (e)
- {
- node.nextSibling.textContent = "";
- }
- }
- else if (this.removeSubstring)
- {
- let str = this.removeSubstring;
- let cmd = commandWidget.value;
- if (cmd.substr(cmd.length - str.length) == str)
- commandWidget.value = cmd.substr(0, cmd.length - str.length);
- }
- delete this.removeSubstring;
- },
-
- reset: function reset(show)
- {
- this.wildIndex = -1;
-
- this.prefix = this.context.value.substring(0, this.start);
- this.value = this.context.value.substring(this.start, this.caret);
- this.suffix = this.context.value.substring(this.caret);
-
- if (show)
- {
- this.itemList.reset();
- this.selected = null;
- this.wildIndex = 0;
- }
-
- this.wildtypes = this.wildmode.values;
- this.preview();
- },
-
- _reset: function _reset()
- {
- this.prefix = this.context.value.substring(0, this.start);
- this.value = this.context.value.substring(this.start, this.caret);
- this.suffix = this.context.value.substring(this.caret);
-
- this.itemList.reset();
- this.itemList.selectItem(this.selected);
-
- this.preview();
- },
-
- select: function select(idx)
- {
- switch (idx)
- {
- case this.UP:
- if (this.selected == null)
- idx = -2;
- else
- idx = this.selected - 1;
- break;
- case this.DOWN:
- if (this.selected == null)
- idx = 0;
- else
- idx = this.selected + 1;
- break;
- case this.RESET:
- idx = null;
- break;
- default:
- idx = util.Math.constrain(idx, 0, this.items.length - 1);
- break;
- }
-
- if (idx == -1 || this.items.length && idx >= this.items.length || idx == null)
- {
- // Wrapped. Start again.
- this.selected = null;
- this.completion = this.value;
- }
- else
- {
- // Wait for contexts to complete if necessary.
- // FIXME: Need to make idx relative to individual contexts.
- let list = this.context.contextList;
- if (idx == -2)
- list = list.slice().reverse();
- let n = 0;
- try
- {
- this.waiting = true;
- for (let [, context] in Iterator(list))
- {
- function done() !(idx >= n + context.items.length || idx == -2 && !context.items.length);
- while (context.incomplete && !done())
- liberator.threadYield(false, true);
-
- if (done())
- break;
-
- n += context.items.length;
- }
- }
- finally
- {
- this.waiting = false;
- }
-
- // See previous FIXME. This will break if new items in
- // a previous context come in.
- if (idx < 0)
- idx = this.items.length - 1;
- if (this.items.length == 0)
- return;
-
- this.selected = idx;
- this.completion = this.items[idx].text;
- }
-
- this.itemList.selectItem(idx);
- },
-
- tabs: [],
-
- tab: function tab(reverse)
- {
- autocompleteTimer.flush();
- // Check if we need to run the completer.
- if (this.context.waitingForTab || this.wildIndex == -1)
- this.complete(true, true);
-
- this.tabs.push(reverse);
- if (this.waiting)
- return;
-
- while (this.tabs.length)
- {
- reverse = this.tabs.shift();
- switch (this.wildtype.replace(/.*:/, ""))
- {
- case "":
- this.select(0);
- break;
- case "longest":
- if (this.items.length > 1)
- {
- if (this.substring && this.substring != this.completion)
- this.completion = this.substring;
- break;
- }
- // Fallthrough
- case "full":
- this.select(reverse ? this.UP : this.DOWN);
- break;
- }
-
- if (this.type.list)
- completionList.show();
-
- this.wildIndex = util.Math.constrain(this.wildIndex + 1, 0, this.wildtypes.length - 1);
- this.preview();
-
- statusTimer.tell();
- }
-
- if (this.items.length == 0)
- liberator.beep();
- }
- }; //}}}
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// TIMERS //////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- var statusTimer = new Timer(5, 100, function statusTell() {
- if (completions == null)
- return;
- if (completions.selected == null)
- statusline.updateProgress("");
- else
- statusline.updateProgress("match " + (completions.selected + 1) + " of " + completions.items.length);
- });
-
- var autocompleteTimer = new Timer(200, 500, function autocompleteTell(tabPressed) {
- if (!events.feedingKeys && completions && options.get("wildoptions").has("auto"))
- {
- completions.complete(true, false);
- completions.itemList.show();
- }
- });
-
- // This timer just prevents <Tab>s from queueing up when the
- // system is under load (and, thus, giving us several minutes of
- // the completion list scrolling). Multiple <Tab> presses are
- // still processed normally, as the time is flushed on "keyup".
- var tabTimer = new Timer(0, 0, function tabTell(event) {
- if (completions)
- completions.tab(event.shiftKey);
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// VARIABLES ///////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- const completionList = new ItemList("liberator-completions");
- var completions = null;
- var history = null;
-
- var startHints = false; // whether we're waiting to start hints mode
- var lastSubstring = "";
-
- // the containing box for the promptWidget and commandWidget
- const commandlineWidget = document.getElementById("liberator-commandline");
- // the prompt for the current command, for example : or /. Can be blank
- const promptWidget = document.getElementById("liberator-commandline-prompt");
- // the command bar which contains the current command
- const commandWidget = document.getElementById("liberator-commandline-command");
-
- const messageBox = document.getElementById("liberator-message");
-
- commandWidget.inputField.QueryInterface(Ci.nsIDOMNSEditableElement);
- messageBox.inputField.QueryInterface(Ci.nsIDOMNSEditableElement);
-
- // the widget used for multiline output
- const multilineOutputWidget = document.getElementById("liberator-multiline-output");
- const outputContainer = multilineOutputWidget.parentNode;
-
- multilineOutputWidget.contentDocument.body.id = "liberator-multiline-output-content";
-
- // the widget used for multiline intput
- const multilineInputWidget = document.getElementById("liberator-multiline-input");
-
- // we need to save the mode which were in before opening the command line
- // this is then used if we focus the command line again without the "official"
- // way of calling "open"
- var currentExtendedMode = null; // the extended mode which we last openend the command line for
- // modules.__defineGetter__("currentExtendedMode", function () _currentExtendedMode)
- // modules.__defineSetter__("currentExtendedMode", function (val) (liberator.dumpStack("currentExtendedMode = " + (val && modes.getMode(val).name)),
- // _currentExtendedMode = val))
- var currentPrompt = null;
- var currentCommand = null;
-
- // save the arguments for the inputMultiline method which are needed in the event handler
- var multilineRegexp = null;
- var multilineCallback = null;
-
- /**
- * Highlight the messageBox according to <b>group</b>.
- */
- function setHighlightGroup(group)
- {
- messageBox.setAttributeNS(NS.uri, "highlight", group);
- }
-
- /**
- * Determines whether the command line should be visible.
- *
- * @returns {boolean}
- */
- function commandShown() modes.main == modes.COMMAND_LINE &&
- !(modes.extended & (modes.INPUT_MULTILINE | modes.OUTPUT_MULTILINE));
-
- /**
- * Set the command-line prompt.
- *
- * @param {string} val
- * @param {string} highlightGroup
- */
- function setPrompt(val, highlightGroup)
- {
- promptWidget.value = val;
- promptWidget.size = val.length;
- promptWidget.collapsed = (val == "");
- promptWidget.setAttributeNS(NS.uri, "highlight", highlightGroup || commandline.HL_NORMAL);
- }
-
- /**
- * Set the command-line input value. The caret is reset to the
- * end of the line.
- *
- * @param {string} cmd
- */
- function setCommand(cmd)
- {
- commandWidget.value = cmd;
- commandWidget.selectionStart = cmd.length;
- commandWidget.selectionEnd = cmd.length;
- }
-
- /**
- * Display a message in the command-line area.
- *
- * @param {string} str
- * @param {string} highlightGroup
- * @param {boolean} forceSingle If provided, don't let over-long
- * messages move to the MOW.
- */
- function echoLine(str, highlightGroup, forceSingle)
- {
- setHighlightGroup(highlightGroup);
- messageBox.value = str;
-
- liberator.triggerObserver("echoLine", str, highlightGroup, forceSingle);
-
- if (!commandShown())
- commandline.hide();
-
- let field = messageBox.inputField;
- if (!forceSingle && field.editor.rootElement.scrollWidth > field.scrollWidth)
- echoMultiline(<span highlight="Message">{str}</span>, highlightGroup);
- }
-
- /**
- * Display a multiline message.
- *
- * @param {string} str
- * @param {string} highlightGroup
- */
- // TODO: resize upon a window resize
- function echoMultiline(str, highlightGroup)
- {
- let doc = multilineOutputWidget.contentDocument;
- let win = multilineOutputWidget.contentWindow;
-
- liberator.triggerObserver("echoMultiline", str, highlightGroup);
-
- // If it's already XML, assume it knows what it's doing.
- // Otherwise, white space is significant.
- // The problem elsewhere is that E4X tends to insert new lines
- // after interpolated data.
- XML.ignoreWhitespace = typeof str != "xml";
- lastMowOutput = <div class="ex-command-output" style="white-space: nowrap" highlight={highlightGroup}>{template.maybeXML(str)}</div>;
- let output = util.xmlToDom(lastMowOutput, doc);
- XML.ignoreWhitespace = true;
-
- // FIXME: need to make sure an open MOW is closed when commands
- // that don't generate output are executed
- if (outputContainer.collapsed)
- doc.body.innerHTML = "";
-
- doc.body.appendChild(output);
-
- commandline.updateOutputHeight(true);
-
- if (options["more"] && win.scrollMaxY > 0)
- {
- // start the last executed command's output at the top of the screen
- let elements = doc.getElementsByClassName("ex-command-output");
- elements[elements.length - 1].scrollIntoView(true);
- }
- else
- win.scrollTo(0, doc.height);
-
- win.focus();
-
- startHints = false;
- modes.set(modes.COMMAND_LINE, modes.OUTPUT_MULTILINE);
- commandline.updateMorePrompt();
- }
-
- /**
- * Ensure that the multiline input widget is the correct size.
- */
- function autosizeMultilineInputWidget()
- {
- let lines = multilineInputWidget.value.split("\n").length - 1;
-
- multilineInputWidget.setAttribute("rows", Math.max(lines, 1));
- }
-
- /**
- * eval() a JavaScript expression and return a string suitable
- * to be echoed.
- *
- * @param {string} arg
- * @param {boolean} useColor When true, the result is a
- * highlighted XML object.
- */
- function echoArgumentToString(arg, useColor)
- {
- if (!arg)
- return "";
-
- try
- {
- arg = liberator.eval(arg);
- }
- catch (e)
- {
- liberator.echoerr(e);
- return null;
- }
-
- if (typeof arg === "object")
- arg = util.objectToString(arg, useColor);
- else if (typeof arg == "string" && /\n/.test(arg))
- arg = <span highlight="CmdOutput">{arg}</span>;
- else
- arg = String(arg);
-
- return arg;
- }
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// OPTIONS /////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- options.add(["history", "hi"],
- "Number of Ex commands and search patterns to store in the command-line history",
- "number", 500,
- { validator: function (value) value >= 0 });
-
- options.add(["maxitems"],
- "Maximum number of items to display at once",
- "number", 20,
- { validator: function (value) value >= 1 });
-
- options.add(["messages", "msgs"],
- "Number of messages to store in the message history",
- "number", 100,
- { validator: function (value) value >= 0 });
-
- options.add(["more"],
- "Pause the message list window when more than one screen of listings is displayed",
- "boolean", true);
-
- options.add(["showmode", "smd"],
- "Show the current mode in the command line",
- "boolean", true);
-
- options.add(["suggestengines"],
- "Engine Alias which has a feature of suggest",
- "stringlist", "google",
- {
- completer: function completer(value)
- {
- let engines = services.get("browserSearch").getEngines({})
- .filter(function (engine) engine.supportsResponseType("application/x-suggestions+json"));
-
- return engines.map(function (engine) [engine.alias, engine.description]);
- },
- validator: Option.validateCompleter
- });
-
- options.add(["complete", "cpt"],
- "Items which are completed at the :open prompts",
- "charlist", typeof(config.defaults["complete"]) == "string" ? config.defaults["complete"] : "slf",
- {
- completer: function (context) [k for each (k in completion.urlCompleters)],
- validator: Option.validateCompleter
- });
-
- options.add(["wildcase", "wic"],
- "Completion case matching mode",
- "string", "smart",
- {
- completer: function () [
- ["smart", "Case is significant when capital letters are typed"],
- ["match", "Case is always significant"],
- ["ignore", "Case is never significant"]
- ],
- validator: Option.validateCompleter
- });
-
- options.add(["wildignore", "wig"],
- "List of file patterns to ignore when completing files",
- "stringlist", "",
- {
- validator: function validator(values)
- {
- // TODO: allow for escaping the ","
- try
- {
- RegExp("^(" + values.join("|") + ")$");
- return true;
- }
- catch (e)
- {
- return false;
- }
- }
- });
-
- options.add(["wildmode", "wim"],
- "Define how command line completion works",
- "stringlist", "list:full",
- {
- completer: function (context) [
- // Why do we need ""?
- ["", "Complete only the first match"],
- ["full", "Complete the next full match"],
- ["longest", "Complete to longest common string"],
- ["list", "If more than one match, list all matches"],
- ["list:full", "List all and complete first match"],
- ["list:longest", "List all and complete common string"]
- ],
- validator: Option.validateCompleter,
- checkHas: function (value, val)
- {
- let [first, second] = value.split(":", 2);
- return first == val || second == val;
- }
- });
-
- options.add(["wildoptions", "wop"],
- "Change how command line completion is done",
- "stringlist", "",
- {
- completer: function completer(value)
- {
- return [
- ["", "Default completion that won't show or sort the results"],
- ["auto", "Automatically show completions while you are typing"],
- ["sort", "Always sort the completion list"]
- ];
- },
- validator: Option.validateCompleter
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// MAPPINGS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- var myModes = [modes.COMMAND_LINE];
-
- // TODO: move "<Esc>", "<C-[>" here from mappings
- mappings.add(myModes,
- ["<C-c>"], "Focus content",
- function () { events.onEscape(); });
-
- // Any "non-keyword" character triggers abbreviation expansion
- // TODO: Add "<CR>" and "<Tab>" to this list
- // At the moment, adding "<Tab>" breaks tab completion. Adding
- // "<CR>" has no effect.
- // TODO: Make non-keyword recognition smarter so that there need not
- // be two lists of the same characters (one here and a regex in
- // mappings.js)
- mappings.add(myModes,
- ["<Space>", '"', "'"], "Expand command line abbreviation",
- function ()
- {
- commandline.resetCompletions();
- return editor.expandAbbreviation("c");
- },
- { route: true });
-
- mappings.add(myModes,
- ["<C-]>", "<C-5>"], "Expand command line abbreviation",
- function () { editor.expandAbbreviation("c"); });
-
- mappings.add([modes.NORMAL],
- ["g<"], "Redisplay the last command output",
- function ()
- {
- if (lastMowOutput)
- echoMultiline(lastMowOutput, commandline.HL_NORMAL);
- else
- liberator.beep();
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// COMMANDS ////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- [
- {
- name: "ec[ho]",
- description: "Echo the expression",
- action: liberator.echo
- },
- {
- name: "echoe[rr]",
- description: "Echo the expression as an error message",
- action: liberator.echoerr
- },
- {
- name: "echom[sg]",
- description: "Echo the expression as an informational message",
- action: liberator.echomsg
- }
- ].forEach(function (command) {
- commands.add([command.name],
- command.description,
- function (args)
- {
- let str = echoArgumentToString(args.string, true);
- if (str != null)
- command.action(str);
- },
- {
- completer: function (context) completion.javascript(context),
- literal: 0
- });
- });
-
- commands.add(["mes[sages]"],
- "Display previously given messages",
- function ()
- {
- // TODO: are all messages single line? Some display an aggregation
- // of single line messages at least. E.g. :source
- if (messageHistory.length == 1)
- {
- let message = messageHistory.messages[0];
- commandline.echo(message.str, message.highlight, commandline.FORCE_SINGLELINE);
- }
- else if (messageHistory.length > 1)
- {
- XML.ignoreWhitespace = false;
- let list = template.map(messageHistory.messages, function (message)
- <div highlight={message.highlight + " Message"}>{message.str}</div>);
- liberator.echo(list, commandline.FORCE_MULTILINE);
- }
- },
- { argCount: "0" });
-
- commands.add(["messc[lear]"],
- "Clear the message history",
- function () { messageHistory.clear(); },
- { argCount: "0" });
-
- commands.add(["sil[ent]"],
- "Run a command silently",
- function (args)
- {
- commandline.runSilently(function () liberator.execute(args[0], null, true));
- },
- {
- completer: function (context) completion.ex(context),
- literal: 0
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// PUBLIC SECTION //////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- const self = {
-
- HL_NORMAL: "Normal",
- HL_ERRORMSG: "ErrorMsg",
- HL_MODEMSG: "ModeMsg",
- HL_MOREMSG: "MoreMsg",
- HL_QUESTION: "Question",
- HL_INFOMSG: "InfoMsg",
- HL_WARNINGMSG: "WarningMsg",
- HL_LINENR: "LineNr",
-
- FORCE_MULTILINE : 1 << 0,
- FORCE_SINGLELINE : 1 << 1,
- DISALLOW_MULTILINE : 1 << 2, // if an echo() should try to use the single line
- // but output nothing when the MOW is open; when also
- // FORCE_MULTILINE is given, FORCE_MULTILINE takes precedence
- APPEND_TO_MESSAGES : 1 << 3, // add the string to the message history
-
- get completionContext() completions.context,
-
- get mode() (modes.extended == modes.EX) ? "cmd" : "search",
-
- get silent() silent,
- set silent(val)
- {
- silent = val;
- this.quiet = this.quiet;
- },
- get quiet() quiet,
- set quiet(val)
- {
- quiet = val;
- Array.forEach(document.getElementById("liberator-commandline").childNodes, function (node) {
- node.style.opacity = quiet || silent ? "0" : "";
- });
- },
-
- // @param type can be:
- // "submit": when the user pressed enter in the command line
- // "change"
- // "cancel"
- // "complete"
- registerCallback: function (type, mode, func)
- {
- if (!(type in callbacks))
- callbacks[type] = {};
- callbacks[type][mode] = func;
- },
-
- triggerCallback: function (type, mode, data)
- {
- if (callbacks[type] && callbacks[type][mode])
- callbacks[type][mode].call(this, data);
- },
-
- runSilently: function (func, self)
- {
- let wasSilent = this.silent;
- this.silent = true;
- try
- {
- func.call(self);
- }
- finally
- {
- this.silent = wasSilent;
- }
- },
-
- get command()
- {
- try
- {
- // The long path is because of complications with the
- // completion preview.
- return commandWidget.inputField.editor.rootElement.firstChild.textContent;
- }
- catch (e)
- {
- return commandWidget.value;
- }
- },
- set command(cmd) commandWidget.value = cmd,
-
- get message() messageBox.value,
-
- /**
- * Open the command line. The main mode is set to
- * COMMAND_LINE, the extended mode to <b>extendedMode</b>.
- * Further, callbacks defined for <b>extendedMode</b> are
- * triggered as appropriate (see {@link #registerCallback}).
- *
- * @param {string} prompt
- * @param {string} cmd
- * @param {number} extendedMode
- */
- open: function open(prompt, cmd, extendedMode)
- {
- // save the current prompts, we need it later if the command widget
- // receives focus without calling the this.open() method
- currentPrompt = prompt || "";
- currentCommand = cmd || "";
- currentExtendedMode = extendedMode || null;
- keepCommand = false;
-
- setPrompt(currentPrompt);
- setCommand(currentCommand);
- commandlineWidget.collapsed = false;
-
- modes.set(modes.COMMAND_LINE, currentExtendedMode);
-
- commandWidget.focus();
-
- history = History(commandWidget.inputField, (modes.extended == modes.EX) ? "command" : "search");
- completions = Completions(commandWidget.inputField);
-
- // open the completion list automatically if wanted
- if (cmd.length)
- commandline.triggerCallback("change", currentExtendedMode, cmd);
- },
-
- /**
- * Closes the command line. This is ordinarily triggered automatically
- * by a mode change. Will not hide the command line immediately if
- * called directly after a successful command, otherwise it will.
- */
- close: function close()
- {
- let mode = currentExtendedMode;
- currentExtendedMode = null;
- commandline.triggerCallback("cancel", mode);
-
- if (history)
- history.save();
-
- this.resetCompletions(); // cancels any asynchronous completion still going on, must be before we set completions = null
- completions = null;
- history = null;
-
- statusline.updateProgress(""); // we may have a "match x of y" visible
- liberator.focusContent(false);
-
- multilineInputWidget.collapsed = true;
- completionList.hide();
-
- if (!keepCommand || this.silent || this.quiet)
- {
- outputContainer.collapsed = true;
- commandline.updateMorePrompt();
- this.hide();
- }
- if (!outputContainer.collapsed)
- {
- modes.set(modes.COMMAND_LINE, modes.OUTPUT_MULTILINE);
- commandline.updateMorePrompt();
- }
- keepCommand = false;
- },
-
- /**
- * Hides the command line, and shows any status messages that
- * are under it.
- */
- hide: function hide()
- {
- commandlineWidget.collapsed = true;
- },
-
- /**
- * Output the given string onto the command line. With no flags, the
- * message will be shown in the status line if it's short enough to
- * fit, and contains no new lines, and isn't XML. Otherwise, it will be
- * shown in the MOW.
- *
- * @param {string} str
- * @param {string} highlightGroup The Highlight group for the
- * message.
- * @default "Normal"
- * @param {number} flags Changes the behavior as follows:
- * commandline.APPEND_TO_MESSAGES - Causes message to be added to the
- * messages history, and shown by :messages.
- * commandline.FORCE_SINGLELINE - Forbids the command from being
- * pushed to the MOW if it's too long or of there are already
- * status messages being shown.
- * commandline.DISALLOW_MULTILINE - Cancels the operation if the MOW
- * is already visible.
- * commandline.FORCE_MULTILINE - Forces the message to appear in
- * the MOW.
- */
- echo: function echo(str, highlightGroup, flags)
- {
- // liberator.echo uses different order of flags as it omits the highlight group, change commandline.echo argument order? --mst
- if (silent)
- return;
-
- highlightGroup = highlightGroup || this.HL_NORMAL;
-
- if (flags & this.APPEND_TO_MESSAGES)
- messageHistory.add({ str: str, highlight: highlightGroup });
-
- // The DOM isn't threadsafe. It must only be accessed from the main thread.
- liberator.callInMainThread(function ()
- {
- if ((flags & this.DISALLOW_MULTILINE) && !outputContainer.collapsed)
- return;
-
- let single = flags & (this.FORCE_SINGLELINE | this.DISALLOW_MULTILINE);
- let action = echoLine;
-
- // TODO: this is all a bit convoluted - clean up.
- // assume that FORCE_MULTILINE output is fully styled
- if (!(flags & this.FORCE_MULTILINE) && !single && (!outputContainer.collapsed || messageBox.value == lastEcho))
- {
- highlightGroup += " Message";
- action = echoMultiline;
- }
-
- if ((flags & this.FORCE_MULTILINE) || (/\n/.test(str) || typeof str == "xml") && !(flags & this.FORCE_SINGLELINE))
- action = echoMultiline;
-
- if (single)
- lastEcho = null;
- else
- {
- if (messageBox.value == lastEcho)
- echoMultiline(<span highlight="Message">{lastEcho}</span>,
- messageBox.getAttributeNS(NS.uri, "highlight"));
- lastEcho = (action == echoLine) && str;
- }
-
- if (action)
- action(str, highlightGroup, single);
- }, this);
- },
-
- /**
- * Prompt the user. Sets modes.main to COMMAND_LINE, which the user may
- * pop at any time to close the prompt.
- *
- * @param {string} prompt The input prompt to use.
- * @param {function(string)} callback
- * @param {Object} extra
- * @... {function} onChange - A function to be called with the current
- * input every time it changes.
- * @... {function(CompletionContext)} completer - A completion function
- * for the user's input.
- * @... {string} promptHighlight - The HighlightGroup used for the
- * prompt. @default "Question"
- * @... {string} default - The initial value that will be returned
- * if the user presses <CR> straightaway. @default ""
- */
- input: function _input(prompt, callback, extra)
- {
- extra = extra || {};
-
- input = {
- submit: callback,
- change: extra.onChange,
- complete: extra.completer,
- cancel: extra.onCancel
- };
-
- modes.push(modes.COMMAND_LINE, modes.PROMPT);
- currentExtendedMode = modes.PROMPT;
-
- setPrompt(prompt, extra.promptHighlight || this.HL_QUESTION);
- setCommand(extra.default || "");
- commandlineWidget.collapsed = false;
- commandWidget.focus();
-
- completions = Completions(commandWidget.inputField);
- },
-
- /**
- * Get a multiline input from a user, up to but not including the line
- * which matches the given regular expression. Then execute the
- * callback with that string as a parameter.
- *
- * @param {RegExp} untilRegexp
- * @param {function(string)} callbackFunc
- */
- // FIXME: Buggy, especially when pasting. Shouldn't use a RegExp.
- inputMultiline: function inputMultiline(untilRegexp, callbackFunc)
- {
- // Kludge.
- let cmd = !commandWidget.collapsed && this.command;
- modes.push(modes.COMMAND_LINE, modes.INPUT_MULTILINE);
- if (cmd != false)
- echoLine(cmd, this.HL_NORMAL);
-
- // save the arguments, they are needed in the event handler onEvent
- multilineRegexp = untilRegexp;
- multilineCallback = callbackFunc;
-
- multilineInputWidget.collapsed = false;
- multilineInputWidget.value = "";
- autosizeMultilineInputWidget();
-
- setTimeout(function () { multilineInputWidget.focus(); }, 10);
- },
-
- /**
- * Handles all command-line events. All key events are passed here when
- * COMMAND_LINE mode is active, as well as all input, keyup, focus, and
- * blur events sent to the command-line XUL element.
- *
- * @param {Event} event
- * @private
- */
- onEvent: function onEvent(event)
- {
- let command = this.command;
-
- if (event.type == "blur")
- {
- // prevent losing focus, there should be a better way, but it just didn't work otherwise
- setTimeout(function () {
- if (commandShown() && event.originalTarget == commandWidget.inputField)
- commandWidget.inputField.focus();
- }, 0);
- }
- else if (event.type == "focus")
- {
- if (!commandShown() && event.target == commandWidget.inputField)
- {
- event.target.blur();
- liberator.beep();
- }
- }
- else if (event.type == "input")
- {
- this.resetCompletions();
- commandline.triggerCallback("change", currentExtendedMode, command);
- }
- else if (event.type == "keypress")
- {
- let key = events.toString(event);
- if (completions)
- completions.previewClear();
- if (!currentExtendedMode)
- return;
-
- // user pressed <Enter> to carry out a command
- // user pressing <Esc> is handled in the global onEscape
- // FIXME: <Esc> should trigger "cancel" event
- if (events.isAcceptKey(key))
- {
- let mode = currentExtendedMode; // save it here, as modes.pop() resets it
- keepCommand = !userContext.hidden_option_no_command_afterimage;
- currentExtendedMode = null; // Don't let modes.pop trigger "cancel"
- modes.pop(!this.silent);
- commandline.triggerCallback("submit", mode, command);
- }
- // user pressed <Up> or <Down> arrow to cycle history completion
- else if (/^(<Up>|<Down>|<S-Up>|<S-Down>|<PageUp>|<PageDown>)$/.test(key))
- {
- // prevent tab from moving to the next field
- event.preventDefault();
- event.stopPropagation();
-
- if (history)
- history.select(/Up/.test(key), !/(Page|S-)/.test(key));
- else
- liberator.beep();
- }
- // user pressed <Tab> to get completions of a command
- else if (key == "<Tab>" || key == "<S-Tab>")
- {
- // prevent tab from moving to the next field
- event.preventDefault();
- event.stopPropagation();
-
- tabTimer.tell(event);
- }
- else if (key == "<BS>")
- {
- // reset the tab completion
- //this.resetCompletions();
-
- // and blur the command line if there is no text left
- if (command.length == 0)
- {
- commandline.triggerCallback("cancel", currentExtendedMode);
- modes.pop();
- }
- }
- else // any other key
- {
- //this.resetCompletions();
- }
- // allow this event to be handled by the host app
- }
- else if (event.type == "keyup")
- {
- let key = events.toString(event);
- if (key == "<Tab>" || key == "<S-Tab>")
- tabTimer.flush();
- }
- },
-
- /**
- * Multiline input events, they will come straight from
- * #liberator-multiline-input in the XUL.
- *
- * @param {Event} event
- */
- onMultilineInputEvent: function onMultilineInputEvent(event)
- {
- if (event.type == "keypress")
- {
- let key = events.toString(event);
- if (events.isAcceptKey(key))
- {
- let text = multilineInputWidget.value.substr(0, multilineInputWidget.selectionStart);
- if (text.match(multilineRegexp))
- {
- text = text.replace(multilineRegexp, "");
- modes.pop();
- multilineInputWidget.collapsed = true;
- multilineCallback.call(this, text);
- }
- }
- else if (events.isCancelKey(key))
- {
- modes.pop();
- multilineInputWidget.collapsed = true;
- }
- }
- else if (event.type == "blur")
- {
- if (modes.extended & modes.INPUT_MULTILINE)
- setTimeout(function () { multilineInputWidget.inputField.focus(); }, 0);
- }
- else if (event.type == "input")
- autosizeMultilineInputWidget();
- return true;
- },
-
- /**
- * Handle events when we are in multiline output mode, these come from
- * liberator when modes.extended & modes.MULTILINE_OUTPUT and also from
- * #liberator-multiline-output in the XUL.
- *
- * @param {Event} event
- */
- // FIXME: if 'more' is set and the MOW is not scrollable we should still
- // allow a down motion after an up rather than closing
- onMultilineOutputEvent: function onMultilineOutputEvent(event)
- {
- let win = multilineOutputWidget.contentWindow;
-
- let showMoreHelpPrompt = false;
- let showMorePrompt = false;
- let closeWindow = false;
- let passEvent = false;
-
- let key = events.toString(event);
-
- // TODO: Wouldn't multiple handlers be cleaner? --djk
- if (event.type == "click" && event.target instanceof HTMLAnchorElement)
- {
- function openLink(where)
- {
- event.preventDefault();
- // FIXME: Why is this needed? --djk
- if (event.target.getAttribute("href") == "#")
- liberator.open(event.target.textContent, where);
- else
- liberator.open(event.target.href, where);
- }
-
- switch (key)
- {
- case "<LeftMouse>":
- if (event.originalTarget.getAttributeNS(NS.uri, "highlight") == "URL buffer-list")
- {
- event.preventDefault();
- tabs.select(parseInt(event.originalTarget.parentNode.parentNode.firstChild.textContent, 10) - 1);
- }
- else
- openLink(liberator.CURRENT_TAB);
- break;
- case "<MiddleMouse>":
- case "<C-LeftMouse>":
- case "<C-M-LeftMouse>":
- openLink(liberator.NEW_BACKGROUND_TAB);
- break;
- case "<S-MiddleMouse>":
- case "<C-S-LeftMouse>":
- case "<C-M-S-LeftMouse>":
- openLink(liberator.NEW_TAB);
- break;
- case "<S-LeftMouse>":
- openLink(liberator.NEW_WINDOW);
- break;
- }
-
- return;
- }
-
- if (startHints)
- {
- statusline.updateInputBuffer("");
- startHints = false;
- hints.show(key, undefined, win);
- return;
- }
-
- function isScrollable() !win.scrollMaxY == 0;
- function atEnd() win.scrollY / win.scrollMaxY >= 1;
-
- switch (key)
- {
- case "<Esc>":
- closeWindow = true;
- break; // handled globally in events.js:onEscape()
-
- case ":":
- commandline.open(":", "", modes.EX);
- return;
-
- // down a line
- case "j":
- case "<Down>":
- if (options["more"] && isScrollable())
- win.scrollByLines(1);
- else
- passEvent = true;
- break;
-
- case "<C-j>":
- case "<C-m>":
- case "<Return>":
- if (options["more"] && isScrollable() && !atEnd())
- win.scrollByLines(1);
- else
- closeWindow = true; // don't propagate the event for accept keys
- break;
-
- // up a line
- case "k":
- case "<Up>":
- case "<BS>":
- if (options["more"] && isScrollable())
- win.scrollByLines(-1);
- else if (options["more"] && !isScrollable())
- showMorePrompt = true;
- else
- passEvent = true;
- break;
-
- // half page down
- case "d":
- if (options["more"] && isScrollable())
- win.scrollBy(0, win.innerHeight / 2);
- else
- passEvent = true;
- break;
-
- // TODO: <LeftMouse> on the prompt line should scroll one page
- // page down
- case "f":
- if (options["more"] && isScrollable())
- win.scrollByPages(1);
- else
- passEvent = true;
- break;
-
- case "<Space>":
- case "<PageDown>":
- if (options["more"] && isScrollable() && !atEnd())
- win.scrollByPages(1);
- else
- passEvent = true;
- break;
-
- // half page up
- case "u":
- // if (more and scrollable)
- if (options["more"] && isScrollable())
- win.scrollBy(0, -(win.innerHeight / 2));
- else
- passEvent = true;
- break;
-
- // page up
- case "b":
- if (options["more"] && isScrollable())
- win.scrollByPages(-1);
- else if (options["more"] && !isScrollable())
- showMorePrompt = true;
- else
- passEvent = true;
- break;
-
- case "<PageUp>":
- if (options["more"] && isScrollable())
- win.scrollByPages(-1);
- else
- passEvent = true;
- break;
-
- // top of page
- case "g":
- if (options["more"] && isScrollable())
- win.scrollTo(0, 0);
- else if (options["more"] && !isScrollable())
- showMorePrompt = true;
- else
- passEvent = true;
- break;
-
- // bottom of page
- case "G":
- if (options["more"] && isScrollable() && !atEnd())
- win.scrollTo(0, win.scrollMaxY);
- else
- passEvent = true;
- break;
-
- // copy text to clipboard
- case "<C-y>":
- util.copyToClipboard(win.getSelection());
- break;
-
- // close the window
- case "q":
- closeWindow = true;
- break;
-
- case ";":
- statusline.updateInputBuffer(";");
- startHints = true;
- break;
-
- // unmapped key
- default:
- if (!options["more"] || !isScrollable() || atEnd() || events.isCancelKey(key))
- passEvent = true;
- else
- showMoreHelpPrompt = true;
- }
-
- if (passEvent || closeWindow)
- {
- modes.pop();
-
- if (passEvent)
- events.onKeyPress(event);
- }
- else
- commandline.updateMorePrompt(showMorePrompt, showMoreHelpPrompt);
- },
-
- getSpaceNeeded: function getSpaceNeeded()
- {
- let rect = commandlineWidget.getBoundingClientRect();
- let offset = rect.bottom - window.innerHeight;
- return Math.max(0, offset);
- },
-
- /**
- * Update or remove the multiline output widget's "MORE" prompt.
- *
- * @param {boolean} force If true, "-- More --" is shown even if we're
- * at the end of the output.
- * @param {boolean} showHelp When true, show the valid key sequences
- * and what they do.
- */
- updateMorePrompt: function updateMorePrompt(force, showHelp)
- {
- if (outputContainer.collapsed)
- return echoLine("", this.HL_NORMAL);
-
- let win = multilineOutputWidget.contentWindow;
- function isScrollable() !win.scrollMaxY == 0;
- function atEnd() win.scrollY / win.scrollMaxY >= 1;
-
- if (showHelp)
- echoLine("-- More -- SPACE/d/j: screen/page/line down, b/u/k: up, q: quit", this.HL_MOREMSG, true);
- else if (force || (options["more"] && isScrollable() && !atEnd()))
- echoLine("-- More --", this.HL_MOREMSG, true);
- else
- echoLine("Press ENTER or type command to continue", this.HL_QUESTION, true);
- },
-
- /**
- * Changes the height of the multilineOutputWidget to fit in the
- * available space.
- *
- * @param {boolean} open If true, the widget will be opened if it's not
- * already so.
- */
- updateOutputHeight: function updateOutputHeight(open)
- {
- if (!open && outputContainer.collapsed)
- return;
-
- let doc = multilineOutputWidget.contentDocument;
-
- availableHeight = config.outputHeight;
- if (!outputContainer.collapsed)
- availableHeight += parseFloat(outputContainer.height);
- doc.body.style.minWidth = commandlineWidget.scrollWidth + "px";
- outputContainer.height = Math.min(doc.height, availableHeight) + "px";
- doc.body.style.minWidth = "";
- outputContainer.collapsed = false;
- },
-
- resetCompletions: function resetCompletions()
- {
- if (completions)
- {
- completions.context.cancelAll();
- completions.wildIndex = -1;
- completions.previewClear();
- }
- if (history)
- history.reset();
- }
- };
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// CALLBACKS ///////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- var input = {};
-
- self.registerCallback("submit", modes.EX, function (command) {
- commands.repeat = command;
- liberator.execute(command);
- });
- self.registerCallback("complete", modes.EX, function (context) {
- context.fork("ex", 0, completion, "ex");
- });
- self.registerCallback("change", modes.EX, function (command) {
- autocompleteTimer.tell(false);
- });
-
- self.registerCallback("cancel", modes.PROMPT, cancelPrompt);
- self.registerCallback("submit", modes.PROMPT, closePrompt);
- self.registerCallback("change", modes.PROMPT, function (str) {
- if (input.complete)
- autocompleteTimer.tell(false);
- if (input.change)
- return input.change.call(commandline, str);
- });
- self.registerCallback("complete", modes.PROMPT, function (context) {
- if (input.complete)
- context.fork("input", 0, commandline, input.complete);
- });
-
- function cancelPrompt(value)
- {
- let callback = input.cancel;
- input = {};
- if (callback)
- callback.call(commandline, value != null ? value : commandline.command);
- }
-
- function closePrompt(value)
- {
- let callback = input.submit;
- input = {};
- if (callback)
- callback.call(commandline, value != null ? value : commandline.command);
- }
- //}}}
-
- return self;
-
-} //}}}
-
-/**
- * The list which is used for the completion box (and QuickFix window in
- * future).
- *
- * @param {string} id The id of the <iframe> which will display the list. It
- * must be in its own container element, whose height it will update as
- * necessary.
- */
-function ItemList(id) //{{{
-{
- ////////////////////////////////////////////////////////////////////////////////
- ////////////////////// PRIVATE SECTION /////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- const WAITING_MESSAGE = "Generating results...";
-
- var completionElements = [];
-
- var iframe = document.getElementById(id);
- if (!iframe)
- {
- liberator.log("No iframe with id: " + id + " found, strange things may happen!"); // "The truth is out there..." -- djk
- return; // XXX
- }
-
- function dom(xml, map) util.xmlToDom(xml, doc, map);
-
- var doc = iframe.contentDocument;
- var container = iframe.parentNode;
-
- doc.body.id = id + "-content";
- doc.body.appendChild(doc.createTextNode(""));
- doc.body.style.borderTop = "1px solid black"; // FIXME: For cases where completions/MOW are shown at once, or ls=0. Should use :highlight.
-
- let gradient = template.gradient("GradientLeft", "GradientRight");
-
- var items = null;
- var startIndex = -1; // The index of the first displayed item
- var endIndex = -1; // The index one *after* the last displayed item
- var selIndex = -1; // The index of the currently selected element
- var div = null;
- var divNodes = {};
- var minHeight = 0;
-
- function autoSize()
- {
- if (container.collapsed)
- div.style.minWidth = document.getElementById("liberator-commandline").scrollWidth + "px";
- minHeight = Math.max(minHeight, divNodes.completions.getBoundingClientRect().bottom);
- container.height = minHeight;
- if (container.collapsed)
- div.style.minWidth = "";
- // FIXME: Belongs elsewhere.
- commandline.updateOutputHeight(false);
- setTimeout(function () { container.height -= commandline.getSpaceNeeded() }, 0);
- }
-
- function getCompletion(index) completionElements.snapshotItem(index - startIndex);
-
- function init()
- {
- div = dom(
- <div class="ex-command-output" highlight="Normal" style="white-space: nowrap">
- <div highlight="Completions" key="noCompletions"><span highlight="Title">No Completions</span></div>
- <div key="completions"/>
- <div highlight="Completions">
- {
- template.map(util.range(0, options["maxitems"] * 2), function (i)
- <span highlight="CompItem">
- <li highlight="NonText">~</li>
- </span>)
- }
- </div>
- </div>, divNodes);
- doc.body.replaceChild(div, doc.body.firstChild);
- //div.scrollIntoView(true);
-
- items.contextList.forEach(function init_eachContext(context) {
- delete context.cache.nodes;
- if (!context.items.length && !context.message && !context.incomplete)
- return;
- context.cache.nodes = [];
- dom(<div key="root" highlight="CompGroup">
- <div highlight="Completions">
- { context.createRow(context.title || [], "CompTitle") }
- </div>
- { gradient }
- <div key="message" highlight="CompMsg"/>
- <div key="up" highlight="CompLess"/>
- <div key="items" highlight="Completions"/>
- <div key="waiting" highlight="CompMsg">{WAITING_MESSAGE}</div>
- <div key="down" highlight="CompMore"/>
- </div>, context.cache.nodes);
- divNodes.completions.appendChild(context.cache.nodes.root);
- });
-
- setTimeout(function () { autoSize(); }, 0);
- }
-
- /**
- * Uses the entries in "items" to fill the listbox and does incremental
- * filling to speed up things.
- *
- * @param {number} offset Start at this index and show options["maxitems"].
- */
- function fill(offset)
- {
- XML.ignoreWhiteSpace = false;
- let diff = offset - startIndex;
- if (items == null || offset == null || diff == 0 || offset < 0)
- return false;
-
- startIndex = offset;
- endIndex = Math.min(startIndex + options["maxitems"], items.allItems.items.length);
-
- let haveCompletions = false;
- let off = 0;
- let end = startIndex + options["maxitems"];
- function getRows(context)
- {
- function fix(n) util.Math.constrain(n, 0, len);
- let len = context.items.length;
- let start = off;
- end -= !!context.message + context.incomplete;
- off += len;
-
- let s = fix(offset - start), e = fix(end - start);
- return [s, e, context.incomplete && e >= offset && off - 1 < end];
- }
-
- items.contextList.forEach(function fill_eachContext(context) {
- let nodes = context.cache.nodes;
- if (!nodes)
- return;
- haveCompletions = true;
-
- let root = nodes.root;
- let items = nodes.items;
- let [start, end, waiting] = getRows(context);
-
- if (context.message)
- nodes.message.textContent = context.message;
- nodes.message.style.display = context.message ? "block" : "none";
- nodes.waiting.style.display = waiting ? "block" : "none";
- nodes.up.style.opacity = "0";
- nodes.down.style.display = "none";
-
- for (let [i, row] in Iterator(context.getRows(start, end, doc)))
- nodes[i] = row;
- for (let [i, row] in util.Array.iteritems(nodes))
- {
- if (!row)
- continue;
- let display = (i >= start && i < end);
- if (display && row.parentNode != items)
- {
- do
- {
- var next = nodes[++i];
- if (next && next.parentNode != items)
- next = null;
- }
- while (!next && i < end)
- items.insertBefore(row, next);
- }
- else if (!display && row.parentNode == items)
- items.removeChild(row);
- }
- if (context.items.length == 0)
- return;
- nodes.up.style.opacity = (start == 0) ? "0" : "1";
- if (end != context.items.length)
- nodes.down.style.display = "block";
- else
- nodes.up.style.display = "block";
- });
-
- divNodes.noCompletions.style.display = haveCompletions ? "none" : "block";
-
- completionElements = util.evaluateXPath("//xhtml:div[@liberator:highlight='CompItem']", doc);
-
- return true;
- }
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// PUBLIC SECTION //////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- return {
-
- clear: function clear() { this.setItems(); doc.body.innerHTML = ""; },
- hide: function hide() { container.collapsed = true; },
- show: function show() { container.collapsed = false; },
- visible: function visible() !container.collapsed,
-
- reset: function ()
- {
- startIndex = endIndex = selIndex = -1;
- div = null;
- this.selectItem(-1);
- },
-
- // if @param selectedItem is given, show the list and select that item
- setItems: function setItems(newItems, selectedItem)
- {
- if (container.collapsed)
- minHeight = 0;
- startIndex = endIndex = selIndex = -1;
- items = newItems;
- this.reset();
- if (typeof selectedItem == "number")
- {
- this.selectItem(selectedItem);
- this.show();
- }
- },
-
- // select index, refill list if necessary
- selectItem: function selectItem(index)
- {
- //if (container.collapsed) // FIXME
- // return;
-
- //let now = Date.now();
-
- if (div == null)
- init();
-
- let sel = selIndex;
- let len = items.allItems.items.length;
- let newOffset = startIndex;
- let maxItems = options["maxitems"];
- let contextLines = Math.min(3, parseInt((maxItems - 1) / 2));
-
- if (index == -1 || index == null || index == len) // wrapped around
- {
- if (selIndex < 0)
- newOffset = 0;
- selIndex = -1;
- index = -1;
- }
- else
- {
- if (index <= startIndex + contextLines)
- newOffset = index - contextLines;
- if (index >= endIndex - contextLines)
- newOffset = index + contextLines - maxItems + 1;
-
- newOffset = Math.min(newOffset, len - maxItems);
- newOffset = Math.max(newOffset, 0);
-
- selIndex = index;
- }
-
- if (sel > -1)
- getCompletion(sel).removeAttribute("selected");
- fill(newOffset);
- if (index >= 0)
- {
- getCompletion(index).setAttribute("selected", "true");
- //getCompletion(index).scrollIntoView(false);
- }
-
- //if (index == 0)
- // this.start = now;
- //if (index == Math.min(len - 1, 100))
- // liberator.dump({ time: Date.now() - this.start });
- },
-
- onEvent: function onEvent(event) false
- };
- //}}}
-} //}}}
-
-function StatusLine() //{{{
-{
- ////////////////////////////////////////////////////////////////////////////////
- ////////////////////// PRIVATE SECTION /////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- var statusBar = document.getElementById("status-bar");
- statusBar.collapsed = true; // it is later restored unless the user sets laststatus=0
-
- // our status bar fields
- var statuslineWidget = document.getElementById("liberator-statusline");
- var urlWidget = document.getElementById("liberator-statusline-field-url");
- var inputBufferWidget = document.getElementById("liberator-statusline-field-inputbuffer");
- var progressWidget = document.getElementById("liberator-statusline-field-progress");
- var tabCountWidget = document.getElementById("liberator-statusline-field-tabcount");
- var bufferPositionWidget = document.getElementById("liberator-statusline-field-bufferposition");
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// OPTIONS /////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- options.add(["laststatus", "ls"],
- "Show the status line",
- "number", 2,
- {
- setter: function setter(value)
- {
- if (value == 0)
- document.getElementById("status-bar").collapsed = true;
- else if (value == 1)
- liberator.echoerr("show status line only with > 1 window not implemented yet");
- else
- document.getElementById("status-bar").collapsed = false;
-
- return value;
- },
- completer: function completer(context) [
- ["0", "Never display status line"],
- ["1", "Display status line only if there are multiple windows"],
- ["2", "Always display status line"]
- ],
- validator: Option.validateCompleter
- });
-
- /////////////////////////////////////////////////////////////////////////////}}}
- ////////////////////// PUBLIC SECTION //////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////{{{
-
- return {
-
- /**
- * Update the status bar to indicate how secure the website is:
- * extended - Secure connection with Extended Validation(EV) certificate.
- * secure - Secure connection with valid certificate.
- * broken - Secure connection with invalid certificate, or
- * mixed content.
- * insecure - Insecure connection.
- *
- * @param {'extended'|'secure'|'broken'|'insecure'} type
- */
- setClass: function setClass(type)
- {
- const highlightGroup = {
- extended: "StatusLineExtended",
- secure: "StatusLineSecure",
- broken: "StatusLineBroken",
- insecure: "StatusLine"
- };
-
- statusBar.setAttributeNS(NS.uri, "highlight", highlightGroup[type]);
- },
-
- // update all fields of the statusline
- update: function update()
- {
- this.updateUrl();
- this.updateInputBuffer();
- this.updateProgress();
- this.updateTabCount();
- this.updateBufferPosition();
- },
-
- /**
- * Update the URL displayed in the status line. Also displays status
- * icons, [+-♥], when there are next and previous pages in the
- * current tab's history, and when the current URL is bookmarked,
- * respectively.
- *
- * @param {string} url The URL to display.
- * @default buffer.URL
- */
- updateUrl: function updateUrl(url)
- {
- // ripped from Firefox; modified
- function losslessDecodeURI(url) {
- // 1. decodeURI decodes %25 to %, which creates unintended
- // encoding sequences.
- url = url.split("%25").map(decodeURI).join("%25");
- // 2. Re-encode whitespace so that it doesn't get eaten away
- // by the location bar (bug 410726).
- url = url.replace(/[\r\n\t]/g, encodeURIComponent);
-
- // Encode invisible characters (soft hyphen, zero-width space, BOM,
- // line and paragraph separator, word joiner, invisible times,
- // invisible separator, object replacement character) (bug 452979)
- url = url.replace(/[\v\x0c\x1c\x1d\x1e\x1f\u00ad\u200b\ufeff\u2028\u2029\u2060\u2062\u2063\ufffc]/g,
- encodeURIComponent);
-
- // Encode bidirectional formatting characters.
- // (RFC 3987 sections 3.2 and 4.1 paragraph 6)
- url = url.replace(/[\u200e\u200f\u202a\u202b\u202c\u202d\u202e]/g,
- encodeURIComponent);
- return url;
- };
-
- if (url == null)
- // TODO: this probably needs a more general solution.
- url = losslessDecodeURI(buffer.URL);
-
- // make it even more Vim-like
- if (url == "about:blank")
- {
- if (!buffer.title)
- url = "[No Name]";
- }
- else
- {
- url = url.replace(RegExp("^liberator://help/(\\S+)#(.*)"), function (m, n1, n2) n1 + " " + decodeURIComponent(n2) + " [Help]")
- .replace(RegExp("^liberator://help/(\\S+)"), "$1 [Help]");
- }
-
- // when session information is available, add [+] when we can go
- // backwards, [-] when we can go forwards
- let modified = "";
- if (window.getWebNavigation)
- {
- let sh = window.getWebNavigation().sessionHistory;
- if (sh && sh.index > 0)
- modified += "+";
- if (sh && sh.index < sh.count -1)
- modified += "-";
- }
- if (liberator.has("bookmarks"))
- {
- if (bookmarks.isBookmarked(buffer.URL))
- modified += "\u2764"; // a heart symbol: ❤
- //modified += "\u2665"; // a heart symbol: ♥
- }
-
- if (modified)
- url += " [" + modified + "]";
-
- urlWidget.value = url;
- },
-
- /**
- * Set the contents of the status line's input buffer to the given
- * string. Used primarily when a key press requires further input
- * before being processed, including mapping counts and arguments,
- * along with multi-key mappings.
- *
- * @param {string} buffer
- */
- updateInputBuffer: function updateInputBuffer(buffer)
- {
- if (!buffer || typeof buffer != "string")
- buffer = "";
-
- inputBufferWidget.value = buffer;
- },
-
- /**
- * Update the page load progress bar.
- *
- * @param {string|number} progress The current progress, as follows:
- * A string - Displayed literally.
- * A ratio 0 < n < 1 - Displayed as a progress bar.
- * A number n <= 0 - Displayed as a "Loading" message.
- * Any other number - The progress is cleared.
- */
- updateProgress: function updateProgress(progress)
- {
- if (!progress)
- progress = "";
-
- if (typeof progress == "string")
- progressWidget.value = progress;
- else if (typeof progress == "number")
- {
- let progressStr = "";
- if (progress <= 0)
- progressStr = "[ Loading... ]";
- else if (progress < 1)
- {
- progress = Math.floor(progress * 20);
- progressStr = "["
- + "====================".substr(0, progress)
- + ">"
- + " ".substr(0, 19 - progress)
- + "]";
- }
- progressWidget.value = progressStr;
- }
- },
-
- /**
- * Display the correct tabcount (e.g., [1/5]) on the status bar.
- *
- * @param {bool} delayed When true, update count after a
- * brief timeout. Useful in the many cases when an
- * event that triggers an update is broadcast before
- * the tab state is fully updated.
- */
- updateTabCount: function updateTabCount(delayed)
- {
- if (liberator.has("tabs"))
- {
- if (delayed)
- return void setTimeout(function () statusline.updateTabCount(false), 0);
-
- // update the ordinal which is used for numbered tabs
- if (options.get("guioptions").has("n", "N"))
- for (let [i, tab] in util.Array.iteritems(getBrowser().mTabs))
- tab.setAttribute("ordinal", i + 1);
-
- tabCountWidget.value = "[" + (tabs.index() + 1) + "/" + tabs.count + "]";
- }
- },
-
- /**
- * Display the main content's vertical scroll position in the status
- * bar.
- *
- * @param {number} percent The position, as a percentage. @optional
- */
- updateBufferPosition: function updateBufferPosition(percent)
- {
- if (!percent || typeof percent != "number")
- {
- let win = document.commandDispatcher.focusedWindow;
- if (!win)
- return;
- percent = win.scrollMaxY == 0 ? -1 : win.scrollY / win.scrollMaxY;
- }
-
- let bufferPositionStr = "";
- percent = Math.round(percent * 100);
- if (percent < 0)
- bufferPositionStr = "All";
- else if (percent == 0)
- bufferPositionStr = "Top";
- else if (percent < 10)
- bufferPositionStr = " " + percent + "%";
- else if (percent >= 100)
- bufferPositionStr = "Bot";
- else
- bufferPositionStr = percent + "%";
-
- bufferPositionWidget.value = bufferPositionStr;
- }
-
- };
- //}}}
-} //}}}
-
-// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/util.js b/common/content/util.js
index baa2c501..b3c26ff8 100644
--- a/common/content/util.js
+++ b/common/content/util.js
@@ -10,7 +10,11 @@ const XUL = Namespace("xul", "http://www.mozilla.org/keymaster/gatekeeper/there.
const NS = Namespace("liberator", "http://vimperator.org/namespaces/liberator");
default xml namespace = XHTML;
-const util = { //{{{
+const Util = Module("util", {
+ init: function () {
+ this.Array = Util.Array;
+ },
+
/**
* Returns true if its argument is an Array object, regardless
* of which context it comes from.
@@ -25,8 +29,7 @@ const util = { //{{{
* @param {Object} obj
* @returns {Object}
*/
- cloneObject: function cloneObject(obj)
- {
+ cloneObject: function cloneObject(obj) {
if (obj instanceof Array)
return obj.slice();
let newObj = {};
@@ -43,8 +46,7 @@ const util = { //{{{
* @param {number} length The length of the returned string.
* @returns {string}
*/
- clip: function clip(str, length)
- {
+ clip: function clip(str, length) {
return str.length <= length ? str : str.substr(0, length - 3) + "...";
},
@@ -64,8 +66,7 @@ const util = { //{{{
* @param {Node} node
* @returns {Object}
*/
- computedStyle: function computedStyle(node)
- {
+ computedStyle: function computedStyle(node) {
while (node instanceof Text && node.parentNode)
node = node.parentNode;
return node.ownerDocument.defaultView.getComputedStyle(node, null);
@@ -78,8 +79,7 @@ const util = { //{{{
* @param {string} str
* @param {boolean} verbose
*/
- copyToClipboard: function copyToClipboard(str, verbose)
- {
+ copyToClipboard: function copyToClipboard(str, verbose) {
const clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
clipboardHelper.copyString(str);
@@ -94,8 +94,7 @@ const util = { //{{{
* @returns {Object}
*/
// FIXME: newURI needed too?
- createURI: function createURI(str)
- {
+ createURI: function createURI(str) {
const fixup = Cc["@mozilla.org/docshell/urifixup;1"].getService(Ci.nsIURIFixup);
return fixup.createFixupURI(str, fixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP);
},
@@ -107,8 +106,7 @@ const util = { //{{{
* @param {string} str
* @returns {string}
*/
- escapeHTML: function escapeHTML(str)
- {
+ escapeHTML: function escapeHTML(str) {
// XXX: the following code is _much_ slower than a simple .replace()
// :history display went down from 2 to 1 second after changing
//
@@ -124,32 +122,28 @@ const util = { //{{{
* @param {string} str
* @returns {string}
*/
- escapeRegex: function escapeRegex(str)
- {
+ escapeRegex: function escapeRegex(str) {
return str.replace(/([\\{}()[\].?*+])/g, "\\$1");
},
/**
* Escapes quotes, newline and tab characters in <b>str</b>. The returned
* string is delimited by <b>delimiter</b> or " if <b>delimiter</b> is not
- * specified.
+ * specified. {@see String#quote}.
*
* @param {string} str
* @param {string} delimiter
* @returns {string}
*/
- escapeString: function escapeString(str, delimiter)
- {
+ escapeString: function escapeString(str, delimiter) {
if (delimiter == undefined)
delimiter = '"';
return delimiter + str.replace(/([\\'"])/g, "\\$1").replace("\n", "\\n", "g").replace("\t", "\\t", "g") + delimiter;
},
- extend: function extend(dest)
- {
+ extend: function extend(dest) {
Array.slice(arguments, 1).filter(util.identity).forEach(function (src) {
- for (let [k, v] in Iterator(src))
- {
+ for (let [k, v] in Iterator(src)) {
let get = src.__lookupGetter__(k),
set = src.__lookupSetter__(k);
if (!get && !set)
@@ -171,8 +165,7 @@ const util = { //{{{
* @param nodes {Array(string)}
* @returns {string}
*/
- makeXPath: function makeXPath(nodes)
- {
+ makeXPath: function makeXPath(nodes) {
return util.Array(nodes).map(function (node) [node, "xhtml:" + node]).flatten()
.map(function (node) "//" + node).join(" | ");
},
@@ -187,8 +180,7 @@ const util = { //{{{
* passed as the first argument, <b>key</b> as the
* second.
*/
- memoize: function memoize(obj, key, getter)
- {
+ memoize: function memoize(obj, key, getter) {
obj.__defineGetter__(key, function () {
delete obj[key];
obj[key] = getter(obj, key);
@@ -209,14 +201,12 @@ const util = { //{{{
* @param {string} str
* @param {RegExp} marker
*/
- splitLiteral: function splitLiteral(str, marker)
- {
+ splitLiteral: function splitLiteral(str, marker) {
let results = [];
let resep = RegExp(/^(([^\\'"]|\\.|'([^\\']|\\.)*'|"([^\\"]|\\.)*")*?)/.source + marker.source);
let cont = true;
- while (cont)
- {
+ while (cont) {
cont = false;
str = str.replace(resep, function (match, before) {
results.push(before);
@@ -238,17 +228,14 @@ const util = { //{{{
* @param {boolean} humanReadable Use byte multiples.
* @returns {string}
*/
- formatBytes: function formatBytes(bytes, decimalPlaces, humanReadable)
- {
+ formatBytes: function formatBytes(bytes, decimalPlaces, humanReadable) {
const unitVal = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
let unitIndex = 0;
let tmpNum = parseInt(bytes, 10) || 0;
let strNum = [tmpNum + ""];
- if (humanReadable)
- {
- while (tmpNum >= 1024)
- {
+ if (humanReadable) {
+ while (tmpNum >= 1024) {
tmpNum /= 1024;
if (++unitIndex > (unitVal.length - 1))
break;
@@ -273,53 +260,7 @@ const util = { //{{{
return strNum[0] + " " + unitVal[unitIndex];
},
- /**
- * Generates an Asciidoc help entry.
- *
- * @param {Command|Map|Option} obj A liberator <b>Command</b>,
- * <b>Map</b> or <b>Option</b> object
- * @param {XMLList} extraHelp Extra help text beyond the description.
- * @returns {string}
- */
- generateHelp: function generateHelp(obj, extraHelp)
- {
- let spec = util.identity;
- let tag = util.identity;
- if (obj instanceof Command)
- tag = spec = function (cmd) <>:{cmd}</>;
- else if (obj instanceof Map && obj.count)
- spec = function (map) <><oa xmlns="">count</oa>{map}</>;
- else if (obj instanceof Option)
- {
- spec = function (opt) <o xmlns="">{opt}</o>;
- tag = function (opt) <>'{opt}'</>;
- }
-
- // E4X has its warts.
- let br = <>
- </>;
-
- default xml namespace = "";
- XML.prettyPrinting = false;
- XML.ignoreWhitespace = false;
-
- return <></> +
- <item>
- <tags>{template.map(obj.names, tag, " ")}</tags>
- <spec>{spec((obj.specs || obj.names)[0])}</spec>{
- !obj.type ? "" : <>
- <type>{obj.type}</type>
- <default>{obj.defaultValue}</default></>}
- <description>{
- obj.description ? br+<p>{obj.description.replace(/\.?$/, ".")}</p> : "" }{
- extraHelp ? br+extraHelp : "" }{
- !(extraHelp || obj.description) ? br+<p>Sorry, no help available.</p> : "" }
- </description>
- </item>.toXMLString();
- },
-
- exportHelp: function (path)
- {
+ exportHelp: function (path) {
const FILE = io.File(path);
const PATH = FILE.leafName.replace(/\..*/, "") + "/";
const TIME = Date.now();
@@ -337,8 +278,7 @@ const util = { //{{{
.split(" ").map(Array.concat));
let chrome = {};
- for (let [file,] in Iterator(services.get("liberator:").FILE_MAP))
- {
+ for (let [file,] in Iterator(services.get("liberator:").FILE_MAP)) {
liberator.open("liberator://help/" + file);
events.waitForPageLoad();
let data = [
@@ -346,10 +286,8 @@ const util = { //{{{
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"\n',
' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n'
];
- function fix(node)
- {
- switch(node.nodeType)
- {
+ function fix(node) {
+ switch(node.nodeType) {
case Node.ELEMENT_NODE:
if (node instanceof HTMLScriptElement)
return;
@@ -358,22 +296,18 @@ const util = { //{{{
if (node instanceof HTMLHtmlElement)
data.push(' xmlns="' + XHTML.uri + '"');
- for (let { name: name, value: value } in util.Array.itervalues(node.attributes))
- {
- if (name == "liberator:highlight")
- {
+ for (let { name: name, value: value } in util.Array.itervalues(node.attributes)) {
+ if (name == "liberator:highlight") {
name = "class";
value = "hl-" + value;
}
- if (name == "href")
- {
+ if (name == "href") {
if (value.indexOf("liberator://help-tag/") == 0)
value = services.get("io").newChannel(value, null, null).originalURI.path.substr(1);
if (!/[#\/]/.test(value))
value += ".xhtml";
}
- if (name == "src" && value.indexOf(":") > 0)
- {
+ if (name == "src" && value.indexOf(":") > 0) {
chrome[value] = value.replace(/.*\//, "");;
value = value.replace(/.*\//, "");
}
@@ -385,8 +319,7 @@ const util = { //{{{
}
if (node.localName in empty)
data.push(" />");
- else
- {
+ else {
data.push(">");
if (node instanceof HTMLHeadElement)
data.push(<link rel="stylesheet" type="text/css" href="help.css"/>.toXMLString());
@@ -429,14 +362,11 @@ const util = { //{{{
* @param {function(XMLHttpRequest)} callback
* @returns {XMLHttpRequest}
*/
- httpGet: function httpGet(url, callback)
- {
- try
- {
+ httpGet: function httpGet(url, callback) {
+ try {
let xmlhttp = new XMLHttpRequest();
xmlhttp.mozBackgroundRequest = true;
- if (callback)
- {
+ if (callback) {
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState == 4)
callback(xmlhttp);
@@ -446,8 +376,7 @@ const util = { //{{{
xmlhttp.send(null);
return xmlhttp;
}
- catch (e)
- {
+ catch (e) {
liberator.log("Error opening " + url + ": " + e, 1);
}
},
@@ -465,8 +394,7 @@ const util = { //{{{
* @param {boolean} asIterator Whether to return the results as an
* XPath iterator.
*/
- evaluateXPath: function (expression, doc, elem, asIterator)
- {
+ evaluateXPath: function (expression, doc, elem, asIterator) {
if (!doc)
doc = window.content.document;
if (!elem)
@@ -475,11 +403,11 @@ const util = { //{{{
expression = util.makeXPath(expression);
let result = doc.evaluate(expression, elem,
- function lookupNamespaceURI(prefix)
- {
+ function lookupNamespaceURI(prefix) {
return {
xhtml: "http://www.w3.org/1999/xhtml",
xhtml2: "http://www.w3.org/2002/06/xhtml2",
+ liberator: NS.uri,
liberator: NS.uri
}[prefix] || null;
},
@@ -526,8 +454,7 @@ const util = { //{{{
* @param {function} func
* @returns {Array}
*/
- map: function map(obj, func)
- {
+ map: function map(obj, func) {
let ary = [];
for (let i in Iterator(obj))
ary.push(func(i));
@@ -558,8 +485,7 @@ const util = { //{{{
* @returns {nsIURI}
*/
// FIXME: createURI needed too?
- newURI: function (uri)
- {
+ newURI: function (uri) {
return services.get("io").newURI(uri, null, null);
},
@@ -571,10 +497,9 @@ const util = { //{{{
* @param {boolean} color Whether the output should be colored.
* @returns {string}
*/
- objectToString: function objectToString(object, color)
- {
+ objectToString: function objectToString(object, color) {
// Use E4X literals so html is automatically quoted
- // only when it's asked for. Noone wants to see &lt;
+ // only when it's asked for. No one wants to see &lt;
// on their console or :map :foo in their buffer
// when they expect :map <C-f> :foo.
XML.prettyPrinting = false;
@@ -591,20 +516,17 @@ const util = { //{{{
[XHTML, 'html'],
[XUL, 'xul']
]);
- if (object instanceof Element)
- {
+ if (object instanceof Element) {
let elem = object;
if (elem.nodeType == elem.TEXT_NODE)
return elem.data;
- function namespaced(node)
- {
+ function namespaced(node) {
var ns = NAMESPACES[node.namespaceURI];
if (ns)
return ns + ":" + node.localName;
return node.localName.toLowerCase();
}
- try
- {
+ try {
let tag = "<" + [namespaced(elem)].concat(
[namespaced(a) + "=" + template.highlight(a.value, true)
for ([i, a] in util.Array.iteritems(elem.attributes))]).join(" ");
@@ -615,42 +537,34 @@ const util = { //{{{
tag += '>...</' + namespaced(elem) + '>';
return tag;
}
- catch (e)
- {
+ catch (e) {
return {}.toString.call(elem);
}
}
- try
- { // for window.JSON
+ try { // for window.JSON
var obj = String(object);
}
- catch (e)
- {
+ catch (e) {
obj = "[Object]";
}
obj = template.highlightFilter(util.clip(obj, 150), "\n", !color ? function () "^J" : function () <span highlight="NonText">^J</span>);
let string = <><span highlight="Title Object">{obj}</span>::<br/>&#xa;</>;
let keys = [];
- try // window.content often does not want to be queried with "var i in object"
- {
+ try { // window.content often does not want to be queried with "var i in object"
let hasValue = !("__iterator__" in object);
- if (modules.isPrototypeOf(object))
- {
+ if (modules.isPrototypeOf(object)) {
object = Iterator(object);
hasValue = false;
}
- for (let i in object)
- {
+ for (let i in object) {
let value = <![CDATA[<no value>]]>;
- try
- {
+ try {
value = object[i];
}
catch (e) {}
- if (!hasValue)
- {
+ if (!hasValue) {
if (i instanceof Array && i.length == 2)
[i, value] = i;
else
@@ -668,8 +582,7 @@ const util = { //{{{
}
catch (e) {}
- function compare(a, b)
- {
+ function compare(a, b) {
if (!isNaN(a[0]) && !isNaN(b[0]))
return a[0] - b[0];
return String.localeCompare(a[0], b[0]);
@@ -688,17 +601,14 @@ const util = { //{{{
* negative. @default 1
* @returns {Iterator(Object)}
*/
- range: function range(start, end, step)
- {
+ range: function range(start, end, step) {
if (!step)
step = 1;
- if (step > 0)
- {
+ if (step > 0) {
for (; start < end; start += step)
yield start;
}
- else
- {
+ else {
while (start > end)
yield start += step;
}
@@ -713,13 +623,10 @@ const util = { //{{{
* @param {number} time The time in milliseconds between thread yields.
* @returns {Iterator(Object)}
*/
- interruptibleRange: function interruptibleRange(start, end, time)
- {
+ interruptibleRange: function interruptibleRange(start, end, time) {
let endTime = Date.now() + time;
- while (start < end)
- {
- if (Date.now() > endTime)
- {
+ while (start < end) {
+ if (Date.now() > endTime) {
liberator.threadYield(true, true);
endTime = Date.now() + time;
}
@@ -735,12 +642,10 @@ const util = { //{{{
*
* @returns {string}
*/
- readFromClipboard: function readFromClipboard()
- {
+ readFromClipboard: function readFromClipboard() {
let str;
- try
- {
+ try {
const clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
const transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
@@ -756,8 +661,7 @@ const util = { //{{{
transferable.getTransferData("text/unicode", data, dataLen);
- if (data)
- {
+ if (data) {
data = data.value.QueryInterface(Ci.nsISupportsString);
str = data.data.substring(0, dataLen.value / 2);
}
@@ -776,8 +680,7 @@ const util = { //{{{
* @param {string} str
* @returns {string[]}
*/
- stringToURLArray: function stringToURLArray(str)
- {
+ stringToURLArray: function stringToURLArray(str) {
let urls;
if (options["urlseparator"])
@@ -786,8 +689,7 @@ const util = { //{{{
urls = [str];
return urls.map(function (url) {
- try
- {
+ try {
// Try to find a matching file.
let file = io.File(url);
if (file.exists() && file.isReadable())
@@ -834,197 +736,129 @@ const util = { //{{{
* stored here, keyed to the value thereof.
* @returns {Node}
*/
- xmlToDom: function xmlToDom(node, doc, nodes)
- {
+ xmlToDom: function xmlToDom(node, doc, nodes) {
XML.prettyPrinting = false;
- if (node.length() != 1)
- {
+ if (node.length() != 1) {
let domnode = doc.createDocumentFragment();
for each (let child in node)
domnode.appendChild(arguments.callee(child, doc, nodes));
return domnode;
}
- switch (node.nodeKind())
- {
- case "text":
- return doc.createTextNode(node);
- case "element":
- let domnode = doc.createElementNS(node.namespace(), node.localName());
- for each (let attr in node.@*)
- domnode.setAttributeNS(attr.name() == "highlight" ? NS.uri : attr.namespace(), attr.name(), String(attr));
- for each (let child in node.*)
- domnode.appendChild(arguments.callee(child, doc, nodes));
- if (nodes && node.@key)
- nodes[node.@key] = domnode;
- return domnode;
+ switch (node.nodeKind()) {
+ case "text":
+ return doc.createTextNode(node);
+ case "element":
+ let domnode = doc.createElementNS(node.namespace(), node.localName());
+ for each (let attr in node.@*)
+ domnode.setAttributeNS(attr.name() == "highlight" ? NS.uri : attr.namespace(), attr.name(), String(attr));
+ for each (let child in node.*)
+ domnode.appendChild(arguments.callee(child, doc, nodes));
+ if (nodes && node.@key)
+ nodes[node.@key] = domnode;
+ return domnode;
}
}
-}; //}}}
-
-// TODO: Why don't we just push all util.BuiltinType up into modules? --djk
-/**
- * Array utility methods.
- */
-util.Array = function Array_(ary) {
- var obj = {
- __proto__: ary,
- __iterator__: function () this.iteritems(),
- __noSuchMethod__: function (meth, args)
- {
- let res = (util.Array[meth] || Array[meth]).apply(null, [this.__proto__].concat(args));
- if (util.Array.isinstance(res))
- return util.Array(res);
- return res;
+}, {
+ // TODO: Why don't we just push all util.BuiltinType up into modules? --djk
+ /**
+ * Array utility methods.
+ */
+ Array: Class("Array", {
+ init: function (ary) {
+ return {
+ __proto__: ary,
+ __iterator__: function () this.iteritems(),
+ __noSuchMethod__: function (meth, args) {
+ let res = (util.Array[meth] || Array[meth]).apply(null, [this.__proto__].concat(args));
+ if (util.Array.isinstance(res))
+ return util.Array(res);
+ return res;
+ },
+ concat: function () [].concat.apply(this.__proto__, arguments),
+ map: function () this.__noSuchMethod__("map", Array.slice(arguments))
+ };
+ },
+ }, {
+ isinstance: function isinstance(obj) {
+ return Object.prototype.toString.call(obj) == "[object Array]";
+ },
+ /**
+ * Converts an array to an object. As in lisp, an assoc is an
+ * array of key-value pairs, which maps directly to an object,
+ * as such:
+ * [["a", "b"], ["c", "d"]] -> { a: "b", c: "d" }
+ *
+ * @param {Array[]} assoc
+ * @... {string} 0 - Key
+ * @... 1 - Value
+ */
+ toObject: function toObject(assoc) {
+ let obj = {};
+ assoc.forEach(function ([k, v]) { obj[k] = v; });
+ return obj;
},
- concat: function () [].concat.apply(this.__proto__, arguments),
- map: function () this.__noSuchMethod__("map", Array.slice(arguments))
- };
- return obj;
-}
-util.Array.isinstance = function isinstance(obj) {
- return Object.prototype.toString.call(obj) == "[object Array]";
-};
-/**
- * Converts an array to an object. As in lisp, an assoc is an
- * array of key-value pairs, which maps directly to an object,
- * as such:
- * [["a", "b"], ["c", "d"]] -> { a: "b", c: "d" }
- *
- * @param {Array[]} assoc
- * @... {string} 0 - Key
- * @... 1 - Value
- */
-util.Array.toObject = function toObject(assoc)
-{
- let obj = {};
- assoc.forEach(function ([k, v]) { obj[k] = v; });
- return obj;
-};
-
-/**
- * Flattens an array, such that all elements of the array are
- * joined into a single array:
- * [["foo", ["bar"]], ["baz"], "quux"] -> ["foo", ["bar"], "baz", "quux"]
- *
- * @param {Array} ary
- * @returns {Array}
- */
-util.Array.flatten = function flatten(ary) Array.concat.apply([], ary),
-
-/**
- * Returns an Iterator for an array's values.
- *
- * @param {Array} ary
- * @returns {Iterator(Object)}
- */
-util.Array.itervalues = function itervalues(ary)
-{
- let length = ary.length;
- for (let i = 0; i < length; i++)
- yield ary[i];
-};
-
-/**
- * Returns an Iterator for an array's indices and values.
- *
- * @param {Array} ary
- * @returns {Iterator([{number}, {Object}])}
- */
-util.Array.iteritems = function iteritems(ary)
-{
- let length = ary.length;
- for (let i = 0; i < length; i++)
- yield [i, ary[i]];
-};
-
-/**
- * Filters out all duplicates from an array. If
- * <b>unsorted</b> is false, the array is sorted before
- * duplicates are removed.
- *
- * @param {Array} ary
- * @param {boolean} unsorted
- * @returns {Array}
- */
-util.Array.uniq = function uniq(ary, unsorted)
-{
- let ret = [];
- if (unsorted)
- {
- for (let [, item] in Iterator(ary))
- if (ret.indexOf(item) == -1)
- ret.push(item);
- }
- else
- {
- for (let [, item] in Iterator(ary.sort()))
- {
- if (item != last || !ret.length)
- ret.push(item);
- var last = item;
- }
- }
- return ret;
-};
-
-function Struct()
-{
- let self = this instanceof Struct ? this : new Struct();
- if (!arguments.length)
- return self;
-
- let args = Array.slice(arguments);
- self.__defineGetter__("length", function () args.length);
- self.__defineGetter__("members", function () args.slice());
- for (let arg in Iterator(args))
- {
- let [i, name] = arg;
- self.__defineGetter__(name, function () this[i]);
- self.__defineSetter__(name, function (val) { this[i] = val; });
- }
- function ConStructor()
- {
- let self = this instanceof arguments.callee ? this : new arguments.callee();
- //for (let [k, v] in Iterator(Array.slice(arguments))) // That is makes using struct twice as slow as the following code:
- for (let i = 0; i < arguments.length; i++)
- {
- if (arguments[i] != undefined)
- self[i] = arguments[i];
- }
- return self;
- }
- ConStructor.prototype = self;
- ConStructor.defaultValue = function (key, val)
- {
- let i = args.indexOf(key);
- ConStructor.prototype.__defineGetter__(i, function () (this[i] = val.call(this), this[i])); // Kludge for FF 3.0
- ConStructor.prototype.__defineSetter__(i, function (val) {
- let value = val;
- this.__defineGetter__(i, function () value);
- this.__defineSetter__(i, function (val) { value = val });
- });
- };
- return self.constructor = ConStructor;
-}
-
-Struct.prototype = {
- clone: function clone()
- {
- return this.constructor.apply(null, this.slice());
- },
- // Iterator over our named members
- __iterator__: function ()
- {
- let self = this;
- return ([v, self[v]] for ([k, v] in Iterator(self.members)))
- }
-};
+ /**
+ * Flattens an array, such that all elements of the array are
+ * joined into a single array:
+ * [["foo", ["bar"]], ["baz"], "quux"] -> ["foo", ["bar"], "baz", "quux"]
+ *
+ * @param {Array} ary
+ * @returns {Array}
+ */
+ flatten: function flatten(ary) Array.concat.apply([], ary),
+
+ /**
+ * Returns an Iterator for an array's values.
+ *
+ * @param {Array} ary
+ * @returns {Iterator(Object)}
+ */
+ itervalues: function itervalues(ary) {
+ let length = ary.length;
+ for (let i = 0; i < length; i++)
+ yield ary[i];
+ },
+
+ /**
+ * Returns an Iterator for an array's indices and values.
+ *
+ * @param {Array} ary
+ * @returns {Iterator([{number}, {Object}])}
+ */
+ iteritems: function iteritems(ary) {
+ let length = ary.length;
+ for (let i = 0; i < length; i++)
+ yield [i, ary[i]];
+ },
+
+ /**
+ * Filters out all duplicates from an array. If
+ * <b>unsorted</b> is false, the array is sorted before
+ * duplicates are removed.
+ *
+ * @param {Array} ary
+ * @param {boolean} unsorted
+ * @returns {Array}
+ */
+ uniq: function uniq(ary, unsorted) {
+ let ret = [];
+ if (unsorted) {
+ for (let [, item] in Iterator(ary))
+ if (ret.indexOf(item) == -1)
+ ret.push(item);
+ }
+ else {
+ for (let [, item] in Iterator(ary.sort())) {
+ if (item != last || !ret.length)
+ ret.push(item);
+ var last = item;
+ }
+ }
+ return ret;
+ },
+ }),
+});
-// Add no-sideeffect array methods. Can't set new Array() as the prototype or
-// get length() won't work.
-for (let [, k] in Iterator(["concat", "every", "filter", "forEach", "indexOf", "join", "lastIndexOf",
- "map", "reduce", "reduceRight", "reverse", "slice", "some", "sort"]))
- Struct.prototype[k] = Array.prototype[k];
// vim: set fdm=marker sw=4 ts=4 et: