Skip to content

Instantly share code, notes, and snippets.

@kmaglione
Created December 22, 2011 06:13
Show Gist options
  • Save kmaglione/1509197 to your computer and use it in GitHub Desktop.
Save kmaglione/1509197 to your computer and use it in GitHub Desktop.
Pentadactyl 1.0rc1 code changes
This file has been truncated, but you can view the full file.
diff --git a/common/bootstrap.js b/common/bootstrap.js
--- a/common/bootstrap.js
+++ b/common/bootstrap.js
@@ -4,52 +4,47 @@
// given in the LICENSE.txt file included with this file.
//
// See https://wiki.mozilla.org/Extension_Manager:Bootstrapped_Extensions
// for details.
const NAME = "bootstrap";
const global = this;
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cu = Components.utils;
-const Cr = Components.results;
+var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
function module(uri) {
let obj = {};
Cu.import(uri, obj);
return obj;
}
const { AddonManager } = module("resource://gre/modules/AddonManager.jsm");
const { XPCOMUtils } = module("resource://gre/modules/XPCOMUtils.jsm");
const { Services } = module("resource://gre/modules/Services.jsm");
const resourceProto = Services.io.getProtocolHandler("resource")
.QueryInterface(Ci.nsIResProtocolHandler);
const categoryManager = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
const manager = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+const DISABLE_ACR = "resource://dactyl-content/disable-acr.jsm";
+const BOOTSTRAP_JSM = "resource://dactyl/bootstrap.jsm";
const BOOTSTRAP_CONTRACT = "@dactyl.googlecode.com/base/bootstrap";
-JSMLoader = JSMLoader || BOOTSTRAP_CONTRACT in Cc && Cc[BOOTSTRAP_CONTRACT].getService().wrappedJSObject.loader;
-
-var JSMLoader = BOOTSTRAP_CONTRACT in Components.classes &&
- Components.classes[BOOTSTRAP_CONTRACT].getService().wrappedJSObject.loader;
-
-// Temporary migration code.
-if (!JSMLoader && "@mozilla.org/fuel/application;1" in Components.classes)
- JSMLoader = Components.classes["@mozilla.org/fuel/application;1"]
- .getService(Components.interfaces.extIApplication)
- .storage.get("dactyl.JSMLoader", null);
+
+var JSMLoader = BOOTSTRAP_CONTRACT in Cc && Cc[BOOTSTRAP_CONTRACT].getService().wrappedJSObject.loader;
+var name = "dactyl";
function reportError(e) {
- dump("\ndactyl: bootstrap: " + e + "\n" + (e.stack || Error().stack) + "\n");
+ dump("\n" + name + ": bootstrap: " + e + "\n" + (e.stack || Error().stack) + "\n");
Cu.reportError(e);
}
+function debug(msg) {
+ dump(name + ": " + msg + "\n");
+}
function httpGet(url) {
let xmlhttp = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
xmlhttp.overrideMimeType("text/plain");
xmlhttp.open("GET", url, false);
xmlhttp.send(null);
return xmlhttp;
}
@@ -58,97 +53,126 @@ let initialized = false;
let addon = null;
let addonData = null;
let basePath = null;
let categories = [];
let components = {};
let resources = [];
let getURI = null;
+function updateLoader() {
+ try {
+ JSMLoader.loader = Cc["@dactyl.googlecode.com/extra/utils"].getService(Ci.dactylIUtils);
+ }
+ catch (e) {};
+}
+
+/**
+ * Performs necessary migrations after a version change.
+ */
function updateVersion() {
try {
function isDev(ver) /^hg|pre$/.test(ver);
if (typeof require === "undefined" || addon === addonData)
return;
require(global, "config");
require(global, "prefs");
config.lastVersion = localPrefs.get("lastVersion", null);
localPrefs.set("lastVersion", addon.version);
+ // We're switching from a nightly version to a stable or
+ // semi-stable version or vice versa.
+ //
+ // Disable automatic updates when switching to nightlies,
+ // restore the default action when switching to stable.
if (!config.lastVersion || isDev(config.lastVersion) != isDev(addon.version))
addon.applyBackgroundUpdates = AddonManager[isDev(addon.version) ? "AUTOUPDATE_DISABLE" : "AUTOUPDATE_DEFAULT"];
}
catch (e) {
reportError(e);
}
}
function startup(data, reason) {
- dump("dactyl: bootstrap: startup " + reasonToString(reason) + "\n");
+ debug("bootstrap: startup " + reasonToString(reason));
basePath = data.installPath;
if (!initialized) {
initialized = true;
- dump("dactyl: bootstrap: init" + " " + data.id + "\n");
+ debug("bootstrap: init" + " " + data.id);
addonData = data;
addon = data;
+ name = data.id.replace(/@.*/, "");
AddonManager.getAddonByID(addon.id, function (a) {
addon = a;
+
+ updateLoader();
updateVersion();
+ if (typeof require !== "undefined")
+ require(global, "main");
});
if (basePath.isDirectory())
getURI = function getURI(path) {
let uri = Services.io.newFileURI(basePath);
uri.path += path;
return Services.io.newFileURI(uri.QueryInterface(Ci.nsIFileURL).file);
};
else
getURI = function getURI(path)
- Services.io.newURI("jar:" + Services.io.newFileURI(basePath).spec + "!/" + path, null, null);
+ Services.io.newURI("jar:" + Services.io.newFileURI(basePath).spec.replace(/!/g, "%21") + "!" +
+ "/" + path, null, null);
try {
init();
}
catch (e) {
reportError(e);
}
}
}
+/**
+ * An XPCOM class factory proxy. Loads the JavaScript module at *url*
+ * when an instance is to be created and calls its NSGetFactory method
+ * to obtain the actual factory.
+ *
+ * @param {string} url The URL of the module housing the real factory.
+ * @param {string} classID The CID of the class this factory represents.
+ */
function FactoryProxy(url, classID) {
this.url = url;
this.classID = Components.ID(classID);
}
FactoryProxy.prototype = {
QueryInterface: XPCOMUtils.generateQI(Ci.nsIFactory),
register: function () {
- dump("dactyl: bootstrap: register: " + this.classID + " " + this.contractID + "\n");
+ debug("bootstrap: register: " + this.classID + " " + this.contractID);
JSMLoader.registerFactory(this);
},
get module() {
- dump("dactyl: bootstrap: create module: " + this.contractID + "\n");
+ debug("bootstrap: create module: " + this.contractID);
Object.defineProperty(this, "module", { value: {}, enumerable: true });
JSMLoader.load(this.url, this.module);
return this.module;
},
createInstance: function (iids) {
return let (factory = this.module.NSGetFactory(this.classID))
factory.createInstance.apply(factory, arguments);
}
}
function init() {
- dump("dactyl: bootstrap: init\n");
+ debug("bootstrap: init");
let manifestURI = getURI("chrome.manifest");
let manifest = httpGet(manifestURI.spec)
.responseText
.replace(/^\s*|\s*$|#.*/g, "")
.replace(/^\s*\n/gm, "");
let suffix = "-";
@@ -178,69 +202,91 @@ function init() {
resourceProto.setSubstitution(fields[1] + suffix, getURI(fields[2]));
}
}
// Flush the cache if necessary, just to be paranoid
let pref = "extensions.dactyl.cacheFlushCheck";
let val = addon.version + "-" + hardSuffix;
if (!Services.prefs.prefHasUserValue(pref) || Services.prefs.getCharPref(pref) != val) {
+ var cacheFlush = true;
Services.obs.notifyObservers(null, "startupcache-invalidate", "");
Services.prefs.setCharPref(pref, val);
}
try {
- module("resource://dactyl-content/disable-acr.jsm").init(addon.id);
+ module(DISABLE_ACR).init(addon.id);
}
catch (e) {
reportError(e);
}
- if (JSMLoader && JSMLoader.bump !== 4) // Temporary hack
+ if (JSMLoader) {
+ // Temporary hacks until platforms and dactyl releases that don't
+ // support Cu.unload are phased out.
+ if (Cu.unload) {
+ // Upgrading from dactyl release without Cu.unload support.
+ Cu.unload(BOOTSTRAP_JSM);
+ for (let [name] in Iterator(JSMLoader.globals))
+ Cu.unload(~name.indexOf(":") ? name : "resource://dactyl" + JSMLoader.suffix + "/" + name);
+ }
+ else if (JSMLoader.bump != 6) {
+ // We're in a version without Cu.unload support and the
+ // JSMLoader interface has changed. Bump off the old one.
Services.scriptloader.loadSubScript("resource://dactyl" + suffix + "/bootstrap.jsm",
- Cu.import("resource://dactyl/bootstrap.jsm", global));
-
- if (!JSMLoader || JSMLoader.bump !== 4)
- Cu.import("resource://dactyl/bootstrap.jsm", global);
-
+ Cu.import(BOOTSTRAP_JSM, global));
+ }
+ }
+
+ if (!JSMLoader || JSMLoader.bump !== 6 || Cu.unload)
+ Cu.import(BOOTSTRAP_JSM, global);
+
+ JSMLoader.name = name;
JSMLoader.bootstrap = this;
- JSMLoader.load("resource://dactyl/bootstrap.jsm", global);
+ JSMLoader.load(BOOTSTRAP_JSM, global);
JSMLoader.init(suffix);
+ JSMLoader.cacheFlush = cacheFlush;
JSMLoader.load("base.jsm", global);
- if (!(BOOTSTRAP_CONTRACT in Cc))
- manager.registerFactory(Components.ID("{f541c8b0-fe26-4621-a30b-e77d21721fb5}"),
- String("{f541c8b0-fe26-4621-a30b-e77d21721fb5}"),
- BOOTSTRAP_CONTRACT, {
- QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory]),
- instance: {
- QueryInterface: XPCOMUtils.generateQI([]),
- contractID: BOOTSTRAP_CONTRACT,
- wrappedJSObject: {}
- },
- createInstance: function () this.instance
- });
+ if (!(BOOTSTRAP_CONTRACT in Cc)) {
+ // Use Sandbox to prevent closures over this scope
+ let sandbox = Cu.Sandbox(Cc["@mozilla.org/systemprincipal;1"].getService());
+ let factory = Cu.evalInSandbox("({ createInstance: function () this })", sandbox);
+
+ factory.classID = Components.ID("{f541c8b0-fe26-4621-a30b-e77d21721fb5}");
+ factory.contractID = BOOTSTRAP_CONTRACT;
+ factory.QueryInterface = XPCOMUtils.generateQI([Ci.nsIFactory]);
+ factory.wrappedJSObject = factory;
+
+ manager.registerFactory(factory.classID, String(factory.classID),
+ BOOTSTRAP_CONTRACT, factory);
+ }
Cc[BOOTSTRAP_CONTRACT].getService().wrappedJSObject.loader = !Cu.unload && JSMLoader;
for each (let component in components)
component.register();
Services.obs.notifyObservers(null, "dactyl-rehash", null);
updateVersion();
- require(global, "overlay");
-}
+
+ updateLoader();
+ if (addon !== addonData)
+ require(global, "main");
+}
function shutdown(data, reason) {
- dump("dactyl: bootstrap: shutdown " + reasonToString(reason) + "\n");
+ debug("bootstrap: shutdown " + reasonToString(reason));
if (reason != APP_SHUTDOWN) {
try {
- module("resource://dactyl-content/disable-acr.jsm").cleanup();
+ module(DISABLE_ACR).cleanup();
+ if (Cu.unload)
+ Cu.unload(DISABLE_ACR);
}
catch (e) {
reportError(e);
}
if (~[ADDON_UPGRADE, ADDON_DOWNGRADE, ADDON_UNINSTALL].indexOf(reason))
Services.obs.notifyObservers(null, "dactyl-purge", null);
@@ -250,21 +296,32 @@ function shutdown(data, reason) {
JSMLoader.purge();
for each (let [category, entry] in categories)
categoryManager.deleteCategoryEntry(category, entry, false);
for each (let resource in resources)
resourceProto.setSubstitution(resource, null);
}
}
+function uninstall(data, reason) {
+ debug("bootstrap: uninstall " + reasonToString(reason));
+ if (reason == ADDON_UNINSTALL) {
+ Services.prefs.deleteBranch("extensions.dactyl.");
+
+ if (BOOTSTRAP_CONTRACT in Cc) {
+ let service = Cc[BOOTSTRAP_CONTRACT].getService().wrappedJSObject;
+ manager.unregisterFactory(service.classID, service);
+ }
+ }
+}
+
function reasonToString(reason) {
for each (let name in ["disable", "downgrade", "enable",
"install", "shutdown", "startup",
"uninstall", "upgrade"])
if (reason == global["ADDON_" + name.toUpperCase()] ||
reason == global["APP_" + name.toUpperCase()])
return name;
}
-function install(data, reason) { dump("dactyl: bootstrap: install " + reasonToString(reason) + "\n"); }
-function uninstall(data, reason) { dump("dactyl: bootstrap: uninstall " + reasonToString(reason) + "\n"); }
+function install(data, reason) { debug("bootstrap: install " + reasonToString(reason)); }
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/chrome.manifest b/common/chrome.manifest
--- a/common/chrome.manifest
+++ b/common/chrome.manifest
@@ -1,22 +1,17 @@
+resource dactyl-local ./
resource dactyl-local-content content/
resource dactyl-local-skin skin/
resource dactyl-local-locale locale/
+resource dactyl-common ../common/
resource dactyl ../common/modules/
resource dactyl-content ../common/content/
resource dactyl-skin ../common/skin/
resource dactyl-locale ../common/locale/
content dactyl ../common/content/
component {16dc34f7-6d22-4aa4-a67f-2921fb5dcb69} components/commandline-handler.js
contract @mozilla.org/commandlinehandler/general-startup;1?type=dactyl {16dc34f7-6d22-4aa4-a67f-2921fb5dcb69}
category command-line-handler m-dactyl @mozilla.org/commandlinehandler/general-startup;1?type=dactyl
-component {c1b67a07-18f7-4e13-b361-2edcc35a5a0d} components/protocols.js
-contract @mozilla.org/network/protocol;1?name=chrome-data {c1b67a07-18f7-4e13-b361-2edcc35a5a0d}
-component {9c8f2530-51c8-4d41-b356-319e0b155c44} components/protocols.js
-contract @mozilla.org/network/protocol;1?name=dactyl {9c8f2530-51c8-4d41-b356-319e0b155c44}
-component {f4506a17-5b4d-4cd9-92d4-2eb4630dc388} components/protocols.js
-contract @dactyl.googlecode.com/base/xpc-interface-shim {f4506a17-5b4d-4cd9-92d4-2eb4630dc388}
-
diff --git a/common/components/commandline-handler.js b/common/components/commandline-handler.js
--- a/common/components/commandline-handler.js
+++ b/common/components/commandline-handler.js
@@ -6,47 +6,82 @@
function reportError(e) {
dump("dactyl: command-line-handler: " + e + "\n" + (e.stack || Error().stack));
Cu.reportError(e);
}
var global = this;
var NAME = "command-line-handler";
-var Cc = Components.classes;
-var Ci = Components.interfaces;
-var Cu = Components.utils;
+var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+function init() {
+ Cu.import("resource://dactyl/bootstrap.jsm");
+ if (!JSMLoader.initialized)
+ JSMLoader.init();
+ JSMLoader.load("base.jsm", global);
+ require(global, "config");
+ require(global, "util");
+}
+
function CommandLineHandler() {
this.wrappedJSObject = this;
-
- Cu.import("resource://dactyl/base.jsm");
- require(global, "util");
- require(global, "config");
}
CommandLineHandler.prototype = {
- classDescription: "Dactyl Command-line Handler",
+ classDescription: "Dactyl command line Handler",
classID: Components.ID("{16dc34f7-6d22-4aa4-a67f-2921fb5dcb69}"),
contractID: "@mozilla.org/commandlinehandler/general-startup;1?type=dactyl",
- _xpcom_categories: [{
+ _xpcom_categories: [
+ {
category: "command-line-handler",
entry: "m-dactyl"
- }],
-
- QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsICommandLineHandler]),
+ },
+
+ // FIXME: Belongs elsewhere
+ {
+ category: "profile-after-change",
+ entry: "m-dactyl"
+ }
+ ],
+
+ observe: function observe(subject, topic, data) {
+ if (topic === "profile-after-change") {
+ init();
+ require(global, "main");
+ }
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsICommandLineHandler]),
handle: function (commandLine) {
-
- // TODO: handle remote launches differently?
+ init();
+ try {
+ var remote = commandLine.handleFlagWithParam(config.name + "-remote", false);
+ }
+ catch (e) {
+ util.dump("option '-" + config.name + "-remote' requires an argument\n");
+ }
+
+ try {
+ if (remote) {
+ commandLine.preventDefault = true;
+ require(global, "services");
+ util.dactyl.execute(remote);
+ }
+ }
+ catch(e) {
+ util.reportError(e)
+ };
+
try {
this.optionValue = commandLine.handleFlagWithParam(config.name, false);
}
catch (e) {
util.dump("option '-" + config.name + "' requires an argument\n");
}
},
diff --git a/common/components/protocols.js b/common/components/protocols.js
deleted file mode 100644
--- a/common/components/protocols.js
+++ /dev/null
@@ -1,385 +0,0 @@
-// Copyright (c) 2008-2010 Kris Maglione <maglione.k at Gmail>
-//
-// This work is licensed for reuse under an MIT license. Details are
-// given in the LICENSE.txt file included with this file.
-"use strict";
-function reportError(e) {
- dump("dactyl: protocols: " + e + "\n" + (e.stack || Error().stack));
- Cu.reportError(e);
-}
-
-/* Adds support for data: URIs with chrome privileges
- * and fragment identifiers.
- *
- * "chrome-data:" <content-type> [; <flag>]* "," [<data>]
- *
- * By Kris Maglione, ideas from Ed Anuff's nsChromeExtensionHandler.
- */
-
-var NAME = "protocols";
-var global = this;
-var Cc = Components.classes;
-var Ci = Components.interfaces;
-var Cu = Components.utils;
-
-var ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
-var systemPrincipal = Cc["@mozilla.org/systemprincipal;1"].getService(Ci.nsIPrincipal);
-
-var DNE = "resource://dactyl/content/does/not/exist";
-var _DNE;
-
-Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
-
-function makeChannel(url, orig) {
- try {
- if (url == null)
- return fakeChannel(orig);
-
- if (typeof url === "function")
- return let ([type, data] = url(orig)) StringChannel(data, type, orig);
-
- if (isArray(url))
- return let ([type, data] = url) StringChannel(data, type, orig);
-
- let uri = ioService.newURI(url, null, null);
- return (new XMLChannel(uri)).channel;
- }
- catch (e) {
- util.reportError(e);
- throw e;
- }
-}
-function fakeChannel(orig) {
- let channel = ioService.newChannel(DNE, null, null);
- channel.originalURI = orig;
- return channel;
-}
-function redirect(to, orig, time) {
- let html = <html><head><meta http-equiv="Refresh" content={(time || 0) + ";" + to}/></head></html>.toXMLString();
- return StringChannel(html, "text/html", ioService.newURI(to, null, null));
-}
-
-function Factory(clas) ({
- __proto__: clas.prototype,
- createInstance: function (outer, iid) {
- try {
- if (outer != null)
- throw Components.results.NS_ERROR_NO_AGGREGATION;
- if (!clas.instance)
- clas.instance = new clas();
- return clas.instance.QueryInterface(iid);
- }
- catch (e) {
- reportError(e);
- throw e;
- }
- }
-});
-
-function ChromeData() {}
-ChromeData.prototype = {
- contractID: "@mozilla.org/network/protocol;1?name=chrome-data",
- classID: Components.ID("{c1b67a07-18f7-4e13-b361-2edcc35a5a0d}"),
- classDescription: "Data URIs with chrome privileges",
- QueryInterface: XPCOMUtils.generateQI([Ci.nsIProtocolHandler]),
- _xpcom_factory: Factory(ChromeData),
-
- scheme: "chrome-data",
- defaultPort: -1,
- allowPort: function (port, scheme) false,
- protocolFlags: Ci.nsIProtocolHandler.URI_NORELATIVE
- | Ci.nsIProtocolHandler.URI_NOAUTH
- | Ci.nsIProtocolHandler.URI_IS_UI_RESOURCE,
-
- newURI: function (spec, charset, baseURI) {
- var uri = Components.classes["@mozilla.org/network/standard-url;1"]
- .createInstance(Components.interfaces.nsIStandardURL)
- .QueryInterface(Components.interfaces.nsIURI);
- uri.init(uri.URLTYPE_STANDARD, this.defaultPort, spec, charset, null);
- return uri;
- },
-
- newChannel: function (uri) {
- try {
- if (uri.scheme == this.scheme) {
- let channel = ioService.newChannel(uri.spec.replace(/^.*?:\/*(.*)(?:#.*)?/, "data:$1"),
- null, null);
- channel.contentCharset = "UTF-8";
- channel.owner = systemPrincipal;
- channel.originalURI = uri;
- return channel;
- }
- }
- catch (e) {}
- return fakeChannel(uri);
- }
-};
-
-function Dactyl() {
- // Kill stupid validator warning.
- this["wrapped" + "JSObject"] = this;
-
- this.HELP_TAGS = {};
- this.FILE_MAP = {};
- this.OVERLAY_MAP = {};
-
- this.pages = {};
- this.providers = {};
-
- Cu.import("resource://dactyl/bootstrap.jsm");
- if (!JSMLoader.initialized)
- JSMLoader.init();
- JSMLoader.load("base.jsm", global);
- require(global, "config");
- require(global, "services");
- require(global, "util");
- _DNE = ioService.newChannel(DNE, null, null).name;
-
- // Doesn't belong here:
- AboutHandler.prototype.register();
-}
-Dactyl.prototype = {
- contractID: "@mozilla.org/network/protocol;1?name=dactyl",
- classID: Components.ID("{9c8f2530-51c8-4d41-b356-319e0b155c44}"),
- classDescription: "Dactyl utility protocol",
- QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsIProtocolHandler]),
- _xpcom_factory: Factory(Dactyl),
-
- init: function (obj) {
- for each (let prop in ["HELP_TAGS", "FILE_MAP", "OVERLAY_MAP"]) {
- this[prop] = this[prop].constructor();
- for (let [k, v] in Iterator(obj[prop] || {}))
- this[prop][k] = v;
- }
- this.initialized = true;
- },
-
- scheme: "dactyl",
- defaultPort: -1,
- allowPort: function (port, scheme) false,
- protocolFlags: 0
- | Ci.nsIProtocolHandler.URI_IS_UI_RESOURCE
- | Ci.nsIProtocolHandler.URI_IS_LOCAL_RESOURCE,
-
- newURI: function newURI(spec, charset, baseURI) {
- var uri = Cc["@mozilla.org/network/standard-url;1"]
- .createInstance(Ci.nsIStandardURL)
- .QueryInterface(Ci.nsIURI);
- if (baseURI && baseURI.host === "data")
- baseURI = null;
- uri.init(uri.URLTYPE_STANDARD, this.defaultPort, spec, charset, baseURI);
- return uri;
- },
-
- newChannel: function newChannel(uri) {
- try {
- if (/^help/.test(uri.host) && !("all" in this.FILE_MAP))
- return redirect(uri.spec, uri, 1);
-
- if (uri.host in this.providers)
- return makeChannel(this.providers[uri.host](uri), uri);
-
- let path = decodeURIComponent(uri.path.replace(/^\/|#.*/g, ""));
- switch(uri.host) {
- case "content":
- return makeChannel(this.pages[path] || "resource://dactyl-content/" + path, uri);
- case "data":
- try {
- var channel = ioService.newChannel(uri.path.replace(/^\/(.*)(?:#.*)?/, "data:$1"),
- null, null);
- }
- catch (e) {
- var error = e;
- break;
- }
- channel.contentCharset = "UTF-8";
- channel.owner = systemPrincipal;
- channel.originalURI = uri;
- return channel;
- case "help":
- return makeChannel(this.FILE_MAP[path], uri);
- case "help-overlay":
- return makeChannel(this.OVERLAY_MAP[path], uri);
- case "help-tag":
- let tag = decodeURIComponent(uri.path.substr(1));
- if (tag in this.FILE_MAP)
- return redirect("dactyl://help/" + tag, uri);
- if (tag in this.HELP_TAGS)
- return redirect("dactyl://help/" + this.HELP_TAGS[tag] + "#" + tag.replace(/#/g, encodeURIComponent), uri);
- break;
- case "locale":
- return LocaleChannel("dactyl-locale", path, uri);
- case "locale-local":
- return LocaleChannel("dactyl-local-locale", path, uri);
- }
- }
- catch (e) {
- util.reportError(e);
- }
- if (error)
- throw error;
- return fakeChannel(uri);
- },
-
- // FIXME: Belongs elsewhere
- _xpcom_categories: [{
- category: "profile-after-change",
- entry: "m-dactyl"
- }],
-
- observe: function observe(subject, topic, data) {
- if (topic === "profile-after-change") {
- Cu.import("resource://dactyl/bootstrap.jsm");
- JSMLoader.init();
- require(global, "overlay");
- }
- }
-};
-
-function LocaleChannel(pkg, path, orig) {
- for each (let locale in [config.locale, "en-US"])
- for each (let sep in "-/") {
- var channel = makeChannel(["resource:/", pkg + sep + config.locale, path].join("/"), orig);
- if (channel.name !== _DNE)
- return channel;
- }
- return channel;
-}
-
-function StringChannel(data, contentType, uri) {
- let channel = services.StreamChannel(uri);
- channel.contentStream = services.CharsetConv("UTF-8").convertToInputStream(data);
- if (contentType)
- channel.contentType = contentType;
- channel.contentCharset = "UTF-8";
- channel.owner = systemPrincipal;
- if (uri)
- channel.originalURI = uri;
- return channel;
-}
-
-function XMLChannel(uri, contentType) {
- try {
- var channel = services.io.newChannelFromURI(uri);
- var channelStream = channel.open();
- }
- catch (e) {
- this.channel = fakeChannel(uri);
- return;
- }
-
- this.uri = uri;
- this.sourceChannel = services.io.newChannelFromURI(uri);
- this.pipe = services.Pipe(true, true, 0, 0, null);
- this.writes = [];
-
- this.channel = services.StreamChannel(uri);
- this.channel.contentStream = this.pipe.inputStream;
- this.channel.contentType = contentType || channel.contentType;
- this.channel.contentCharset = "UTF-8";
- this.channel.owner = systemPrincipal;
-
- let stream = services.InputStream(channelStream);
- let [, pre, doctype, url, open, post] = util.regexp(<![CDATA[
- ^ ([^]*?)
- (?:
- (<!DOCTYPE \s+ \S+ \s+) SYSTEM \s+ "([^"]*)"
- (\s+ \[)?
- ([^]*)
- )?
- $
- ]]>, "x").exec(stream.read(4096));
- this.writes.push(pre);
- if (doctype) {
- this.writes.push(doctype + "[\n");
- try {
- this.writes.push(services.io.newChannel(url, null, null).open());
- }
- catch (e) {}
- if (!open)
- this.writes.push("\n]");
- this.writes.push(post);
- }
- this.writes.push(channelStream);
-
- this.writeNext();
-}
-XMLChannel.prototype = {
- QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver]),
- writeNext: function () {
- try {
- if (!this.writes.length)
- this.pipe.outputStream.close();
- else {
- let stream = this.writes.shift();
- if (isString(stream))
- stream = services.StringStream(stream);
-
- services.StreamCopier(stream, this.pipe.outputStream, null,
- false, true, 4096, true, false)
- .asyncCopy(this, null);
- }
- }
- catch (e) {
- util.reportError(e);
- }
- },
-
- onStartRequest: function (request, context) {},
- onStopRequest: function (request, context, statusCode) {
- this.writeNext();
- }
-};
-
-function AboutHandler() {}
-AboutHandler.prototype = {
- register: function () {
- try {
- JSMLoader.registerFactory(Factory(AboutHandler));
- }
- catch (e) {
- util.reportError(e);
- }
- },
-
- get classDescription() "About " + config.appName + " Page",
-
- classID: Components.ID("81495d80-89ee-4c36-a88d-ea7c4e5ac63f"),
-
- get contractID() "@mozilla.org/network/protocol/about;1?what=" + config.name,
-
- QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]),
-
- newChannel: function (uri) {
- let channel = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService)
- .newChannel("dactyl://content/about.xul", null, null);
- channel.originalURI = uri;
- return channel;
- },
-
- getURIFlags: function (uri) Ci.nsIAboutModule.ALLOW_SCRIPT,
-};
-
-// A hack to get information about interfaces.
-// Doesn't belong here.
-function Shim() {}
-Shim.prototype = {
- contractID: "@dactyl.googlecode.com/base/xpc-interface-shim",
- classID: Components.ID("{f4506a17-5b4d-4cd9-92d4-2eb4630dc388}"),
- classDescription: "XPCOM empty interface shim",
- QueryInterface: function (iid) {
- if (iid.equals(Ci.nsISecurityCheckedComponent))
- throw Components.results.NS_ERROR_NO_INTERFACE;
- return this;
- },
- getHelperForLanguage: function () null,
- getInterfaces: function (count) { count.value = 0; }
-};
-
-if (XPCOMUtils.generateNSGetFactory)
- var NSGetFactory = XPCOMUtils.generateNSGetFactory([ChromeData, Dactyl, Shim]);
-else
- var NSGetModule = XPCOMUtils.generateNSGetModule([ChromeData, Dactyl, Shim]);
-var EXPORTED_SYMBOLS = ["NSGetFactory", "global"];
-
-// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/abbreviations.js b/common/content/abbreviations.js
--- a/common/content/abbreviations.js
+++ b/common/content/abbreviations.js
@@ -1,15 +1,15 @@
// Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
// Copyright (c) 2010 by anekos <anekos@snca.net>
// Copyright (c) 2010-2011 by Kris Maglione <maglione.k at Gmail>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
/** @scope modules */
/**
* A user-defined input mode binding of a typed string to an automatically
* inserted expansion string.
*
* Abbreviations have a left-hand side (LHS) whose text is replaced by that of
@@ -345,17 +345,19 @@ var Abbreviations = Module("abbreviation
description: "Expand this abbreviation by evaluating its right-hand-side as JavaScript"
}
],
serialize: function () [
{
command: this.name,
arguments: [abbr.lhs],
literalArg: abbr.rhs,
- options: callable(abbr.rhs) ? {"-javascript": null} : {}
+ options: {
+ "-javascript": abbr.rhs ? null : undefined
+ }
}
for ([, abbr] in Iterator(abbreviations.user.merged))
if (abbr.modesEqual(modes))
]
});
commands.add([ch + "una[bbreviate]"],
"Remove an abbreviation" + modeDescription,
@@ -371,14 +373,15 @@ var Abbreviations = Module("abbreviation
bang: true,
completer: function (context, args) completion.abbreviation(context, modes, args["-group"]),
literal: 0,
options: [contexts.GroupFlag("abbrevs")]
});
}
addAbbreviationCommands([modes.INSERT, modes.COMMAND_LINE], "", "");
- addAbbreviationCommands([modes.INSERT], "i", "INSERT");
- addAbbreviationCommands([modes.COMMAND_LINE], "c", "COMMAND_LINE");
- }
-});
+ [modes.INSERT, modes.COMMAND_LINE].forEach(function (mode) {
+ addAbbreviationCommands([mode], mode.char, mode.displayName);
+ });
+ }
+});
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/about.xul b/common/content/about.xul
--- a/common/content/about.xul
+++ b/common/content/about.xul
@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<?xml-stylesheet href="resource://dactyl-local-skin/about.css" type="text/css"?>
<!DOCTYPE overlay SYSTEM "dactyl://content/dtd">
<page id="about-&dactyl.name;" orient="vertical" title="About &dactyl.appName;"
xmlns="&xmlns.xul;" xmlns:html="&xmlns.html;">
- <html:link rel="icon" href="chrome://&dactyl.name;/skin/icon.png"
+ <html:link rel="icon" href="resource://dactyl-local-skin/icon.png"
type="image/png" style="display: none;"/>
<spring flex="1"/>
<hbox>
<spring flex="1"/>
<div xmlns="&xmlns.html;" style="text-align: center" id="text-container">
<img src="chrome://&dactyl.name;/content/logo.png" alt="&dactyl.appName;" />
diff --git a/common/content/autocommands.js b/common/content/autocommands.js
--- a/common/content/autocommands.js
+++ b/common/content/autocommands.js
@@ -1,26 +1,26 @@
// Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
// Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
/** @scope modules */
var AutoCommand = Struct("event", "filter", "command");
update(AutoCommand.prototype, {
- eventName: Class.memoize(function () this.event.toLowerCase()),
+ eventName: Class.Memoize(function () this.event.toLowerCase()),
match: function (event, pattern) {
- return (!event || this.eventName == event.toLowerCase()) && (!pattern || String(this.filter) === pattern);
- }
-});
+ return (!event || this.eventName == event.toLowerCase()) && (!pattern || String(this.filter) === String(pattern));
+ }
+});
var AutoCmdHive = Class("AutoCmdHive", Contexts.Hive, {
init: function init(group) {
init.supercall(this, group);
this._store = [];
},
__iterator__: function () array.iterValues(this._store),
@@ -38,49 +38,45 @@ var AutoCmdHive = Class("AutoCmdHive", C
if (!callable(pattern))
pattern = Group.compileFilter(pattern);
for (let event in values(events))
this._store.push(AutoCommand(event, pattern, cmd));
},
/**
- * Returns all autocommands with a matching *event* and *regexp*.
+ * Returns all autocommands with a matching *event* and *filter*.
*
* @param {string} event The event name filter.
- * @param {string} pattern The URL pattern filter.
+ * @param {string} filter The URL pattern filter.
* @returns {[AutoCommand]}
*/
- get: function (event, pattern) {
- return this._store.filter(function (autoCmd) autoCmd.match(event, regexp));
- },
-
- /**
- * Deletes all autocommands with a matching *event* and *regexp*.
+ get: function (event, filter) {
+ filter = filter && String(Group.compileFilter(filter));
+ return this._store.filter(function (autoCmd) autoCmd.match(event, filter));
+ },
+
+ /**
+ * Deletes all autocommands with a matching *event* and *filter*.
*
* @param {string} event The event name filter.
- * @param {string} regexp The URL pattern filter.
- */
- remove: function (event, regexp) {
- this._store = this._store.filter(function (autoCmd) !autoCmd.match(event, regexp));
- },
-});
+ * @param {string} filter The URL pattern filter.
+ */
+ remove: function (event, filter) {
+ filter = filter && String(Group.compileFilter(filter));
+ this._store = this._store.filter(function (autoCmd) !autoCmd.match(event, filter));
+ },
+});
/**
* @instance autocommands
*/
var AutoCommands = Module("autocommands", {
init: function () {
- update(this, {
- hives: contexts.Hives("autocmd", AutoCmdHive),
- user: contexts.hives.autocmd.user,
- allHives: contexts.allGroups.autocmd,
- matchingHives: function matchingHives(uri, doc) contexts.matchingGroups(uri, doc).autocmd
- });
- },
+ },
get activeHives() contexts.allGroups.autocmd.filter(function (h) h._store.length),
add: deprecated("group.autocmd.add", { get: function add() autocommands.user.closure.add }),
get: deprecated("group.autocmd.get", { get: function get() autocommands.user.closure.get }),
remove: deprecated("group.autocmd.remove", { get: function remove() autocommands.user.closure.remove }),
/**
@@ -102,32 +98,34 @@ var AutoCommands = Module("autocommands"
if (autoCmd.match(event, regexp)) {
cmds[autoCmd.event] = cmds[autoCmd.event] || [];
cmds[autoCmd.event].push(autoCmd);
}
});
return cmds;
}
+ XML.prettyPrinting = XML.ignoreWhitespace = false;
commandline.commandOutput(
<table>
<tr highlight="Title">
<td colspan="3">----- Auto Commands -----</td>
</tr>
{
template.map(hives, function (hive)
- <tr highlight="Title">
- <td colspan="3">{hive.name}</td>
+ <tr>
+ <td colspan="3"><span highlight="Title">{hive.name}</span>
+ {hive.filter}</td>
</tr> +
<tr style="height: .5ex;"/> +
template.map(cmds(hive), function ([event, items])
<tr style="height: .5ex;"/> +
template.map(items, function (item, i)
<tr>
- <td highlight="Title" style="padding-right: 1em;">{i == 0 ? event : ""}</td>
+ <td highlight="Title" style="padding-left: 1em; padding-right: 1em;">{i == 0 ? event : ""}</td>
<td>{item.filter.toXML ? item.filter.toXML() : item.filter}</td>
<td>{item.command}</td>
</tr>) +
<tr style="height: .5ex;"/>) +
<tr style="height: .5ex;"/>)
}
</table>);
},
@@ -149,68 +147,74 @@ var AutoCommands = Module("autocommands"
var { url, doc } = args;
if (url)
uri = util.createURI(url);
else
var { uri, doc } = buffer;
event = event.toLowerCase();
for (let hive in values(this.matchingHives(uri, doc))) {
- let args = update({},
- hive.argsExtra(arguments[1]),
- arguments[1]);
+ let args = hive.makeArgs(doc, null, arguments[1]);
for (let autoCmd in values(hive._store))
if (autoCmd.eventName === event && autoCmd.filter(uri, doc)) {
if (!lastPattern || lastPattern !== String(autoCmd.filter))
dactyl.echomsg(_("autocmd.executing", event, autoCmd.filter), 8);
lastPattern = String(autoCmd.filter);
dactyl.echomsg(_("autocmd.autocommand", autoCmd.command), 9);
dactyl.trapErrors(autoCmd.command, autoCmd, args);
}
}
}
}, {
}, {
+ contexts: function () {
+ update(AutoCommands.prototype, {
+ hives: contexts.Hives("autocmd", AutoCmdHive),
+ user: contexts.hives.autocmd.user,
+ allHives: contexts.allGroups.autocmd,
+ matchingHives: function matchingHives(uri, doc) contexts.matchingGroups(uri, doc).autocmd
+ });
+ },
commands: function () {
commands.add(["au[tocmd]"],
"Execute commands automatically on events",
function (args) {
- let [event, regexp, cmd] = args;
+ let [event, filter, cmd] = args;
let events = [];
if (event) {
// NOTE: event can only be a comma separated list for |:au {event} {pat} {cmd}|
let validEvents = Object.keys(config.autocommands).map(String.toLowerCase);
validEvents.push("*");
events = Option.parse.stringlist(event);
dactyl.assert(events.every(function (event) validEvents.indexOf(event.toLowerCase()) >= 0),
_("autocmd.noGroup", event));
}
if (args.length > 2) { // add new command, possibly removing all others with the same event/pattern
if (args.bang)
- args["-group"].remove(event, regexp);
+ args["-group"].remove(event, filter);
cmd = contexts.bindMacro(args, "-ex", function (params) params);
- args["-group"].add(events, regexp, cmd);
+ args["-group"].add(events, filter, 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] != "*" || args.length > 1)
- args["-group"].remove(event, regexp); // remove all
+ args["-group"].remove(event, filter); // remove all
}
else
- autocommands.list(event, regexp, args.explicitOpts["-group"] ? [args["-group"]] : null); // list all
+ autocommands.list(event, filter, args.explicitOpts["-group"] ? [args["-group"]] : null); // list all
}
}, {
bang: true,
completer: function (context, args) {
if (args.length == 1)
return completion.autocmdEvent(context);
if (args.length == 3)
return args["-javascript"] ? completion.javascript(context) : completion.ex(context);
@@ -277,17 +281,17 @@ var AutoCommands = Module("autocommands"
});
},
completion: function () {
completion.autocmdEvent = function autocmdEvent(context) {
context.completions = Iterator(config.autocommands);
};
},
javascript: function () {
- JavaScript.setCompleter(autocommands.user.get, [function () Iterator(config.autocommands)]);
+ JavaScript.setCompleter(AutoCmdHive.prototype.get, [function () Iterator(config.autocommands)]);
},
options: function () {
options.add(["eventignore", "ei"],
"List of autocommand event names which should be ignored",
"stringlist", "",
{
values: iter(update({ all: "All Events" }, config.autocommands)).toArray(),
has: Option.has.toggleAll
diff --git a/common/content/bindings.xml b/common/content/bindings.xml
--- a/common/content/bindings.xml
+++ b/common/content/bindings.xml
@@ -21,16 +21,20 @@
<binding id="compitem-td">
<!-- No white space. The table is white-space: pre; :( -->
<content><html:span class="td-strut"/><html:span class="td-span"><children/></html:span></content>
</binding>
<binding id="tab" display="xul:hbox"
extends="chrome://browser/content/tabbrowser.xml#tabbrowser-tab">
+ <implementation>
+ <property name="dactylOrdinal" onget="parseInt(this.getAttribute('dactylOrdinal'))"
+ onset="this.setAttribute('dactylOrdinal', val)"/>
+ </implementation>
<content closetabtext="Close Tab">
<xul:stack class="tab-icon dactyl-tab-stack">
<xul:image xbl:inherits="validate,src=image" role="presentation" class="tab-icon-image"/>
<xul:vbox>
<xul:spring flex="1"/>
<xul:label xbl:inherits="value=dactylOrdinal" dactyl:highlight="TabIconNumber"/>
<xul:spring flex="1"/>
</xul:vbox>
@@ -40,16 +44,20 @@
flex="1" class="tab-text" role="presentation"/>
<xul:toolbarbutton anonid="close-button" tabindex="-1"
class="tab-close-button"/>
</content>
</binding>
<binding id="tab-mac"
extends="chrome://browser/content/tabbrowser.xml#tabbrowser-tab">
+ <implementation>
+ <property name="dactylOrdinal" onget="parseInt(this.getAttribute('dactylOrdinal'))"
+ onset="this.setAttribute('dactylOrdinal', val)"/>
+ </implementation>
<content chromedir="ltr" closetabtext="Close Tab">
<xul:hbox class="tab-image-left" xbl:inherits="selected"/>
<xul:hbox class="tab-image-middle" flex="1" align="center" xbl:inherits="selected">
<xul:stack class="tab-icon dactyl-tab-stack">
<xul:image xbl:inherits="validate,src=image" class="tab-icon-image"/>
<xul:image class="tab-extra-status"/>
<xul:vbox>
<xul:spring flex="1"/>
diff --git a/common/content/bookmarks.js b/common/content/bookmarks.js
--- a/common/content/bookmarks.js
+++ b/common/content/bookmarks.js
@@ -1,17 +1,15 @@
// Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
// Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
-
-var DEFAULT_FAVICON = "chrome://mozapps/skin/places/defaultFavicon.png";
+/* use strict */
// also includes methods for dealing with keywords and search engines
var Bookmarks = Module("bookmarks", {
init: function () {
this.timer = Timer(0, 100, function () {
this.checkBookmarked(buffer.uri);
}, this);
@@ -33,17 +31,17 @@ var Bookmarks = Module("bookmarks", {
statusline.bookmarked = false;
this.checkBookmarked(uri);
}
},
get format() ({
anchored: false,
title: ["URL", "Info"],
- keys: { text: "url", description: "title", icon: "icon", extra: "extra", tags: "tags" },
+ keys: { text: "url", description: "title", icon: "icon", extra: "extra", tags: "tags", isURI: function () true },
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);
},
@@ -58,76 +56,81 @@ var Bookmarks = Module("bookmarks", {
* @param {string} keyword The keyword of the new bookmark.
* @optional
* @param {[string]} tags The tags for the new bookmark.
* @optional
* @param {boolean} force If true, a new bookmark is always added.
* Otherwise, if a bookmark for the given URL exists it is
* updated instead.
* @optional
- * @returns {boolean} True if the bookmark was added or updated
- * successfully.
+ * @returns {boolean} True if the bookmark was updated, false if a
+ * new bookmark was added.
*/
add: function add(unfiled, title, url, keyword, tags, force) {
// FIXME
if (isObject(unfiled))
- var { unfiled, title, url, keyword, tags, post, charset, force } = unfiled;
-
- try {
+ var { id, unfiled, title, url, keyword, tags, post, charset, force } = unfiled;
+
let uri = util.createURI(url);
- if (!force && bookmarkcache.isBookmarked(uri))
- for (var bmark in bookmarkcache)
- if (bmark.url == uri.spec) {
- if (title)
- bmark.title = title;
+ if (id != null)
+ var bmark = bookmarkcache.bookmarks[id];
+ else if (!force) {
+ if (keyword && Set.has(bookmarkcache.keywords, keyword))
+ bmark = bookmarkcache.keywords[keyword];
+ else if (bookmarkcache.isBookmarked(uri))
+ for (bmark in bookmarkcache)
+ if (bmark.url == uri.spec)
break;
}
if (tags) {
PlacesUtils.tagging.untagURI(uri, null);
PlacesUtils.tagging.tagURI(uri, tags);
}
+
+ let updated = !!bmark;
if (bmark == undefined)
bmark = bookmarkcache.bookmarks[
services.bookmarks.insertBookmark(
services.bookmarks[unfiled ? "unfiledBookmarksFolder" : "bookmarksMenuFolder"],
uri, -1, title || url)];
- if (!bmark)
- return false;
+ else {
+ if (title)
+ bmark.title = title;
+ if (!uri.equals(bmark.uri))
+ bmark.uri = uri;
+ }
+
+ util.assert(bmark);
if (charset !== undefined)
bmark.charset = charset;
if (post !== undefined)
bmark.post = post;
if (keyword)
bmark.keyword = keyword;
- }
- catch (e) {
- util.reportError(e);
- return false;
- }
-
- return true;
- },
+
+ return updated;
+ },
/**
* Opens the command line in Ex mode pre-filled with a :bmark
* command to add a new search keyword for the given form element.
*
* @param {Element} elem A form element for which to add a keyword.
*/
addSearchKeyword: function addSearchKeyword(elem) {
if (elem instanceof HTMLFormElement || elem.form)
- var [url, post, charset] = util.parseForm(elem);
+ var { url, postData, charset } = DOM(elem).formData;
else
- var [url, post, charset] = [elem.href || elem.src, null, elem.ownerDocument.characterSet];
+ var [url, postData, charset] = [elem.href || elem.src, null, elem.ownerDocument.characterSet];
let options = { "-title": "Search " + elem.ownerDocument.title };
- if (post != null)
- options["-post"] = post;
+ if (postData != null)
+ options["-post"] = postData;
if (charset != null && charset !== "UTF-8")
options["-charset"] = charset;
CommandExMode().open(
commands.commandToString({ command: "bmark", options: options, arguments: [url] }) + " -keyword ");
},
checkBookmarked: function checkBookmarked(uri) {
@@ -158,16 +161,17 @@ var Bookmarks = Module("bookmarks", {
let count = this.remove(url);
if (count > 0)
dactyl.echomsg({ domains: [util.getHost(url)], message: _("bookmark.removed", url) });
else {
let title = buffer.uri.spec == url && buffer.title || url;
let extra = "";
if (title != url)
extra = " (" + title + ")";
+
this.add({ unfiled: true, title: title, url: url });
dactyl.echomsg({ domains: [util.getHost(url)], message: _("bookmark.added", url + extra) });
}
},
isBookmarked: deprecated("bookmarkcache.isBookmarked", { get: function isBookmarked() bookmarkcache.closure.isBookmarked }),
/**
@@ -242,39 +246,59 @@ var Bookmarks = Module("bookmarks", {
* suggestions.
* @param {function([string])} callback The function to call when
* results are returned.
* @returns {[string] | null}
*/
getSuggestions: function getSuggestions(engineName, query, callback) {
const responseType = "application/x-suggestions+json";
+ if (Set.has(this.suggestionProviders, engineName))
+ return this.suggestionProviders[engineName](query, callback);
+
let engine = Set.has(this.searchEngines, engineName) && this.searchEngines[engineName];
if (engine && engine.supportsResponseType(responseType))
var queryURI = engine.getSubmission(query, responseType).uri.spec;
if (!queryURI)
return (callback || util.identity)([]);
+ function parse(req) JSON.parse(req.responseText)[1].filter(isString);
+ return this.makeSuggestions(queryURI, parse, callback);
+ },
+
+ /**
+ * Given a query URL, response parser, and optionally a callback,
+ * fetch and parse search query results for {@link getSuggestions}.
+ *
+ * @param {string} url The URL to fetch.
+ * @param {function(XMLHttpRequest):[string]} parser The function which
+ * parses the response.
+ */
+ makeSuggestions: function makeSuggestions(url, parser, callback) {
function process(req) {
let results = [];
try {
- results = JSON.parse(req.responseText)[1].filter(isString);
- }
- catch (e) {}
+ results = parser(req);
+ }
+ catch (e) {
+ util.reportError(e);
+ }
if (callback)
return callback(results);
return results;
}
- let req = util.httpGet(queryURI, callback && process);
+ let req = util.httpGet(url, callback && process);
if (callback)
return req;
return process(req);
},
+ suggestionProviders: {},
+
/**
* Returns an array containing a search URL and POST data for the
* given search string. If *useDefsearch* is true, the string is
* always passed to the default search engine. If it is not, the
* search engine name is retrieved from the first space-separated
* token of the given string.
*
* Returns null if no search engine is found for the passed string.
@@ -373,24 +397,16 @@ var Bookmarks = Module("bookmarks", {
dactyl.echoerr(_("bookmark.noMatchingTags", tags.map(String.quote)));
else
dactyl.echoerr(_("bookmark.none"));
return null;
}
}, {
}, {
commands: function () {
- commands.add(["ju[mps]"],
- "Show jumplist",
- function () {
- let sh = history.session;
- commandline.commandOutput(template.jumps(sh.index, sh));
- },
- { argCount: "0" });
-
// TODO: Clean this up.
const tags = {
names: ["-tags", "-T"],
description: "A comma-separated list of tags",
completer: function tags(context, args) {
context.generate = function () array(b.tags for (b in bookmarkcache) if (b.tags)).flatten().uniq().array;
context.keys = { text: util.identity, description: util.identity };
},
@@ -427,40 +443,44 @@ var Bookmarks = Module("bookmarks", {
const keyword = {
names: ["-keyword", "-k"],
description: "Keyword by which this bookmark may be opened (:open {keyword})",
completer: function keyword(context, args) {
context.keys.text = "keyword";
return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: context.filter, title: args["-title"] });
},
type: CommandOption.STRING,
- validator: function (arg) /^\S+$/.test(arg)
- };
+ validator: bind("test", /^\S+$/)
+ };
commands.add(["bma[rk]"],
"Add a bookmark",
function (args) {
+ dactyl.assert(!args.bang || args["-id"] == null,
+ _("bookmark.bangOrID"));
+
let opts = {
force: args.bang,
unfiled: false,
+ id: args["-id"],
keyword: args["-keyword"] || null,
charset: args["-charset"],
post: args["-post"],
tags: args["-tags"] || [],
title: args["-title"] || (args.length === 0 ? buffer.title : null),
url: args.length === 0 ? buffer.uri.spec : args[0]
};
- if (bookmarks.add(opts)) {
+ let updated = bookmarks.add(opts);
+ let action = updated ? "updated" : "added";
+
let extra = (opts.title == opts.url) ? "" : " (" + opts.title + ")";
- dactyl.echomsg({ domains: [util.getHost(opts.url)], message: _("bookmark.added", opts.url + extra) },
+
+ dactyl.echomsg({ domains: [util.getHost(opts.url)], message: _("bookmark." + action, opts.url + extra) },
1, commandline.FORCE_SINGLELINE);
- }
- else
- dactyl.echoerr(_("bookmark.cantAdd", opts.title.quote()));
}, {
argCount: "?",
bang: true,
completer: function (context, args) {
if (!args.bang) {
context.title = ["Page URL"];
let frames = buffer.allFrames();
context.completions = [
@@ -472,16 +492,21 @@ var Bookmarks = Module("bookmarks", {
},
options: [keyword, title, tags, post,
{
names: ["-charset", "-c"],
description: "The character encoding of the bookmark",
type: CommandOption.STRING,
completer: function (context) completion.charset(context),
validator: Option.validateCompleter
+ },
+ {
+ names: ["-id"],
+ description: "The ID of the bookmark to update",
+ type: CommandOption.INT
}
]
});
commands.add(["bmarks"],
"List or open multiple bookmarks",
function (args) {
bookmarks.list(args.join(" "), args["-tags"] || [], args.bang, args["-max"],
@@ -548,16 +573,17 @@ var Bookmarks = Module("bookmarks", {
let options = {};
let url = buffer.uri.spec;
let bmarks = bookmarks.get(url).filter(function (bmark) bmark.url == url);
if (bmarks.length == 1) {
let bmark = bmarks[0];
+ options["-id"] = bmark.id;
options["-title"] = bmark.title;
if (bmark.charset)
options["-charset"] = bmark.charset;
if (bmark.keyword)
options["-keyword"] = bmark.keyword;
if (bmark.post)
options["-post"] = bmark.post;
if (bmark.tags.length > 0)
@@ -622,16 +648,17 @@ var Bookmarks = Module("bookmarks", {
context.fork("suggest", keyword.length + space.length, this, "searchEngineSuggest",
keyword, true);
let item = keywords[keyword];
if (item && item.url.indexOf("%s") > -1)
context.fork("keyword/" + keyword, keyword.length + space.length, null, function (context) {
context.format = history.format;
context.title = [/*L*/keyword + " Quick Search"];
+ context.keys = { text: "url", description: "title", icon: "icon" };
// context.background = true;
context.compare = CompletionContext.Sort.unsorted;
context.generate = function () {
let [begin, end] = item.url.split("%s");
return history.get({ uri: util.newURI(begin), uriIsPrefix: true }).map(function (item) {
let rest = item.url.length - end.length;
let query = item.url.substring(begin.length, rest);
@@ -658,25 +685,29 @@ var Bookmarks = Module("bookmarks", {
completion.searchEngineSuggest = function searchEngineSuggest(context, engineAliases, kludge) {
if (!context.filter)
return;
let engineList = (engineAliases || options["suggestengines"].join(",") || "google").split(",");
engineList.forEach(function (name) {
+ var desc = name;
let engine = bookmarks.searchEngines[name];
- if (!engine)
+ if (engine)
+ var desc = engine.description;
+ else if (!Set.has(bookmarks.suggestionProviders, name))
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 = [/*L*/engine.description + " Suggestions"];
+ ctxt.title = [/*L*/desc + " Suggestions"];
ctxt.keys = { text: util.identity, description: function () "" };
ctxt.compare = CompletionContext.Sort.unsorted;
ctxt.filterFunc = null;
let words = ctxt.filter.toLowerCase().split(/\s+/g);
ctxt.completions = ctxt.completions.filter(function (i) words.every(function (w) i.toLowerCase().indexOf(w) >= 0));
ctxt.hasItems = ctxt.completions.length;
@@ -684,15 +715,15 @@ var Bookmarks = Module("bookmarks", {
ctxt.cache.request = bookmarks.getSuggestions(name, ctxt.filter, function (compl) {
ctxt.incomplete = false;
ctxt.completions = array.uniq(ctxt.completions.filter(function (c) compl.indexOf(c) >= 0)
.concat(compl), true);
});
});
};
- completion.addUrlCompleter("S", "Suggest engines", completion.searchEngineSuggest);
- completion.addUrlCompleter("b", "Bookmarks", completion.bookmark);
- completion.addUrlCompleter("s", "Search engines and keyword URLs", completion.search);
- }
-});
+ completion.addUrlCompleter("suggestion", "Search engine suggestions", completion.searchEngineSuggest);
+ completion.addUrlCompleter("bookmark", "Bookmarks", completion.bookmark);
+ completion.addUrlCompleter("search", "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
--- a/common/content/browser.js
+++ b/common/content/browser.js
@@ -1,24 +1,24 @@
// Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
// Copyright (c) 2008-2011 by Kris Maglione <maglione.k at Gmail>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
/** @scope modules */
/**
* @instance browser
*/
var Browser = Module("browser", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
init: function init() {
- this.cleanupProgressListener = util.overlayObject(window.XULBrowserWindow,
+ this.cleanupProgressListener = overlay.overlayObject(window.XULBrowserWindow,
this.progressListener);
util.addObserver(this);
},
destroy: function () {
this.cleanupProgressListener();
this.observe.unregister();
},
@@ -143,24 +143,27 @@ var Browser = Module("browser", XPCOM(Ci
// happens when the users switches tabs
onLocationChange: util.wrapCallback(function onLocationChange(webProgress, request, uri) {
onLocationChange.superapply(this, arguments);
dactyl.applyTriggerObserver("browser.locationChange", arguments);
let win = webProgress.DOMWindow;
if (win && uri) {
- let oldURI = win.document.dactylURI;
- if (win.document.dactylLoadIdx === webProgress.loadedTransIndex
+ Buffer(win).updateZoom();
+
+ let oldURI = overlay.getData(win.document)["uri"];
+ if (overlay.getData(win.document)["load-idx"] === webProgress.loadedTransIndex
|| !oldURI || uri.spec.replace(/#.*/, "") !== oldURI.replace(/#.*/, ""))
for (let frame in values(buffer.allFrames(win)))
- frame.document.dactylFocusAllowed = false;
- win.document.dactylURI = uri.spec;
- win.document.dactylLoadIdx = webProgress.loadedTransIndex;
- }
+ overlay.setData(frame.document, "focus-allowed", false);
+
+ overlay.setData(win.document, "uri", uri.spec);
+ overlay.setData(win.document, "load-idx", webProgress.loadedTransIndex);
+ }
// Workaround for bugs 591425 and 606877, dactyl bug #81
let collapse = uri && uri.scheme === "dactyl" && webProgress.isLoadingDocument;
if (collapse)
dactyl.focus(document.documentElement);
config.browser.mCurrentBrowser.collapsed = collapse;
util.timeout(function () {
@@ -202,44 +205,65 @@ var Browser = Module("browser", XPCOM(Ci
window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
.redraw();
statusline.updateStatus();
commandline.clear();
},
{ argCount: "0" });
},
mappings: function initMappings(dactyl, modules, window) {
- // opening websites
- mappings.add([modes.NORMAL],
- ["o"], "Open one or more URLs",
- function () { CommandExMode().open("open "); });
+ let openModes = array.toObject([
+ [dactyl.CURRENT_TAB, ""],
+ [dactyl.NEW_TAB, "tab"],
+ [dactyl.NEW_BACKGROUND_TAB, "background tab"],
+ [dactyl.NEW_WINDOW, "win"]
+ ]);
+
+ function open(mode, args) {
+ if (dactyl.forceTarget in openModes)
+ mode = openModes[dactyl.forceTarget];
+
+ CommandExMode().open(mode + "open " + (args || ""))
+ }
function decode(uri) util.losslessDecodeURI(uri)
.replace(/%20(?!(?:%20)*$)/g, " ")
.replace(RegExp(options["urlseparator"], "g"), encodeURIComponent);
+ mappings.add([modes.NORMAL],
+ ["o"], "Open one or more URLs",
+ function () { open(""); });
+
mappings.add([modes.NORMAL], ["O"],
"Open one or more URLs, based on current location",
- function () { CommandExMode().open("open " + decode(buffer.uri.spec)); });
+ function () { open("", decode(buffer.uri.spec)); });
+
+ mappings.add([modes.NORMAL], ["s"],
+ "Open a search prompt",
+ function () { open("", options["defsearch"] + " "); });
+
+ mappings.add([modes.NORMAL], ["S"],
+ "Open a search prompt for a new tab",
+ function () { open("tab", options["defsearch"] + " "); });
mappings.add([modes.NORMAL], ["t"],
"Open one or more URLs in a new tab",
function () { CommandExMode().open("tabopen "); });
mappings.add([modes.NORMAL], ["T"],
"Open one or more URLs in a new tab, based on current location",
- function () { CommandExMode().open("tabopen " + decode(buffer.uri.spec)); });
+ function () { open("tab", decode(buffer.uri.spec)); });
mappings.add([modes.NORMAL], ["w"],
"Open one or more URLs in a new window",
- function () { CommandExMode().open("winopen "); });
+ function () { open("win"); });
mappings.add([modes.NORMAL], ["W"],
"Open one or more URLs in a new window, based on current location",
- function () { CommandExMode().open("winopen " + decode(buffer.uri.spec)); });
+ function () { open("win", decode(buffer.uri.spec)); });
mappings.add([modes.NORMAL], ["<open-home-directory>", "~"],
"Open home directory",
function () { dactyl.open("~"); });
mappings.add([modes.NORMAL], ["<open-homepage>", "gh"],
"Open homepage",
function () { BrowserHome(); });
diff --git a/common/content/commandline.js b/common/content/commandline.js
--- a/common/content/commandline.js
+++ b/common/content/commandline.js
@@ -1,31 +1,31 @@
// Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
// Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
/** @scope modules */
var CommandWidgets = Class("CommandWidgets", {
depends: ["statusline"],
init: function init() {
let s = "dactyl-statusline-field-";
XML.ignoreWhitespace = true;
- util.overlayWindow(window, {
+ overlay.overlayWindow(window, {
objects: {
eventTarget: commandline
},
append: <e4x xmlns={XUL} xmlns:dactyl={NS}>
- <vbox id={config.commandContainer}>
+ <vbox id={config.ids.commandContainer}>
<vbox class="dactyl-container" hidden="false" collapsed="true">
<iframe class="dactyl-completions" id="dactyl-completions-dactyl-commandline" src="dactyl://content/buffer.xhtml"
contextmenu="dactyl-contextmenu"
flex="1" hidden="false" collapsed="false"
highlight="Events" events="mowEvents" />
</vbox>
<stack orient="horizontal" align="stretch" class="dactyl-container" id="dactyl-container" highlight="CmdLine CmdCmdLine">
@@ -148,16 +148,18 @@ var CommandWidgets = Class("CommandWidge
if (!options.get("guioptions").has("M"))
if (this.commandbar.container.clientHeight == 0 ||
value && !this.commandbar.commandline.collapsed)
return this.statusbar;
return this.commandbar;
}
});
this.updateVisibility();
+
+ this.initialized = true;
},
addElement: function addElement(obj) {
const self = this;
this.elements[obj.name] = obj;
function get(prefix, map, id) (obj.getElement || util.identity)(map[id] || document.getElementById(prefix + id));
this.active.__defineGetter__(obj.name, function () self.activeGroup[obj.name][obj.name]);
@@ -238,66 +240,67 @@ var CommandWidgets = Class("CommandWidge
}
}
}
// Hack. Collapse hidden elements in the stack.
// Might possibly be better to use a deck and programmatically
// choose which element to select.
function check(node) {
- if (util.computedStyle(node).display === "-moz-stack") {
+ if (DOM(node).style.display === "-moz-stack") {
let nodes = Array.filter(node.children, function (n) !n.collapsed && n.boxObject.height);
nodes.forEach(function (node, i) node.style.opacity = (i == nodes.length - 1) ? "" : "0");
}
Array.forEach(node.children, check);
}
[this.commandbar.container, this.statusbar.container].forEach(check);
- },
-
- active: Class.memoize(Object),
- activeGroup: Class.memoize(Object),
- commandbar: Class.memoize(function () ({ group: "Cmd" })),
- statusbar: Class.memoize(function () ({ group: "Status" })),
+
+ if (this.initialized && loaded.mow && mow.visible)
+ mow.resize(false);
+ },
+
+ active: Class.Memoize(Object),
+ activeGroup: Class.Memoize(Object),
+ commandbar: Class.Memoize(function () ({ group: "Cmd" })),
+ statusbar: Class.Memoize(function () ({ group: "Status" })),
_ready: function _ready(elem) {
return elem.contentDocument.documentURI === elem.getAttribute("src") &&
["viewable", "complete"].indexOf(elem.contentDocument.readyState) >= 0;
},
_whenReady: function _whenReady(id, init) {
let elem = document.getElementById(id);
while (!this._ready(elem))
yield 10;
if (init)
init.call(this, elem);
yield elem;
},
- completionContainer: Class.memoize(function () this.completionList.parentNode),
-
- contextMenu: Class.memoize(function () {
+ completionContainer: Class.Memoize(function () this.completionList.parentNode),
+
+ contextMenu: Class.Memoize(function () {
["copy", "copylink", "selectall"].forEach(function (tail) {
// some host apps use "hostPrefixContext-copy" ids
let xpath = "//xul:menuitem[contains(@id, '" + "ontext-" + tail + "') and not(starts-with(@id, 'dactyl-'))]";
document.getElementById("dactyl-context-" + tail).style.listStyleImage =
- util.computedStyle(util.evaluateXPath(xpath, document).snapshotItem(0)).listStyleImage;
+ DOM(DOM.XPath(xpath, document).snapshotItem(0)).style.listStyleImage;
});
return document.getElementById("dactyl-contextmenu");
}),
- multilineOutput: Class.memoize(function () this._whenReady("dactyl-multiline-output", function (elem) {
- elem.contentWindow.addEventListener("unload", function (event) { event.preventDefault(); }, true);
- elem.contentDocument.documentElement.id = "dactyl-multiline-output-top";
- elem.contentDocument.body.id = "dactyl-multiline-output-content";
+ multilineOutput: Class.Memoize(function () this._whenReady("dactyl-multiline-output", function (elem) {
+ highlight.highlightNode(elem.contentDocument.body, "MOW");
}), true),
- multilineInput: Class.memoize(function () document.getElementById("dactyl-multiline-input")),
-
- mowContainer: Class.memoize(function () document.getElementById("dactyl-multiline-output-container"))
+ multilineInput: Class.Memoize(function () document.getElementById("dactyl-multiline-input")),
+
+ mowContainer: Class.Memoize(function () document.getElementById("dactyl-multiline-output-container"))
}, {
getEditor: function getEditor(elem) {
elem.inputField.QueryInterface(Ci.nsIDOMNSEditableElement);
return elem;
}
});
var CommandMode = Class("CommandMode", {
@@ -305,30 +308,38 @@ var CommandMode = Class("CommandMode", {
this.keepCommand = userContext.hidden_option_command_afterimage;
},
get autocomplete() options["autocomplete"].length,
get command() this.widgets.command[1],
set command(val) this.widgets.command = val,
- get prompt() this.widgets.prompt,
- set prompt(val) this.widgets.prompt = val,
+ get prompt() this._open ? this.widgets.prompt : this._prompt,
+ set prompt(val) {
+ if (this._open)
+ this.widgets.prompt = val;
+ else
+ this._prompt = val;
+ },
open: function CM_open(command) {
dactyl.assert(isinstance(this.mode, modes.COMMAND_LINE),
- /*L*/"Not opening command line in non-command-line mode.");
+ /*L*/"Not opening command line in non-command-line mode.",
+ false);
this.messageCount = commandline.messageCount;
modes.push(this.mode, this.extendedMode, this.closure);
this.widgets.active.commandline.collapsed = false;
this.widgets.prompt = this.prompt;
this.widgets.command = command || "";
+ this._open = true;
+
this.input = this.widgets.active.command.inputField;
if (this.historyKey)
this.history = CommandLine.History(this.input, this.historyKey, this);
if (this.complete)
this.completions = CommandLine.Completions(this.input, this);
if (this.completions && command && commandline.commandSession === this)
@@ -352,28 +363,32 @@ var CommandMode = Class("CommandMode", {
}
},
leave: function CM_leave(stack) {
if (!stack.push) {
commandline.commandSession = null;
this.input.dactylKeyPress = undefined;
+ let waiting = this.accepted && this.completions && this.completions.waiting;
+ if (waiting)
+ this.completions.onComplete = bind("onSubmit", this);
+
if (this.completions)
this.completions.cleanup();
if (this.history)
this.history.save();
- this.resetCompletions();
commandline.hideCompletions();
modes.delay(function () {
if (!this.keepCommand || commandline.silent || commandline.quiet)
commandline.hide();
+ if (!waiting)
this[this.accepted ? "onSubmit" : "onCancel"](commandline.command);
if (commandline.messageCount === this.messageCount)
commandline.clearMessage();
}, this);
}
},
events: {
@@ -381,17 +396,17 @@ var CommandMode = Class("CommandMode", {
if (this.completions) {
this.resetCompletions();
this.completions.autocompleteTimer.tell(false);
}
this.onChange(commandline.command);
},
keyup: function CM_onKeyUp(event) {
- let key = events.toString(event);
+ let key = DOM.Event.stringify(event);
if (/-?Tab>$/.test(key) && this.completions)
this.completions.tabTimer.flush();
}
},
keepCommand: false,
onKeyPress: function CM_onKeyPress(events) {
@@ -405,37 +420,39 @@ var CommandMode = Class("CommandMode", {
onChange: function (value) {},
onHistory: function (value) {},
onSubmit: function (value) {},
resetCompletions: function CM_resetCompletions() {
- if (this.completions) {
- this.completions.context.cancelAll();
- this.completions.wildIndex = -1;
- this.completions.previewClear();
- }
+ if (this.completions)
+ this.completions.clear();
if (this.history)
this.history.reset();
},
});
var CommandExMode = Class("CommandExMode", CommandMode, {
get mode() modes.EX,
historyKey: "command",
prompt: ["Normal", ":"],
complete: function CEM_complete(context) {
+ try {
context.fork("ex", 0, completion, "ex");
- },
+ }
+ catch (e) {
+ context.message = _("error.error", e);
+ }
+ },
onSubmit: function CEM_onSubmit(command) {
contexts.withContext({ file: /*L*/"[Command Line]", line: 1 },
function _onSubmit() {
io.withSavedValues(["readHeredoc"], function _onSubmit() {
this.readHeredoc = commandline.readHeredoc;
commands.repeat = command;
dactyl.execute(command);
@@ -572,17 +589,17 @@ var CommandLine = Module("commandline",
this._quiet = val;
["commandbar", "statusbar"].forEach(function (nodeSet) {
Array.forEach(this.widgets[nodeSet].commandline.children, function (node) {
node.style.opacity = this._quiet || this._silent ? "0" : "";
}, this);
}, this);
},
- widgets: Class.memoize(function () CommandWidgets()),
+ widgets: Class.Memoize(function () CommandWidgets()),
runSilently: function runSilently(func, self) {
this.withSavedValues(["silent"], function () {
this.silent = true;
func.call(self);
});
},
@@ -590,17 +607,19 @@ var CommandLine = Module("commandline",
let node = this.widgets.active.commandline;
if (this.commandSession && this.commandSession.completionList)
node = document.getElementById(this.commandSession.completionList);
if (!node.completionList) {
let elem = document.getElementById("dactyl-completions-" + node.id);
util.waitFor(bind(this.widgets._ready, null, elem));
- node.completionList = ItemList(elem.id);
+ node.completionList = ItemList(elem);
+ node.completionList.isAboveMow = node.id ==
+ this.widgets.statusbar.commandline.id
}
return node.completionList;
},
hideCompletions: function hideCompletions() {
for (let nodeSet in values([this.widgets.statusbar, this.widgets.commandbar]))
if (nodeSet.commandline.completionList)
nodeSet.commandline.completionList.visible = false;
@@ -887,17 +906,17 @@ var CommandLine = Module("commandline",
let output = [];
function observe(str, highlight, dom) {
output.push(dom && !isString(str) ? dom : str);
}
this.savingOutput = true;
dactyl.trapErrors.apply(dactyl, [fn, self].concat(Array.slice(arguments, 2)));
this.savingOutput = false;
- return output.map(function (elem) elem instanceof Node ? util.domToString(elem) : elem)
+ return output.map(function (elem) elem instanceof Node ? DOM.stringify(elem) : elem)
.join("\n");
}
}, {
/**
* A class for managing the history of an input field.
*
* @param {HTMLInputElement} inputField
* @param {string} mode The mode for which we need history.
@@ -923,23 +942,20 @@ var CommandLine = Module("commandline",
*/
save: function save() {
if (events.feedingKeys)
return;
let str = this.input.value;
if (/^\s*$/.test(str))
return;
this.store = this.store.filter(function (line) (line.value || line) != str);
- try {
+ dactyl.trapErrors(function () {
this.store.push({ value: str, timestamp: Date.now()*1000, privateData: this.checkPrivate(str) });
- }
- catch (e) {
- dactyl.reportError(e);
- }
- this.store = this.store.slice(-options["history"]);
+ }, this);
+ this.store = this.store.slice(Math.max(0, this.store.length - options["history"]));
},
/**
* @property {function} Returns whether a data item should be
* considered private.
*/
checkPrivate: function checkPrivate(str) {
// Not really the ideal place for this check.
if (this.mode == "command")
@@ -947,22 +963,26 @@ var CommandLine = Module("commandline",
return false;
},
/**
* Replace the current input field value.
*
* @param {string} val The new value.
*/
replace: function replace(val) {
+ editor.withSavedValues(["skipSave"], function () {
+ editor.skipSave = true;
+
this.input.dactylKeyPress = undefined;
if (this.completions)
this.completions.previewClear();
this.input.value = val;
this.session.onHistory(val);
- },
+ }, this);
+ },
/**
* Move forward or backward in history.
*
* @param {boolean} backward Direction to move.
* @param {boolean} matchCurrent Search for matches starting
* with the current input value.
*/
@@ -1011,324 +1031,541 @@ var CommandLine = Module("commandline",
}),
/**
* A class for tab completions on an input field.
*
* @param {Object} input
*/
Completions: Class("Completions", {
+ UP: {},
+ DOWN: {},
+ CTXT_UP: {},
+ CTXT_DOWN: {},
+ PAGE_UP: {},
+ PAGE_DOWN: {},
+ RESET: null,
+
init: function init(input, session) {
+ let self = this;
+
this.context = CompletionContext(input.QueryInterface(Ci.nsIDOMNSEditableElement).editor);
- this.context.onUpdate = this.closure._reset;
+ this.context.onUpdate = function onUpdate() { self.asyncUpdate(this); };
+
this.editor = input.editor;
this.input = input;
this.session = session;
- this.selected = null;
+
this.wildmode = options.get("wildmode");
this.wildtypes = this.wildmode.value;
+
this.itemList = commandline.completionList;
- this.itemList.setItems(this.context);
+ this.itemList.open(this.context);
dactyl.registerObserver("events.doneFeeding", this.closure.onDoneFeeding, true);
this.autocompleteTimer = Timer(200, 500, function autocompleteTell(tabPressed) {
if (events.feedingKeys && !tabPressed)
this.ignoredCount++;
else if (this.session.autocomplete) {
this.itemList.visible = true;
this.complete(true, false);
}
}, this);
+
this.tabTimer = Timer(0, 0, function tabTell(event) {
- this.tab(event.shiftKey, event.altKey && options["altwildmode"]);
+ let tabCount = this.tabCount;
+ this.tabCount = 0;
+ this.tab(tabCount, event.altKey && options["altwildmode"]);
}, this);
},
- cleanup: function () {
- dactyl.unregisterObserver("events.doneFeeding", this.closure.onDoneFeeding);
- this.previewClear();
- this.tabTimer.reset();
- this.autocompleteTimer.reset();
- this.itemList.visible = false;
- this.input.dactylKeyPress = undefined;
- },
+ tabCount: 0,
ignoredCount: 0,
+
+ /**
+ * @private
+ */
onDoneFeeding: function onDoneFeeding() {
if (this.ignoredCount)
this.autocompleteTimer.flush(true);
this.ignoredCount = 0;
},
- UP: {},
- DOWN: {},
- PAGE_UP: {},
- PAGE_DOWN: {},
- RESET: null,
-
- lastSubstring: "",
-
+ /**
+ * @private
+ */
+ onTab: function onTab(event) {
+ this.tabCount += event.shiftKey ? -1 : 1;
+ this.tabTimer.tell(event);
+ },
+
+ get activeContexts() this.context.contextList
+ .filter(function (c) c.items.length || c.incomplete),
+
+ /**
+ * Returns the current completion string relative to the
+ * offset of the currently selected context.
+ */
get completion() {
- let str = commandline.command;
- return str.substring(this.prefix.length, str.length - this.suffix.length);
- },
- set completion(completion) {
+ let offset = this.selected ? this.selected[0].offset : this.start;
+ return commandline.command.slice(offset, this.caret);
+ },
+
+ /**
+ * Updates the input field from *offset* to {@link #caret}
+ * with the value *value*. Afterward, the caret is moved
+ * just after the end of the updated text.
+ *
+ * @param {number} offset The offset in the original input
+ * string at which to insert *value*.
+ * @param {string} value The value to insert.
+ */
+ setCompletion: function setCompletion(offset, value) {
+ editor.withSavedValues(["skipSave"], function () {
+ editor.skipSave = true;
this.previewClear();
+ if (value == null)
+ var [input, caret] = [this.originalValue, this.originalCaret];
+ else {
+ input = this.getCompletion(offset, value);
+ caret = offset + value.length;
+ }
+
// Change the completion text.
// The second line is a hack to deal with some substring
// preview corner cases.
- let value = this.prefix + completion + this.suffix;
- commandline.widgets.active.command.value = value;
- this.editor.selection.focusNode.textContent = value;
-
- // Reset the caret to one position after the completion.
- this.caret = this.prefix.length + completion.length;
+ commandline.widgets.active.command.value = input;
+ this.editor.selection.focusNode.textContent = input;
+
+ this.caret = caret;
this._caret = this.caret;
this.input.dactylKeyPress = undefined;
- },
+ }, this);
+ },
+
+ /**
+ * For a given offset and completion string, returns the
+ * full input value after selecting that item.
+ *
+ * @param {number} offset The offset at which to insert the
+ * completion.
+ * @param {string} value The value to insert.
+ * @returns {string};
+ */
+ getCompletion: function getCompletion(offset, value) {
+ return this.originalValue.substr(0, offset)
+ + value
+ + this.originalValue.substr(this.originalCaret);
+ },
+
+ get selected() this.itemList.selected,
+ set selected(tuple) {
+ if (!array.equals(tuple || [],
+ this.itemList.selected || []))
+ this.itemList.select(tuple);
+
+ if (!tuple)
+ this.setCompletion(null);
+ else {
+ let [ctxt, idx] = tuple;
+ this.setCompletion(ctxt.offset, ctxt.items[idx].result);
+ }
+ },
get caret() this.editor.selection.getRangeAt(0).startOffset,
set caret(offset) {
- this.editor.selection.getRangeAt(0).setStart(this.editor.rootElement.firstChild, offset);
- this.editor.selection.getRangeAt(0).setEnd(this.editor.rootElement.firstChild, offset);
- },
+ this.editor.selection.collapse(this.editor.rootElement.firstChild, offset);
+ },
get start() this.context.allItems.start,
get items() this.context.allItems.items,
get substring() this.context.longestAllSubstring,
get wildtype() this.wildtypes[this.wildIndex] || "",
+ /**
+ * Cleanup resources used by this completion session. This
+ * instance should not be used again once this method is
+ * called.
+ */
+ cleanup: function cleanup() {
+ dactyl.unregisterObserver("events.doneFeeding", this.closure.onDoneFeeding);
+ this.previewClear();
+
+ this.tabTimer.reset();
+ this.autocompleteTimer.reset();
+ if (!this.onComplete)
+ this.context.cancelAll();
+
+ this.itemList.visible = false;
+ this.input.dactylKeyPress = undefined;
+ this.hasQuit = true;
+ },
+
+ /**
+ * Run the completer.
+ *
+ * @param {boolean} show Passed to {@link #reset}.
+ * @param {boolean} tabPressed Should be set to true if, and
+ * only if, this function is being called in response
+ * to a <Tab> press.
+ */
complete: function complete(show, tabPressed) {
this.session.ignoredCount = 0;
+
this.context.reset();
this.context.tabPressed = tabPressed;
+
this.session.complete(this.context);
if (!this.session.active)
return;
- this.context.updateAsync = true;
+
this.reset(show, tabPressed);
this.wildIndex = 0;
this._caret = this.caret;
},
+ /**
+ * Clear any preview string and cancel any pending
+ * asynchronous context. Called when there is further input
+ * to be processed.
+ */
+ clear: function clear() {
+ this.context.cancelAll();
+ this.wildIndex = -1;
+ this.previewClear();
+ },
+
+ /**
+ * Saves the current input state. To be called before an
+ * item is selected in a new set of completion responses.
+ * @private
+ */
+ saveInput: function saveInput() {
+ this.originalValue = this.context.value;
+ this.originalCaret = this.caret;
+ },
+
+ /**
+ * Resets the completion state.
+ *
+ * @param {boolean} show If true and options allow the
+ * completion list to be shown, show it.
+ */
+ reset: function reset(show) {
+ this.waiting = null;
+ this.wildIndex = -1;
+
+ this.saveInput();
+
+ if (show) {
+ this.itemList.update();
+ this.context.updateAsync = true;
+ if (this.haveType("list"))
+ this.itemList.visible = true;
+ this.wildIndex = 0;
+ }
+
+ this.preview();
+ },
+
+ /**
+ * Calls when an asynchronous completion context has new
+ * results to return.
+ *
+ * @param {CompletionContext} context The changed context.
+ * @private
+ */
+ asyncUpdate: function asyncUpdate(context) {
+ if (this.hasQuit) {
+ let item = this.getItem(this.waiting);
+ if (item && this.waiting && this.onComplete) {
+ util.trapErrors("onComplete", this,
+ this.getCompletion(this.waiting[0].offset,
+ item.result));
+ this.waiting = null;
+ this.context.cancelAll();
+ }
+ return;
+ }
+
+ let value = this.editor.selection.focusNode.textContent;
+ this.saveInput();
+
+ if (this.itemList.visible)
+ this.itemList.updateContext(context);
+
+ if (this.waiting && this.waiting[0] == context)
+ this.select(this.waiting);
+ else if (!this.waiting) {
+ let cursor = this.selected;
+ if (cursor && cursor[0] == context) {
+ let item = this.getItem(cursor);
+ if (!item || this.completion != item.result)
+ this.itemList.select(null);
+ }
+
+ this.preview();
+ }
+ },
+
+ /**
+ * Returns true if the currently selected 'wildmode' index
+ * has the given completion type.
+ */
haveType: function haveType(type)
this.wildmode.checkHas(this.wildtype, type == "first" ? "" : type),
+ /**
+ * Returns the completion item for the given selection
+ * tuple.
+ *
+ * @param {[CompletionContext,number]} tuple The spec of the
+ * item to return.
+ * @default {@link #selected}
+ * @returns {object}
+ */
+ getItem: function getItem(tuple) {
+ tuple = tuple || this.selected;
+ return tuple && tuple[0] && tuple[0].items[tuple[1]];
+ },
+
+ /**
+ * Returns a tuple representing the next item, at the given
+ * *offset*, from *tuple*.
+ *
+ * @param {[CompletionContext,number]} tuple The offset from
+ * which to search.
+ * @default {@link #selected}
+ * @param {number} offset The positive or negative offset to
+ * find.
+ * @default 1
+ * @param {boolean} noWrap If true, and the search would
+ * wrap, return null.
+ */
+ nextItem: function nextItem(tuple, offset, noWrap) {
+ if (tuple === undefined)
+ tuple = this.selected;
+
+ return this.itemList.getRelativeItem(offset || 1, tuple, noWrap);
+ },
+
+ /**
+ * The last previewed substring.
+ * @private
+ */
+ lastSubstring: "",
+
+ /**
+ * Displays a preview of the text provided by the next <Tab>
+ * press if the current input is an anchored substring of
+ * that result.
+ */
preview: function preview() {
this.previewClear();
- if (this.wildIndex < 0 || this.suffix || !this.items.length)
+ if (this.wildIndex < 0 || this.caret < this.input.value.length
+ || !this.activeContexts.length || this.waiting)
return;
let substring = "";
switch (this.wildtype.replace(/.*:/, "")) {
case "":
- substring = this.items[0].result;
+ var cursor = this.nextItem(null);
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.result;
+ cursor = this.nextItem();
break;
}
+ if (cursor)
+ substring = this.getItem(cursor).result;
// Don't show 1-character substrings unless we've just hit backspace
- if (substring.length < 2 && this.lastSubstring.indexOf(substring) !== 0)
+ if (substring.length < 2 && this.lastSubstring.indexOf(substring))
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>,
+ let node = DOM.fromXML(<span highlight="Preview">{substring}</span>,
document);
- let start = this.caret;
+
+ this.withSavedValues(["caret"], function () {
this.editor.insertNode(node, this.editor.rootElement, 1);
- this.caret = start;
- },
-
+ });
+ },
+
+ /**
+ * Clears the currently displayed next-<Tab> preview string.
+ */
previewClear: function previewClear() {
let node = this.editor.rootElement.firstChild;
if (node && node.nextSibling) {
try {
- this.editor.deleteNode(node.nextSibling);
+ DOM(node.nextSibling).remove();
}
catch (e) {
node.nextSibling.textContent = "";
}
}
else if (this.removeSubstring) {
let str = this.removeSubstring;
let cmd = commandline.widgets.active.command.value;
if (cmd.substr(cmd.length - str.length) == str)
commandline.widgets.active.command.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();
- if (this.haveType("list"))
- this.itemList.visible = true;
- this.selected = null;
- this.wildIndex = 0;
- }
-
- this.preview();
- },
-
- _reset: function _reset() {
- let value = this.editor.selection.focusNode.textContent;
- this.prefix = value.substring(0, this.start);
- this.value = value.substring(this.start, this.caret);
- this.suffix = value.substring(this.caret);
-
- this.itemList.reset();
- this.itemList.selectItem(this.selected);
-
- this.preview();
- },
-
- select: function select(idx) {
+ /**
+ * Selects a completion based on the value of *idx*.
+ *
+ * @param {[CompletionContext,number]|const object} The
+ * (context,index) tuple of the item to select, or an
+ * offset constant from this object.
+ * @param {number} count When given an offset constant,
+ * select *count* units.
+ * @default 1
+ * @param {boolean} fromTab If true, this function was
+ * called by {@link #tab}.
+ * @private
+ */
+ select: function select(idx, count, fromTab) {
+ count = count || 1;
+
switch (idx) {
case this.UP:
- if (this.selected == null)
- idx = -2;
- else
- idx = this.selected - 1;
+ case this.DOWN:
+ idx = this.nextItem(this.waiting || this.selected,
+ idx == this.UP ? -count : count,
+ true);
break;
- case this.DOWN:
- if (this.selected == null)
- idx = 0;
- else
- idx = this.selected + 1;
+
+ case this.CTXT_UP:
+ case this.CTXT_DOWN:
+ let groups = this.itemList.activeGroups;
+ let i = Math.max(0, groups.indexOf(this.itemList.selectedGroup));
+
+ i += idx == this.CTXT_DOWN ? 1 : -1;
+ i %= groups.length;
+ if (i < 0)
+ i += groups.length;
+
+ var position = 0;
+ idx = [groups[i].context, 0];
break;
+
+ case this.PAGE_UP:
+ case this.PAGE_DOWN:
+ idx = this.itemList.getRelativePage(idx == this.PAGE_DOWN ? 1 : -1);
+ break;
+
case this.RESET:
idx = null;
break;
+
default:
- idx = 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)) {
- let done = function done() !(idx >= n + context.items.length || idx == -2 && !context.items.length);
-
- util.waitFor(function () !context.incomplete || done());
- 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)
+ if (!fromTab)
+ this.wildIndex = this.wildtypes.length - 1;
+
+ if (idx && idx[1] >= idx[0].items.length) {
+ this.waiting = idx;
+ statusline.progress = _("completion.waitingForResults");
return;
-
+ }
+
+ this.waiting = null;
+
+ this.itemList.select(idx, null, position);
this.selected = idx;
- this.completion = this.items[idx].result;
- }
-
- this.itemList.selectItem(idx);
- },
-
- tabs: [],
-
- tab: function tab(reverse, wildmode) {
+
+ this.preview();
+
+ if (this.selected == null)
+ statusline.progress = "";
+ else
+ statusline.progress = _("completion.matchIndex",
+ this.itemList.getOffset(idx),
+ this.itemList.itemCount);
+ },
+
+ /**
+ * Selects a completion result based on the 'wildmode'
+ * option, or the value of the *wildmode* parameter.
+ *
+ * @param {number} offset The positive or negative number of
+ * tab presses to process.
+ * @param {[string]} wildmode A 'wildmode' value to
+ * substitute for the value of the 'wildmode' option.
+ * @optional
+ */
+ tab: function tab(offset, wildmode) {
this.autocompleteTimer.flush();
this.ignoredCount = 0;
if (this._caret != this.caret)
this.reset();
this._caret = this.caret;
// Check if we need to run the completer.
if (this.context.waitingForTab || this.wildIndex == -1)
this.complete(true, true);
- this.tabs.push([reverse, wildmode || options["wildmode"]]);
- if (this.waiting)
- return;
-
- while (this.tabs.length) {
- [reverse, this.wildtypes] = this.tabs.shift();
-
+ this.wildtypes = wildmode || options["wildmode"];
+ let count = Math.abs(offset);
+ let steps = Math.constrain(this.wildtypes.length - this.wildIndex,
+ 1, count);
+ count = Math.max(1, count - steps);
+
+ while (steps--) {
this.wildIndex = Math.min(this.wildIndex, this.wildtypes.length - 1);
switch (this.wildtype.replace(/.*:/, "")) {
case "":
- this.select(0);
+ this.select(this.nextItem(null));
break;
case "longest":
- if (this.items.length > 1) {
+ if (this.itemList.itemCount > 1) {
if (this.substring && this.substring.length > this.completion.length)
- this.completion = this.substring;
+ this.setCompletion(this.start, this.substring);
break;
}
// Fallthrough
case "full":
- this.select(reverse ? this.UP : this.DOWN);
+ let c = steps ? 1 : count;
+ this.select(offset < 0 ? this.UP : this.DOWN, c, true);
break;
}
if (this.haveType("list"))
this.itemList.visible = true;
this.wildIndex++;
- this.preview();
-
- if (this.selected == null)
- statusline.progress = "";
- else
- statusline.progress = _("completion.matchIndex", this.selected + 1, this.items.length);
- }
-
- if (this.items.length == 0)
+ }
+
+ if (this.items.length == 0 && !this.waiting)
dactyl.beep();
}
}),
/**
* Evaluate a JavaScript expression and return a string suitable
* to be echoed.
*
@@ -1338,18 +1575,20 @@ var CommandLine = Module("commandline",
*/
echoArgumentToString: function (arg, useColor) {
if (!arg)
return "";
arg = dactyl.userEval(arg);
if (isObject(arg))
arg = util.objectToString(arg, useColor);
- else
- arg = String(arg);
+ else if (callable(arg))
+ arg = String.replace(arg, "/* use strict */ \n", "/* use strict */ ");
+ else if (!isString(arg) && useColor)
+ arg = template.highlight(arg);
return arg;
}
}, {
commands: function init_commands() {
[
{
name: "ec[ho]",
description: "Echo the expression",
@@ -1405,16 +1644,18 @@ var CommandLine = Module("commandline",
commandline.runSilently(function () commands.execute(args[0] || "", null, true));
}, {
completer: function (context) completion.ex(context),
literal: 0,
subCommand: 0
});
},
modes: function initModes() {
+ initModes.require("editor");
+
modes.addMode("COMMAND_LINE", {
char: "c",
description: "Active when the command line is focused",
insert: true,
ownsFocus: true,
get mappingSelf() commandline.commandSession
});
// this._extended modes, can include multiple modes, and even main modes
@@ -1453,62 +1694,107 @@ var CommandLine = Module("commandline",
return function () self.callback.call(commandline, text);
}
return Events.PASS;
});
let bind = function bind()
mappings.add.apply(mappings, [[modes.COMMAND_LINE]].concat(Array.slice(arguments)))
+ bind(["<Esc>", "<C-[>"], "Stop waiting for completions or exit Command Line mode",
+ function ({ self }) {
+ if (self.completions && self.completions.waiting)
+ self.completions.waiting = null;
+ else
+ return Events.PASS;
+ });
+
// 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 regexp in
// mappings.js)
bind(["<Space>", '"', "'"], "Expand command line abbreviation",
function ({ self }) {
self.resetCompletions();
editor.expandAbbreviation(modes.COMMAND_LINE);
return Events.PASS;
});
bind(["<Return>", "<C-j>", "<C-m>"], "Accept the current input",
function ({ self }) {
+ if (self.completions)
+ self.completions.tabTimer.flush();
+
let command = commandline.command;
self.accepted = true;
return function () { modes.pop(); };
});
[
- [["<Up>", "<A-p>"], "previous matching", true, true],
- [["<S-Up>", "<C-p>", "<PageUp>"], "previous", true, false],
- [["<Down>", "<A-n>"], "next matching", false, true],
- [["<S-Down>", "<C-n>", "<PageDown>"], "next", false, false]
+ [["<Up>", "<A-p>", "<cmd-prev-match>"], "previous matching", true, true],
+ [["<S-Up>", "<C-p>", "<cmd-prev>"], "previous", true, false],
+ [["<Down>", "<A-n>", "<cmd-next-match>"], "next matching", false, true],
+ [["<S-Down>", "<C-n>", "<cmd-next>"], "next", false, false]
].forEach(function ([keys, desc, up, search]) {
bind(keys, "Recall the " + desc + " command line from the history list",
function ({ self }) {
dactyl.assert(self.history);
self.history.select(up, search);
});
});
- bind(["<A-Tab>", "<Tab>"], "Select the next matching completion item",
+ bind(["<A-Tab>", "<Tab>", "<A-compl-next>", "<compl-next>"],
+ "Select the next matching completion item",
function ({ keypressEvents, self }) {
dactyl.assert(self.completions);
- self.completions.tabTimer.tell(keypressEvents[0]);
- });
-
- bind(["<A-S-Tab>", "<S-Tab>"], "Select the previous matching completion item",
+ self.completions.onTab(keypressEvents[0]);
+ });
+
+ bind(["<A-S-Tab>", "<S-Tab>", "<A-compl-prev>", "<compl-prev>"],
+ "Select the previous matching completion item",
function ({ keypressEvents, self }) {
dactyl.assert(self.completions);
- self.completions.tabTimer.tell(keypressEvents[0]);
- });
+ self.completions.onTab(keypressEvents[0]);
+ });
+
+ bind(["<C-Tab>", "<A-f>", "<compl-next-group>"],
+ "Select the next matching completion group",
+ function ({ keypressEvents, self }) {
+ dactyl.assert(self.completions);
+ self.completions.tabTimer.flush();
+ self.completions.select(self.completions.CTXT_DOWN);
+ });
+
+ bind(["<C-S-Tab>", "<A-S-f>", "<compl-prev-group>"],
+ "Select the previous matching completion group",
+ function ({ keypressEvents, self }) {
+ dactyl.assert(self.completions);
+ self.completions.tabTimer.flush();
+ self.completions.select(self.completions.CTXT_UP);
+ });
+
+ bind(["<C-f>", "<PageDown>", "<compl-next-page>"],
+ "Select the next page of completions",
+ function ({ keypressEvents, self }) {
+ dactyl.assert(self.completions);
+ self.completions.tabTimer.flush();
+ self.completions.select(self.completions.PAGE_DOWN);
+ });
+
+ bind(["<C-b>", "<PageUp>", "<compl-prev-page>"],
+ "Select the previous page of completions",
+ function ({ keypressEvents, self }) {
+ dactyl.assert(self.completions);
+ self.completions.tabTimer.flush();
+ self.completions.select(self.completions.PAGE_UP);
+ });
bind(["<BS>", "<C-h>"], "Delete the previous character",
function () {
if (!commandline.command)
modes.pop();
else
return Events.PASS;
});
@@ -1566,273 +1852,534 @@ var CommandLine = Module("commandline",
commandline._messageHistory.filter(function (item) !timespan.contains(item.timestamp * 1000) ||
host && (!item.domains || !item.domains.some(function (d) util.isSubdomain(d, host))));
}
});
}
});
/**
- * The list which is used for the completion box (and QuickFix window in
- * future).
+ * The list which is used for the completion box.
*
* @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.
*/
+
var ItemList = Class("ItemList", {
- init: function init(id) {
- this._completionElements = [];
-
- var iframe = document.getElementById(id);
-
- this._doc = iframe.contentDocument;
- this._win = iframe.contentWindow;
- this._container = iframe.parentNode;
-
- this._doc.documentElement.id = id + "-top";
- this._doc.body.id = id + "-content";
- this._doc.body.className = iframe.className + "-content";
- this._doc.body.appendChild(this._doc.createTextNode(""));
- this._doc.body.style.borderTop = "1px solid black"; // FIXME: For cases where completions/MOW are shown at once, or ls=0. Should use :highlight.
-
- 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 _dom(xml, map) util.xmlToDom(xml instanceof XML ? xml : <>{xml}</>, this._doc, map),
-
- _autoSize: function _autoSize() {
- if (!this._div)
- return;
-
- if (this._container.collapsed)
- this._div.style.minWidth = document.getElementById("dactyl-commandline").scrollWidth + "px";
-
- this._minHeight = Math.max(this._minHeight,
- this._win.scrollY + this._divNodes.completions.getBoundingClientRect().bottom);
-
- if (this._container.collapsed)
- this._div.style.minWidth = "";
-
+ CONTEXT_LINES: 2,
+
+ init: function init(frame) {
+ this.frame = frame;
+
+ this.doc = frame.contentDocument;
+ this.win = frame.contentWindow;
+ this.body = this.doc.body;
+ this.container = frame.parentNode;
+
+ highlight.highlightNode(this.doc.body, "Comp");
+
+ this._onResize = Timer(20, 400, function _onResize(event) {
+ if (this.visible)
+ this.onResize(event);
+ }, this);
+ this._resize = Timer(20, 400, function _resize(flags) {
+ if (this.visible)
+ this.resize(flags);
+ }, this);
+
+ DOM(this.win).resize(this._onResize.closure.tell);
+ },
+
+ get rootXML() <e4x>
+ <div highlight="Normal" style="white-space: nowrap" key="root">
+ <div key="wrapper">
+ <div highlight="Completions" key="noCompletions"><span highlight="Title">{_("completion.noCompletions")}</span></div>
+ <div key="completions"/>
+ </div>
+
+ <div highlight="Completions">{
+ template.map(util.range(0, options["maxitems"] * 2), function (i)
+ <div highlight="CompItem NonText"><li>~</li></div>)
+ }</div>
+ </div>
+ </e4x>.elements(),
+
+ get itemCount() this.context.contextList.reduce(function (acc, ctxt) acc + ctxt.items.length, 0),
+
+ get visible() !this.container.collapsed,
+ set visible(val) this.container.collapsed = !val,
+
+ get activeGroups() this.context.contextList
+ .filter(function (c) c.items.length || c.message || c.incomplete)
+ .map(this.getGroup, this),
+
+ get selected() let (g = this.selectedGroup) g && g.selectedIdx != null
+ ? [g.context, g.selectedIdx] : null,
+
+ getRelativeItem: function getRelativeItem(offset, tuple, noWrap) {
+ let groups = this.activeGroups;
+ if (!groups.length)
+ return null;
+
+ let group = this.selectedGroup || groups[0];
+ let start = group.selectedIdx || 0;
+ if (tuple === null) { // Kludge.
+ if (offset > 0)
+ tuple = [this.activeGroups[0], -1];
+ else {
+ let group = this.activeGroups.slice(-1)[0];
+ tuple = [group, group.itemCount];
+ }
+ }
+ if (tuple)
+ [group, start] = tuple;
+
+ group = this.getGroup(group);
+
+ start = (group.offsets.start + start + offset);
+ if (!noWrap)
+ start %= this.itemCount || 1;
+ if (start < 0 && (!noWrap || arguments[1] === null))
+ start += this.itemCount;
+
+ if (noWrap && offset > 0) {
+ // Check if we've passed any incomplete contexts
+
+ let i = groups.indexOf(group);
+ for (; i < groups.length; i++) {
+ let end = groups[i].offsets.start + groups[i].itemCount;
+ if (start >= end && groups[i].context.incomplete)
+ return [groups[i].context, start - groups[i].offsets.start];
+
+ if (start >= end);
+ break;
+ }
+ }
+
+ if (start < 0 || start >= this.itemCount)
+ return null;
+
+ group = array.nth(groups, function (g) let (i = start - g.offsets.start) i >= 0 && i < g.itemCount, 0)
+ return [group.context, start - group.offsets.start];
+ },
+
+ getRelativePage: function getRelativePage(offset, tuple, noWrap) {
+ offset *= this.maxItems;
+ // Try once with wrapping disabled.
+ let res = this.getRelativeItem(offset, tuple, true);
+
+ if (!res) {
+ // Wrapped.
+ let sign = offset / Math.abs(offset);
+
+ let off = this.getOffset(tuple === null ? null : tuple || this.selected);
+ if (off == null)
+ // Unselected. Defer to getRelativeItem.
+ res = this.getRelativeItem(offset, null, noWrap);
+ else if (~[0, this.itemCount - 1].indexOf(off))
+ // At start or end. Jump to other end.
+ res = this.getRelativeItem(sign, null, noWrap);
+ else
+ // Wrapped. Go to beginning or end.
+ res = this.getRelativeItem(-sign, null);
+ }
+ return res;
+ },
+
+ /**
+ * Initializes the ItemList for use with a new root completion
+ * context.
+ *
+ * @param {CompletionContext} context The new root context.
+ */
+ open: function open(context) {
+ this.context = context;
+ this.nodes = {};
+ this.container.height = 0;
+ this.minHeight = 0;
+ this.maxItems = options["maxitems"];
+
+ DOM(this.rootXML, this.doc, this.nodes)
+ .appendTo(DOM(this.body).empty());
+
+ this.update();
+ },
+
+ /**
+ * Updates the absolute result indices of all groups after
+ * results have changed.
+ * @private
+ */
+ updateOffsets: function updateOffsets() {
+ let total = this.itemCount;
+ let count = 0;
+ for (let group in values(this.activeGroups)) {
+ group.offsets = { start: count, end: total - count - group.itemCount };
+ count += group.itemCount;
+ }
+ },
+
+ /**
+ * Updates the set and state of active groups for a new set of
+ * completion results.
+ */
+ update: function update() {
+ DOM(this.nodes.completions).empty();
+
+ let container = DOM(this.nodes.completions);
+ let groups = this.activeGroups;
+ for (let group in values(groups)) {
+ group.reset();
+ container.append(group.nodes.root);
+ }
+
+ this.updateOffsets();
+
+ DOM(this.nodes.noCompletions).toggle(!groups.length);
+
+ this.startPos = null;
+ this.select(groups[0] && groups[0].context, null);
+
+ this._resize.tell();
+ },
+
+ /**
+ * Updates the group for *context* after an asynchronous update
+ * push.
+ *
+ * @param {CompletionContext} context The context which has
+ * changed.
+ */
+ updateContext: function updateContext(context) {
+ let group = this.getGroup(context);
+ this.updateOffsets();
+
+ if (~this.activeGroups.indexOf(group))
+ group.update();
+ else {
+ DOM(group.nodes.root).remove();
+ if (this.selectedGroup == group)
+ this.selectedGroup = null;
+ }
+
+ let g = this.selectedGroup;
+ this.select(g, g && g.selectedIdx);
+ },
+
+ /**
+ * Updates the DOM to reflect the current state of all groups.
+ * @private
+ */
+ draw: function draw() {
+ for (let group in values(this.activeGroups))
+ group.draw();
+
+ // We need to collect all of the rescrolling functions in
+ // one go, as the height calculation that they need to do
+ // would force a reflow after each DOM modification.
+ this.activeGroups.filter(function (g) !g.collapsed)
+ .map(function (g) g.rescrollFunc)
+ .forEach(call);
+
+ if (!this.selected)
+ this.win.scrollTo(0, 0);
+
+ this._resize.tell(ItemList.RESIZE_BRIEF);
+ },
+
+ onResize: function onResize() {
+ if (this.selectedGroup)
+ this.selectedGroup.rescrollFunc();
+ },
+
+ minHeight: 0,
+
+ /**
+ * Resizes the list after an update.
+ * @private
+ */
+ resize: function resize(flags) {
+ let { completions, root } = this.nodes;
+
+ if (!this.visible)
+ root.style.minWidth = document.getElementById("dactyl-commandline").scrollWidth + "px";
+
+ let { minHeight } = this;
+ if (mow.visible && this.isAboveMow) // Kludge.
+ minHeight -= mow.wantedHeight;
+
+ let needed = this.win.scrollY + DOM(completions).rect.bottom;
+ this.minHeight = Math.max(minHeight, needed);
+
+ if (!this.visible)
+ root.style.minWidth = "";
+
+ let height = this.visible ? parseFloat(this.container.height) : 0;
+ if (this.minHeight <= minHeight || !mow.visible)
+ this.container.height = Math.min(this.minHeight,
+ height + config.outputHeight - mow.spaceNeeded);
+ else {
// FIXME: Belongs elsewhere.
- mow.resize(false, Math.max(0, this._minHeight - this._container.height));
-
- this._container.height = this._minHeight;
- this._container.height -= mow.spaceNeeded;
+ mow.resize(false, Math.max(0, this.minHeight - this.container.height));
+
+ this.container.height = this.minHeight - mow.spaceNeeded;
mow.resize(false);
this.timeout(function () {
- this._container.height -= mow.spaceNeeded;
- });
- },
-
- _getCompletion: function _getCompletion(index) this._completionElements.snapshotItem(index - this._startIndex),
-
- _init: function _init() {
- this._div = this._dom(
- <div class="ex-command-output" highlight="Normal" style="white-space: nowrap">
- <div highlight="Completions" key="noCompletions"><span highlight="Title">{_("completion.noCompletions")}</span></div>
- <div key="completions"/>
+ this.container.height -= mow.spaceNeeded;
+ });
+ }
+ },
+
+ /**
+ * Selects the item at the given *group* and *index*.
+ *
+ * @param {CompletionContext|[CompletionContext,number]} *group* The
+ * completion context to select, or a tuple specifying the
+ * context and item index.
+ * @param {number} index The item index in *group* to select.
+ * @param {number} position If non-null, try to position the
+ * selected item at the *position*th row from the top of
+ * the screen. Note that at least {@link #CONTEXT_LINES}
+ * lines will be visible above and below the selected item
+ * unless there aren't enough results to make this possible.
+ * @optional
+ */
+ select: function select(group, index, position) {
+ if (isArray(group))
+ [group, index] = group;
+
+ group = this.getGroup(group);
+
+ if (this.selectedGroup && (!group || group != this.selectedGroup))
+ this.selectedGroup.selectedIdx = null;
+
+ this.selectedGroup = group;
+
+ if (group)
+ group.selectedIdx = index;
+
+ let groups = this.activeGroups;
+
+ if (position != null || !this.startPos && groups.length)
+ this.startPos = [group || groups[0], position || 0];
+
+ if (groups.length) {
+ group = group || groups[0];
+ let idx = groups.indexOf(group);
+
+ let start = this.startPos[0].getOffset(this.startPos[1]);
+ if (group) {
+ let idx = group.selectedIdx || 0;
+ let off = group.getOffset(idx);
+
+ start = Math.constrain(start,
+ off + Math.min(this.CONTEXT_LINES, group.itemCount - idx + group.offsets.end)
+ - this.maxItems + 1,
+ off - Math.min(this.CONTEXT_LINES, idx + group.offsets.start));
+ }
+
+ let count = this.maxItems;
+ for (let group in values(groups)) {
+ let off = Math.max(0, start - group.offsets.start);
+
+ group.count = Math.constrain(group.itemCount - off, 0, count);
+ count -= group.count;
+
+ group.collapsed = group.offsets.start >= start + this.maxItems
+ || group.offsets.start + group.itemCount < start;
+
+ group.range = ItemList.Range(off, off + group.count);
+
+ if (!startPos)
+ var startPos = [group, group.range.start];
+ }
+ this.startPos = startPos;
+ }
+ this.draw();
+ },
+
+ /**
+ * Returns an ItemList group for the given completion context,
+ * creating one if necessary.
+ *
+ * @param {CompletionContext} context
+ * @returns {ItemList.Group}
+ */
+ getGroup: function getGroup(context)
+ context instanceof ItemList.Group ? context
+ : context && context.getCache("itemlist-group",
+ bind("Group", ItemList, this, context)),
+
+ getOffset: function getOffset(tuple) tuple && this.getGroup(tuple[0]).getOffset(tuple[1])
+}, {
+ RESIZE_BRIEF: 1 << 0,
+
+ WAITING_MESSAGE: _("completion.generating"),
+
+ Group: Class("ItemList.Group", {
+ init: function init(parent, context) {
+ this.parent = parent;
+ this.context = context;
+ this.offsets = {};
+ this.range = ItemList.Range(0, 0);
+ },
+
+ get rootXML()
+ <div key="root" highlight="CompGroup">
<div highlight="Completions">
- {
- template.map(util.range(0, options["maxitems"] * 2), function (i)
- <div highlight="CompItem NonText">
- <li>~</li>
- </div>)
- }
- </div>
- </div>, this._divNodes);
- this._doc.body.replaceChild(this._div, this._doc.body.firstChild);
- util.scrollIntoView(this._div, 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") }
+ { this.context.createRow(this.context.title || [], "CompTitle") }
</div>
<div highlight="CompTitleSep"/>
- <div key="message" highlight="CompMsg"/>
+ <div key="contents">
<div key="up" highlight="CompLess"/>
+ <div key="message" highlight="CompMsg">{this.context.message}</div>
+ <div key="itemsContainer" class="completion-items-container">
<div key="items" highlight="Completions"/>
+ </div>
<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);
-
- this.timeout(this._autoSize);
- },
-
- /**
- * 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"].
- */
- _fill: function _fill(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) 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)
+ </div>
+ </div>,
+
+ get doc() this.parent.doc,
+ get win() this.parent.win,
+ get maxItems() this.parent.maxItems,
+
+ get itemCount() this.context.items.length,
+
+ /**
+ * Returns a function which will update the scroll offsets
+ * and heights of various DOM members.
+ * @private
+ */
+ get rescrollFunc() {
+ let container = this.nodes.itemsContainer;
+ let pos = DOM(container).rect.top;
+ let start = DOM(this.getRow(this.range.start)).rect.top;
+ let height = DOM(this.getRow(this.range.end - 1)).rect.bottom - start || 0;
+ let scroll = start + container.scrollTop - pos;
+
+ let win = this.win;
+ let row = this.selectedRow;
+ if (row && this.parent.minHeight) {
+ let { rect } = DOM(this.selectedRow);
+ var scrollY = this.win.scrollY + rect.bottom - this.win.innerHeight;
+ }
+
+ return function () {
+ container.style.height = height + "px";
+ container.scrollTop = scroll;
+ if (scrollY != null)
+ win.scrollTo(0, Math.max(scrollY, 0));
+ }
+ },
+
+ /**
+ * Reset this group for use with a new set of results.
+ */
+ reset: function reset() {
+ this.nodes = {};
+ this.generatedRange = ItemList.Range(0, 0);
+
+ DOM.fromXML(this.rootXML, this.doc, this.nodes);
+ },
+
+ /**
+ * Update this group after an asynchronous results push.
+ */
+ update: function update() {
+ this.generatedRange = ItemList.Range(0, 0);
+ DOM(this.nodes.items).empty();
+
+ if (this.context.message)
+ DOM(this.nodes.message).empty().append(<>{this.context.message}</>);
+
+ if (!this.selectedIdx > this.itemCount)
+ this.selectedIdx = null;
+ },
+
+ /**
+ * Updates the DOM to reflect the current state of this
+ * group.
+ * @private
+ */
+ draw: function draw() {
+ DOM(this.nodes.contents).toggle(!this.collapsed);
+ if (this.collapsed)
return;
- haveCompletions = true;
-
- let root = nodes.root;
- let items = nodes.items;
- let [start, end, waiting] = getRows(context);
-
- if (context.message) {
- nodes.message.textContent = "";
- nodes.message.appendChild(this._dom(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 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";
- if (start == end) {
- nodes.up.style.display = "none";
- nodes.down.style.display = "none";
- }
- }, this);
-
- this._divNodes.noCompletions.style.display = haveCompletions ? "none" : "block";
-
- this._completionElements = util.evaluateXPath("//xhtml:div[@dactyl:highlight='CompItem']", this._doc);
-
- return true;
- },
-
- clear: function clear() { this.setItems(); this._doc.body.innerHTML = ""; },
- get visible() !this._container.collapsed,
- set visible(val) this._container.collapsed = !val,
-
- reset: function reset(brief) {
- this._startIndex = this._endIndex = this._selIndex = -1;
- this._div = null;
- if (!brief)
- this.selectItem(-1);
- },
-
- // if @param selectedItem is given, show the list and select that item
- setItems: function setItems(newItems, selectedItem) {
- if (this._selItem > -1)
- this._getCompletion(this._selItem).removeAttribute("selected");
- if (this._container.collapsed) {
- this._minHeight = 0;
- this._container.height = 0;
- }
- this._startIndex = this._endIndex = this._selIndex = -1;
- this._items = newItems;
- this.reset(true);
- if (typeof selectedItem == "number") {
- this.selectItem(selectedItem);
- this.visible = true;
- }
- },
-
- // select index, refill list if necessary
- selectItem: function selectItem(index) {
- //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;
- }
+
+ DOM(this.nodes.message).toggle(this.context.message && this.range.start == 0);
+ DOM(this.nodes.waiting).toggle(this.context.incomplete && this.range.end <= this.itemCount);
+ DOM(this.nodes.up).toggle(this.range.start > 0);
+ DOM(this.nodes.down).toggle(this.range.end < this.itemCount);
+
+ if (!this.generatedRange.contains(this.range)) {
+ if (this.generatedRange.end == 0)
+ var [start, end] = this.range;
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");
- if (this._container.height != 0)
- util.scrollIntoView(this._getCompletion(index));
- }
-
- //if (index == 0)
- // this.start = now;
- //if (index == Math.min(len - 1, 100))
- // util.dump({ time: Date.now() - this.start });
- },
-
- onKeyPress: function onKeyPress(event) false
-}, {
- WAITING_MESSAGE: _("completion.generating")
-});
+ start = this.range.start - (this.range.start <= this.generatedRange.start
+ ? this.maxItems / 2 : 0);
+ end = this.range.end + (this.range.end > this.generatedRange.end
+ ? this.maxItems / 2 : 0);
+ }
+
+ let range = ItemList.Range(Math.max(0, start - start % 2),
+ Math.min(this.itemCount, end));
+
+ let first;
+ for (let [i, row] in this.context.getRows(this.generatedRange.start,
+ this.generatedRange.end,
+ this.doc))
+ if (!range.contains(i))
+ DOM(row).remove();
+ else if (!first)
+ first = row;
+
+ let container = DOM(this.nodes.items);
+ let before = first ? DOM(first).closure.before
+ : DOM(this.nodes.items).closure.append;
+
+ for (let [i, row] in this.context.getRows(range.start, range.end,
+ this.doc)) {
+ if (i < this.generatedRange.start)
+ before(row);
+ else if (i >= this.generatedRange.end)
+ container.append(row);
+ if (i == this.selectedIdx)
+ this.selectedIdx = this.selectedIdx;
+ }
+
+ this.generatedRange = range;
+ }
+ },
+
+ getRow: function getRow(idx) this.context.getRow(idx, this.doc),
+
+ getOffset: function getOffset(idx) this.offsets.start + (idx || 0),
+
+ get selectedRow() this.getRow(this._selectedIdx),
+
+ get selectedIdx() this._selectedIdx,
+ set selectedIdx(idx) {
+ if (this.selectedRow && this._selectedIdx != idx)
+ DOM(this.selectedRow).attr("selected", null);
+
+ this._selectedIdx = idx;
+
+ if (this.selectedRow)
+ DOM(this.selectedRow).attr("selected", true);
+ }
+ }),
+
+ Range: Class.Memoize(function () {
+ let Range = Struct("ItemList.Range", "start", "end");
+ update(Range.prototype, {
+ contains: function contains(idx)
+ typeof idx == "number" ? idx >= this.start && idx < this.end
+ : this.contains(idx.start) &&
+ idx.end >= this.start && idx.end <= this.end
+ });
+ return Range;
+ })
+});
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/dactyl.js b/common/content/dactyl.js
--- a/common/content/dactyl.js
+++ b/common/content/dactyl.js
@@ -1,15 +1,15 @@
// Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
// Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
/** @scope modules */
default xml namespace = XHTML;
XML.ignoreWhitespace = false;
XML.prettyPrinting = false;
var EVAL_ERROR = "__dactyl_eval_error";
@@ -24,28 +24,24 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
Object.defineProperty(window, "liberator", prop);
Object.defineProperty(modules, "liberator", prop);
this.commands = {};
this.indices = {};
this.modules = modules;
this._observers = {};
util.addObserver(this);
- this.commands["dactyl.help"] = function (event) {
- let elem = event.originalTarget;
- dactyl.help(elem.getAttribute("tag") || elem.textContent);
- };
this.commands["dactyl.restart"] = function (event) {
dactyl.restart();
};
styles.registerSheet("resource://dactyl-skin/dactyl.css");
this.cleanups = [];
- this.cleanups.push(util.overlayObject(window, {
+ this.cleanups.push(overlay.overlayObject(window, {
focusAndSelectUrlBar: function focusAndSelectUrlBar() {
switch (options.get("strictfocus").getKey(document.documentURIObject || util.newURI(document.documentURI), "moderate")) {
case "laissez-faire":
if (!Events.isHidden(window.gURLBar, true))
return focusAndSelectUrlBar.superapply(this, arguments);
default:
// Evil. Ignore.
}
@@ -55,20 +51,26 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
cleanup: function () {
for (let cleanup in values(this.cleanups))
cleanup.call(this);
delete window.dactyl;
delete window.liberator;
+ // Prevents box ordering bugs after our stylesheet is removed.
+ styles.system.add("cleanup-sheet", config.styleableChrome, <![CDATA[
+ #TabsToolbar tab { display: none; }
+ ]]>);
styles.unregisterSheet("resource://dactyl-skin/dactyl.css");
- },
+ DOM('#TabsToolbar tab', document).style.display;
+ },
destroy: function () {
+ this.observe.unregister();
autocommands.trigger("LeavePre", {});
dactyl.triggerObserver("shutdown", null);
util.dump("All dactyl modules destroyed\n");
autocommands.trigger("Leave", {});
},
// initially hide all GUI elements, they are later restored unless the user
// has :set go= or something similar in his config
@@ -92,93 +94,93 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
for (let mod in values(modules.moduleList.reverse())) {
mod.stale = true;
if ("cleanup" in mod)
this.trapErrors("cleanup", mod, reason);
if ("destroy" in mod)
this.trapErrors("destroy", mod, reason);
}
- for (let mod in values(modules.ownPropertyValues.reverse()))
- if (mod instanceof Class && "INIT" in mod && "cleanup" in mod.INIT)
- this.trapErrors(mod.cleanup, mod, dactyl, modules, window, reason);
+ modules.moduleManager.initDependencies("cleanup");
for (let name in values(Object.getOwnPropertyNames(modules).reverse()))
try {
delete modules[name];
}
catch (e) {}
modules.__proto__ = {};
}
},
- /** @property {string} The name of the current user profile. */
- profileName: Class.memoize(function () {
- // NOTE: services.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.
-
- let dir = services.directory.get("ProfD", Ci.nsIFile);
- for (let prof in iter(services.profile.profiles))
- if (prof.QueryInterface(Ci.nsIToolkitProfile).rootDir.path === dir.path)
- return prof.name;
- return "unknown";
- }),
+ signals: {
+ "io.source": function ioSource(context, file, modTime) {
+ if (context.INFO)
+ help.flush("help/plugins.xml", modTime);
+ }
+ },
+
+ profileName: deprecated("config.profileName", { get: function profileName() config.profileName }),
/**
* @property {Modes.Mode} The current main mode.
* @see modes#mainModes
*/
mode: deprecated("modes.main", {
get: function mode() modes.main,
set: function mode(val) modes.main = val
}),
- get menuItems() {
- function dispatch(node, name) {
- let event = node.ownerDocument.createEvent("Events");
- event.initEvent(name, false, false);
- node.dispatchEvent(event);
- }
-
+ getMenuItems: function getMenuItems(targetPath) {
function addChildren(node, parent) {
+ DOM(node).createContents();
+
if (~["menu", "menupopup"].indexOf(node.localName) && node.children.length)
- dispatch(node, "popupshowing");
+ DOM(node).popupshowing({ bubbles: false });
for (let [, item] in Iterator(node.childNodes)) {
if (item.childNodes.length == 0 && item.localName == "menuitem"
&& !item.hidden
&& !/rdf:http:/.test(item.getAttribute("label"))) { // FIXME
item.dactylPath = parent + item.getAttribute("label");
+ if (!targetPath || targetPath.indexOf(item.dactylPath) == 0)
items.push(item);
}
else {
let path = parent;
if (item.localName == "menu")
path += item.getAttribute("label") + ".";
+ if (!targetPath || targetPath.indexOf(path) == 0)
addChildren(item, path);
}
}
}
let items = [];
addChildren(document.getElementById(config.guioptions["m"][1]), "");
return items;
},
+ get menuItems() this.getMenuItems(),
+
// Global constants
CURRENT_TAB: "here",
NEW_TAB: "tab",
NEW_BACKGROUND_TAB: "background-tab",
NEW_WINDOW: "window",
- forceNewTab: false,
- forceNewWindow: false,
+ forceBackground: null,
+ forceTarget: null,
+
+ get forceOpen() ({ background: this.forceBackground,
+ target: this.forceTarget }),
+ set forceOpen(val) {
+ for (let [k, v] in Iterator({ background: "forceBackground", target: "forceTarget" }))
+ if (k in val)
+ this[v] = val[k];
+ },
version: deprecated("config.version", { get: function version() config.version }),
/**
* @property {Object} The map of command-line options. These are
* specified in the argument to the host application's -{config.name}
* option. E.g. $ firefox -pentadactyl '+u=/tmp/rcfile ++noplugin'
* Supported options:
@@ -197,30 +199,29 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
preCommands: null,
/** @property An Ex command to run after all initialization has been performed. */
postCommands: null
},
registerObserver: function registerObserver(type, callback, weak) {
if (!(type in this._observers))
this._observers[type] = [];
- this._observers[type].push(weak ? Cu.getWeakReference(callback) : { get: function () callback });
- },
+ this._observers[type].push(weak ? util.weakReference(callback) : { get: function () callback });
+ },
registerObservers: function registerObservers(obj, prop) {
for (let [signal, func] in Iterator(obj[prop || "signals"]))
this.registerObserver(signal, obj.closure(func), false);
},
unregisterObserver: function unregisterObserver(type, callback) {
if (type in this._observers)
this._observers[type] = this._observers[type].filter(function (c) c.get() != callback);
},
- // TODO: "zoom": if the zoom value of the current buffer changed
applyTriggerObserver: function triggerObserver(type, args) {
if (type in this._observers)
this._observers[type] = this._observers[type].filter(function (callback) {
if (callback.get()) {
try {
try {
callback.get().apply(null, args);
}
@@ -244,49 +245,51 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
addUsageCommand: function (params) {
function keys(item) (item.names || [item.name]).concat(item.description, item.columns || []);
let name = commands.add(params.name, params.description,
function (args) {
let results = array(params.iterate(args))
.sort(function (a, b) String.localeCompare(a.name, b.name));
- let filters = args.map(function (arg) util.regexp("\\b" + util.regexp.escape(arg) + "\\b", "i"));
+ let filters = args.map(function (arg) let (re = util.regexp.escape(arg))
+ util.regexp("\\b" + re + "\\b|(?:^|[()\\s])" + re + "(?:$|[()\\s])", "i"));
if (filters.length)
results = results.filter(function (item) filters.every(function (re) keys(item).some(re.closure.test)));
commandline.commandOutput(
template.usage(results, params.format));
},
{
argCount: "*",
completer: function (context, args) {
context.keys.text = util.identity;
context.keys.description = function () seen[this.text] + /*L*/" matching items";
+ context.ignoreCase = true;
let seen = {};
context.completions = array(keys(item).join(" ").toLowerCase().split(/[()\s]+/)
for (item in params.iterate(args)))
- .flatten().filter(function (w) /^\w[\w-_']+$/.test(w))
+ .flatten()
.map(function (k) {
seen[k] = (seen[k] || 0) + 1;
return k;
}).uniq();
},
options: params.options || []
});
if (params.index)
this.indices[params.index] = function () {
let results = array((params.iterateIndex || params.iterate).call(params, commands.get(name).newArgs()))
.array.sort(function (a, b) String.localeCompare(a.name, b.name));
- let tags = services["dactyl:"].HELP_TAGS;
+ let haveTag = Set.has(help.tags);
for (let obj in values(results)) {
let res = dactyl.generateHelp(obj, null, null, true);
- if (!Set.has(tags, obj.helpTag))
+ if (!haveTag(obj.helpTag))
res[1].@tag = obj.helpTag;
yield res;
}
};
},
/**
@@ -298,17 +301,17 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
this.triggerObserver("beep");
if (options["visualbell"]) {
let elems = {
bell: document.getElementById("dactyl-bell"),
strut: document.getElementById("dactyl-bell-strut")
};
XML.ignoreWhitespace = true;
if (!elems.bell)
- util.overlayWindow(window, {
+ overlay.overlayWindow(window, {
objects: elems,
prepend: <>
<window id={document.documentElement.id} xmlns={XUL}>
<hbox style="display: none" highlight="Bell" id="dactyl-bell" key="bell"/>
</window>
</>,
append: <>
<window id={document.documentElement.id} xmlns={XUL}>
@@ -330,26 +333,30 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
},
/**
* Reads a string from the system clipboard.
*
* This is same as Firefox's readFromClipboard function, but is needed for
* apps like Thunderbird which do not provide it.
*
+ * @param {string} which Which clipboard to write to. Either
+ * "global" or "selection". If not provided, both clipboards are
+ * updated.
+ * @optional
* @returns {string}
*/
- clipboardRead: function clipboardRead(getClipboard) {
- try {
- const clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
- const transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
-
+ clipboardRead: function clipboardRead(which) {
+ try {
+ const { clipboard } = services;
+
+ let transferable = services.Transferable();
transferable.addDataFlavor("text/unicode");
- let source = clipboard[getClipboard || !clipboard.supportsSelectionClipboard() ?
+ let source = clipboard[which == "global" || !clipboard.supportsSelectionClipboard() ?
"kGlobalClipboard" : "kSelectionClipboard"];
clipboard.getData(transferable, source);
let str = {}, len = {};
transferable.getTransferData("text/unicode", str, len);
if (str)
return str.value.QueryInterface(Ci.nsISupportsString)
@@ -358,22 +365,29 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
catch (e) {}
return null;
},
/**
* Copies a string to the system clipboard. If *verbose* is specified the
* copied string is also echoed to the command line.
*
- * @param {string} str
- * @param {boolean} verbose
- */
- clipboardWrite: function clipboardWrite(str, verbose) {
- const clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
- clipboardHelper.copyString(str);
+ * @param {string} str The string to write.
+ * @param {boolean} verbose If true, the user is notified of the copied data.
+ * @param {string} which Which clipboard to write to. Either
+ * "global" or "selection". If not provided, both clipboards are
+ * updated.
+ * @optional
+ */
+ clipboardWrite: function clipboardWrite(str, verbose, which) {
+ if (which == null || which == "selection" && !services.clipboard.supportsSelectionClipboard())
+ services.clipboardHelper.copyString(str);
+ else
+ services.clipboardHelper.copyStringToClipboard(str,
+ services.clipboard["k" + util.capitalize(which) + "Clipboard"]);
if (verbose) {
let message = { message: _("dactyl.yank", str) };
try {
message.domains = [util.newURI(str).host];
}
catch (e) {};
dactyl.echomsg(message);
@@ -401,18 +415,20 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
*
* @param {string} str The message to output.
* @param {number} flags These control the multi-line message behavior.
* See {@link CommandLine#echo}.
*/
echoerr: function echoerr(str, flags) {
flags |= commandline.APPEND_TO_MESSAGES;
- if (isinstance(str, ["DOMException", "Error", "Exception"]) || isinstance(str, ["XPCWrappedNative_NoHelper"]) && /^\[Exception/.test(str))
+ if (isinstance(str, ["DOMException", "Error", "Exception", ErrorBase])
+ || isinstance(str, ["XPCWrappedNative_NoHelper"]) && /^\[Exception/.test(str))
dactyl.reportError(str);
+
if (isObject(str) && "echoerr" in str)
str = str.echoerr;
else if (isinstance(str, ["Error", FailedAssertion]) && str.fileName)
str = <>{str.fileName.replace(/^.* -> /, "")}: {str.lineNumber}: {str}</>;
if (options["errorbells"])
dactyl.beep();
@@ -460,37 +476,44 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
* should be loaded.
*/
loadScript: function (uri, context) {
JSMLoader.loadSubScript(uri, context, File.defaultEncoding);
},
userEval: function (str, context, fileName, lineNumber) {
let ctxt;
- if (jsmodules.__proto__ != window)
+ if (jsmodules.__proto__ != window && jsmodules.__proto__ != XPCNativeWrapper(window) &&
+ jsmodules.isPrototypeOf(context))
str = "with (window) { with (modules) { (this.eval || eval)(" + str.quote() + ") } }";
let info = contexts.context;
if (fileName == null)
- if (info && info.file[0] !== "[")
+ if (info)
({ file: fileName, line: lineNumber, context: ctxt }) = info;
- if (!context && fileName && fileName[0] !== "[")
+ if (fileName && fileName[0] == "[")
+ fileName = "dactyl://command-line/";
+ else if (!context)
context = ctxt || _userContext;
if (isinstance(context, ["Sandbox"]))
return Cu.evalInSandbox(str, context, "1.8", fileName, lineNumber);
- else
- try {
+
if (!context)
context = userContext || ctxt;
+ if (services.has("dactyl") && services.dactyl.evalInContext)
+ return services.dactyl.evalInContext(str, context, fileName, lineNumber);
+
+ try {
context[EVAL_ERROR] = null;
context[EVAL_STRING] = str;
context[EVAL_RESULT] = null;
+
this.loadScript("resource://dactyl-content/eval.js", context);
if (context[EVAL_ERROR]) {
try {
context[EVAL_ERROR].fileName = info.file;
context[EVAL_ERROR].lineNumber += info.line;
}
catch (e) {}
throw context[EVAL_ERROR];
@@ -538,30 +561,18 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
throw FailedAssertion(_("dactyl.notCommand", config.appName, args.commandString));
res = res && command.execute(args, modifiers);
}
return res;
},
focus: function focus(elem, flags) {
- flags = flags || services.focus.FLAG_BYMOUSE;
- try {
- if (elem instanceof Document)
- elem = elem.defaultView;
- if (elem instanceof Element)
- services.focus.setFocus(elem, flags);
- else if (elem instanceof Window)
- services.focus.focusedWindow = elem;
- }
- catch (e) {
- util.dump(elem);
- util.reportError(e);
- }
- },
+ DOM(elem).focus(flags);
+ },
/**
* Focuses the content window.
*
* @param {boolean} clearFocusedElement Remove focus from any focused
* element.
*/
focusContent: function focusContent(clearFocusedElement) {
@@ -617,416 +628,50 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
* Returns whether this Dactyl extension supports *feature*.
*
* @param {string} feature The feature name.
* @returns {boolean}
*/
has: function (feature) Set.has(config.features, feature),
/**
- * Returns the URL of the specified help *topic* if it exists.
- *
- * @param {string} topic The help topic to look up.
- * @param {boolean} consolidated Whether to search the consolidated help page.
- * @returns {string}
- */
- findHelp: function (topic, consolidated) {
- if (!consolidated && topic in services["dactyl:"].FILE_MAP)
- return topic;
- let items = completion._runCompleter("help", topic, null, !!consolidated).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
*/
initDocument: function initDocument(doc) {
try {
if (doc.location.protocol === "dactyl:") {
dactyl.initHelp();
config.styleHelp();
}
}
catch (e) {
util.reportError(e);
}
},
+ help: deprecated("help.help", { get: function help() modules.help.closure.help }),
+ findHelp: deprecated("help.findHelp", { get: function findHelp() help.closure.findHelp }),
+
/**
* @private
* Initialize the help system.
*/
- initHelp: function (force) {
- // Waits for the add-on to become available, if necessary.
- config.addon;
- config.version;
-
- if (force || !this.helpInitialized) {
- if ("noscriptOverlay" in window) {
- noscriptOverlay.safeAllow("chrome-data:", true, false);
+ initHelp: function initHelp() {
+ if ("noscriptOverlay" in window)
noscriptOverlay.safeAllow("dactyl:", true, false);
- }
-
- // Find help and overlay files with the given name.
- let findHelpFile = function findHelpFile(file) {
- let result = [];
- for (let [, namespace] in Iterator(namespaces)) {
- let url = ["dactyl://", namespace, "/", 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;
- };
- // Find the tags in the document.
- let addTags = function addTags(file, doc) {
- for (let elem in util.evaluateXPath("//@tag|//dactyl:tags/text()|//dactyl:tag/text()", doc))
- for (let tag in values((elem.value || elem.textContent).split(/\s+/)))
- tagMap[tag] = file;
- };
-
- let namespaces = ["locale-local", "locale"];
- services["dactyl:"].init({});
-
- let tagMap = services["dactyl:"].HELP_TAGS;
- let fileMap = services["dactyl:"].FILE_MAP;
- let overlayMap = services["dactyl:"].OVERLAY_MAP;
-
- // Scrape the list of help files from all.xml
- // Manually process main and overlay files, since XSLTProcessor and
- // XMLHttpRequest don't allow access to chrome documents.
- tagMap["all"] = tagMap["all.xml"] = "all";
- tagMap["versions"] = tagMap["versions.xml"] = "versions";
- let files = findHelpFile("all").map(function (doc)
- [f.value for (f in util.evaluateXPath("//dactyl:include/@href", doc))]);
-
- // Scrape the tags from the rest of the help files.
- array.flatten(files).forEach(function (file) {
- tagMap[file + ".xml"] = file;
- findHelpFile(file).forEach(function (doc) {
- addTags(file, doc);
- });
- });
-
- // Process plugin help entries.
- XML.ignoreWhiteSpace = XML.prettyPrinting = false;
-
- let body = XML();
- for (let [, context] in Iterator(plugins.contexts))
- try {
- let info = contexts.getDocs(context);
- if (info instanceof XML) {
- if (info.*.@lang.length()) {
- let lang = config.bestLocale(String(a) for each (a in info.*.@lang));
-
- info.* = info.*.(function::attribute("lang").length() == 0 || @lang == lang);
-
- for each (let elem in info.NS::info)
- for each (let attr in ["@name", "@summary", "@href"])
- if (elem[attr].length())
- info[attr] = elem[attr];
- }
- body += <h2 xmlns={NS.uri} tag={info.@name + '-plugin'}>{info.@summary}</h2> +
- info;
- }
- }
- catch (e) {
- util.reportError(e);
- }
-
- let help =
- '<?xml version="1.0"?>\n' +
- '<?xml-stylesheet type="text/xsl" href="dactyl://content/help.xsl"?>\n' +
- '<!DOCTYPE document SYSTEM "resource://dactyl-content/dactyl.dtd">\n' +
- <document xmlns={NS}
- name="plugins" title={config.appName + " Plugins"}>
- <h1 tag="using-plugins">{_("help.title.Using Plugins")}</h1>
- <toc start="2"/>
-
- {body}
- </document>.toXMLString();
- fileMap["plugins"] = function () ['text/xml;charset=UTF-8', help];
-
- fileMap["versions"] = function () {
- let NEWS = util.httpGet(config.addon.getResourceURI("NEWS").spec,
- { mimeType: "text/plain;charset=UTF-8" })
- .responseText;
-
- let re = util.regexp(<![CDATA[
- ^ (?P<comment> \s* # .*\n)
-
- | ^ (?P<space> \s*)
- (?P<char> [-•*+]) \ //
- (?P<content> .*\n
- (?: \2\ \ .*\n | \s*\n)* )
-
- | (?P<par>
- (?: ^ [^\S\n]*
- (?:[^-•*+\s] | [-•*+]\S)
- .*\n
- )+
- )
-
- | (?: ^ [^\S\n]* \n) +
- ]]>, "gmxy");
-
- let betas = util.regexp(/\[(b\d)\]/, "gx");
-
- let beta = array(betas.iterate(NEWS))
- .map(function (m) m[1]).uniq().slice(-1)[0];
-
- default xml namespace = NS;
- function rec(text, level, li) {
- XML.ignoreWhitespace = XML.prettyPrinting = false;
-
- let res = <></>;
- let list, space, i = 0;
-
- for (let match in re.iterate(text)) {
- if (match.comment)
- continue;
- else if (match.char) {
- if (!list)
- res += list = <ul/>;
- let li = <li/>;
- li.* += rec(match.content.replace(RegExp("^" + match.space, "gm"), ""), level + 1, li);
- list.* += li;
- }
- else if (match.par) {
- let [, par, tags] = /([^]*?)\s*((?:\[[^\]]+\])*)\n*$/.exec(match.par);
- let t = tags;
- tags = array(betas.iterate(tags)).map(function (m) m[1]);
-
- let group = !tags.length ? "" :
- !tags.some(function (t) t == beta) ? "HelpNewsOld" : "HelpNewsNew";
- if (i === 0 && li) {
- li.@highlight = group;
- group = "";
- }
-
- list = null;
- if (level == 0 && /^.*:\n$/.test(match.par)) {
- let text = par.slice(0, -1);
- res += <h2 tag={"news-" + text}>{template.linkifyHelp(text, true)}</h2>;
- }
- else {
- let [, a, b] = /^(IMPORTANT:?)?([^]*)/.exec(par);
- res += <p highlight={group + " HelpNews"}>{
- !tags.length ? "" :
- <hl key="HelpNewsTag">{tags.join(" ")}</hl>
- }{
- a ? <hl key="HelpWarning">{a}</hl> : ""
- }{
- template.linkifyHelp(b, true)
- }</p>;
- }
- }
- i++;
- }
- for each (let attr in res..@highlight) {
- attr.parent().@NS::highlight = attr;
- delete attr.parent().@highlight;
- }
- return res;
- }
-
- XML.ignoreWhitespace = XML.prettyPrinting = false;
- let body = rec(NEWS, 0);
- for each (let li in body..li) {
- let list = li..li.(@NS::highlight == "HelpNewsOld");
- if (list.length() && list.length() == li..li.(@NS::highlight != "").length()) {
- for each (let li in list)
- li.@NS::highlight = "";
- li.@NS::highlight = "HelpNewsOld";
- }
- }
-
- return ["application/xml",
- '<?xml version="1.0"?>\n' +
- '<?xml-stylesheet type="text/xsl" href="dactyl://content/help.xsl"?>\n' +
- '<!DOCTYPE document SYSTEM "resource://dactyl-content/dactyl.dtd">\n' +
- <document xmlns={NS} xmlns:dactyl={NS}
- name="versions" title={config.appName + " Versions"}>
- <h1 tag="versions news NEWS">{config.appName} Versions</h1>
- <toc start="2"/>
-
- {body}
- </document>.toXMLString()
- ];
- }
- addTags("versions", util.httpGet("dactyl://help/versions").responseXML);
- addTags("plugins", util.httpGet("dactyl://help/plugins").responseXML);
-
- default xml namespace = NS;
-
- overlayMap["index"] = ['text/xml;charset=UTF-8',
- '<?xml version="1.0"?>\n' +
- <overlay xmlns={NS}>{
- template.map(dactyl.indices, function ([name, iter])
- <dl insertafter={name + "-index"}>{
- template.map(iter(), util.identity)
- }</dl>, <>{"\n\n"}</>)
- }</overlay>];
- addTags("index", util.httpGet("dactyl://help-overlay/index").responseXML);
-
- overlayMap["gui"] = ['text/xml;charset=UTF-8',
- '<?xml version="1.0"?>\n' +
- <overlay xmlns={NS}>
- <dl insertafter="dialog-list">{
- template.map(config.dialogs, function ([name, val])
- (!val[2] || val[2]())
- ? <><dt>{name}</dt><dd>{val[0]}</dd></>
- : undefined,
- <>{"\n"}</>)
- }</dl>
- </overlay>];
-
-
- this.helpInitialized = true;
- }
- },
+
+ help.initialize();
+ },
stringifyXML: function (xml) {
XML.prettyPrinting = false;
XML.ignoreWhitespace = false;
return UTF8(xml.toXMLString());
},
- exportHelp: JavaScript.setCompleter(function (path) {
- const FILE = io.File(path);
- const PATH = FILE.leafName.replace(/\..*/, "") + "/";
- const TIME = Date.now();
-
- if (!FILE.exists() && (/\/$/.test(path) && !/\./.test(FILE.leafName)))
- FILE.create(FILE.DIRECTORY_TYPE, octal(755));
-
- dactyl.initHelp();
- if (FILE.isDirectory()) {
- var addDataEntry = function addDataEntry(file, data) FILE.child(file).write(data);
- var addURIEntry = function addURIEntry(file, uri) addDataEntry(file, util.httpGet(uri).responseText);
- }
- else {
- var zip = services.ZipWriter();
- zip.open(FILE, File.MODE_CREATE | File.MODE_WRONLY | File.MODE_TRUNCATE);
-
- addURIEntry = function addURIEntry(file, uri)
- zip.addEntryChannel(PATH + file, TIME, 9,
- services.io.newChannel(uri, null, null), false);
- addDataEntry = function addDataEntry(file, data) // Unideal to an extreme.
- addURIEntry(file, "data:text/plain;charset=UTF-8," + encodeURI(data));
- }
-
- let empty = Set("area base basefont br col frame hr img input isindex link meta param"
- .split(" "));
- function fix(node) {
- switch(node.nodeType) {
- case Node.ELEMENT_NODE:
- if (isinstance(node, [HTMLBaseElement]))
- return;
-
- data.push("<"); data.push(node.localName);
- if (node instanceof HTMLHtmlElement)
- data.push(" xmlns=" + XHTML.uri.quote(),
- " xmlns:dactyl=" + NS.uri.quote());
-
- for (let { name, value } in array.iterValues(node.attributes)) {
- if (name == "dactyl:highlight") {
- Set.add(styles, value);
- name = "class";
- value = "hl-" + value;
- }
- if (name == "href") {
- value = node.href || value;
- if (value.indexOf("dactyl://help-tag/") == 0) {
- let uri = services.io.newChannel(value, null, null).originalURI;
- value = uri.spec == value ? "javascript:;" : uri.path.substr(1);
- }
- if (!/^#|[\/](#|$)|^[a-z]+:/.test(value))
- value = value.replace(/(#|$)/, ".xhtml$1");
- }
- if (name == "src" && value.indexOf(":") > 0) {
- chromeFiles[value] = value.replace(/.*\//, "");
- value = value.replace(/.*\//, "");
- }
-
- data.push(" ", name, '="',
- <>{value}</>.toXMLString().replace(/"/g, "&quot;"),
- '"');
- }
- if (node.localName in empty)
- data.push(" />");
- else {
- data.push(">");
- if (node instanceof HTMLHeadElement)
- data.push(<link rel="stylesheet" type="text/css" href="help.css"/>.toXMLString());
- Array.map(node.childNodes, fix);
- data.push("</", node.localName, ">");
- }
- break;
- case Node.TEXT_NODE:
- data.push(<>{node.textContent}</>.toXMLString());
- }
- }
-
- let chromeFiles = {};
- let styles = {};
- for (let [file, ] in Iterator(services["dactyl:"].FILE_MAP)) {
- let url = "dactyl://help/" + file;
- dactyl.open(url);
- util.waitFor(function () content.location.href == url && buffer.loaded
- && content.document.documentElement instanceof HTMLHtmlElement,
- 15000);
- events.waitForPageLoad();
- var data = [
- '<?xml version="1.0" encoding="UTF-8"?>\n',
- '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"\n',
- ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n'
- ];
- fix(content.document.documentElement);
- addDataEntry(file + ".xhtml", data.join(""));
- }
-
- let data = [h for (h in highlight) if (Set.has(styles, h.class) || /^Help/.test(h.class))]
- .map(function (h) h.selector
- .replace(/^\[.*?=(.*?)\]/, ".hl-$1")
- .replace(/html\|/g, "") + "\t" + "{" + h.cssText + "}")
- .join("\n");
- addDataEntry("help.css", data.replace(/chrome:[^ ")]+\//g, ""));
-
- addDataEntry("tag-map.json", JSON.stringify(services["dactyl:"].HELP_TAGS));
-
- let m, re = /(chrome:[^ ");]+\/)([^ ");]+)/g;
- while ((m = re.exec(data)))
- chromeFiles[m[0]] = m[2];
-
- for (let [uri, leaf] in Iterator(chromeFiles))
- addURIEntry(leaf, uri);
-
- if (zip)
- zip.close();
- }, [function (context, args) completion.file(context)]),
-
/**
* Generates a help entry and returns it as a string.
*
* @param {Command|Map|Option} obj A dactyl *Command*, *Map* or *Option*
* object
* @param {XMLList} extraHelp Extra help text beyond the description.
* @returns {string}
*/
@@ -1035,64 +680,70 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
let link, tag, spec;
link = tag = spec = util.identity;
let args = null;
if (obj instanceof Command) {
link = function (cmd) <ex>{cmd}</ex>;
args = obj.parseArgs("", CompletionContext(str || ""));
+ tag = function (cmd) <>:{cmd}</>;
spec = function (cmd) <>{
obj.count ? <oa>count</oa> : <></>
}{
cmd
}{
obj.bang ? <oa>!</oa> : <></>
}</>;
}
else if (obj instanceof Map) {
spec = function (map) obj.count ? <><oa>count</oa>{map}</> : <>{map}</>;
+ tag = function (map) <>{
+ let (c = obj.modes[0].char) c ? c + "_" : ""
+ }{ map }</>;
link = function (map) {
let [, mode, name, extra] = /^(?:(.)_)?(?:<([^>]+)>)?(.*)$/.exec(map);
let k = <k>{extra}</k>;
if (name)
k.@name = name;
if (mode)
k.@mode = mode;
return k;
};
}
else if (obj instanceof Option) {
- tag = spec = function (name) <>'{name}'</>;
+ spec = function () template.map(obj.names, tag, " ");
+ tag = function (name) <>'{name}'</>;
link = function (opt, name) <o>{name}</o>;
args = { value: "", values: [] };
}
XML.prettyPrinting = false;
XML.ignoreWhitespace = false;
default xml namespace = NS;
// E4X has its warts.
let br = <>
</>;
let res = <res>
- <dt>{link(obj.helpTag || obj.name, obj.name)}</dt> <dd>{
+ <dt>{link(obj.helpTag || tag(obj.name), obj.name)}</dt> <dd>{
template.linkifyHelp(obj.description ? obj.description.replace(/\.$/, "") : "", true)
}</dd></res>;
if (specOnly)
return res.elements();
res.* += <>
<item>
<tags>{template.map(obj.names.slice().reverse(), tag, " ")}</tags>
- <spec>{
- spec(template.highlightRegexp((obj.specs || obj.names)[0],
+ <spec>{let (name = (obj.specs || obj.names)[0])
+ spec(template.highlightRegexp(tag(name),
/\[(.*?)\]/g,
- function (m, n0) <oa>{n0}</oa>))
+ function (m, n0) <oa>{n0}</oa>),
+ name)
}</spec>{
!obj.type ? "" : <>
<type>{obj.type}</type>
<default>{obj.stringDefaultValue}</default></>}
<description>{
obj.description ? br + <p>{template.linkifyHelp(obj.description.replace(/\.?$/, "."), true)}</p> : "" }{
extraHelp ? br + extraHelp : "" }{
!(extraHelp || obj.description) ? br + <p><!--L-->Sorry, no help available.</p> : "" }
@@ -1124,40 +775,16 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
]));
return res.*.toXMLString()
.replace(' xmlns="' + NS + '"', "", "g")
.replace(/^ {12}|[ \t]+$/gm, "")
.replace(/^\s*\n|\n\s*$/g, "") + "\n";
},
/**
- * Opens the help page containing the specified *topic* if it exists.
- *
- * @param {string} topic The help topic to open.
- * @param {boolean} consolidated Whether to use the consolidated help page.
- */
- help: function (topic, consolidated) {
- dactyl.initHelp();
- if (!topic) {
- let helpFile = consolidated ? "all" : options["helpfile"];
-
- if (helpFile in services["dactyl:"].FILE_MAP)
- dactyl.open("dactyl://help/" + helpFile, { from: "help" });
- else
- dactyl.echomsg(_("help.noFile", helpFile.quote()));
- return;
- }
-
- let page = this.findHelp(topic, consolidated);
- dactyl.assert(page != null, _("help.noTopic", topic));
-
- dactyl.open("dactyl://help/" + page, { from: "help" });
- },
-
- /**
* The map of global variables.
*
* These are set and accessed with the "g:" prefix.
*/
_globalVariables: {},
globalVariables: deprecated(_("deprecated.for.theOptionsSystem"), {
get: function globalVariables() this._globalVariables
}),
@@ -1168,17 +795,19 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
dactyl.log(_("dactyl.sourcingPlugins", dir.path), 3);
let loadplugins = options.get("loadplugins");
if (args)
loadplugins = { __proto__: loadplugins, value: args.map(Option.parseRegexp) };
dir.readDirectory(true).forEach(function (file) {
- if (file.isFile() && loadplugins.getKey(file.path)
+ if (file.leafName[0] == ".")
+ ;
+ else if (file.isFile() && loadplugins.getKey(file.path)
&& !(!force && file.path in dactyl.pluginFiles && dactyl.pluginFiles[file.path] >= file.lastModifiedTime)) {
try {
io.source(file.path);
dactyl.pluginFiles[file.path] = file.lastModifiedTime;
}
catch (e) {
dactyl.reportError(e);
}
@@ -1213,55 +842,61 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
* 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 *level* will be printed. If *msg* 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 = localPrefs.get("loglevel", 0);
+ let verbose = config.prefs.get("loglevel", 0);
if (!level || level <= verbose) {
if (isObject(msg) && !isinstance(msg, _))
msg = util.objectToString(msg, false);
services.console.logStringMessage(config.name + ": " + msg);
}
},
- onClick: function onClick(event) {
- if (event.originalTarget instanceof Element) {
- let command = event.originalTarget.getAttributeNS(NS, "command");
+ events: {
+ click: function onClick(event) {
+ let elem = event.originalTarget;
+
+ if (elem instanceof Element && services.security.isSystemPrincipal(elem.nodePrincipal)) {
+ let command = elem.getAttributeNS(NS, "command");
if (command && event.button == 0) {
event.preventDefault();
if (dactyl.commands[command])
- dactyl.withSavedValues(["forceNewTab"], function () {
- dactyl.forceNewTab = event.ctrlKey || event.shiftKey || event.button == 1;
+ dactyl.withSavedValues(["forceTarget"], function () {
+ if (event.ctrlKey || event.shiftKey || event.button == 1)
+ dactyl.forceTarget = dactyl.NEW_TAB;
dactyl.commands[command](event);
});
}
}
},
- onExecute: function onExecute(event) {
+ "dactyl.execute": function onExecute(event) {
let cmd = event.originalTarget.getAttribute("dactyl-execute");
commands.execute(cmd, null, false, null,
{ file: /*L*/"[Command Line]", line: 1 });
- },
+ }
+ },
/**
* Opens one or more URLs. Returns true when load was initiated, or
* false on error.
*
- * @param {string|Array} urls A representation of the URLs to open. May be
+ * @param {string|object|Array} urls A representation of the URLs to open. May be
* either a string, which will be passed to
- * {@see Dactyl#parseURLs}, or an array in the same format as
- * would be returned by the same.
+ * {@link Dactyl#parseURLs}, an array in the same format as
+ * would be returned by the same, or an object as returned by
+ * {@link DOM#formData}.
* @param {object} params A set of parameters specifying how to open the
* URLs. The following properties are recognized:
*
* • background If true, new tabs are opened in the background.
*
* • from The designation of the opener, as appears in
* 'activate' and 'newtab' options. If present,
* the newtab option provides the default 'where'
@@ -1292,66 +927,67 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
if (isString(params))
params = { where: params };
let flags = 0;
for (let [opt, flag] in Iterator({ replace: "REPLACE_HISTORY", hide: "BYPASS_HISTORY" }))
flags |= params[opt] && Ci.nsIWebNavigation["LOAD_FLAGS_" + flag];
let where = params.where || dactyl.CURRENT_TAB;
- let background = ("background" in params) ? params.background
+ let background = dactyl.forceBackground != null ? dactyl.forceBackground :
+ ("background" in params) ? params.background
: params.where == dactyl.NEW_BACKGROUND_TAB;
if (params.from && dactyl.has("tabs")) {
if (!params.where && options.get("newtab").has(params.from))
where = dactyl.NEW_TAB;
background ^= !options.get("activate").has(params.from);
}
if (urls.length == 0)
return;
let browser = config.tabbrowser;
- function open(urls, where) {
- try {
- let url = Array.concat(urls)[0];
- let postdata = Array.concat(urls)[1];
+ function open(loc, where) {
+ try {
+ if (isArray(loc))
+ loc = { url: loc[0], postData: loc[1] };
+ else if (isString(loc))
+ loc = { url: loc };
// decide where to load the first url
switch (where) {
case dactyl.NEW_TAB:
if (!dactyl.has("tabs"))
- return open(urls, dactyl.NEW_WINDOW);
+ return open(loc, dactyl.NEW_WINDOW);
return prefs.withContext(function () {
prefs.set("browser.tabs.loadInBackground", true);
- return browser.loadOneTab(url, null, null, postdata, background).linkedBrowser.contentDocument;
- });
+ return browser.loadOneTab(loc.url, null, null, loc.postData, background).linkedBrowser.contentDocument;
+ });
case dactyl.NEW_WINDOW:
let win = window.openDialog(document.documentURI, "_blank", "chrome,all,dialog=no");
util.waitFor(function () win.document.readyState === "complete");
browser = win.dactyl && win.dactyl.modules.config.tabbrowser || win.getBrowser();
// FALLTHROUGH
case dactyl.CURRENT_TAB:
- browser.loadURIWithFlags(url, flags, null, null, postdata);
+ browser.loadURIWithFlags(loc.url, flags, null, null, loc.postData);
return browser.contentWindow;
}
}
catch (e) {}
// Unfortunately, failed page loads throw exceptions and
// cause a lot of unwanted noise. This solution means that
// any genuine errors go unreported.
}
- if (dactyl.forceNewTab)
- where = dactyl.NEW_TAB;
- else if (dactyl.forceNewWindow)
- where = dactyl.NEW_WINDOW;
+ if (dactyl.forceTarget)
+ where = dactyl.forceTarget;
else if (!where)
where = dactyl.CURRENT_TAB;
return urls.map(function (url) {
let res = open(url, where);
where = dactyl.NEW_TAB;
background = true;
return res;
@@ -1373,29 +1009,29 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
if (options["urlseparator"])
urls = util.splitLiteral(str, util.regexp("\\s*" + options["urlseparator"] + "\\s*"));
else
urls = [str];
return urls.map(function (url) {
url = url.trim();
- if (/^(\.{0,2}|~)(\/|$)/.test(url) || util.OS.isWindows && /^[a-z]:/i.test(url)) {
+ if (/^(\.{0,2}|~)(\/|$)/.test(url) || config.OS.isWindows && /^[a-z]:/i.test(url)) {
try {
// Try to find a matching file.
let file = io.File(url);
if (file.exists() && file.isReadable())
return services.io.newFileURI(file).spec;
}
catch (e) {}
}
// If it starts with a valid protocol, pass it through.
let proto = /^([-\w]+):/.exec(url);
- if (proto && "@mozilla.org/network/protocol;1?name=" + proto[1] in Cc)
+ if (proto && services.PROTOCOL + proto[1] in Cc)
return url;
// Check for a matching search keyword.
let searchURL = this.has("bookmarks") && bookmarks.getSearchURL(url, false);
if (searchURL)
return searchURL;
// If it looks like URL-ish (foo.com/bar), let Gecko figure it out.
@@ -1403,17 +1039,17 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
return util.createURI(url).spec;
// Pass it off to the default search engine or, failing
// that, let Gecko deal with it as is.
return bookmarks.getSearchURL(url, true) || util.createURI(url).spec;
}, this);
},
stringToURLArray: deprecated("dactyl.parseURLs", "parseURLs"),
- urlish: Class.memoize(function () util.regexp(<![CDATA[
+ urlish: Class.Memoize(function () util.regexp(<![CDATA[
^ (
<domain>+ (:\d+)? (/ .*) |
<domain>+ (:\d+) |
<domain>+ \. [a-z0-9]+ |
localhost
) $
]]>, "ix", {
domain: util.regexp(String.replace(<![CDATA[
@@ -1465,20 +1101,22 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
prefs.safeSet(pref, 1);
services.appStartup.quit(Ci.nsIAppStartup[force ? "eForceQuit" : "eAttemptQuit"]);
},
/**
* Restart the host application.
*/
- restart: function () {
+ restart: function (args) {
if (!this.confirmQuit())
return;
+ config.prefs.set("commandline-args", args);
+
services.appStartup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
},
get assert() util.assert,
/**
* Traps errors in the called function, possibly reporting them.
*
@@ -1487,45 +1125,53 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
*/
trapErrors: function trapErrors(func, self) {
try {
if (isString(func))
func = self[func];
return func.apply(self || this, Array.slice(arguments, 2));
}
catch (e) {
+ try {
dactyl.reportError(e, true);
+ }
+ catch (e) {
+ util.reportError(e);
+ }
return e;
}
},
/**
* Reports an error to both the console and the host application's
* Error Console.
*
* @param {Object} error The error object.
*/
reportError: function reportError(error, echo) {
if (error instanceof FailedAssertion && error.noTrace || error.message === "Interrupted") {
let context = contexts.context;
let prefix = context ? context.file + ":" + context.line + ": " : "";
- if (error.message && error.message.indexOf(prefix) !== 0)
+ if (error.message && error.message.indexOf(prefix) !== 0 &&
+ prefix != "[Command Line]:1: ")
error.message = prefix + error.message;
if (error.message)
dactyl.echoerr(template.linkifyHelp(error.message));
else
dactyl.beep();
if (!error.noTrace)
util.reportError(error);
return;
}
+
if (error.result == Cr.NS_BINDING_ABORTED)
return;
+
if (echo)
dactyl.echoerr(error, commandline.FORCE_SINGLELINE);
else
util.reportError(error);
},
/**
* Parses a Dactyl command-line string i.e. the value of the
@@ -1540,20 +1186,19 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
try {
return commands.get("rehash").parseArgs(cmdline);
}
catch (e) {
dactyl.reportError(e, true);
return [];
}
},
-
wrapCallback: function (callback, self) {
self = self || this;
- let save = ["forceNewTab", "forceNewWindow"];
+ let save = ["forceOpen"];
let saved = save.map(function (p) dactyl[p]);
return function wrappedCallback() {
let args = arguments;
return dactyl.withSavedValues(save, function () {
saved.forEach(function (p, i) dactyl[save[i]] = p);
try {
return callback.apply(self, args);
}
@@ -1568,19 +1213,100 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
* @property {[Window]} Returns an array of all the host application's
* open windows.
*/
get windows() [win for (win in iter(services.windowMediator.getEnumerator("navigator:browser"))) if (win.dactyl)],
}, {
toolbarHidden: function hidden(elem) (elem.getAttribute("autohide") || elem.getAttribute("collapsed")) == "true"
}, {
+ cache: function () {
+ cache.register("help/plugins.xml", function () {
+ // Process plugin help entries.
+ XML.ignoreWhiteSpace = XML.prettyPrinting = false;
+
+ let body = XML();
+ for (let [, context] in Iterator(plugins.contexts))
+ try {
+ let info = contexts.getDocs(context);
+ if (info instanceof XML) {
+ if (info.*.@lang.length()) {
+ let lang = config.bestLocale(String(a) for each (a in info.*.@lang));
+
+ info.* = info.*.(function::attribute("lang").length() == 0 || @lang == lang);
+
+ for each (let elem in info.NS::info)
+ for (let attr in values(["@name", "@summary", "@href"]))
+ if (elem[attr].length())
+ info[attr] = elem[attr];
+ }
+ body += <h2 xmlns={NS.uri} tag={info.@name + '-plugin'}>{info.@summary}</h2> +
+ info;
+ }
+ }
+ catch (e) {
+ util.reportError(e);
+ }
+
+ return '<?xml version="1.0"?>\n' +
+ '<?xml-stylesheet type="text/xsl" href="dactyl://content/help.xsl"?>\n' +
+ '<!DOCTYPE document SYSTEM "resource://dactyl-content/dactyl.dtd">\n' +
+ <document xmlns={NS}
+ name="plugins" title={config.appName + " Plugins"}>
+ <h1 tag="using-plugins">{_("help.title.Using Plugins")}</h1>
+ <toc start="2"/>
+
+ {body}
+ </document>.toXMLString();
+ });
+
+ cache.register("help/index.xml", function () {
+ default xml namespace = NS;
+
+ return '<?xml version="1.0"?>\n' +
+ <overlay xmlns={NS}>{
+ template.map(dactyl.indices, function ([name, iter])
+ <dl insertafter={name + "-index"}>{
+ template.map(iter(), util.identity)
+ }</dl>, <>{"\n\n"}</>)
+ }</overlay>;
+ });
+
+ cache.register("help/gui.xml", function () {
+ default xml namespace = NS;
+
+ return '<?xml version="1.0"?>\n' +
+ <overlay xmlns={NS}>
+ <dl insertafter="dialog-list">{
+ template.map(config.dialogs, function ([name, val])
+ (!val[2] || val[2]())
+ ? <><dt>{name}</dt><dd>{val[0]}</dd></>
+ : undefined,
+ <>{"\n"}</>)
+ }</dl>
+ </overlay>;
+ });
+
+ cache.register("help/privacy.xml", function () {
+ default xml namespace = NS;
+
+ return '<?xml version="1.0"?>\n' +
+ <overlay xmlns={NS}>
+ <dl insertafter="sanitize-items">{
+ template.map(options.get("sanitizeitems").values
+ .sort(function (a, b) String.localeCompare(a.name, b.name)),
+ function ({ name, description })
+ <><dt>{name}</dt><dd>{template.linkifyHelp(description, true)}</dd></>,
+ <>{"\n"}</>)
+ }</dl>
+ </overlay>;
+ });
+ },
events: function () {
- events.listen(window, "click", dactyl.closure.onClick, true);
- events.listen(window, "dactyl.execute", dactyl.closure.onExecute, true);
+ events.listen(window, dactyl, "events", true);
},
// Only general options are added here, which are valid for all Dactyl extensions
options: function () {
options.add(["errorbells", "eb"],
"Ring the bell when an error message is displayed",
"boolean", false);
options.add(["exrc", "ex"],
@@ -1666,21 +1392,21 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
validator: function (opts) dactyl.has("Gecko2") ||
Option.validIf(!/[nN]/.test(opts), "Tab numbering not available in this " + config.host + " version")
*/
}
].filter(function (group) !group.feature || dactyl.has(group.feature));
options.add(["guioptions", "go"],
"Show or hide certain GUI elements like the menu or toolbar",
- "charlist", config.defaults.guioptions || "", {
+ "charlist", "", {
// FIXME: cleanup
cleanupValue: config.cleanups.guioptions ||
- "r" + [k for ([k, v] in iter(groups[1].opts))
+ "rb" + [k for ([k, v] in iter(groups[1].opts))
if (!Dactyl.toolbarHidden(document.getElementById(v[1][0])))].join(""),
values: array(groups).map(function (g) [[k, v[0]] for ([k, v] in Iterator(g.opts))]).flatten(),
setter: function (value) {
for (let group in values(groups))
group.setter(value);
events.checkFocus();
@@ -1695,21 +1421,24 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
"string", "intro");
options.add(["loadplugins", "lpl"],
"A regexp list that defines which plugins are loaded at startup and via :loadplugins",
"regexplist", "'\\.(js|" + config.fileExtension + ")$'");
options.add(["titlestring"],
"The string shown at the end of the window title",
- "string", config.defaults.titlestring || config.host,
+ "string", config.host,
{
setter: function (value) {
let win = document.documentElement;
function updateTitle(old, current) {
+ if (config.browser.updateTitlebar)
+ config.browser.updateTitlebar();
+ else
document.title = document.title.replace(RegExp("(.*)" + util.regexp.escape(old)), "$1" + current);
}
if (services.has("privateBrowsing")) {
let oldValue = win.getAttribute("titlemodifier_normal");
let suffix = win.getAttribute("titlemodifier_privatebrowsing").substr(oldValue.length);
win.setAttribute("titlemodifier_normal", value);
@@ -1739,31 +1468,23 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
{ validator: function (value) Option.validIf(value >= 0 && value <= 15, "Value must be between 0 and 15") });
options.add(["visualbell", "vb"],
"Use visual bell instead of beeping on errors",
"boolean", false,
{
setter: function (value) {
prefs.safeSet("accessibility.typeaheadfind.enablesound", !value,
- _("option.visualbell.safeSet"));
+ _("option.safeSet", "visualbell"));
return value;
}
});
},
mappings: function () {
- mappings.add([modes.MAIN], ["<open-help>", "<F1>"],
- "Open the introductory help page",
- function () { dactyl.help(); });
-
- mappings.add([modes.MAIN], ["<open-single-help>", "<A-F1>"],
- "Open the single, consolidated help page",
- function () { ex.helpall(); });
-
if (dactyl.has("session"))
mappings.add([modes.NORMAL], ["ZQ"],
"Quit and don't save the session",
function () { dactyl.quit(false); });
mappings.add([modes.NORMAL], ["ZZ"],
"Quit and save the session",
function () { dactyl.quit(true); });
@@ -1792,17 +1513,17 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
completion.dialog(context);
}
});
commands.add(["em[enu]"],
"Execute the specified menu item from the command line",
function (args) {
let arg = args[0] || "";
- let items = dactyl.menuItems;
+ let items = dactyl.getMenuItems(arg);
dactyl.assert(items.some(function (i) i.dactylPath == arg),
_("emenu.notFound", arg));
for (let [, item] in Iterator(items)) {
if (item.dactylPath == arg) {
dactyl.assert(!item.disabled, _("error.disabled", item.dactylPath));
item.doCommand();
@@ -1824,40 +1545,16 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
catch (e) {
dactyl.echoerr(e);
}
}, {
completer: function (context) completion.javascript(context),
literal: 0
});
- [
- {
- name: "h[elp]",
- description: "Open the introductory help page"
- }, {
- name: "helpa[ll]",
- description: "Open the single consolidated help page"
- }
- ].forEach(function (command) {
- let consolidated = command.name == "helpa[ll]";
-
- commands.add([command.name],
- command.description,
- function (args) {
- dactyl.assert(!args.bang, _("help.dontPanic"));
- dactyl.help(args.literalArg, consolidated);
- }, {
- argCount: "?",
- bang: true,
- completer: function (context) completion.help(context, consolidated),
- literal: 0
- });
- });
-
commands.add(["loadplugins", "lpl"],
"Load all or matching plugins",
function (args) {
dactyl.loadPlugins(args.length ? args : null, args.bang);
},
{
argCount: "*",
bang: true,
@@ -1898,57 +1595,76 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
window.close();
else
dactyl.quit(false, args.bang);
}, {
argCount: "0",
bang: true
});
+ let startupOptions = [
+ {
+ names: ["+u"],
+ description: "The initialization file to execute at startup",
+ type: CommandOption.STRING
+ },
+ {
+ names: ["++noplugin"],
+ description: "Do not automatically load plugins"
+ },
+ {
+ names: ["++cmd"],
+ description: "Ex commands to execute prior to initialization",
+ type: CommandOption.STRING,
+ multiple: true
+ },
+ {
+ names: ["+c"],
+ description: "Ex commands to execute after initialization",
+ type: CommandOption.STRING,
+ multiple: true
+ },
+ {
+ names: ["+purgecaches"],
+ description: "Purge " + config.appName + " caches at startup",
+ type: CommandOption.NOARG
+ }
+ ];
+
commands.add(["reh[ash]"],
"Reload the " + config.appName + " add-on",
function (args) {
if (args.trailing)
storage.session.rehashCmd = args.trailing; // Hack.
args.break = true;
+
+ if (args["+purgecaches"])
+ cache.flush();
+
util.rehash(args);
},
{
argCount: "0", // FIXME
- options: [
+ options: startupOptions
+ });
+
+ commands.add(["res[tart]"],
+ "Force " + config.host + " to restart",
+ function (args) {
+ if (args["+purgecaches"])
+ cache.flush();
+
+ dactyl.restart(args.string);
+ },
{
- names: ["+u"],
- description: "The initialization file to execute at startup",
- type: CommandOption.STRING
- },
- {
- names: ["++noplugin"],
- description: "Do not automatically load plugins"
- },
- {
- names: ["++cmd"],
- description: "Ex commands to execute prior to initialization",
- type: CommandOption.STRING,
- multiple: true
- },
- {
- names: ["+c"],
- description: "Ex commands to execute after initialization",
- type: CommandOption.STRING,
- multiple: true
- }
- ]
- });
-
- commands.add(["res[tart]"],
- "Force " + config.appName + " to restart",
- function () { dactyl.restart(); },
- { argCount: "0" });
-
- function findToolbar(name) util.evaluateXPath(
+ argCount: "0",
+ options: startupOptions
+ });
+
+ function findToolbar(name) DOM.XPath(
"//*[@toolbarname=" + util.escapeString(name, "'") + " or " +
"@toolbarname=" + util.escapeString(name.trim(), "'") + "]",
document).snapshotItem(0);
var toolbox = document.getElementById("navigator-toolbox");
if (toolbox) {
let toolbarCommand = function (names, desc, action, filter) {
commands.add(names, desc,
@@ -2087,75 +1803,78 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
subCommand: 0
});
commands.add(["ve[rsion]"],
"Show version information",
function (args) {
if (args.bang)
dactyl.open("about:");
- else
- commandline.commandOutput(<>
- {config.appName} {config.version} running on:<br/>{navigator.userAgent}
- </>);
+ else {
+ let date = config.buildDate;
+ date = date ? " (" + date + ")" : "";
+
+ commandline.commandOutput(
+ <div>{config.appName} {config.version}{date} running on: </div> +
+ <div>{navigator.userAgent}</div>)
+ }
}, {
argCount: "0",
bang: true
});
},
completion: function () {
completion.dialog = function dialog(context) {
context.title = ["Dialog"];
context.filters.push(function ({ item }) !item[2] || item[2]());
context.completions = [[k, v[0], v[2]] for ([k, v] in Iterator(config.dialogs))];
};
- completion.help = function help(context, consolidated) {
- dactyl.initHelp();
- context.title = ["Help"];
- context.anchored = false;
- context.completions = services["dactyl:"].HELP_TAGS;
- if (consolidated)
- context.keys = { text: 0, description: function () "all" };
- };
-
completion.menuItem = function menuItem(context) {
context.title = ["Menu Path", "Label"];
context.anchored = false;
context.keys = {
text: "dactylPath",
description: function (item) item.getAttribute("label"),
highlight: function (item) item.disabled ? "Disabled" : ""
};
context.generate = function () dactyl.menuItems;
};
var toolbox = document.getElementById("navigator-toolbox");
completion.toolbar = function toolbar(context) {
context.title = ["Toolbar"];
context.keys = { text: function (item) item.getAttribute("toolbarname"), description: function () "" };
- context.completions = util.evaluateXPath("//*[@toolbarname]", document);
+ context.completions = DOM.XPath("//*[@toolbarname]", document);
};
completion.window = function window(context) {
context.title = ["Window", "Title"];
context.keys = { text: function (win) dactyl.windows.indexOf(win) + 1, description: function (win) win.document.title };
context.completions = dactyl.windows;
};
},
load: function () {
dactyl.triggerObserver("load");
dactyl.log(_("dactyl.modulesLoaded"), 3);
+ userContext.DOM = Class("DOM", DOM, { init: function DOM_(sel, ctxt) DOM(sel, ctxt || buffer.focusedFrame.document) });
+ userContext.$ = modules.userContext.DOM;
+
dactyl.timeout(function () {
try {
- var args = storage.session.commandlineArgs || services.commandLineHandler.optionValue;
+ var args = config.prefs.get("commandline-args")
+ || storage.session.commandlineArgs
+ || services.commandLineHandler.optionValue;
+
+ config.prefs.reset("commandline-args");
+
if (isString(args))
args = dactyl.parseCommandLine(args);
if (args) {
dactyl.commandLineOptions.rcFile = args["+u"];
dactyl.commandLineOptions.noPlugins = "++noplugin" in args;
dactyl.commandLineOptions.postCommands = args["+c"];
dactyl.commandLineOptions.preCommands = args["++cmd"];
@@ -2163,27 +1882,24 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
}
}
catch (e) {
dactyl.echoerr(_("dactyl.parsingCommandLine", e));
}
dactyl.log(_("dactyl.commandlineOpts", util.objectToString(dactyl.commandLineOptions)), 3);
- // first time intro message
- const firstTime = "extensions." + config.name + ".firsttime";
- if (prefs.get(firstTime, true)) {
+ if (config.prefs.get("first-run", true))
dactyl.timeout(function () {
- this.withSavedValues(["forceNewTab"], function () {
- this.forceNewTab = true;
- this.help();
- prefs.set(firstTime, false);
+ config.prefs.set("first-run", false);
+ this.withSavedValues(["forceTarget"], function () {
+ this.forceTarget = dactyl.NEW_TAB;
+ help.help();
});
}, 1000);
- }
// TODO: we should have some class where all this guioptions stuff fits well
// dactyl.hideGUI();
if (dactyl.userEval("typeof document", null, "test.js") === "undefined")
jsmodules.__proto__ = XPCSafeJSObjectWrapper(window);
if (dactyl.commandLineOptions.preCommands)
diff --git a/common/content/disable-acr.jsm b/common/content/disable-acr.jsm
--- a/common/content/disable-acr.jsm
+++ b/common/content/disable-acr.jsm
@@ -3,18 +3,17 @@
var ADDON_ID;
const OVERLAY_URLS = [
"about:addons",
"chrome://mozapps/content/extensions/extensions.xul"
];
-const Ci = Components.interfaces;
-const Cu = Components.utils;
+let { interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const TOPIC = "chrome-document-global-created";
function observe(window, topic, url) {
if (topic === TOPIC)
@@ -59,16 +58,19 @@ function checkDocument(doc, disable, for
function chromeDocuments() {
let windows = Services.wm.getXULWindowEnumerator(null);
while (windows.hasMoreElements()) {
let window = windows.getNext().QueryInterface(Ci.nsIXULWindow);
for each (let type in ["typeChrome", "typeContent"]) {
let docShells = window.docShell.getDocShellEnumerator(Ci.nsIDocShellTreeItem[type],
Ci.nsIDocShell.ENUMERATE_FORWARDS);
while (docShells.hasMoreElements())
+ try {
yield docShells.getNext().QueryInterface(Ci.nsIDocShell).contentViewer.DOMDocument;
}
+ catch (e) {}
+ }
}
}
var EXPORTED_SYMBOLS = ["cleanup", "init"];
// vim: set fdm=marker sw=4 ts=4 et ft=javascript:
diff --git a/common/content/editor.js b/common/content/editor.js
--- a/common/content/editor.js
+++ b/common/content/editor.js
@@ -1,230 +1,358 @@
// Copyright (c) 2008-2011 Kris Maglione <maglione.k at Gmail>
// 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.
-"use strict";
+/* use strict */
/** @scope modules */
// command names taken from:
// http://developer.mozilla.org/en/docs/Editor_Embedding_Guide
/** @instance editor */
-var Editor = Module("editor", {
+var Editor = Module("editor", XPCOM(Ci.nsIEditActionListener, ModuleBase), {
+ init: function init(elem) {
+ if (elem)
+ this.element = elem;
+ else
+ this.__defineGetter__("element", function () {
+ let elem = dactyl.focusedElement;
+ if (elem)
+ return elem.inputField || elem;
+
+ let win = document.commandDispatcher.focusedWindow;
+ return DOM(win).isEditable && win || null;
+ });
+ },
+
+ get registers() storage.newMap("registers", { privateData: true, store: true }),
+ get registerRing() storage.newArray("register-ring", { privateData: true, store: true }),
+
+ skipSave: false,
+
+ // Fixme: Move off this object.
+ currentRegister: null,
+
+ /**
+ * Temporarily set the default register for the span of the next
+ * mapping.
+ */
+ pushRegister: function pushRegister(arg) {
+ let restore = this.currentRegister;
+ this.currentRegister = arg;
+ mappings.afterCommands(2, function () {
+ this.currentRegister = restore;
+ }, this);
+ },
+
+ defaultRegister: "*",
+
+ selectionRegisters: {
+ "*": "selection",
+ "+": "global"
+ },
+
+ /**
+ * Get the value of the register *name*.
+ *
+ * @param {string|number} name The name of the register to get.
+ * @returns {string|null}
+ * @see #setRegister
+ */
+ getRegister: function getRegister(name) {
+ if (name == null)
+ name = editor.currentRegister || editor.defaultRegister;
+
+ if (name == '"')
+ name = 0;
+ if (name == "_")
+ var res = null;
+ else if (Set.has(this.selectionRegisters, name))
+ res = { text: dactyl.clipboardRead(this.selectionRegisters[name]) || "" };
+ else if (!/^[0-9]$/.test(name))
+ res = this.registers.get(name);
+ else
+ res = this.registerRing.get(name);
+
+ return res != null ? res.text : res;
+ },
+
+ /**
+ * Sets the value of register *name* to value. The following
+ * registers have special semantics:
+ *
+ * * - Tied to the PRIMARY selection value on X11 systems.
+ * + - Tied to the primary global clipboard.
+ * _ - The null register. Never has any value.
+ * " - Equivalent to 0.
+ * 0-9 - These act as a kill ring. Setting any of them pushes the
+ * values of higher numbered registers up one slot.
+ *
+ * @param {string|number} name The name of the register to set.
+ * @param {string|Range|Selection|Node} value The value to save to
+ * the register.
+ */
+ setRegister: function setRegister(name, value, verbose) {
+ if (name == null)
+ name = editor.currentRegister || editor.defaultRegister;
+
+ if (isinstance(value, [Ci.nsIDOMRange, Ci.nsIDOMNode, Ci.nsISelection]))
+ value = DOM.stringify(value);
+ value = { text: value, isLine: modes.extended & modes.LINE, timestamp: Date.now() * 1000 };
+
+ if (name == '"')
+ name = 0;
+ if (name == "_")
+ ;
+ else if (Set.has(this.selectionRegisters, name))
+ dactyl.clipboardWrite(value.text, verbose, this.selectionRegisters[name]);
+ else if (!/^[0-9]$/.test(name))
+ this.registers.set(name, value);
+ else {
+ this.registerRing.insert(value, name);
+ this.registerRing.truncate(10);
+ }
+ },
+
get isCaret() modes.getStack(1).main == modes.CARET,
get isTextEdit() modes.getStack(1).main == modes.TEXT_EDIT,
- unselectText: function (toEnd) {
- try {
- Editor.getEditor(null).selection[toEnd ? "collapseToEnd" : "collapseToStart"]();
- }
- catch (e) {}
- },
-
- selectedText: function () String(Editor.getEditor(null).selection),
-
- pasteClipboard: function (clipboard, toStart) {
- let elem = dactyl.focusedElement;
- if (elem.inputField)
- elem = elem.inputField;
-
- if (elem.setSelectionRange) {
- let text = dactyl.clipboardRead(clipboard);
- if (!text)
- return;
- if (isinstance(elem, [HTMLInputElement, XULTextBoxElement]))
- text = text.replace(/\n+/g, "");
-
- // This is a hacky fix - but it works.
- // <s-insert> in the bottom of a long textarea bounces up
- let top = elem.scrollTop;
- let left = elem.scrollLeft;
-
- let start = elem.selectionStart; // caret position
- let end = elem.selectionEnd;
- let value = elem.value.substring(0, start) + text + elem.value.substring(end);
- elem.value = value;
-
- if (/^(search|text)$/.test(elem.type))
- Editor.getEditor(elem).rootElement.firstChild.textContent = value;
-
- elem.selectionStart = Math.min(start + (toStart ? 0 : text.length), elem.value.length);
- elem.selectionEnd = elem.selectionStart;
-
- elem.scrollTop = top;
- elem.scrollLeft = left;
-
- events.dispatch(elem, events.create(elem.ownerDocument, "input"));
- }
- },
+ get editor() DOM(this.element).editor,
+
+ getController: function getController(cmd) {
+ let controllers = this.element && this.element.controllers;
+ dactyl.assert(controllers);
+
+ return controllers.getControllerForCommand(cmd || "cmd_beginLine");
+ },
+
+ get selection() this.editor && this.editor.selection || null,
+ get selectionController() this.editor && this.editor.selectionController || null,
+
+ deselect: function () {
+ if (this.selection && this.selection.focusNode)
+ this.selection.collapse(this.selection.focusNode,
+ this.selection.focusOffset);
+ },
+
+ get selectedRange() {
+ if (!this.selection)
+ return null;
+
+ if (!this.selection.rangeCount) {
+ let range = RangeFind.nodeContents(this.editor.rootElement.ownerDocument);
+ range.collapse(true);
+ this.selectedRange = range;
+ }
+ return this.selection.getRangeAt(0);
+ },
+ set selectedRange(range) {
+ this.selection.removeAllRanges();
+ if (range != null)
+ this.selection.addRange(range);
+ },
+
+ get selectedText() String(this.selection),
+
+ get preserveSelection() this.editor && !this.editor.shouldTxnSetSelection,
+ set preserveSelection(val) {
+ if (this.editor)
+ this.editor.setShouldTxnSetSelection(!val);
+ },
+
+ copy: function copy(range, name) {
+ range = range || this.selection;
+
+ if (!range.collapsed)
+ this.setRegister(name, range);
+ },
+
+ cut: function cut(range, name) {
+ if (range)
+ this.selectedRange = range;
+
+ if (!this.selection.isCollapsed)
+ this.setRegister(name, this.selection);
+
+ this.editor.deleteSelection(0);
+ },
+
+ paste: function paste(name) {
+ let text = this.getRegister(name);
+ dactyl.assert(text && this.editor instanceof Ci.nsIPlaintextEditor);
+
+ this.editor.insertText(text);
+ },
// count is optional, defaults to 1
- executeCommand: function (cmd, count) {
- let editor = Editor.getEditor(null);
- let controller = Editor.getController();
- dactyl.assert(callable(cmd) ||
- controller &&
+ executeCommand: function executeCommand(cmd, count) {
+ if (!callable(cmd)) {
+ var controller = this.getController(cmd);
+ util.assert(controller &&
controller.supportsCommand(cmd) &&
controller.isCommandEnabled(cmd));
+ cmd = bind("doCommand", controller, cmd);
+ }
// XXX: better as a precondition
if (count == null)
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
+
+ // What huh? --Kris
try {
- if (callable(cmd))
- cmd(editor, controller);
- else
- controller.doCommand(cmd);
+ cmd(this.editor, controller);
didCommand = true;
}
catch (e) {
util.reportError(e);
dactyl.assert(didCommand);
break;
}
}
},
- // cmd = y, d, c
- // motion = b, 0, gg, G, etc.
- selectMotion: function selectMotion(cmd, motion, count) {
- // XXX: better as a precondition
- if (count == null)
- count = 1;
-
- if (cmd == motion) {
- motion = "j";
- count--;
- }
-
- if (modes.main != modes.VISUAL)
- modes.push(modes.VISUAL);
-
- 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:
- dactyl.beep();
- return;
- }
- },
-
- // 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;
- }
-
- if (forward) {
- if (pos <= Editor.getEditor().selectionEnd || pos > Editor.getEditor().value.length)
- return;
-
- 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;
-
- do { // TODO: test code for endless loops
- this.executeCommand("cmd_selectCharPrevious", 1);
- }
- while (Editor.getEditor().selectionStart != pos);
- }
- },
-
- findChar: function (key, count, backward) {
-
- let editor = Editor.getEditor();
- if (!editor)
- return -1;
-
- // XXX
- if (count == null)
- count = 1;
-
- let code = events.fromString(key)[0].charCode;
+ moveToPosition: function (pos, select) {
+ if (isObject(pos))
+ var { startContainer, startOffset } = pos;
+ else
+ [startOffset, startOffset] = [this.selection.focusNode, pos];
+ this.selection[select ? "extend" : "collapse"](startContainer, startOffset);
+ },
+
+ mungeRange: function mungeRange(range, munger, selectEnd) {
+ let { editor } = this;
+ editor.beginPlaceHolderTransaction(null);
+
+ let [container, offset] = ["startContainer", "startOffset"];
+ if (selectEnd)
+ [container, offset] = ["endContainer", "endOffset"];
+
+ try {
+ // :(
+ let idx = range[offset];
+ let parent = range[container].parentNode;
+ let parentIdx = Array.indexOf(parent.childNodes,
+ range[container]);
+
+ let delta = 0;
+ for (let node in Editor.TextsIterator(range)) {
+ let text = node.textContent;
+ let start = 0, end = text.length;
+ if (node == range.startContainer)
+ start = range.startOffset;
+ if (node == range.endContainer)
+ end = range.endOffset;
+
+ if (start == 0 && end == text.length)
+ text = munger(text);
+ else
+ text = text.slice(0, start)
+ + munger(text.slice(start, end))
+ + text.slice(end);
+
+ if (text == node.textContent)
+ continue;
+
+ if (selectEnd)
+ delta = text.length - node.textContent.length;
+
+ if (editor instanceof Ci.nsIPlaintextEditor) {
+ this.selectedRange = RangeFind.nodeContents(node);
+ editor.insertText(text);
+ }
+ else
+ node.textContent = text;
+ }
+ let node = parent.childNodes[parentIdx];
+ if (node instanceof Text)
+ idx = Math.constrain(idx + delta, 0, node.textContent.length);
+ this.selection.collapse(node, idx);
+ }
+ finally {
+ editor.endPlaceHolderTransaction();
+ }
+ },
+
+ findChar: function findNumber(key, count, backward, offset) {
+ count = count || 1; // XXX ?
+ offset = (offset || 0) - !!backward;
+
+ // Grab the charcode of the key spec. Using the key name
+ // directly will break keys like <
+ let code = DOM.Event.parse(key)[0].charCode;
+ let char = String.fromCharCode(code);
util.assert(code);
- let char = String.fromCharCode(code);
-
- let text = editor.value;
- let caret = editor.selectionEnd;
- if (backward) {
- let end = text.lastIndexOf("\n", caret);
- while (caret > end && caret >= 0 && count--)
- caret = text.lastIndexOf(char, caret - 1);
- }
- else {
- let end = text.indexOf("\n", caret);
- if (end == -1)
- end = text.length;
-
- while (caret < end && caret >= 0 && count--)
- caret = text.indexOf(char, caret + 1);
- }
-
- if (count > 0)
- caret = -1;
- if (caret == -1)
- dactyl.beep();
- return caret;
- },
+
+ let range = this.selectedRange.cloneRange();
+ let collapse = DOM(this.element).whiteSpace == "normal";
+
+ // Find the *count*th occurance of *char* before a non-collapsed
+ // \n, ignoring the character at the caret.
+ let i = 0;
+ function test(c) (collapse || c != "\n") && !!(!i++ || c != char || --count)
+
+ Editor.extendRange(range, !backward, { test: test }, true);
+ dactyl.assert(count == 0);
+ range.collapse(backward);
+
+ // Skip to any requested offset.
+ count = Math.abs(offset);
+ Editor.extendRange(range, offset > 0, { test: function (c) !!count-- }, true);
+ range.collapse(offset < 0);
+
+ return range;
+ },
+
+ findNumber: function findNumber(range) {
+ if (!range)
+ range = this.selectedRange.cloneRange();
+
+ // Find digit (or \n).
+ Editor.extendRange(range, true, /[^\n\d]/, true);
+ range.collapse(false);
+ // Select entire number.
+ Editor.extendRange(range, true, /\d/, true);
+ Editor.extendRange(range, false, /\d/, true);
+
+ // Sanity check.
+ dactyl.assert(/^\d+$/.test(range));
+
+ if (false) // Skip for now.
+ if (range.startContainer instanceof Text && range.startOffset > 2) {
+ if (range.startContainer.textContent.substr(range.startOffset - 2, 2) == "0x")
+ range.setStart(range.startContainer, range.startOffset - 2);
+ }
+
+ // Grab the sign, if it's there.
+ Editor.extendRange(range, false, /[+-]/, true);
+
+ return range;
+ },
+
+ modifyNumber: function modifyNumber(delta, range) {
+ range = this.findNumber(range);
+ let number = parseInt(range) + delta;
+ if (/^[+-]?0x/.test(range))
+ number = number.toString(16).replace(/^[+-]?/, "$&0x");
+ else if (/^[+-]?0\d/.test(range))
+ number = number.toString(8).replace(/^[+-]?/, "$&0");
+
+ this.selectedRange = range;
+ this.editor.insertText(String(number));
+ this.selection.modify("move", "backward", "character");
+ },
/**
* Edits the given file in the external editor as specified by the
* 'editor' option.
*
* @param {object|File|string} args An object specifying the file, line,
* and column to edit. If a non-object is specified, it is treated as
* the file parameter of the object.
@@ -244,57 +372,77 @@ var Editor = Module("editor", {
},
// TODO: clean up with 2 functions for textboxes and currentEditor?
editFieldExternally: function editFieldExternally(forceEditing) {
if (!options["editor"])
return;
let textBox = config.isComposeWindow ? null : dactyl.focusedElement;
+ if (!DOM(textBox).isInput)
+ textBox = null;
+
let line, column;
+ let keepFocus = modes.stack.some(function (m) isinstance(m.main, modes.COMMAND_LINE));
if (!forceEditing && textBox && textBox.type == "password") {
commandline.input(_("editor.prompt.editPassword") + " ",
function (resp) {
if (resp && resp.match(/^y(es)?$/i))
editor.editFieldExternally(true);
});
return;
}
if (textBox) {
var text = textBox.value;
- let pre = text.substr(0, textBox.selectionStart);
- line = 1 + pre.replace(/[^\n]/g, "").length;
- column = 1 + pre.replace(/[^]*\n/, "").length;
+ var pre = text.substr(0, textBox.selectionStart);
}
else {
var editor_ = window.GetCurrentEditor ? GetCurrentEditor()
: Editor.getEditor(document.commandDispatcher.focusedWindow);
dactyl.assert(editor_);
- text = Array.map(editor_.rootElement.childNodes, function (e) util.domToString(e, true)).join("");
- }
-
- let origGroup = textBox && textBox.getAttributeNS(NS, "highlight") || "";
+ text = Array.map(editor_.rootElement.childNodes, function (e) DOM.stringify(e, true)).join("");
+
+ if (!editor_.selection.rangeCount)
+ var sel = "";
+ else {
+ let range = RangeFind.nodeContents(editor_.rootElement);
+ let end = editor_.selection.getRangeAt(0);
+ range.setEnd(end.startContainer, end.startOffset);
+ pre = DOM.stringify(range, true);
+ if (range.startContainer instanceof Text)
+ pre = pre.replace(/^(?:<[^>"]+>)+/, "");
+ if (range.endContainer instanceof Text)
+ pre = pre.replace(/(?:<\/[^>"]+>)+$/, "");
+ }
+ }
+
+ line = 1 + pre.replace(/[^\n]/g, "").length;
+ column = 1 + pre.replace(/[^]*\n/, "").length;
+
+ let origGroup = DOM(textBox).highlight.toString();
let cleanup = util.yieldable(function cleanup(error) {
if (timer)
timer.cancel();
let blink = ["EditorBlink1", "EditorBlink2"];
if (error) {
dactyl.reportError(error, true);
blink[1] = "EditorError";
}
else
dactyl.trapErrors(update, null, true);
if (tmpfile && tmpfile.exists())
tmpfile.remove(false);
if (textBox) {
+ DOM(textBox).highlight.remove("EditorEditing");
+ if (!keepFocus)
dactyl.focus(textBox);
for (let group in values(blink.concat(blink, ""))) {
highlight.highlightNode(textBox, origGroup + " " + group);
yield 100;
}
}
});
@@ -302,37 +450,40 @@ var Editor = Module("editor", {
if (force !== true && tmpfile.lastModifiedTime <= lastUpdate)
return;
lastUpdate = Date.now();
let val = tmpfile.read();
if (textBox) {
textBox.value = val;
- textBox.setAttributeNS(NS, "modifiable", true);
- util.computedStyle(textBox).MozUserInput;
- events.dispatch(textBox, events.create(textBox.ownerDocument, "input", {}));
- textBox.removeAttributeNS(NS, "modifiable");
+ if (false) {
+ let elem = DOM(textBox);
+ elem.attrNS(NS, "modifiable", true)
+ .style.MozUserInput;
+ elem.input().attrNS(NS, "modifiable", null);
+ }
}
else {
while (editor_.rootElement.firstChild)
editor_.rootElement.removeChild(editor_.rootElement.firstChild);
editor_.rootElement.innerHTML = val;
}
}
try {
var tmpfile = io.createTempFile();
if (!tmpfile)
throw Error(_("io.cantCreateTempFile"));
if (textBox) {
- highlight.highlightNode(textBox, origGroup + " EditorEditing");
+ if (!keepFocus)
textBox.blur();
- }
+ DOM(textBox).highlight.add("EditorEditing");
+ }
if (!tmpfile.write(text))
throw Error(_("io.cantEncode"));
var lastUpdate = Date.now();
var timer = services.Timer(update, 100, services.Timer.TYPE_REPEATING_SLACK);
this.editFileExternally({ file: tmpfile.path, line: line, column: column }, cleanup);
@@ -344,48 +495,177 @@ var Editor = Module("editor", {
/**
* Expands an abbreviation in the currently active textbox.
*
* @param {string} mode The mode filter.
* @see Abbreviation#expand
*/
expandAbbreviation: function (mode) {
- let elem = dactyl.focusedElement;
- if (!(elem && elem.value))
+ if (!this.selection)
return;
- let text = elem.value;
- let start = elem.selectionStart;
- let end = elem.selectionEnd;
- let abbrev = abbreviations.match(mode, text.substring(0, start).replace(/.*\s/g, ""));
+ let range = this.selectedRange.cloneRange();
+ if (!range.collapsed)
+ return;
+
+ Editor.extendRange(range, false, /\S/, true);
+ let abbrev = abbreviations.match(mode, String(range));
if (abbrev) {
- let len = abbrev.lhs.length;
- let rhs = abbrev.expand(elem);
- elem.value = text.substring(0, start - len) + rhs + text.substring(start);
- elem.selectionStart = start - len + rhs.length;
- elem.selectionEnd = end - len + rhs.length;
- }
- },
+ range.setStart(range.startContainer, range.endOffset - abbrev.lhs.length);
+ this.selectedRange = range;
+ this.editor.insertText(abbrev.expand(this.element));
+ }
+ },
+
+ // nsIEditActionListener:
+ WillDeleteNode: util.wrapCallback(function WillDeleteNode(node) {
+ if (!editor.skipSave && node.textContent)
+ this.setRegister(0, node);
+ }),
+ WillDeleteSelection: util.wrapCallback(function WillDeleteSelection(selection) {
+ if (!editor.skipSave && !selection.isCollapsed)
+ this.setRegister(0, selection);
+ }),
+ WillDeleteText: util.wrapCallback(function WillDeleteText(node, start, length) {
+ if (!editor.skipSave && length)
+ this.setRegister(0, node.textContent.substr(start, length));
+ })
}, {
- extendRange: function extendRange(range, forward, re, sameWord) {
+ TextsIterator: Class("TextsIterator", {
+ init: function init(range, context, after) {
+ this.after = after;
+ this.start = context || range[after ? "endContainer" : "startContainer"];
+ if (after)
+ this.context = this.start;
+ this.range = range;
+ },
+
+ __iterator__: function __iterator__() {
+ while (this.nextNode())
+ yield this.context;
+ },
+
+ prevNode: function prevNode() {
+ if (!this.context)
+ return this.context = this.start;
+
+ var node = this.context;
+ if (!this.after)
+ node = node.previousSibling;
+
+ if (!node)
+ node = this.context.parentNode;
+ else
+ while (node.lastChild)
+ node = node.lastChild;
+
+ if (!node || !RangeFind.containsNode(this.range, node, true))
+ return null;
+ this.after = false;
+ return this.context = node;
+ },
+
+ nextNode: function nextNode() {
+ if (!this.context)
+ return this.context = this.start;
+
+ if (!this.after)
+ var node = this.context.firstChild;
+
+ if (!node) {
+ node = this.context;
+ while (node.parentNode && node != this.range.endContainer
+ && !node.nextSibling)
+ node = node.parentNode;
+
+ node = node.nextSibling;
+ }
+
+ if (!node || !RangeFind.containsNode(this.range, node, true))
+ return null;
+ this.after = false;
+ return this.context = node;
+ },
+
+ getPrev: function getPrev() {
+ return this.filter("prevNode");
+ },
+
+ getNext: function getNext() {
+ return this.filter("nextNode");
+ },
+
+ filter: function filter(meth) {
+ let node;
+ while (node = this[meth]())
+ if (node instanceof Ci.nsIDOMText &&
+ DOM(node).isVisible &&
+ DOM(node).style.MozUserSelect != "none")
+ return node;
+ }
+ }),
+
+ extendRange: function extendRange(range, forward, re, sameWord, root, end) {
function advance(positive) {
- let idx = range.endOffset;
- while (idx < text.length && re.test(text[idx++]) == positive)
- range.setEnd(range.endContainer, idx);
+ while (true) {
+ while (idx == text.length && (node = iterator.getNext())) {
+ if (node == iterator.start)
+ idx = range[offset];
+
+ start = text.length;
+ text += node.textContent;
+ range[set](node, idx - start);
+ }
+
+ if (idx >= text.length || re.test(text[idx]) != positive)
+ break;
+ range[set](range[container], ++idx - start);
+ }
}
function retreat(positive) {
- let idx = range.startOffset;
- while (idx > 0 && re.test(text[--idx]) == positive)
- range.setStart(range.startContainer, idx);
- }
-
- let nodeRange = range.cloneRange();
- nodeRange.selectNodeContents(range.startContainer);
- let text = String(nodeRange);
+ while (true) {
+ while (idx == 0 && (node = iterator.getPrev())) {
+ let str = node.textContent;
+ if (node == iterator.start)
+ idx = range[offset];
+ else
+ idx = str.length;
+
+ text = str + text;
+ range[set](node, idx);
+ }
+ if (idx == 0 || re.test(text[idx - 1]) != positive)
+ break;
+ range[set](range[container], --idx);
+ }
+ }
+
+ if (end == null)
+ end = forward ? "end" : "start";
+ let [container, offset, set] = [end + "Container", end + "Offset",
+ "set" + util.capitalize(end)];
+
+ if (!root)
+ for (root = range[container];
+ root.parentNode instanceof Element && !DOM(root).isEditable;
+ root = root.parentNode)
+ ;
+ if (root instanceof Ci.nsIDOMNSEditableElement)
+ root = root.editor;
+ if (root instanceof Ci.nsIEditor)
+ root = root.rootElement;
+
+ let node = range[container];
+ let iterator = Editor.TextsIterator(RangeFind.nodeContents(root),
+ node, !forward);
+
+ let text = "";
+ let idx = 0;
+ let start = 0;
if (forward) {
advance(true);
if (!sameWord)
advance(false);
}
else {
if (!sameWord)
@@ -400,162 +680,270 @@ var Editor = Module("editor", {
dactyl.assert(dactyl.focusedElement);
return dactyl.focusedElement;
}
if (!elem)
elem = dactyl.focusedElement || document.commandDispatcher.focusedWindow;
dactyl.assert(elem);
- try {
- if (elem instanceof Element)
- return elem.QueryInterface(Ci.nsIDOMNSEditableElement).editor;
- return elem.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
- .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIEditingSession)
- .getEditorForWindow(elem);
- }
- catch (e) {
- return null;
- }
- },
-
- getController: function () {
- let ed = dactyl.focusedElement;
- if (!ed || !ed.controllers)
- return null;
-
- return ed.controllers.getControllerForCommand("cmd_beginLine");
+ return DOM(elem).editor;
}
}, {
- mappings: function () {
+ modes: function init_modes() {
+ modes.addMode("OPERATOR", {
+ char: "o",
+ description: "Mappings which move the cursor",
+ bases: []
+ });
+ modes.addMode("VISUAL", {
+ char: "v",
+ description: "Active when text is selected",
+ display: function () "VISUAL" + (this._extended & modes.LINE ? " LINE" : ""),
+ bases: [modes.COMMAND],
+ ownsFocus: true
+ }, {
+ enter: function (stack) {
+ if (editor.selectionController)
+ editor.selectionController.setCaretVisibilityDuringSelection(true);
+ },
+ leave: function (stack, newMode) {
+ if (newMode.main == modes.CARET) {
+ let selection = content.getSelection();
+ if (selection && !selection.isCollapsed)
+ selection.collapseToStart();
+ }
+ else if (stack.pop)
+ editor.deselect();
+ }
+ });
+ modes.addMode("TEXT_EDIT", {
+ char: "t",
+ description: "Vim-like editing of input elements",
+ bases: [modes.COMMAND],
+ ownsFocus: true
+ }, {
+ onKeyPress: function (eventList) {
+ const KILL = false, PASS = true;
+
+ // Hack, really.
+ if (eventList[0].charCode || /^<(?:.-)*(?:BS|Del|C-h|C-w|C-u|C-k)>$/.test(DOM.Event.stringify(eventList[0]))) {
+ dactyl.beep();
+ return KILL;
+ }
+ return PASS;
+ }
+ });
+
+ modes.addMode("INSERT", {
+ char: "i",
+ description: "Active when an input element is focused",
+ insert: true,
+ ownsFocus: true
+ });
+ modes.addMode("AUTOCOMPLETE", {
+ description: "Active when an input autocomplete pop-up is active",
+ display: function () "AUTOCOMPLETE (insert)",
+ bases: [modes.INSERT]
+ });
+ },
+ commands: function init_commands() {
+ commands.add(["reg[isters]"],
+ "List the contents of known registers",
+ function (args) {
+ completion.listCompleter("register", args[0]);
+ },
+ { argCount: "*" });
+ },
+ completion: function init_completion() {
+ completion.register = function complete_register(context) {
+ context = context.fork("registers");
+ context.keys = { text: util.identity, description: editor.closure.getRegister };
+
+ context.match = function (r) !this.filter || ~this.filter.indexOf(r);
+
+ context.fork("clipboard", 0, this, function (ctxt) {
+ ctxt.match = context.match;
+ ctxt.title = ["Clipboard Registers"];
+ ctxt.completions = Object.keys(editor.selectionRegisters);
+ });
+ context.fork("kill-ring", 0, this, function (ctxt) {
+ ctxt.match = context.match;
+ ctxt.title = ["Kill Ring Registers"];
+ ctxt.completions = Array.slice("0123456789");
+ });
+ context.fork("user", 0, this, function (ctxt) {
+ ctxt.match = context.match;
+ ctxt.title = ["User Defined Registers"];
+ ctxt.completions = editor.registers.keys();
+ });
+ };
+ },
+ mappings: function init_mappings() {
+
+ Map.types["editor"] = {
+ preExecute: function preExecute(args) {
+ if (editor.editor && !this.editor) {
+ this.editor = editor.editor;
+ this.editor.beginTransaction();
+ }
+ editor.inEditMap = true;
+ },
+ postExecute: function preExecute(args) {
+ editor.inEditMap = false;
+ if (this.editor) {
+ this.editor.endTransaction();
+ this.editor = null;
+ }
+ },
+ };
+ Map.types["operator"] = {
+ preExecute: function preExecute(args) {
+ editor.inEditMap = true;
+ },
+ postExecute: function preExecute(args) {
+ editor.inEditMap = true;
+ if (modes.main == modes.OPERATOR)
+ modes.pop();
+ }
+ };
// add mappings for commands like h,j,k,l,etc. in CARET, VISUAL and TEXT_EDIT mode
function addMovementMap(keys, description, hasCount, caretModeMethod, caretModeArg, textEditCommand, visualTextEditCommand) {
- let extraInfo = {};
- if (hasCount)
- extraInfo.count = true;
-
- function caretExecute(arg, again) {
- function fixSelection() {
- sel.removeAllRanges();
- sel.addRange(RangeFind.endpoint(
- RangeFind.nodeRange(buffer.focusedFrame.document.documentElement),
- true));
- }
-
- let controller = buffer.selectionController;
+ let extraInfo = {
+ count: !!hasCount,
+ type: "operator"
+ };
+
+ function caretExecute(arg) {
+ let win = document.commandDispatcher.focusedWindow;
+ let controller = util.selectionController(win);
let sel = controller.getSelection(controller.SELECTION_NORMAL);
+
+ let buffer = Buffer(win);
if (!sel.rangeCount) // Hack.
- fixSelection();
-
- try {
+ buffer.resetCaret();
+
+ if (caretModeMethod == "pageMove") { // Grr.
+ buffer.scrollVertical("pages", caretModeArg ? 1 : -1);
+ buffer.resetCaret();
+ }
+ else
controller[caretModeMethod](caretModeArg, arg);
}
- catch (e) {
- dactyl.assert(again && e.result === Cr.NS_ERROR_FAILURE);
- fixSelection();
- caretExecute(arg, false);
- }
- }
-
- mappings.add([modes.CARET], keys, description,
- function ({ count }) {
- if (!count)
- count = 1;
-
- while (count--)
- caretExecute(false, true);
- },
- extraInfo);
mappings.add([modes.VISUAL], keys, description,
function ({ count }) {
- if (!count)
- count = 1;
-
- let editor_ = Editor.getEditor(null);
+ count = count || 1;
+
+ let caret = !dactyl.focusedElement;
let controller = buffer.selectionController;
+
while (count-- && modes.main == modes.VISUAL) {
- if (editor.isTextEdit) {
+ if (caret)
+ caretExecute(true, true);
+ else {
if (callable(visualTextEditCommand))
- visualTextEditCommand(editor_);
+ visualTextEditCommand(editor.editor);
else
editor.executeCommand(visualTextEditCommand);
}
- else
- caretExecute(true, true);
}
},
extraInfo);
- mappings.add([modes.TEXT_EDIT], keys, description,
+ mappings.add([modes.CARET, modes.TEXT_EDIT, modes.OPERATOR], keys, description,
function ({ count }) {
- if (!count)
- count = 1;
-
+ count = count || 1;
+
+ if (editor.editor)
editor.executeCommand(textEditCommand, count);
+ else {
+ while (count--)
+ caretExecute(false);
+ }
},
extraInfo);
}
// add mappings for commands like i,a,s,c,etc. in TEXT_EDIT mode
function addBeginInsertModeMap(keys, commands, description) {
mappings.add([modes.TEXT_EDIT], keys, description || "",
function () {
- commands.forEach(function (cmd)
- editor.executeCommand(cmd, 1));
+ commands.forEach(function (cmd) { editor.executeCommand(cmd, 1) });
modes.push(modes.INSERT);
- });
- }
+ },
+ { type: "editor" });
+ }
function selectPreviousLine() {
editor.executeCommand("cmd_selectLinePrevious");
- if ((modes.extended & modes.LINE) && !editor.selectedText())
+ if ((modes.extended & modes.LINE) && !editor.selectedText)
editor.executeCommand("cmd_selectLinePrevious");
}
function selectNextLine() {
editor.executeCommand("cmd_selectLineNext");
- if ((modes.extended & modes.LINE) && !editor.selectedText())
+ if ((modes.extended & modes.LINE) && !editor.selectedText)
editor.executeCommand("cmd_selectLineNext");
}
- function updateRange(editor, forward, re, modify) {
- let range = Editor.extendRange(editor.selection.getRangeAt(0),
- forward, re, false);
+ function updateRange(editor, forward, re, modify, sameWord) {
+ let sel = editor.selection;
+ let range = sel.getRangeAt(0);
+
+ let end = range.endContainer == sel.focusNode && range.endOffset == sel.focusOffset;
+ if (range.collapsed)
+ end = forward;
+
+ Editor.extendRange(range, forward, re, sameWord,
+ editor.rootElement, end ? "end" : "start");
modify(range);
- editor.selection.removeAllRanges();
- editor.selection.addRange(range);
- }
- function move(forward, re)
+ editor.selectionController.repaintSelection(editor.selectionController.SELECTION_NORMAL);
+ }
+
+ function clear(forward, re)
+ function _clear(editor) {
+ updateRange(editor, forward, re, function (range) {});
+ dactyl.assert(!editor.selection.isCollapsed);
+ editor.selection.deleteFromDocument();
+ let parent = DOM(editor.rootElement.parentNode);
+ if (parent.isInput)
+ parent.input();
+ }
+
+ function move(forward, re, sameWord)
function _move(editor) {
- updateRange(editor, forward, re, function (range) { range.collapse(!forward); });
+ updateRange(editor, forward, re,
+ function (range) { range.collapse(!forward); },
+ sameWord);
}
function select(forward, re)
function _select(editor) {
- updateRange(editor, forward, re, function (range) {});
+ updateRange(editor, forward, re,
+ function (range) {});
}
function beginLine(editor_) {
editor.executeCommand("cmd_beginLine");
- move(true, /\S/)(editor_);
- }
+ move(true, /\s/, true)(editor_);
+ }
// COUNT CARET TEXT_EDIT VISUAL_TEXT_EDIT
addMovementMap(["k", "<Up>"], "Move up one line",
true, "lineMove", false, "cmd_linePrevious", selectPreviousLine);
addMovementMap(["j", "<Down>", "<Return>"], "Move down one line",
true, "lineMove", true, "cmd_lineNext", selectNextLine);
addMovementMap(["h", "<Left>", "<BS>"], "Move left one character",
true, "characterMove", false, "cmd_charPrevious", "cmd_selectCharPrevious");
addMovementMap(["l", "<Right>", "<Space>"], "Move right one character",
true, "characterMove", true, "cmd_charNext", "cmd_selectCharNext");
addMovementMap(["b", "<C-Left>"], "Move left one word",
- true, "wordMove", false, "cmd_wordPrevious", "cmd_selectWordPrevious");
+ true, "wordMove", false, move(false, /\w/), select(false, /\w/));
addMovementMap(["w", "<C-Right>"], "Move right one word",
- true, "wordMove", true, "cmd_wordNext", "cmd_selectWordNext");
+ true, "wordMove", true, move(true, /\w/), select(true, /\w/));
addMovementMap(["B"], "Move left to the previous white space",
true, "wordMove", false, move(false, /\S/), select(false, /\S/));
addMovementMap(["W"], "Move right to just beyond the next white space",
true, "wordMove", true, move(true, /\S/), select(true, /\S/));
addMovementMap(["e"], "Move to the end of the current word",
true, "wordMove", true, move(true, /\W/), select(true, /\W/));
addMovementMap(["E"], "Move right to the next white space",
true, "wordMove", true, move(true, /\s/), select(true, /\s/));
@@ -577,85 +965,166 @@ var Editor = Module("editor", {
addBeginInsertModeMap(["i", "<Insert>"], [], "Insert text before the cursor");
addBeginInsertModeMap(["a"], ["cmd_charNext"], "Append text after the cursor");
addBeginInsertModeMap(["I"], ["cmd_beginLine"], "Insert text at the beginning of the line");
addBeginInsertModeMap(["A"], ["cmd_endLine"], "Append text at the end of the line");
addBeginInsertModeMap(["s"], ["cmd_deleteCharForward"], "Delete the character in front of the cursor and start insert");
addBeginInsertModeMap(["S"], ["cmd_deleteToEndOfLine", "cmd_deleteToBeginningOfLine"], "Delete the current line and start insert");
addBeginInsertModeMap(["C"], ["cmd_deleteToEndOfLine"], "Delete from the cursor to the end of the line and start insert");
- function addMotionMap(key, desc, cmd, mode) {
- mappings.add([modes.TEXT_EDIT], [key],
+ function addMotionMap(key, desc, select, cmd, mode, caretOk) {
+ function doTxn(range, editor) {
+ try {
+ editor.editor.beginTransaction();
+ cmd(editor, range, editor.editor);
+ }
+ finally {
+ editor.editor.endTransaction();
+ }
+ }
+
+ mappings.add([modes.TEXT_EDIT], key,
+ desc,
+ function ({ command, count, motion }) {
+ let start = editor.selectedRange.cloneRange();
+
+ mappings.pushCommand();
+ modes.push(modes.OPERATOR, null, {
+ forCommand: command,
+
+ count: count,
+
+ leave: function leave(stack) {
+ try {
+ if (stack.push || stack.fromEscape)
+ return;
+
+ editor.withSavedValues(["inEditMap"], function () {
+ this.inEditMap = true;
+
+ let range = RangeFind.union(start, editor.selectedRange);
+ editor.selectedRange = select ? range : start;
+ doTxn(range, editor);
+ });
+
+ editor.currentRegister = null;
+ modes.delay(function () {
+ if (mode)
+ modes.push(mode);
+ });
+ }
+ finally {
+ if (!stack.push)
+ mappings.popCommand();
+ }
+ }
+ });
+ },
+ { count: true, type: "motion" });
+
+ mappings.add([modes.VISUAL], key,
desc,
function ({ count, motion }) {
- editor.selectMotion(key, motion, Math.max(count, 1));
- if (callable(cmd))
- cmd.call(events, Editor.getEditor(null));
- else {
- editor.executeCommand(cmd, 1);
- modes.pop(modes.TEXT_EDIT);
- }
- if (mode)
- modes.push(mode);
- },
- { count: true, motion: true });
- }
-
- addMotionMap("d", "Delete motion", "cmd_delete");
- addMotionMap("c", "Change motion", "cmd_delete", modes.INSERT);
- addMotionMap("y", "Yank motion", "cmd_copy");
-
- mappings.add([modes.INPUT],
- ["<C-w>"], "Delete previous word",
- function () { editor.executeCommand("cmd_deleteWordBackward", 1); });
-
- mappings.add([modes.INPUT],
- ["<C-u>"], "Delete until beginning of current line",
+ dactyl.assert(caretOk || editor.isTextEdit);
+ if (editor.isTextEdit)
+ doTxn(editor.selectedRange, editor);
+ else
+ cmd(editor, buffer.selection.getRangeAt(0));
+ },
+ { count: true, type: "motion" });
+ }
+
+ addMotionMap(["d", "x"], "Delete text", true, function (editor) { editor.cut(); });
+ addMotionMap(["c"], "Change text", true, function (editor) { editor.cut(); }, modes.INSERT);
+ addMotionMap(["y"], "Yank text", false, function (editor, range) { editor.copy(range); }, null, true);
+
+ addMotionMap(["gu"], "Lowercase text", false,
+ function (editor, range) {
+ editor.mungeRange(range, String.toLocaleLowerCase);
+ });
+
+ addMotionMap(["gU"], "Uppercase text", false,
+ function (editor, range) {
+ editor.mungeRange(range, String.toLocaleUpperCase);
+ });
+
+ mappings.add([modes.OPERATOR],
+ ["c", "d", "y"], "Select the entire line",
+ function ({ command, count }) {
+ dactyl.assert(command == modes.getStack(0).params.forCommand);
+
+ let sel = editor.selection;
+ sel.modify("move", "backward", "lineboundary");
+ sel.modify("extend", "forward", "lineboundary");
+
+ if (command != "c")
+ sel.modify("extend", "forward", "character");
+ },
+ { count: true, type: "operator" });
+
+ let bind = function bind(names, description, action, params)
+ mappings.add([modes.INPUT], names, description,
+ action, update({ type: "editor" }, params));
+
+ bind(["<C-w>"], "Delete previous word",
+ function () {
+ if (editor.editor)
+ clear(false, /\w/)(editor.editor);
+ else
+ editor.executeCommand("cmd_deleteWordBackward", 1);
+ });
+
+ bind(["<C-u>"], "Delete until beginning of current line",
function () {
// Deletes the whole line. What the hell.
// editor.executeCommand("cmd_deleteToBeginningOfLine", 1);
editor.executeCommand("cmd_selectBeginLine", 1);
- if (Editor.getController().isCommandEnabled("cmd_delete"))
+ if (editor.selection && editor.selection.isCollapsed) {
+ editor.executeCommand("cmd_deleteCharBackward", 1);
+ editor.executeCommand("cmd_selectBeginLine", 1);
+ }
+
+ if (editor.getController("cmd_delete").isCommandEnabled("cmd_delete"))
editor.executeCommand("cmd_delete", 1);
});
- mappings.add([modes.INPUT],
- ["<C-k>"], "Delete until end of current line",
+ bind(["<C-k>"], "Delete until end of current line",
function () { editor.executeCommand("cmd_deleteToEndOfLine", 1); });
- mappings.add([modes.INPUT],
- ["<C-a>"], "Move cursor to beginning of current line",
+ bind(["<C-a>"], "Move cursor to beginning of current line",
function () { editor.executeCommand("cmd_beginLine", 1); });
- mappings.add([modes.INPUT],
- ["<C-e>"], "Move cursor to end of current line",
+ bind(["<C-e>"], "Move cursor to end of current line",
function () { editor.executeCommand("cmd_endLine", 1); });
- mappings.add([modes.INPUT],
- ["<C-h>"], "Delete character to the left",
+ bind(["<C-h>"], "Delete character to the left",
function () { events.feedkeys("<BS>", true); });
- mappings.add([modes.INPUT],
- ["<C-d>"], "Delete character to the right",
+ bind(["<C-d>"], "Delete character to the right",
function () { editor.executeCommand("cmd_deleteCharForward", 1); });
- mappings.add([modes.INPUT],
- ["<S-Insert>"], "Insert clipboard/selection",
- function () { editor.pasteClipboard(); });
-
- mappings.add([modes.INPUT, modes.TEXT_EDIT],
- ["<C-i>"], "Edit text field with an external editor",
+ bind(["<S-Insert>"], "Insert clipboard/selection",
+ function () { editor.paste(); });
+
+ bind(["<C-i>"], "Edit text field with an external editor",
function () { editor.editFieldExternally(); });
- mappings.add([modes.INPUT],
- ["<C-t>"], "Edit text field in Vi mode",
+ bind(["<C-t>"], "Edit text field in Text Edit mode",
function () {
- dactyl.assert(dactyl.focusedElement);
- dactyl.assert(!editor.isTextEdit);
+ dactyl.assert(!editor.isTextEdit && editor.editor);
+ dactyl.assert(dactyl.focusedElement ||
+ // Sites like Google like to use a
+ // hidden, editable window for keyboard
+ // focus and use their own WYSIWYG editor
+ // implementations for the visible area,
+ // which we can't handle.
+ let (f = document.commandDispatcher.focusedWindow.frameElement)
+ f && Hints.isVisible(f, true));
+
modes.push(modes.TEXT_EDIT);
});
// Ugh.
mappings.add([modes.INPUT, modes.CARET],
["<*-CR>", "<*-BS>", "<*-Del>", "<*-Left>", "<*-Right>", "<*-Up>", "<*-Down>",
"<*-Home>", "<*-End>", "<*-PageUp>", "<*-PageDown>",
"<M-c>", "<M-v>", "<*-Tab>"],
@@ -668,178 +1137,198 @@ var Editor = Module("editor", {
editor.expandAbbreviation(modes.INSERT);
return Events.PASS_THROUGH;
});
mappings.add([modes.INSERT],
["<C-]>", "<C-5>"], "Expand Insert mode abbreviation",
function () { editor.expandAbbreviation(modes.INSERT); });
+ let bind = function bind(names, description, action, params)
+ mappings.add([modes.TEXT_EDIT], names, description,
+ action, update({ type: "editor" }, params));
+
+
+ bind(["<C-a>"], "Increment the next number",
+ function ({ count }) { editor.modifyNumber(count || 1) },
+ { count: true });
+
+ bind(["<C-x>"], "Decrement the next number",
+ function ({ count }) { editor.modifyNumber(-(count || 1)) },
+ { count: true });
+
// text edit mode
- mappings.add([modes.TEXT_EDIT],
- ["u"], "Undo changes",
+ bind(["u"], "Undo changes",
function (args) {
editor.executeCommand("cmd_undo", Math.max(args.count, 1));
- editor.unselectText();
+ editor.deselect();
},
{ count: true });
- mappings.add([modes.TEXT_EDIT],
- ["<C-r>"], "Redo undone changes",
+ bind(["<C-r>"], "Redo undone changes",
function (args) {
editor.executeCommand("cmd_redo", Math.max(args.count, 1));
- editor.unselectText();
+ editor.deselect();
},
{ count: true });
- mappings.add([modes.TEXT_EDIT],
- ["D"], "Delete the characters under the cursor until the end of the line",
+ bind(["D"], "Delete characters from the cursor to the end of the line",
function () { editor.executeCommand("cmd_deleteToEndOfLine"); });
- mappings.add([modes.TEXT_EDIT],
- ["o"], "Open line below current",
+ bind(["o"], "Open line below current",
function () {
editor.executeCommand("cmd_endLine", 1);
modes.push(modes.INSERT);
events.feedkeys("<Return>");
});
- mappings.add([modes.TEXT_EDIT],
- ["O"], "Open line above current",
+ bind(["O"], "Open line above current",
function () {
editor.executeCommand("cmd_beginLine", 1);
modes.push(modes.INSERT);
events.feedkeys("<Return>");
editor.executeCommand("cmd_linePrevious", 1);
});
- mappings.add([modes.TEXT_EDIT],
- ["X"], "Delete character to the left",
+ bind(["X"], "Delete character to the left",
function (args) { editor.executeCommand("cmd_deleteCharBackward", Math.max(args.count, 1)); },
{ count: true });
- mappings.add([modes.TEXT_EDIT],
- ["x"], "Delete character to the right",
+ bind(["x"], "Delete character to the right",
function (args) { editor.executeCommand("cmd_deleteCharForward", Math.max(args.count, 1)); },
{ count: true });
// visual mode
mappings.add([modes.CARET, modes.TEXT_EDIT],
["v"], "Start Visual mode",
function () { modes.push(modes.VISUAL); });
mappings.add([modes.VISUAL],
["v", "V"], "End Visual mode",
function () { modes.pop(); });
- mappings.add([modes.TEXT_EDIT],
- ["V"], "Start Visual Line mode",
+ bind(["V"], "Start Visual Line mode",
function () {
modes.push(modes.VISUAL, modes.LINE);
editor.executeCommand("cmd_beginLine", 1);
editor.executeCommand("cmd_selectLineNext", 1);
});
mappings.add([modes.VISUAL],
- ["c", "s"], "Change selected text",
+ ["s"], "Change selected text",
function () {
dactyl.assert(editor.isTextEdit);
editor.executeCommand("cmd_cut");
modes.push(modes.INSERT);
});
mappings.add([modes.VISUAL],
- ["d", "x"], "Delete selected text",
+ ["o"], "Move cursor to the other end of the selection",
function () {
- dactyl.assert(editor.isTextEdit);
- editor.executeCommand("cmd_cut");
- });
-
- mappings.add([modes.VISUAL],
- ["y"], "Yank selected text",
- function () {
- if (editor.isTextEdit) {
- editor.executeCommand("cmd_copy");
- modes.pop();
- }
+ if (editor.isTextEdit)
+ var selection = editor.selection;
else
- dactyl.clipboardWrite(buffer.currentWord, true);
- });
-
- mappings.add([modes.VISUAL, modes.TEXT_EDIT],
- ["p"], "Paste clipboard contents",
+ selection = buffer.focusedFrame.getSelection();
+
+ util.assert(selection.focusNode);
+ let { focusOffset, anchorOffset, focusNode, anchorNode } = selection;
+ selection.collapse(focusNode, focusOffset);
+ selection.extend(anchorNode, anchorOffset);
+ });
+
+ bind(["p"], "Paste clipboard contents",
function ({ count }) {
dactyl.assert(!editor.isCaret);
- editor.executeCommand("cmd_paste", count || 1);
- modes.pop(modes.TEXT_EDIT);
+ editor.executeCommand(modules.bind("paste", editor, null),
+ count || 1);
},
{ count: true });
+ mappings.add([modes.COMMAND],
+ ['"'], "Bind a register to the next command",
+ function ({ arg }) {
+ editor.pushRegister(arg);
+ },
+ { arg: true });
+
+ mappings.add([modes.INPUT],
+ ["<C-'>", '<C-">'], "Bind a register to the next command",
+ function ({ arg }) {
+ editor.pushRegister(arg);
+ },
+ { arg: true });
+
+ let bind = function bind(names, description, action, params)
+ mappings.add([modes.TEXT_EDIT, modes.OPERATOR, modes.VISUAL],
+ names, description,
+ action, update({ type: "editor" }, params));
+
// finding characters
- mappings.add([modes.TEXT_EDIT, modes.VISUAL],
- ["f"], "Move to a character on the current line after the cursor",
+ function offset(backward, before, pos) {
+ if (!backward && modes.main != modes.TEXT_EDIT)
+ return before ? 0 : 1;
+ if (before)
+ return backward ? +1 : -1;
+ return 0;
+ }
+
+ bind(["f"], "Find a character on the current line, forwards",
function ({ arg, count }) {
- let pos = editor.findChar(arg, Math.max(count, 1));
- if (pos >= 0)
- editor.moveToPosition(pos, true, modes.main == modes.VISUAL);
- },
- { arg: true, count: true });
-
- mappings.add([modes.TEXT_EDIT, modes.VISUAL],
- ["F"], "Move to a character on the current line before the cursor",
+ editor.moveToPosition(editor.findChar(arg, Math.max(count, 1), false,
+ offset(false, false)),
+ modes.main == modes.VISUAL);
+ },
+ { arg: true, count: true, type: "operator" });
+
+ bind(["F"], "Find a character on the current line, backwards",
function ({ arg, count }) {
- let pos = editor.findChar(arg, Math.max(count, 1), true);
- if (pos >= 0)
- editor.moveToPosition(pos, false, modes.main == modes.VISUAL);
- },
- { arg: true, count: true });
-
- mappings.add([modes.TEXT_EDIT, modes.VISUAL],
- ["t"], "Move before a character on the current line",
+ editor.moveToPosition(editor.findChar(arg, Math.max(count, 1), true,
+ offset(true, false)),
+ modes.main == modes.VISUAL);
+ },
+ { arg: true, count: true, type: "operator" });
+
+ bind(["t"], "Find a character on the current line, forwards, and move to the character before it",
function ({ arg, count }) {
- let pos = editor.findChar(arg, Math.max(count, 1));
- if (pos >= 0)
- editor.moveToPosition(pos - 1, true, modes.main == modes.VISUAL);
- },
- { arg: true, count: true });
-
- mappings.add([modes.TEXT_EDIT, modes.VISUAL],
- ["T"], "Move before a character on the current line, backwards",
+ editor.moveToPosition(editor.findChar(arg, Math.max(count, 1), false,
+ offset(false, true)),
+ modes.main == modes.VISUAL);
+ },
+ { arg: true, count: true, type: "operator" });
+
+ bind(["T"], "Find a character on the current line, backwards, and move to the character after it",
function ({ arg, count }) {
- let pos = editor.findChar(arg, Math.max(count, 1), true);
- if (pos >= 0)
- editor.moveToPosition(pos + 1, false, modes.main == modes.VISUAL);
- },
- { arg: true, count: true });
+ editor.moveToPosition(editor.findChar(arg, Math.max(count, 1), true,
+ offset(true, true)),
+ modes.main == modes.VISUAL);
+ },
+ { arg: true, count: true, type: "operator" });
// text edit and visual mode
mappings.add([modes.TEXT_EDIT, 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;
- count = Math.max(count, 1);
-
- // FIXME: do this in one pass?
- while (count-- > 0) {
- let text = Editor.getEditor().value;
- let pos = Editor.getEditor().selectionStart;
- dactyl.assert(pos < text.length);
-
- 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);
- }
+ function munger(range)
+ String(range).replace(/./g, function (c) {
+ let lc = c.toLocaleLowerCase();
+ return c == lc ? c.toLocaleUpperCase() : lc;
+ });
+
+ var range = editor.selectedRange;
+ if (range.collapsed) {
+ count = count || 1;
+ Editor.extendRange(range, true, { test: function (c) !!count-- }, true);
+ }
+ editor.mungeRange(range, munger, count != null);
+
modes.pop(modes.TEXT_EDIT);
},
{ count: true });
- function bind() mappings.add.apply(mappings,
+ let bind = function bind() mappings.add.apply(mappings,
[[modes.AUTOCOMPLETE]].concat(Array.slice(arguments)))
bind(["<Esc>"], "Return to Insert mode",
function () Events.PASS_THROUGH);
bind(["<C-[>"], "Return to Insert mode",
function () { events.feedkeys("<Esc>", { skipmap: true }); });
@@ -850,18 +1339,17 @@ var Editor = Module("editor", {
function () { events.feedkeys("<Up>", { skipmap: true }); });
bind(["<Down>"], "Select the next autocomplete result",
function () Events.PASS_THROUGH);
bind(["<C-n>"], "Select the next autocomplete result",
function () { events.feedkeys("<Down>", { skipmap: true }); });
},
-
- options: function () {
+ options: function init_options() {
options.add(["editor"],
"The external text editor",
"string", 'gvim -f +<line> +"sil! call cursor(0, <column>)" <file>', {
format: function (obj, value) {
let args = commands.parseArgs(value || this.value, { argCount: "*", allowUnknownOptions: true })
.map(util.compileMacro).filter(function (fmt) fmt.valid(obj))
.map(function (fmt) fmt(obj));
if (obj["file"] && !this.has("file"))
@@ -873,12 +1361,48 @@ var Editor = Module("editor", {
this.format({}, value);
return Object.keys(util.compileMacro(value).seen).every(function (k) ["column", "file", "line"].indexOf(k) >= 0);
}
});
options.add(["insertmode", "im"],
"Enter Insert mode rather than Text Edit mode when focusing text areas",
"boolean", true);
- }
-});
+
+ options.add(["spelllang", "spl"],
+ "The language used by the spell checker",
+ "string", config.locale,
+ {
+ initValue: function () {},
+ getter: function getter() {
+ try {
+ return services.spell.dictionary || "";
+ }
+ catch (e) {
+ return "";
+ }
+ },
+ setter: function setter(val) { services.spell.dictionary = val; },
+ completer: function completer(context) {
+ let res = {};
+ services.spell.getDictionaryList(res, {});
+ context.completions = res.value;
+ context.keys = { text: util.identity, description: util.identity };
+ }
+ });
+ },
+ sanitizer: function () {
+ sanitizer.addItem("registers", {
+ description: "Register values",
+ persistent: true,
+ action: function (timespan, host) {
+ if (!host) {
+ for (let [k, v] in editor.registers)
+ if (timespan.contains(v.timestamp))
+ editor.registers.remove(k);
+ editor.registerRing.truncate(0);
+ }
+ }
+ });
+ }
+});
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/events.js b/common/content/events.js
--- a/common/content/events.js
+++ b/common/content/events.js
@@ -1,506 +1,209 @@
// Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
// Copyright (c) 2008-2011 by Kris Maglione <maglione.k at Gmail>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
/** @scope modules */
-var ProcessorStack = Class("ProcessorStack", {
- init: function (mode, hives, builtin) {
- this.main = mode.main;
- this._actions = [];
- this.actions = [];
- this.buffer = "";
- this.events = [];
-
- events.dbg("STACK " + mode);
-
- let main = { __proto__: mode.main, params: mode.params };
- this.modes = array([mode.params.keyModes, main, mode.main.allBases.slice(1)]).flatten().compact();
-
- if (builtin)
- hives = hives.filter(function (h) h.name === "builtin");
-
- this.processors = this.modes.map(function (m) hives.map(function (h) KeyProcessor(m, h)))
- .flatten().array;
- this.ownsBuffer = !this.processors.some(function (p) p.main.ownsBuffer);
-
- for (let [i, input] in Iterator(this.processors)) {
- let params = input.main.params;
-
- if (params.preExecute)
- input.preExecute = params.preExecute;
-
- if (params.postExecute)
- input.postExecute = params.postExecute;
-
- if (params.onKeyPress && input.hive === mappings.builtin)
- input.fallthrough = function fallthrough(events) {
- return params.onKeyPress(events) === false ? Events.KILL : Events.PASS;
- };
- }
-
- let hive = options.get("passkeys")[this.main.input ? "inputHive" : "commandHive"];
- if (!builtin && hive.active && (!dactyl.focusedElement || events.isContentNode(dactyl.focusedElement)))
- this.processors.unshift(KeyProcessor(modes.BASE, hive));
- },
-
- passUnknown: Class.memoize(function () options.get("passunknown").getKey(this.modes)),
-
- notify: function () {
- events.dbg("NOTIFY()");
- events.keyEvents = [];
- events.processor = null;
- if (!this.execute(undefined, true)) {
- events.processor = this;
- events.keyEvents = this.keyEvents;
- }
- },
-
- _result: function (result) (result === Events.KILL ? "KILL" :
- result === Events.PASS ? "PASS" :
- result === Events.PASS_THROUGH ? "PASS_THROUGH" :
- result === Events.ABORT ? "ABORT" :
- callable(result) ? result.toSource().substr(0, 50) : result),
-
- execute: function execute(result, force) {
- events.dbg("EXECUTE(" + this._result(result) + ", " + force + ") events:" + this.events.length
- + " processors:" + this.processors.length + " actions:" + this.actions.length);
-
- let processors = this.processors;
- let length = 1;
-
- if (force)
- this.processors = [];
-
- if (this.ownsBuffer)
- statusline.inputBuffer = this.processors.length ? this.buffer : "";
-
- if (!this.processors.some(function (p) !p.extended) && this.actions.length) {
- // We have matching actions and no processors other than
- // those waiting on further arguments. Execute actions as
- // long as they continue to return PASS.
-
- for (var action in values(this.actions)) {
- while (callable(action)) {
- length = action.eventLength;
- action = dactyl.trapErrors(action);
- events.dbg("ACTION RES: " + length + " " + this._result(action));
- }
- if (action !== Events.PASS)
- break;
- }
-
- // Result is the result of the last action. Unless it's
- // PASS, kill any remaining argument processors.
- result = action !== undefined ? action : Events.KILL;
- if (action !== Events.PASS)
- this.processors.length = 0;
- }
- else if (this.processors.length) {
- // We're still waiting on the longest matching processor.
- // Kill the event, set a timeout to give up waiting if applicable.
-
- result = Events.KILL;
- if (options["timeout"] && (this.actions.length || events.hasNativeKey(this.events[0], this.main, this.passUnknown)))
- this.timer = services.Timer(this, options["timeoutlen"], services.Timer.TYPE_ONE_SHOT);
- }
- else if (result !== Events.KILL && !this.actions.length &&
- !(this.events[0].isReplay || this.passUnknown
- || this.modes.some(function (m) m.passEvent(this), this.events[0]))) {
- // No patching processors, this isn't a fake, pass-through
- // event, we're not in pass-through mode, and we're not
- // choosing to pass unknown keys. Kill the event and beep.
-
- result = Events.ABORT;
- if (!Events.isEscape(this.events.slice(-1)[0]))
- dactyl.beep();
- events.feedingKeys = false;
- }
- else if (result === undefined)
- // No matching processors, we're willing to pass this event,
- // and we don't have a default action from a processor. Just
- // pass the event.
- result = Events.PASS;
-
- events.dbg("RESULT: " + length + " " + this._result(result) + "\n\n");
-
- if (result !== Events.PASS || this.events.length > 1)
- if (result !== Events.ABORT || !this.events[0].isReplay)
- Events.kill(this.events[this.events.length - 1]);
-
- if (result === Events.PASS_THROUGH || result === Events.PASS && this.passUnknown)
- events.passing = true;
-
- if (result === Events.PASS_THROUGH && this.keyEvents.length)
- events.dbg("PASS_THROUGH:\n\t" + this.keyEvents.map(function (e) [e.type, events.toString(e)]).join("\n\t"));
-
- if (result === Events.PASS_THROUGH)
- events.feedevents(null, this.keyEvents, { skipmap: true, isMacro: true, isReplay: true });
- else {
- let list = this.events.filter(function (e) e.getPreventDefault() && !e.dactylDefaultPrevented);
-
- if (result === Events.PASS)
- events.dbg("PASS THROUGH: " + list.slice(0, length).filter(function (e) e.type === "keypress").map(events.closure.toString));
- if (list.length > length)
- events.dbg("REFEED: " + list.slice(length).filter(function (e) e.type === "keypress").map(events.closure.toString));
-
- if (result === Events.PASS)
- events.feedevents(null, list.slice(0, length), { skipmap: true, isMacro: true, isReplay: true });
- if (list.length > length && this.processors.length === 0)
- events.feedevents(null, list.slice(length));
- }
-
- return this.processors.length === 0;
- },
-
- process: function process(event) {
- if (this.timer)
- this.timer.cancel();
-
- let key = events.toString(event);
- this.events.push(event);
- if (this.keyEvents)
- this.keyEvents.push(event);
-
- this.buffer += key;
-
- let actions = [];
- let processors = [];
-
- events.dbg("PROCESS(" + key + ") skipmap: " + event.skipmap + " macro: " + event.isMacro + " replay: " + event.isReplay);
-
- for (let [i, input] in Iterator(this.processors)) {
- let res = input.process(event);
- if (res !== Events.ABORT)
- var result = res;
-
- events.dbg("RES: " + input + " " + this._result(res));
-
- if (res === Events.KILL)
- break;
-
- if (callable(res))
- actions.push(res);
-
- if (res === Events.WAIT || input.waiting)
- processors.push(input);
- if (isinstance(res, KeyProcessor))
- processors.push(res);
- }
-
- events.dbg("RESULT: " + event.getPreventDefault() + " " + this._result(result));
- events.dbg("ACTIONS: " + actions.length + " " + this.actions.length);
- events.dbg("PROCESSORS:", processors, "\n");
-
- this._actions = actions;
- this.actions = actions.concat(this.actions);
-
- for (let action in values(actions))
- if (!("eventLength" in action))
- action.eventLength = this.events.length;
-
- if (result === Events.KILL)
- this.actions = [];
- else if (!this.actions.length && !processors.length)
- for (let input in values(this.processors))
- if (input.fallthrough) {
- if (result === Events.KILL)
- break;
- result = dactyl.trapErrors(input.fallthrough, input, this.events);
- }
-
- this.processors = processors;
-
- return this.execute(result, options["timeout"] && options["timeoutlen"] === 0);
- }
-});
-
-var KeyProcessor = Class("KeyProcessor", {
- init: function init(main, hive) {
- this.main = main;
- this.events = [];
- this.hive = hive;
- this.wantCount = this.main.count;
- },
-
- get toStringParams() [this.main.name, this.hive.name],
-
- countStr: "",
- command: "",
- get count() this.countStr ? Number(this.countStr) : null,
-
- append: function append(event) {
- this.events.push(event);
- let key = events.toString(event);
-
- if (this.wantCount && !this.command &&
- (this.countStr ? /^[0-9]$/ : /^[1-9]$/).test(key))
- this.countStr += key;
- else
- this.command += key;
- return this.events;
- },
-
- process: function process(event) {
- this.append(event);
- this.waiting = false;
- return this.onKeyPress(event);
- },
-
- execute: function execute(map, args)
- let (self = this)
- function execute() {
- if (self.preExecute)
- self.preExecute.apply(self, args);
-
- args.self = self.main.params.mappingSelf || self.main.mappingSelf || map;
- let res = map.execute.call(map, args);
-
- if (self.postExecute)
- self.postExecute.apply(self, args);
- return res;
- },
-
- onKeyPress: function onKeyPress(event) {
- if (event.skipmap)
- return Events.ABORT;
-
- if (!this.command)
- return Events.WAIT;
-
- var map = this.hive.get(this.main, this.command);
- this.waiting = this.hive.getCandidates(this.main, this.command);
- if (map) {
- if (map.arg)
- return KeyArgProcessor(this, map, false, "arg");
- else if (map.motion)
- return KeyArgProcessor(this, map, true, "motion");
-
- return this.execute(map, {
- keyEvents: this.keyEvents,
- command: this.command,
- count: this.count,
- keypressEvents: this.events
- });
- }
-
- if (!this.waiting)
- return this.main.insert ? Events.PASS : Events.ABORT;
-
- return Events.WAIT;
- }
-});
-
-var KeyArgProcessor = Class("KeyArgProcessor", KeyProcessor, {
- init: function init(input, map, wantCount, argName) {
- init.supercall(this, input.main, input.hive);
- this.map = map;
- this.parent = input;
- this.argName = argName;
- this.wantCount = wantCount;
- },
-
- extended: true,
-
- onKeyPress: function onKeyPress(event) {
- if (Events.isEscape(event))
- return Events.KILL;
- if (!this.command)
- return Events.WAIT;
-
- let args = {
- command: this.parent.command,
- count: this.count || this.parent.count,
- events: this.parent.events.concat(this.events)
- };
- args[this.argName] = this.command;
-
- return this.execute(this.map, args);
- }
-});
-
/**
* A hive used mainly for tracking event listeners and cleaning them up when a
* group is destroyed.
*/
var EventHive = Class("EventHive", Contexts.Hive, {
init: function init(group) {
init.supercall(this, group);
this.sessionListeners = [];
},
cleanup: function cleanup() {
this.unlisten(null);
},
+ _events: function _events(event, callback) {
+ if (!isObject(event))
+ var [self, events] = [null, array.toObject([[event, callback]])];
+ else
+ [self, events] = [event, event[callback || "events"]];
+
+ if (Set.has(events, "input") && !Set.has(events, "dactyl-input"))
+ events["dactyl-input"] = events.input;
+
+ return [self, events];
+ },
+
/**
* Adds an event listener for this session and removes it on
* dactyl shutdown.
*
* @param {Element} target The element on which to listen.
* @param {string} event The event to listen for.
* @param {function} callback The function to call when the event is received.
* @param {boolean} capture When true, listen during the capture
* phase, otherwise during the bubbling phase.
* @param {boolean} allowUntrusted When true, allow capturing of
* untrusted events.
*/
listen: function (target, event, callback, capture, allowUntrusted) {
- if (!isObject(event))
- var [self, events] = [null, array.toObject([[event, callback]])];
- else {
- [self, events] = [event, event[callback || "events"]];
- [, , capture, allowUntrusted] = arguments;
- }
-
- if (Set.has(events, "input") && !Set.has(events, "dactyl-input"))
- events["dactyl-input"] = events.input;
+ var [self, events] = this._events(event, callback);
for (let [event, callback] in Iterator(events)) {
- let args = [Cu.getWeakReference(target),
+ let args = [util.weakReference(target),
+ util.weakReference(self),
event,
this.wrapListener(callback, self),
capture,
allowUntrusted];
- target.addEventListener.apply(target, args.slice(1));
+ target.addEventListener.apply(target, args.slice(2));
this.sessionListeners.push(args);
}
},
/**
* Remove an event listener.
*
* @param {Element} target The element on which to listen.
* @param {string} event The event to listen for.
* @param {function} callback The function to call when the event is received.
* @param {boolean} capture When true, listen during the capture
* phase, otherwise during the bubbling phase.
*/
unlisten: function (target, event, callback, capture) {
+ if (target != null)
+ var [self, events] = this._events(event, callback);
+
this.sessionListeners = this.sessionListeners.filter(function (args) {
- if (target == null || args[0].get() == target && args[1] == event && args[2] == callback && args[3] == capture) {
- args[0].get().removeEventListener.apply(args[0].get(), args.slice(1));
+ let elem = args[0].get();
+ if (target == null || elem == target
+ && self == args[1].get()
+ && Set.has(events, args[2])
+ && args[3].wrapped == events[args[2]]
+ && args[4] == capture) {
+
+ elem.removeEventListener.apply(elem, args.slice(2));
return false;
}
- return !args[0].get();
- });
- }
-});
+ return elem;
+ });
+ },
+
+ get wrapListener() events.closure.wrapListener
+});
/**
* @instance events
*/
var Events = Module("events", {
dbg: function () {},
init: function () {
this.keyEvents = [];
- update(this, {
- hives: contexts.Hives("events", EventHive),
- user: contexts.hives.events.user,
- builtin: contexts.hives.events.builtin
- });
-
- EventHive.prototype.wrapListener = this.closure.wrapListener;
-
XML.ignoreWhitespace = true;
- util.overlayWindow(window, {
+ overlay.overlayWindow(window, {
append: <e4x xmlns={XUL}>
<window id={document.documentElement.id}>
<!-- http://developer.mozilla.org/en/docs/XUL_Tutorial:Updating_Commands -->
<commandset id="dactyl-onfocus" commandupdater="true" events="focus"
oncommandupdate="dactyl.modules.events.onFocusChange(event);"/>
<commandset id="dactyl-onselect" commandupdater="true" events="select"
oncommandupdate="dactyl.modules.events.onSelectionChange(event);"/>
</window>
</e4x>.elements()
});
this._fullscreen = window.fullScreen;
this._lastFocus = null;
this._macroKeys = [];
this._lastMacro = "";
- this._macros = storage.newMap("macros", { privateData: true, store: true });
- for (let [k, m] in this._macros)
- if (isString(m))
- m = { keys: m, timeRecorded: Date.now() };
-
- // 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 dactyl's source code for
- // comparisons like if (key == "<Esc>") { ... }
- this._keyTable = {
- add: ["Plus", "Add"],
- back_space: ["BS"],
- count: ["count"],
- delete: ["Del"],
- escape: ["Esc", "Escape"],
- insert: ["Insert", "Ins"],
- leader: ["Leader"],
- left_shift: ["LT", "<"],
- nop: ["Nop"],
- pass: ["Pass"],
- return: ["Return", "CR", "Enter"],
- right_shift: [">"],
- space: ["Space", " "],
- subtract: ["Minus", "Subtract"]
+ this._macros = storage.newMap("registers", { privateData: true, store: true });
+ if (storage.exists("macros")) {
+ for (let [k, m] in storage.newMap("macros", { store: true }))
+ this._macros.set(k, { text: m.keys, timestamp: m.timeRecorded * 1000 });
+ storage.remove("macros");
+ }
+
+ this.popups = {
+ active: [],
+
+ activeMenubar: null,
+
+ update: function update(elem) {
+ if (elem) {
+ if (elem instanceof Ci.nsIAutoCompletePopup
+ || elem.localName == "tooltip"
+ || !elem.popupBoxObject)
+ return;
+
+ if (!~this.active.indexOf(elem))
+ this.active.push(elem);
+ }
+
+ this.active = this.active.filter(function (e) e.popupBoxObject.popupState != "closed");
+
+ if (!this.active.length && !this.activeMenubar)
+ modes.remove(modes.MENU, true);
+ else if (modes.main != modes.MENU)
+ modes.push(modes.MENU);
+ },
+
+ events: {
+ DOMMenuBarActive: function onDOMMenuBarActive(event) {
+ this.activeMenubar = event.target;
+ if (modes.main != modes.MENU)
+ modes.push(modes.MENU);
+ },
+
+ DOMMenuBarInactive: function onDOMMenuBarInactive(event) {
+ this.activeMenubar = null;
+ modes.remove(modes.MENU, true);
+ },
+
+ popupshowing: function onPopupShowing(event) {
+ this.update(event.originalTarget);
+ },
+
+ popupshown: function onPopupShown(event) {
+ let elem = event.originalTarget;
+ this.update(elem);
+
+ if (elem instanceof Ci.nsIAutoCompletePopup) {
+ if (modes.main != modes.AUTOCOMPLETE)
+ modes.push(modes.AUTOCOMPLETE);
+ }
+ else if (elem.hidePopup && elem.localName !== "tooltip"
+ && Events.isHidden(elem)
+ && Events.isHidden(elem.parentNode)) {
+ elem.hidePopup();
+ }
+ },
+
+ popuphidden: function onPopupHidden(event) {
+ this.update();
+ modes.remove(modes.AUTOCOMPLETE);
+ }
+ }
};
- this._pseudoKeys = Set(["count", "leader", "nop", "pass"]);
-
- this._key_key = {};
- this._code_key = {};
- this._key_code = {};
- this._code_nativeKey = {};
-
- for (let list in values(this._keyTable))
- for (let v in values(list)) {
- if (v.length == 1)
- v = v.toLowerCase();
- this._key_key[v.toLowerCase()] = v;
- }
-
- for (let [k, v] in Iterator(KeyEvent)) {
- this._code_nativeKey[v] = k.substr(4);
-
- k = k.substr(7).toLowerCase();
- let names = [k.replace(/(^|_)(.)/g, function (m, n1, n2) n2.toUpperCase())
- .replace(/^NUMPAD/, "k")];
-
- if (names[0].length == 1)
- names[0] = names[0].toLowerCase();
-
- if (k in this._keyTable)
- names = this._keyTable[k];
- this._code_key[v] = names[0];
- for (let [, name] in Iterator(names)) {
- this._key_key[name.toLowerCase()] = name;
- this._key_code[name.toLowerCase()] = v;
- }
- }
-
- // 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";
- }
-
- this._activeMenubar = false;
- this.listen(window, this, "events");
- },
+ this.listen(window, this, "events", true);
+ this.listen(window, this.popups, "events", true);
+ },
+
+ cleanup: function cleanup() {
+ let elem = dactyl.focusedElement;
+ if (DOM(elem).isEditable)
+ util.trapErrors("removeEditActionListener",
+ DOM(elem).editor, editor);
+ },
signals: {
"browser.locationChange": function (webProgress, request, uri) {
options.get("passkeys").flush();
},
"modes.change": function (oldMode, newMode) {
delete this.processor;
}
@@ -509,17 +212,18 @@ var Events = Module("events", {
get listen() this.builtin.closure.listen,
addSessionListener: deprecated("events.listen", { get: function addSessionListener() this.listen }),
/**
* Wraps an event listener to ensure that errors are reported.
*/
wrapListener: function wrapListener(method, self) {
self = self || this;
- method.wrapped = wrappedListener;
+ method.wrapper = wrappedListener;
+ wrappedListener.wrapped = method;
function wrappedListener(event) {
try {
method.apply(self, arguments);
}
catch (e) {
dactyl.reportError(e);
if (e.message == "Interrupted")
dactyl.echoerr(_("error.interrupted"), commandline.FORCE_SINGLELINE);
@@ -544,110 +248,104 @@ var Events = Module("events", {
*/
_recording: null,
get recording() this._recording,
set recording(macro) {
dactyl.assert(macro == null || /[a-zA-Z0-9]/.test(macro),
_("macro.invalid", macro));
- modes.recording = !!macro;
-
- if (/[A-Z]/.test(macro)) { // uppercase (append)
+ modes.recording = macro;
+
+ if (/[A-Z]/.test(macro)) { // Append.
macro = macro.toLowerCase();
- this._macroKeys = events.fromString((this._macros.get(macro) || { keys: "" }).keys, true)
- .map(events.closure.toString);
- }
- else if (macro) {
+ this._macroKeys = DOM.Event.iterKeys(editor.getRegister(macro))
+ .toArray();
+ }
+ else if (macro) { // Record afresh.
this._macroKeys = [];
}
- else {
- this._macros.set(this.recording, {
- keys: this._macroKeys.join(""),
- timeRecorded: Date.now()
- });
+ else if (this.recording) { // Save.
+ editor.setRegister(this.recording, this._macroKeys.join(""));
dactyl.log(_("macro.recorded", this.recording, this._macroKeys.join("")), 9);
dactyl.echomsg(_("macro.recorded", this.recording));
}
this._recording = macro || null;
},
/**
* Replays a macro.
*
* @param {string} The name of the macro to replay.
* @returns {boolean}
*/
playMacro: function (macro) {
- let res = false;
- dactyl.assert(/^[a-zA-Z0-9@]$/.test(macro), _("macro.invalid", macro));
+ dactyl.assert(/^[a-zA-Z0-9@]$/.test(macro),
+ _("macro.invalid", macro));
if (macro == "@")
dactyl.assert(this._lastMacro, _("macro.noPrevious"));
else
this._lastMacro = macro.toLowerCase(); // XXX: sets last played macro, even if it does not yet exist
- if (this._macros.get(this._lastMacro)) {
- try {
- modes.replaying = true;
- res = events.feedkeys(this._macros.get(this._lastMacro).keys, { noremap: true });
- }
- finally {
- modes.replaying = false;
- }
- }
- else
+ let keys = editor.getRegister(this._lastMacro);
+ if (keys)
+ return modes.withSavedValues(["replaying"], function () {
+ this.replaying = true;
+ return events.feedkeys(keys, { noremap: true });
+ });
+
// TODO: ignore this like Vim?
dactyl.echoerr(_("macro.noSuch", this._lastMacro));
- return res;
- },
+ return false;
+ },
/**
* Returns all macros matching *filter*.
*
* @param {string} filter A regular expression filter string. A null
* filter selects all macros.
*/
getMacros: function (filter) {
let re = RegExp(filter || "");
- return ([k, m.keys] for ([k, m] in events._macros) if (re.test(k)));
- },
+ return ([k, m.text] for ([k, m] in editor.registers) if (re.test(k)));
+ },
/**
* Deletes all macros matching *filter*.
*
* @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) {
+ for (let [item, ] in editor.registers) {
if (!filter || re.test(item))
- this._macros.remove(item);
- }
- },
+ editor.registers.remove(item);
+ }
+ },
/**
* Feeds a list of events to *target* or the originalTarget member
* of each event if *target* is null.
*
* @param {EventTarget} target The destination node for the events.
* @optional
* @param {[Event]} list The events to dispatch.
* @param {object} extra Extra properties for processing by dactyl.
* @optional
*/
feedevents: function feedevents(target, list, extra) {
list.forEach(function _feedevent(event, i) {
let elem = target || event.originalTarget;
if (elem) {
let doc = elem.ownerDocument || elem.document || elem;
- let evt = events.create(doc, event.type, event);
- events.dispatch(elem, evt, extra);
+ let evt = DOM.Event(doc, event.type, event);
+ DOM.Event.dispatch(elem, evt, extra);
}
else if (i > 0 && event.type === "keypress")
events.events.keypress.call(events, event);
});
},
/**
* Pushes keys onto the event queue from dactyl. It is similar to
@@ -671,421 +369,84 @@ var Events = Module("events", {
this.feedingKeys = true;
var wasQuiet = commandline.quiet;
if (quiet)
commandline.quiet = quiet;
keys = mappings.expandLeader(keys);
- for (let [, evt_obj] in Iterator(events.fromString(keys))) {
+ for (let [, evt_obj] in Iterator(DOM.Event.parse(keys))) {
let now = Date.now();
- let key = events.toString(evt_obj);
+ let key = DOM.Event.stringify(evt_obj);
for (let type in values(["keydown", "keypress", "keyup"])) {
let evt = update({}, evt_obj, { type: type });
if (type !== "keypress" && !evt.keyCode)
evt.keyCode = evt._keyCode || 0;
if (isObject(noremap))
update(evt, noremap);
else
evt.noremap = !!noremap;
evt.isMacro = true;
evt.dactylMode = mode;
evt.dactylSavedEvents = savedEvents;
- this.feedingEvent = evt;
+ DOM.Event.feedingEvent = evt;
let doc = document.commandDispatcher.focusedWindow.document;
- let event = events.create(doc, type, evt);
+
let target = dactyl.focusedElement
|| ["complete", "interactive"].indexOf(doc.readyState) >= 0 && doc.documentElement
|| doc.defaultView;
if (target instanceof Element && !Events.isInputElement(target) &&
["<Return>", "<Space>"].indexOf(key) == -1)
target = target.ownerDocument.documentElement;
+ let event = DOM.Event(doc, type, evt);
if (!evt_obj.dactylString && !mode)
- events.dispatch(target, event, evt);
+ DOM.Event.dispatch(target, event, evt);
else if (type === "keypress")
events.events.keypress.call(events, event);
}
if (!this.feedingKeys)
return false;
}
}
catch (e) {
util.reportError(e);
}
finally {
- this.feedingEvent = null;
+ DOM.Event.feedingEvent = null;
this.feedingKeys = wasFeeding;
if (quiet)
commandline.quiet = wasQuiet;
dactyl.triggerObserver("events.doneFeeding");
}
return true;
},
- /**
- * 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. @optional
- */
- create: function (doc, type, opts) {
- const DEFAULTS = {
- HTML: {
- type: type, bubbles: true, cancelable: false
- },
- 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
- }
- };
-
- opts = opts || {};
-
- var t = this._create_types[type];
- var evt = doc.createEvent((t || "HTML") + "Events");
-
- let defaults = DEFAULTS[t || "HTML"];
-
- let args = Object.keys(defaults)
- .map(function (k) k in opts ? opts[k] : defaults[k]);
-
- evt["init" + t + "Event"].apply(evt, args);
- return evt;
- },
-
- _create_types: Class.memoize(function () iter(
- {
- Mouse: "click mousedown mouseout mouseover mouseup",
- Key: "keydown keypress keyup",
- "": "change dactyl-input input submit"
- }
- ).map(function ([k, v]) v.split(" ").map(function (v) [v, k]))
- .flatten()
- .toObject()),
-
- /**
- * 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 untypeable key-combinations
- * in macros.
- *
- * canonicalKeys(canonicalKeys(x)) == canonicalKeys(x) for all values
- * of x.
- *
- * @param {string} keys Messy form.
- * @param {boolean} unknownOk Whether unknown keys are passed
- * through rather than being converted to <lt>keyname>.
- * @default false
- * @returns {string} Canonical form.
- */
- canonicalKeys: function (keys, unknownOk) {
- if (arguments.length === 1)
- unknownOk = true;
- return events.fromString(keys, unknownOk).map(events.closure.toString).join("");
- },
-
- iterKeys: function (keys) iter(function () {
- let match, re = /<.*?>?>|[^<]/g;
- while (match = re.exec(keys))
- yield match[0];
- }()),
-
- /**
- * Dispatches an event to an element as if it were a native event.
- *
- * @param {Node} target The DOM node to which to dispatch the event.
- * @param {Event} event The event to dispatch.
- */
- dispatch: Class.memoize(function ()
- util.haveGecko("2b")
- ? function dispatch(target, event, extra) {
- try {
- this.feedingEvent = extra;
- if (target instanceof Element)
- // This causes a crash on Gecko<2.0, it seems.
- return (target.ownerDocument || target.document || target).defaultView
- .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
- .dispatchDOMEventViaPresShell(target, event, true);
- else {
- target.dispatchEvent(event);
- return !event.getPreventDefault();
- }
- }
- catch (e) {
- util.reportError(e);
- }
- finally {
- this.feedingEvent = null;
- }
- }
- : function dispatch(target, event, extra) {
- try {
- this.feedingEvent = extra;
- target.dispatchEvent(update(event, extra));
- }
- finally {
- this.feedingEvent = null;
- }
- }),
+ canonicalKeys: deprecated("DOM.Event.canonicalKeys", { get: function canonicalKeys() DOM.Event.closure.canonicalKeys }),
+ create: deprecated("DOM.Event", function create() DOM.Event.apply(null, arguments)),
+ dispatch: deprecated("DOM.Event.dispatch", function dispatch() DOM.Event.dispatch.apply(DOM.Event, arguments)),
+ fromString: deprecated("DOM.Event.parse", { get: function fromString() DOM.Event.closure.parse }),
+ iterKeys: deprecated("DOM.Event.iterKeys", { get: function iterKeys() DOM.Event.closure.iterKeys }),
+
+ toString: function toString() {
+ if (!arguments.length)
+ return toString.supercall(this);
+
+ deprecated.warn(toString, "toString", "DOM.Event.stringify");
+ return DOM.Event.stringify.apply(DOM.Event, arguments);
+ },
get defaultTarget() dactyl.focusedElement || content.document.body || document.documentElement,
/**
- * 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, .dactylString and
- * .dactylShift these are set for characters that can never by
- * typed, but may appear in mappings, for example <Nop> is passed as
- * dactylString, and dactylShift is set when a user specifies
- * <S-@> where @ is a non-case-changeable, non-space character.
- *
- * @param {string} keys The string to parse.
- * @param {boolean} unknownOk Whether unknown keys are passed
- * through rather than being converted to <lt>keyname>.
- * @default false
- * @returns {Array[Object]}
- */
- fromString: function (input, unknownOk) {
-
- if (arguments.length === 1)
- unknownOk = true;
-
- let out = [];
- for (let match in util.regexp.iterate(/<.*?>?>|[^<]|<(?!.*>)/g, 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) {
- evt_obj.charCode = evt_str.charCodeAt(0);
- evt_obj._keyCode = this._key_code[evt_str[0].toLowerCase()];
- evt_obj.shiftKey = evt_str !== evt_str.toLowerCase();
- }
- else {
- let [match, modifier, keyname] = evt_str.match(/^<((?:[*12CASM⌘]-)*)(.+?)>$/i) || [false, '', ''];
- modifier = Set(modifier.toUpperCase());
- keyname = keyname.toLowerCase();
- evt_obj.dactylKeyname = keyname;
- if (/^u[0-9a-f]+$/.test(keyname))
- keyname = String.fromCharCode(parseInt(keyname.substr(1), 16));
-
- if (keyname && (unknownOk || keyname.length == 1 || /mouse$/.test(keyname) ||
- this._key_code[keyname] || Set.has(this._pseudoKeys, keyname))) {
- evt_obj.globKey ="*" in modifier;
- evt_obj.ctrlKey ="C" in modifier;
- evt_obj.altKey ="A" in modifier;
- evt_obj.shiftKey ="S" in modifier;
- evt_obj.metaKey ="M" in modifier || "⌘" in modifier;
- evt_obj.dactylShift = evt_obj.shiftKey;
-
- if (keyname.length == 1) { // normal characters
- if (evt_obj.shiftKey)
- keyname = keyname.toUpperCase();
-
- evt_obj.charCode = keyname.charCodeAt(0);
- evt_obj._keyCode = this._key_code[keyname.toLowerCase()];
- }
- else if (Set.has(this._pseudoKeys, keyname)) {
- evt_obj.dactylString = "<" + this._key_key[keyname] + ">";
- }
- 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 { // an invalid sequence starting with <, treat as a literal
- out = out.concat(events.fromString("<lt>" + evt_str.substr(1)));
- continue;
- }
- }
-
- // 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>
-
- evt_obj.modifiers = (evt_obj.ctrlKey && Ci.nsIDOMNSEvent.CONTROL_MASK)
- | (evt_obj.altKey && Ci.nsIDOMNSEvent.ALT_MASK)
- | (evt_obj.shiftKey && Ci.nsIDOMNSEvent.SHIFT_MASK)
- | (evt_obj.metaKey && Ci.nsIDOMNSEvent.META_MASK);
-
- out.push(evt_obj);
- }
- return out;
- },
-
- /**
- * Converts the specified event to a string in dactyl key-code
- * notation. Returns null for an unknown event.
- *
- * @param {Event} event
- * @returns {string}
- */
- toString: function toString(event) {
- if (!event)
- return toString.supercall(this);
-
- if (event.dactylString)
- return event.dactylString;
-
- let key = null;
- let modifier = "";
-
- if (event.globKey)
- modifier += "*-";
- if (event.ctrlKey)
- modifier += "C-";
- if (event.altKey)
- modifier += "A-";
- if (event.metaKey)
- modifier += "M-";
-
- if (/^key/.test(event.type)) {
- let charCode = event.type == "keyup" ? 0 : event.charCode; // Why? --Kris
- if (charCode == 0) {
- if (event.keyCode in this._code_key) {
- key = this._code_key[event.keyCode];
-
- if (event.shiftKey && (key.length > 1 || event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
- modifier += "S-";
- else if (!modifier && key.length === 1)
- if (event.shiftKey)
- key = key.toUpperCase();
- else
- key = key.toLowerCase();
-
- if (!modifier && /^[a-z0-9]$/i.test(key))
- return key;
- }
- }
- // [Ctrl-Bug] special handling of mysterious <C-[>, <C-\\>, <C-]>, <C-^>, <C-_> bugs (OS/X)
- // (i.e., cntrl codes 27--31)
- // ---
- // For more information, see:
- // [*] Referenced mailing list msg: http://www.mozdev.org/pipermail/pentadactyl/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?id=416227
- // [*] Mozilla bug 432951: Ctrl+'foo' doesn't seem same charCode as Meta+'foo' on Cocoa
- // https://bugzilla.mozilla.org/show_bug.cgi?id=432951
- // ---
- //
- // The following fixes are only activated if util.OS.isMacOSX.
- // 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 util.OS.isMacOSX if desired.
- //
- else if (util.OS.isMacOSX && event.ctrlKey && charCode >= 27 && charCode <= 31) {
- if (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(charCode + 64);
- }
- // a normal key like a, b, c, 0, etc.
- else if (charCode > 0) {
- key = String.fromCharCode(charCode);
-
- if (!/^[a-z0-9]$/i.test(key) && 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.dactylShift)
- modifier += "S-";
-
- key = this._code_key[this._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.dactylShift)
- modifier += "S-";
- if (/^\s$/.test(key))
- key = let (s = charCode.toString(16)) "U" + "0000".substr(4 - s.length) + s;
- else if (modifier.length == 0)
- return key;
- }
- }
- if (key == null) {
- if (event.shiftKey)
- modifier += "S-";
- key = this._key_key[event.dactylKeyname] || event.dactylKeyname;
- }
- if (key == null)
- return null;
- }
- 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;
- }
- }
-
- if (key == null)
- return null;
-
- return "<" + modifier + key + ">";
- },
-
- /**
* Returns true if there's a known native key handler for the given
* event in the given mode.
*
* @param {Event} event A keypress event.
* @param {Modes.Mode} mode The main mode.
* @param {boolean} passUnknown Whether unknown keys should be passed.
*/
hasNativeKey: function hasNativeKey(event, mode, passUnknown) {
@@ -1101,17 +462,17 @@ var Events = Module("events", {
if (event.keyCode)
filters.push(["keycode", this._code_nativeKey[event.keyCode]]);
if (event.charCode) {
let key = String.fromCharCode(event.charCode);
filters.push(["key", key.toUpperCase()],
["key", key.toLowerCase()]);
}
- let accel = util.OS.isMacOSX ? "metaKey" : "ctrlKey";
+ let accel = config.OS.isMacOSX ? "metaKey" : "ctrlKey";
let access = iter({ 1: "shiftKey", 2: "ctrlKey", 4: "altKey", 8: "metaKey" })
.filter(function ([k, v]) this & k, prefs.get("ui.key.chromeAccess"))
.map(function ([k, v]) [v, true])
.toObject();
outer:
for (let [, key] in iter(elements))
@@ -1207,29 +568,22 @@ var Events = Module("events", {
document.commandDispatcher.focusedWindow = content;
// onFocusChange needs to die.
this.onFocusChange();
}
}
},
events: {
- DOMMenuBarActive: function () {
- this._activeMenubar = true;
- if (modes.main != modes.MENU)
- modes.push(modes.MENU);
- },
-
- DOMMenuBarInactive: function () {
- this._activeMenubar = false;
- modes.remove(modes.MENU, true);
- },
-
blur: function onBlur(event) {
let elem = event.originalTarget;
+ if (DOM(elem).isEditable)
+ util.trapErrors("removeEditActionListener",
+ DOM(elem).editor, editor);
+
if (elem instanceof Window && services.focus.activeWindow == null
&& document.commandDispatcher.focusedWindow !== window) {
// Deals with circumstances where, after the main window
// blurs while a collapsed frame has focus, re-activating
// the main window does not restore focus and we lose key
// input.
services.focus.clearFocus(window);
document.commandDispatcher.focusedWindow = Editor.getEditor(content) ? window : content;
@@ -1240,24 +594,31 @@ var Events = Module("events", {
dactyl.focus(hold);
this.timeout(function () { dactyl.focus(hold); });
}
},
// TODO: Merge with onFocusChange
focus: function onFocus(event) {
let elem = event.originalTarget;
-
+ if (DOM(elem).isEditable)
+ util.trapErrors("addEditActionListener",
+ DOM(elem).editor, editor);
+
+ if (elem == window)
+ overlay.activeWindow = window;
+
+ overlay.setData(elem, "had-focus", true);
if (event.target instanceof Ci.nsIDOMXULTextBoxElement)
if (Events.isHidden(elem, true))
elem.blur();
let win = (elem.ownerDocument || elem).defaultView || elem;
- if (!(services.focus.getLastFocusMethod(win) & 0x7000)
+ if (!(services.focus.getLastFocusMethod(win) & 0x3000)
&& events.isContentNode(elem)
&& !buffer.focusAllowed(elem)
&& isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement, Window])) {
if (elem.frameElement)
dactyl.focusContent(true);
else if (!(elem instanceof Window) || Editor.getEditor(elem))
dactyl.focus(window);
@@ -1300,32 +661,32 @@ var Events = Module("events", {
// the command-line has focus
// TODO: ...help me...please...
keypress: function onKeyPress(event) {
event.dactylDefaultPrevented = event.getPreventDefault();
let duringFeed = this.duringFeed || [];
this.duringFeed = [];
try {
- if (this.feedingEvent)
- for (let [k, v] in Iterator(this.feedingEvent))
+ if (DOM.Event.feedingEvent)
+ for (let [k, v] in Iterator(DOM.Event.feedingEvent))
if (!(k in event))
event[k] = v;
- this.feedingEvent = null;
-
- let key = events.toString(event);
+ DOM.Event.feedingEvent = null;
+
+ let key = DOM.Event.stringify(event);
// Hack to deal with <BS> and so forth not dispatching input
// events
if (key && event.originalTarget instanceof HTMLInputElement && !modes.main.passthrough) {
let elem = event.originalTarget;
elem.dactylKeyPress = elem.value;
util.timeout(function () {
if (elem.dactylKeyPress !== undefined && elem.value !== elem.dactylKeyPress)
- events.dispatch(elem, events.create(elem.ownerDocument, "dactyl-input"));
+ DOM(elem).dactylInput();
elem.dactylKeyPress = undefined;
});
}
if (!key)
return null;
if (modes.recording && !event.isReplay)
@@ -1404,50 +765,54 @@ var Events = Module("events", {
}
finally {
[duringFeed, this.duringFeed] = [this.duringFeed, duringFeed];
if (this.feedingKeys)
this.duringFeed = this.duringFeed.concat(duringFeed);
else
for (let event in values(duringFeed))
try {
- this.dispatch(event.originalTarget, event, event);
+ DOM.Event.dispatch(event.originalTarget, event, event);
}
catch (e) {
util.reportError(e);
}
}
},
keyup: function onKeyUp(event) {
if (event.type == "keydown")
this.keyEvents.push(event);
else if (!this.processor)
this.keyEvents = [];
let pass = this.passing && !event.isMacro ||
- this.feedingEvent && this.feedingEvent.isReplay ||
+ DOM.Event.feedingEvent && DOM.Event.feedingEvent.isReplay ||
event.isReplay ||
modes.main == modes.PASS_THROUGH ||
modes.main == modes.QUOTE
&& modes.getStack(1).main !== modes.PASS_THROUGH
&& !this.shouldPass(event) ||
!modes.passThrough && this.shouldPass(event) ||
!this.processor && event.type === "keydown"
&& options.get("passunknown").getKey(modes.main.allBases)
- && let (key = events.toString(event))
+ && let (key = DOM.Event.stringify(event))
!modes.main.allBases.some(
function (mode) mappings.hives.some(
function (hive) hive.get(mode, key) || hive.getCandidates(mode, key)));
+ events.dbg("ON " + event.type.toUpperCase() + " " + DOM.Event.stringify(event) +
+ " passing: " + this.passing + " " +
+ " pass: " + pass +
+ " replay: " + event.isReplay +
+ " macro: " + event.isMacro);
+
if (event.type === "keydown")
this.passing = pass;
- events.dbg("ON " + event.type.toUpperCase() + " " + this.toString(event) + " pass: " + pass + " replay: " + event.isReplay + " macro: " + event.isMacro);
-
// Prevents certain sites from transferring focus to an input box
// before we get a chance to process our key bindings on the
// "keypress" event.
if (!pass)
event.stopPropagation();
},
keydown: function onKeyDown(event) {
if (!event.isMacro)
@@ -1456,56 +821,36 @@ var Events = Module("events", {
},
mousedown: function onMouseDown(event) {
let elem = event.target;
let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem;
for (; win; win = win != win.parent && win.parent) {
for (; elem instanceof Element; elem = elem.parentNode)
- elem.dactylFocusAllowed = true;
- win.document.dactylFocusAllowed = true;
- }
- },
-
- popupshown: function onPopupShown(event) {
- let elem = event.originalTarget;
- if (elem instanceof Ci.nsIAutoCompletePopup) {
- if (modes.main != modes.AUTOCOMPLETE)
- modes.push(modes.AUTOCOMPLETE);
- }
- else if (elem.localName !== "tooltip")
- if (Events.isHidden(elem)) {
- if (elem.hidePopup && Events.isHidden(elem.parentNode))
- elem.hidePopup();
- }
- else if (modes.main != modes.MENU)
- modes.push(modes.MENU);
- },
-
- popuphidden: function onPopupHidden(event) {
- if (window.gContextMenu == null && !this._activeMenubar)
- modes.remove(modes.MENU, true);
- modes.remove(modes.AUTOCOMPLETE);
- },
+ overlay.setData(elem, "focus-allowed", true);
+ overlay.setData(win.document, "focus-allowed", true);
+ }
+ },
resize: function onResize(event) {
if (window.fullScreen != this._fullscreen) {
statusline.statusBar.removeAttribute("moz-collapsed");
this._fullscreen = window.fullScreen;
dactyl.triggerObserver("fullscreen", this._fullscreen);
autocommands.trigger("Fullscreen", { url: this._fullscreen ? "on" : "off", state: this._fullscreen });
}
- }
- },
+ statusline.updateZoomLevel();
+ }
+ },
// argument "event" is deliberately not used, as i don't seem to have
// access to the real focus target
// Huh? --djk
- onFocusChange: function onFocusChange(event) {
+ onFocusChange: util.wrapCallback(function onFocusChange(event) {
function hasHTMLDocument(win) win && win.document && win.document instanceof HTMLDocument
if (dactyl.ignoreFocus)
return;
let win = window.document.commandDispatcher.focusedWindow;
let elem = window.document.commandDispatcher.focusedElement;
if (elem == null && Editor.getEditor(win))
@@ -1514,131 +859,134 @@ var Events = Module("events", {
if (win && win.top == content && dactyl.has("tabs"))
buffer.focusedFrame = win;
try {
if (elem && elem.readOnly)
return;
if (isinstance(elem, [HTMLEmbedElement, HTMLEmbedElement])) {
+ if (!modes.main.passthrough && modes.main != modes.EMBED)
modes.push(modes.EMBED);
return;
}
let haveInput = modes.stack.some(function (m) m.main.input);
- if (elem instanceof HTMLTextAreaElement
- || elem instanceof Element && util.computedStyle(elem).MozUserModify === "read-write"
- || elem == null && win && Editor.getEditor(win)) {
-
- if (modes.main == modes.VISUAL && elem.selectionEnd == elem.selectionStart)
- modes.pop();
-
+ if (DOM(elem || win).isEditable) {
if (!haveInput)
+ if (!isinstance(modes.main, [modes.INPUT, modes.TEXT_EDIT, modes.VISUAL]))
if (options["insertmode"])
modes.push(modes.INSERT);
else {
modes.push(modes.TEXT_EDIT);
if (elem.selectionEnd - elem.selectionStart > 0)
modes.push(modes.VISUAL);
}
if (hasHTMLDocument(win))
+ buffer.lastInputField = elem || win;
+ return;
+ }
+
+ if (elem && Events.isInputElement(elem)) {
+ if (!haveInput)
+ modes.push(modes.INSERT);
+
+ if (hasHTMLDocument(win))
buffer.lastInputField = elem;
return;
}
- if (Events.isInputElement(elem)) {
- if (!haveInput)
- modes.push(modes.INSERT);
-
- if (hasHTMLDocument(win))
- buffer.lastInputField = elem;
- return;
- }
-
if (config.focusChange) {
config.focusChange(win);
return;
}
let urlbar = document.getElementById("urlbar");
if (elem == null && urlbar && urlbar.inputField == this._lastFocus)
util.threadYield(true); // Why? --Kris
- while (modes.main.ownsFocus && modes.topOfStack.params.ownsFocus != elem
+ while (modes.main.ownsFocus
+ && modes.topOfStack.params.ownsFocus != elem
+ && modes.topOfStack.params.ownsFocus != win
&& !modes.topOfStack.params.holdFocus)
modes.pop(null, { fromFocus: true });
}
finally {
this._lastFocus = elem;
if (modes.main.ownsFocus)
modes.topOfStack.params.ownsFocus = elem;
}
- },
+ }),
onSelectionChange: function onSelectionChange(event) {
+ // Ignore selection events caused by editor commands.
+ if (editor.inEditMap || modes.main == modes.OPERATOR)
+ return;
+
let controller = document.commandDispatcher.getControllerForCommand("cmd_copy");
let couldCopy = controller && controller.isCommandEnabled("cmd_copy");
- if (modes.main == modes.VISUAL) {
- if (!couldCopy)
- modes.pop(); // Really not ideal.
- }
- else if (couldCopy) {
- if (modes.main == modes.TEXT_EDIT && !options["insertmode"])
+ if (couldCopy) {
+ if (modes.main == modes.TEXT_EDIT)
modes.push(modes.VISUAL);
else if (modes.main == modes.CARET)
modes.push(modes.VISUAL);
}
},
shouldPass: function shouldPass(event)
!event.noremap && (!dactyl.focusedElement || events.isContentNode(dactyl.focusedElement)) &&
- options.get("passkeys").has(events.toString(event))
+ options.get("passkeys").has(DOM.Event.stringify(event))
}, {
ABORT: {},
KILL: true,
PASS: false,
PASS_THROUGH: {},
WAIT: null,
isEscape: function isEscape(event)
- let (key = isString(event) ? event : events.toString(event))
+ let (key = isString(event) ? event : DOM.Event.stringify(event))
key === "<Esc>" || key === "<C-[>",
isHidden: function isHidden(elem, aggressive) {
- if (util.computedStyle(elem).visibility !== "visible")
+ if (DOM(elem).style.visibility !== "visible")
return true;
if (aggressive)
for (let e = elem; e instanceof Element; e = e.parentNode) {
if (!/set$/.test(e.localName) && e.boxObject && e.boxObject.height === 0)
return true;
else if (e.namespaceURI == XUL && e.localName === "panel")
break;
}
return false;
},
isInputElement: function isInputElement(elem) {
- return elem instanceof HTMLInputElement && Set.has(util.editableInputs, elem.type) ||
- isinstance(elem, [HTMLEmbedElement,
- HTMLObjectElement, HTMLSelectElement,
- HTMLTextAreaElement,
- Ci.nsIDOMXULTextBoxElement]) ||
- elem instanceof Window && Editor.getEditor(elem);
- },
+ return DOM(elem).isEditable ||
+ isinstance(elem, [HTMLEmbedElement, HTMLObjectElement,
+ HTMLSelectElement])
+ },
kill: function kill(event) {
event.stopPropagation();
event.preventDefault();
}
}, {
+ contexts: function initContexts(dactyl, modules, window) {
+ update(Events.prototype, {
+ hives: contexts.Hives("events", EventHive),
+ user: contexts.hives.events.user,
+ builtin: contexts.hives.events.builtin
+ });
+ },
+
commands: function () {
commands.add(["delmac[ros]"],
"Delete macros",
function (args) {
dactyl.assert(!args.bang || !args[0], _("error.invalidArgument"));
if (args.bang)
events.deleteMacros();
@@ -1672,17 +1020,20 @@ var Events = Module("events", {
["<A-b>", "<pass-next-key-builtin>"], "Process the next key as a builtin mapping",
function () {
events.processor = ProcessorStack(modes.getStack(0), mappings.hives.array, true);
events.processor.keyEvents = events.keyEvents;
});
mappings.add([modes.MAIN],
["<C-z>", "<pass-all-keys>"], "Temporarily ignore all " + config.appName + " key bindings",
- function () { modes.push(modes.PASS_THROUGH); });
+ function () {
+ if (modes.main != modes.PASS_THROUGH)
+ modes.push(modes.PASS_THROUGH);
+ });
mappings.add([modes.MAIN, modes.PASS_THROUGH, modes.QUOTE],
["<C-v>", "<pass-next-key>"], "Pass through next key",
function () {
if (modes.main == modes.QUOTE)
return Events.PASS;
modes.push(modes.QUOTE);
});
@@ -1705,16 +1056,17 @@ var Events = Module("events", {
{ skipmap: true, isMacro: true, isReplay: true });
};
});
// macros
mappings.add([modes.COMMAND],
["q", "<record-macro>"], "Record a key sequence into a macro",
function ({ arg }) {
+ util.assert(arg == null || /^[a-z]$/i.test(arg));
events._macroKeys.pop();
events.recording = arg;
},
{ get arg() !modes.recording });
mappings.add([modes.COMMAND],
["@", "<play-macro>"], "Play a macro",
function ({ arg, count }) {
@@ -1770,30 +1122,35 @@ var Events = Module("events", {
memoize(this, "commandHive", function hive() Hive(this.filters, "command"));
memoize(this, "inputHive", function hive() Hive(this.filters, "input"));
},
has: function (key) Set.has(this.pass, key) || Set.has(this.commandHive.stack.mappings, key),
get pass() (this.flush(), this.pass),
+ parse: function parse() {
+ let value = parse.superapply(this, arguments);
+ value.forEach(function (filter) {
+ let vals = Option.splitList(filter.result);
+ filter.keys = DOM.Event.parse(vals[0]).map(DOM.Event.closure.stringify);
+
+ filter.commandKeys = vals.slice(1).map(DOM.Event.closure.canonicalKeys);
+ filter.inputKeys = filter.commandKeys.filter(bind("test", /^<[ACM]-/));
+ });
+ return value;
+ },
+
keepQuotes: true,
- setter: function (values) {
- values.forEach(function (filter) {
- let vals = Option.splitList(filter.result);
- filter.keys = events.fromString(vals[0]).map(events.closure.toString);
-
- filter.commandKeys = vals.slice(1).map(events.closure.canonicalKeys);
- filter.inputKeys = filter.commandKeys.filter(bind("test", /^<[ACM]-/));
- });
+ setter: function (value) {
this.flush();
- return values;
- }
- });
+ return value;
+ }
+ });
options.add(["strictfocus", "sf"],
"Prevent scripts from focusing input elements without user intervention",
"sitemap", "'chrome:*':laissez-faire,*:moderate",
{
values: {
despotic: "Only allow focus changes when explicitly requested by the user",
moderate: "Allow focus changes after user-initiated focus change",
@@ -1803,24 +1160,12 @@ var Events = Module("events", {
options.add(["timeout", "tmo"],
"Whether to execute a shorter key command after a timeout when a longer command exists",
"boolean", true);
options.add(["timeoutlen", "tmol"],
"Maximum time (milliseconds) to wait for a longer key command when a shorter one exists",
"number", 1000);
- },
- sanitizer: function () {
- sanitizer.addItem("macros", {
- description: "Saved macros",
- persistent: true,
- action: function (timespan, host) {
- if (!host)
- for (let [k, m] in events._macros)
- if (timespan.contains(m.timeRecorded * 1000))
- events._macros.remove(k);
- }
- });
- }
-});
+ }
+});
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/help.js b/common/content/help.js
--- a/common/content/help.js
+++ b/common/content/help.js
@@ -1,13 +1,13 @@
// Copyright (c) 2009-2011 by Kris Maglione <kris@vimperator.org>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
function checkFragment() {
document.title = document.getElementsByTagNameNS("http://www.w3.org/1999/xhtml", "title")[0].textContent;
function action() {
content.scrollTo(0, content.scrollY + elem.getBoundingClientRect().top - 10); // 10px context
}
diff --git a/common/content/help.xsl b/common/content/help.xsl
--- a/common/content/help.xsl
+++ b/common/content/help.xsl
@@ -90,16 +90,17 @@
<xsl:with-param name="root-node" select="document(@href)/dactyl:document"/>
</xsl:call-template>
</div>
</xsl:template>
<xsl:template match="@*|node()" mode="overlay">
<xsl:apply-templates select="." mode="overlay-2"/>
</xsl:template>
+ <xsl:template match="@*[starts-with(local-name(), 'on')]|*[local-name() = 'script']" mode="overlay-2"/>
<xsl:template match="@*|node()" mode="overlay-2">
<xsl:copy>
<xsl:apply-templates select="@*|node()" mode="overlay"/>
</xsl:copy>
</xsl:template>
<!-- Root {{{1 -->
@@ -227,17 +228,17 @@
<div style="clear: both;"/>
<div dactyl:highlight="HelpSpec"><xsl:apply-templates mode="help-1"/></div>
</xsl:template>
-->
<xsl:template match="dactyl:default[not(@type='plain')]" mode="help-2">
<xsl:variable name="type" select="preceding-sibling::dactyl:type[1] | following-sibling::dactyl:type[1]"/>
<span dactyl:highlight="HelpDefault">
- <xsl:copy-of select="@*"/>
+ <xsl:copy-of select="@*[not(starts-with(local-name(), 'on'))]"/>
<xsl:text>(default: </xsl:text>
<xsl:choose>
<xsl:when test="$type = 'string'">
<span dactyl:highlight="HelpString" delim="'"><xsl:apply-templates mode="help-1"/></span>
</xsl:when>
<xsl:when test="contains($type, 'list') or contains($type, 'map')">
<span dactyl:highlight="HelpString" delim=""><xsl:apply-templates mode="help-1"/></span>
<xsl:if test=". = ''"><!--L-->(empty)</xsl:if>
@@ -290,24 +291,31 @@
</div>
</xsl:template>
<!-- Tag Links {{{1 -->
<xsl:template name="linkify-tag">
<xsl:param name="contents" select="text()"/>
<xsl:variable name="tag" select="$contents"/>
+ <xsl:variable name="tag-url" select="
+ regexp:replace(regexp:replace($tag, '%', 'g', '%25'),
+ '#', 'g', '%23')"/>
+
<a style="color: inherit;">
<xsl:if test="not(@link) or @link != 'false'">
<xsl:choose>
+ <xsl:when test="@link and @link != 'false'">
+ <xsl:attribute name="href">dactyl://help-tag/<xsl:value-of select="@link"/></xsl:attribute>
+ </xsl:when>
<xsl:when test="contains(ancestor::*/@document-tags, concat(' ', $tag, ' '))">
- <xsl:attribute name="href">#<xsl:value-of select="$tag"/></xsl:attribute>
+ <xsl:attribute name="href">#<xsl:value-of select="$tag-url"/></xsl:attribute>
</xsl:when>
<xsl:otherwise>
- <xsl:attribute name="href">dactyl://help-tag/<xsl:value-of select="$tag"/></xsl:attribute>
+ <xsl:attribute name="href">dactyl://help-tag/<xsl:value-of select="$tag-url"/></xsl:attribute>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
<xsl:value-of select="$contents"/>
</a>
</xsl:template>
<xsl:template match="dactyl:o" mode="help-2">
@@ -481,27 +489,27 @@
<xsl:attribute name="topic">'<xsl:value-of select="@opt"/>'</xsl:attribute>
</xsl:if>
<hl key="HelpOpt"><xsl:value-of select="@opt"/></hl>
</link>
<xsl:choose>
<xsl:when test="@op and @op != ''"><xsl:value-of select="@op"/></xsl:when>
<xsl:otherwise>=</xsl:otherwise>
</xsl:choose>
- <xsl:copy-of select="@*|node()"/>
+ <xsl:copy-of select="@*[not(starts-with(local-name(), 'on'))]|node()[local-name() != 'script']"/>
</html:span>
</xsl:variable>
<xsl:apply-templates select="exsl:node-set($nodes)" mode="help-1"/>
</xsl:template>
<xsl:template match="dactyl:set" mode="help-2">
<xsl:variable name="nodes">
<code xmlns="&xmlns.dactyl;">
<se opt="{@opt}" op="{@op}" link="{@link}">
- <xsl:copy-of select="@*|node()"/>
+ <xsl:copy-of select="@*[not(starts-with(local-name(), 'on'))]|node()[local-name() != 'script']"/>
</se>
</code>
</xsl:variable>
<xsl:apply-templates select="exsl:node-set($nodes)" mode="help-1"/>
</xsl:template>
<xsl:template match="dactyl:description | dactyl:example | dactyl:spec" mode="help-2">
<div>
@@ -526,22 +534,25 @@
</xsl:template>
<xsl:template match="dactyl:str" mode="help-2">
<span dactyl:highlight="HelpString">
<xsl:apply-templates select="@*|node()" mode="help-1"/>
</span>
</xsl:template>
<xsl:template match="dactyl:xml-block" mode="help-2">
<div dactyl:highlight="HelpXMLBlock">
- <xsl:call-template name="xml-highlight"/>
+ <xsl:apply-templates mode="xml-highlight"/>
</div>
</xsl:template>
<xsl:template match="dactyl:xml-highlight" mode="help-2">
- <xsl:call-template name="xml-highlight"/>
- </xsl:template>
+ <div dactyl:highlight="HelpXMLBlock">
+ <xsl:apply-templates mode="xml-highlight"/>
+ </div>
+ </xsl:template>
+
<!-- Plugins {{{1 -->
<xsl:template name="info">
<xsl:param name="label"/>
<xsl:param name="link" select="@href"/>
<xsl:param name="nodes" select="node()"/>
<xsl:param name="extra"/>
@@ -600,32 +611,27 @@
<!-- Special Element Templates {{{1 -->
<xsl:template match="dactyl:logo" mode="help-1">
<span dactyl:highlight="Logo"/>
</xsl:template>
<!-- Process Tree {{{1 -->
+ <xsl:template match="@*[starts-with(local-name(), 'on')]|*[local-name() = 'script']" mode="help-2"/>
<xsl:template match="@*|node()" mode="help-2">
<xsl:copy>
<xsl:apply-templates select="@*|node()" mode="help-1"/>
</xsl:copy>
</xsl:template>
<xsl:template match="@*|node()" mode="help-1">
<xsl:apply-templates select="." mode="help-2"/>
</xsl:template>
<!-- XML Highlighting (xsl:import doesn't work in Firefox 3.x) {{{1 -->
- <xsl:template name="xml-highlight">
- <div dactyl:highlight="HelpXML">
- <xsl:apply-templates mode="xml-highlight"/>
- </div>
- </xsl:template>
-
<xsl:template name="xml-namespace">
<xsl:param name="node" select="."/>
<xsl:if test="name($node) != local-name($node)">
<span dactyl:highlight="HelpXMLNamespace">
<xsl:value-of select="substring-before(name($node), ':')"/>
</span>
</xsl:if>
<xsl:value-of select="local-name($node)"/>
diff --git a/common/content/hints.js b/common/content/hints.js
--- a/common/content/hints.js
+++ b/common/content/hints.js
@@ -1,45 +1,45 @@
// Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
// Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
/** @scope modules */
/** @instance hints */
var HintSession = Class("HintSession", CommandMode, {
get extendedMode() modes.HINTS,
init: function init(mode, opts) {
init.supercall(this);
opts = opts || {};
- // Hack.
- if (!opts.window && modes.main == modes.OUTPUT_MULTILINE)
- opts.window = commandline.widgets.multilineOutput.contentWindow;
+ if (!opts.window)
+ opts.window = modes.getStack(0).params.window;
this.hintMode = hints.modes[mode];
dactyl.assert(this.hintMode);
this.activeTimeout = null; // needed for hinttimeout > 0
this.continue = Boolean(opts.continue);
this.docs = [];
- this.hintKeys = events.fromString(options["hintkeys"]).map(events.closure.toString);
+ this.hintKeys = DOM.Event.parse(options["hintkeys"]).map(DOM.Event.closure.stringify);
this.hintNumber = 0;
this.hintString = opts.filter || "";
this.pageHints = [];
this.prevInput = "";
this.usedTabKey = false;
this.validHints = []; // store the indices of the "hints" array with valid elements
+ mappings.pushCommand();
this.open();
this.top = opts.window || content;
this.top.addEventListener("resize", this.closure._onResize, true);
this.top.addEventListener("dactyl-commandupdate", this.closure._onResize, false, true);
this.generate();
@@ -93,16 +93,18 @@ var HintSession = Class("HintSession", C
get mode() modes.HINTS,
get prompt() ["Question", UTF8(this.hintMode.prompt) + ": "],
leave: function leave(stack) {
leave.superapply(this, arguments);
if (!stack.push) {
+ mappings.popCommand();
+
if (hints.hintSession == this)
hints.hintSession = null;
if (this.top) {
this.top.removeEventListener("resize", this.closure._onResize, true);
this.top.removeEventListener("dactyl-commandupdate", this.closure._onResize, true);
}
this.removeHints(0);
@@ -250,17 +252,17 @@ var HintSession = Class("HintSession", C
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
getContainerOffsets: function _getContainerOffsets(doc) {
let body = doc.body || doc.documentElement;
// TODO: getComputedStyle returns null for Facebook channel_iframe doc - probable Gecko bug.
- let style = util.computedStyle(body);
+ let style = DOM(body).style;
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];
},
@@ -293,33 +295,32 @@ var HintSession = Class("HintSession", C
function isVisible(elem) {
let rect = elem.getBoundingClientRect();
if (!rect ||
rect.top > offsets.bottom || rect.bottom < offsets.top ||
rect.left > offsets.right || rect.right < offsets.left)
return false;
if (!rect.width || !rect.height)
- if (!Array.some(elem.childNodes, function (elem) elem instanceof Element && util.computedStyle(elem).float != "none" && isVisible(elem)))
+ if (!Array.some(elem.childNodes, function (elem) elem instanceof Element && DOM(elem).style.float != "none" && isVisible(elem)))
return false;
let computedStyle = doc.defaultView.getComputedStyle(elem, null);
if (computedStyle.visibility != "visible" || computedStyle.display == "none")
return false;
return true;
}
let body = doc.body || doc.querySelector("body");
if (body) {
- let fragment = util.xmlToDom(<div highlight="hints"/>, doc);
- body.appendChild(fragment);
- util.computedStyle(fragment).height; // Force application of binding.
- let container = doc.getAnonymousElementByAttribute(fragment, "anonid", "hints") || fragment;
-
- let baseNodeAbsolute = util.xmlToDom(<span highlight="Hint" style="display: none;"/>, doc);
+ let fragment = DOM(<div highlight="hints"/>, doc).appendTo(body);
+ fragment.style.height; // Force application of binding.
+ let container = doc.getAnonymousElementByAttribute(fragment[0], "anonid", "hints") || fragment[0];
+
+ let baseNode = DOM(<span highlight="Hint" style="display: none;"/>, doc)[0];
let mode = this.hintMode;
let res = mode.matcher(doc);
let start = this.pageHints.length;
let _hints = [];
for (let elem in res)
if (isVisible(elem) && (!mode.filter || mode.filter(elem)))
@@ -337,17 +338,17 @@ var HintSession = Class("HintSession", C
[hint.text, hint.showText] = [elem.getAttributeNS(NS, "hint"), true];
else if (isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement]))
[hint.text, hint.showText] = hints.getInputHint(elem, doc);
else if (elem.firstElementChild instanceof HTMLImageElement && /^\s*$/.test(elem.textContent))
[hint.text, hint.showText] = [elem.firstElementChild.alt || elem.firstElementChild.title, true];
else
hint.text = elem.textContent.toLowerCase();
- hint.span = baseNodeAbsolute.cloneNode(false);
+ hint.span = baseNode.cloneNode(false);
let leftPos = Math.max((rect.left + offsetX), offsetX);
let topPos = Math.max((rect.top + offsetY), offsetY);
if (elem instanceof HTMLAreaElement)
[leftPos, topPos] = this.getAreaOffset(elem, leftPos, topPos);
hint.span.setAttribute("style", ["display: none; left:", leftPos, "px; top:", topPos, "px"].join(""));
@@ -397,17 +398,17 @@ var HintSession = Class("HintSession", C
/**
* Handle a hints mode event.
*
* @param {Event} event The event to handle.
*/
onKeyPress: function onKeyPress(eventList) {
const KILL = false, PASS = true;
- let key = events.toString(eventList[0]);
+ let key = DOM.Event.stringify(eventList[0]);
this.clearTimeout();
if (!this.escapeNumbers && this.isHintKey(key)) {
this.prevInput = "number";
let oldHintNumber = this.hintNumber;
if (this.usedTabKey) {
@@ -495,25 +496,27 @@ var HintSession = Class("HintSession", C
hints.setClass(elem, n % 2);
else
hints.setClass(elem, this.validHints[Math.max(0, this.hintNumber - 1)].elem === elem);
if (n--)
this.timeout(next, 50);
}).call(this);
+ mappings.pushCommand();
if (!this.continue) {
modes.pop();
if (timeout)
modes.push(modes.IGNORE, modes.HINTS);
}
dactyl.trapErrors("action", this.hintMode,
elem, elem.href || elem.src || "",
this.extendedhintCount, top);
+ mappings.popCommand();
this.timeout(function () {
if (modes.main == modes.IGNORE && !this.continue)
modes.pop();
commandline.lastEcho = null; // Hack.
if (this.continue && this.top)
this.show();
}, timeout);
@@ -527,17 +530,17 @@ var HintSession = Class("HintSession", C
* @param {number} timeout The number of milliseconds before the active
* hint disappears.
*/
removeHints: function _removeHints(timeout) {
for (let { doc, start, end } in values(this.docs)) {
// Goddamn stupid fucking Gecko 1.x security manager bullshit.
try { delete doc.dactylLabels; } catch (e) { doc.dactylLabels = undefined; }
- for (let elem in util.evaluateXPath("//*[@dactyl:highlight='hints']", doc))
+ for (let elem in DOM.XPath("//*[@dactyl:highlight='hints']", doc))
elem.parentNode.removeChild(elem);
for (let i in util.range(start, end + 1)) {
this.pageHints[i].ambiguous = false;
this.pageHints[i].valid = false;
}
}
styles.system.remove("hint-positions");
@@ -749,19 +752,19 @@ var Hints = Module("hints", {
this.addMode("w", "Follow hint in a new window", function (elem) buffer.followLink(elem, dactyl.NEW_WINDOW));
this.addMode("O", "Generate an ‘:open URL’ prompt", function (elem, loc) CommandExMode().open("open " + loc));
this.addMode("T", "Generate a ‘:tabopen URL’ prompt", function (elem, loc) CommandExMode().open("tabopen " + loc));
this.addMode("W", "Generate a ‘:winopen URL’ prompt", function (elem, loc) CommandExMode().open("winopen " + loc));
this.addMode("a", "Add a bookmark", function (elem) bookmarks.addSearchKeyword(elem));
this.addMode("S", "Add a search keyword", function (elem) bookmarks.addSearchKeyword(elem));
this.addMode("v", "View hint source", function (elem, loc) buffer.viewSource(loc, false));
this.addMode("V", "View hint source in external editor", function (elem, loc) buffer.viewSource(loc, true));
- this.addMode("y", "Yank hint location", function (elem, loc) dactyl.clipboardWrite(loc, true));
- this.addMode("Y", "Yank hint description", function (elem) dactyl.clipboardWrite(elem.textContent || "", true));
- this.addMode("c", "Open context menu", function (elem) buffer.openContextMenu(elem));
+ this.addMode("y", "Yank hint location", function (elem, loc) editor.setRegister(null, loc, true));
+ this.addMode("Y", "Yank hint description", function (elem) editor.setRegister(null, elem.textContent || "", true));
+ this.addMode("c", "Open context menu", function (elem) DOM(elem).contextmenu());
this.addMode("i", "Show image", function (elem) dactyl.open(elem.src));
this.addMode("I", "Show image in a new tab", function (elem) dactyl.open(elem.src, dactyl.NEW_TAB));
function isScrollable(elem) isinstance(elem, [HTMLFrameElement, HTMLIFrameElement]) ||
Buffer.isScrollable(elem, 0, true) || Buffer.isScrollable(elem, 0, false);
},
hintSession: Modes.boundProperty(),
@@ -819,17 +822,17 @@ var Hints = Module("hints", {
// <input type="password"/> Never use the value, use label or name
// <input type="text|file"/> <textarea/> Use value if set or label or name
// <input type="image"/> Use the alt text if present (showText) or label or name
// <input type="hidden"/> Never gets here
// <select/> Use the text of the selected item or label or name
let type = elem.type;
- if (elem instanceof HTMLInputElement && Set.has(util.editableInputs, elem.type))
+ if (DOM(elem).isInput)
return [elem.value, false];
else {
for (let [, option] in Iterator(options["hintinputs"])) {
if (option == "value") {
if (elem instanceof HTMLSelectElement) {
if (elem.selectedIndex >= 0)
return [elem.item(elem.selectedIndex).text.toLowerCase(), false];
}
@@ -1026,25 +1029,31 @@ var Hints = Module("hints", {
case "custom" : return dactyl.plugins.customHintMatcher(hintString);
default : dactyl.echoerr(_("hints.noMatcher", hintMatching));
}
return null;
}, //}}}
open: function open(mode, opts) {
this._extendedhintCount = opts.count;
- commandline.input(["Normal", mode], "", {
+
+ opts = opts || {};
+
+ mappings.pushCommand();
+ commandline.input(["Normal", mode], null, {
autocomplete: false,
completer: function (context) {
context.compare = function () 0;
context.completions = [[k, v.prompt] for ([k, v] in Iterator(hints.modes))];
},
+ onCancel: mappings.closure.popCommand,
onSubmit: function (arg) {
if (arg)
hints.show(arg, opts);
+ mappings.popCommand();
},
onChange: function (arg) {
if (Object.keys(hints.modes).some(function (m) m != arg && m.indexOf(arg) == 0))
return;
this.accepted = true;
modes.pop();
}
@@ -1072,17 +1081,34 @@ var Hints = Module("hints", {
elem.dactylHighlight = null;
}
},
show: function show(mode, opts) {
this.hintSession = HintSession(mode, opts);
}
}, {
- translitTable: Class.memoize(function () {
+ isVisible: function isVisible(elem, offScreen) {
+ let rect = elem.getBoundingClientRect();
+ if (!rect.width || !rect.height)
+ if (!Array.some(elem.childNodes, function (elem) elem instanceof Element && DOM(elem).style.float != "none" && isVisible(elem)))
+ return false;
+
+ let win = elem.ownerDocument.defaultView;
+ if (offScreen && (rect.top + win.scrollY < 0 || rect.left + win.scrollX < 0 ||
+ rect.bottom + win.scrollY > win.scrolMaxY + win.innerHeight ||
+ rect.right + win.scrollX > win.scrolMaxX + win.innerWidth))
+ return false;
+
+ if (!DOM(elem).isVisible)
+ return false;
+ return true;
+ },
+
+ translitTable: Class.Memoize(function () {
const table = {};
[
[0x00c0, 0x00c6, ["A"]], [0x00c7, 0x00c7, ["C"]],
[0x00c8, 0x00cb, ["E"]], [0x00cc, 0x00cf, ["I"]],
[0x00d1, 0x00d1, ["N"]], [0x00d2, 0x00d6, ["O"]],
[0x00d8, 0x00d8, ["O"]], [0x00d9, 0x00dc, ["U"]],
[0x00dd, 0x00dd, ["Y"]], [0x00e0, 0x00e6, ["a"]],
[0x00e7, 0x00e7, ["c"]], [0x00e8, 0x00eb, ["e"]],
@@ -1186,103 +1212,108 @@ var Hints = Module("hints", {
extended: true,
description: "Active when selecting elements with hints",
bases: [modes.COMMAND_LINE],
input: true,
ownsBuffer: true
});
},
mappings: function () {
- var myModes = config.browserModes.concat(modes.OUTPUT_MULTILINE);
- mappings.add(myModes, ["f"],
+ let bind = function bind(names, description, action, params)
+ mappings.add(config.browserModes, names, description,
+ action, params);
+
+ bind(["f"],
"Start Hints mode",
function () { hints.show("o"); });
- mappings.add(myModes, ["F"],
+ bind(["F"],
"Start Hints mode, but open link in a new tab",
function () { hints.show(options.get("activate").has("links") ? "t" : "b"); });
- mappings.add(myModes, [";"],
+ bind([";"],
"Start an extended hints mode",
function ({ count }) { hints.open(";", { count: count }); },
{ count: true });
- mappings.add(myModes, ["g;"],
+ bind(["g;"],
"Start an extended hints mode and stay there until <Esc> is pressed",
function ({ count }) { hints.open("g;", { continue: true, count: count }); },
{ count: true });
- mappings.add(modes.HINTS, ["<Return>"],
+ let bind = function bind(names, description, action, params)
+ mappings.add([modes.HINTS], names, description,
+ action, params);
+
+ bind(["<Return>"],
"Follow the selected hint",
function ({ self }) { self.update(true); });
- mappings.add(modes.HINTS, ["<Tab>"],
+ bind(["<Tab>"],
"Focus the next matching hint",
function ({ self }) { self.tab(false); });
- mappings.add(modes.HINTS, ["<S-Tab>"],
+ bind(["<S-Tab>"],
"Focus the previous matching hint",
function ({ self }) { self.tab(true); });
- mappings.add(modes.HINTS, ["<BS>", "<C-h>"],
+ bind(["<BS>", "<C-h>"],
"Delete the previous character",
function ({ self }) self.backspace());
- mappings.add(modes.HINTS, ["<Leader>"],
+ bind(["<Leader>"],
"Toggle hint filtering",
function ({ self }) { self.escapeNumbers = !self.escapeNumbers; });
},
options: function () {
- function xpath(arg) util.makeXPath(arg);
-
options.add(["extendedhinttags", "eht"],
"XPath or CSS selector strings of hintable elements for extended hint modes",
"regexpmap", {
"[iI]": "img",
- "[asOTvVWy]": ["a[href]", "area[href]", "img[src]", "iframe[src]"],
+ "[asOTvVWy]": [":-moz-any-link", "area[href]", "img[src]", "iframe[src]"],
"[f]": "body",
"[F]": ["body", "code", "div", "html", "p", "pre", "span"],
"[S]": ["input:not([type=hidden])", "textarea", "button", "select"]
},
{
keepQuotes: true,
getKey: function (val, default_)
let (res = array.nth(this.value, function (re) let (match = re.exec(val)) match && match[0] == val, 0))
res ? res.matcher : default_,
setter: function (vals) {
for (let value in values(vals))
- value.matcher = util.compileMatcher(Option.splitList(value.result));
+ value.matcher = DOM.compileMatcher(Option.splitList(value.result));
return vals;
},
- validator: util.validateMatcher
+ validator: DOM.validateMatcher
});
options.add(["hinttags", "ht"],
"XPath or CSS selector strings of hintable elements for Hints mode",
- "stringlist", "input:not([type=hidden]),a[href],area,iframe,textarea,button,select," +
+ "stringlist", ":-moz-any-link,area,button,iframe,input:not([type=hidden]),select,textarea," +
"[onclick],[onmouseover],[onmousedown],[onmouseup],[oncommand]," +
"[tabindex],[role=link],[role=button],[contenteditable=true]",
{
setter: function (values) {
- this.matcher = util.compileMatcher(values);
+ this.matcher = DOM.compileMatcher(values);
return values;
},
- validator: util.validateMatcher
+ validator: DOM.validateMatcher
});
options.add(["hintkeys", "hk"],
"The keys used to label and select hints",
"string", "0123456789",
{
values: {
"0123456789": "Numbers",
"asdfg;lkjh": "Home Row"
},
validator: function (value) {
- let values = events.fromString(value).map(events.closure.toString);
+ let values = DOM.Event.parse(value).map(DOM.Event.closure.stringify);
return Option.validIf(array.uniq(values).length === values.length && values.length > 1,
_("option.hintkeys.duplicate"));
}
});
options.add(["hinttimeout", "hto"],
"Timeout before automatically following a non-unique numerical hint",
"number", 0,
diff --git a/common/content/history.js b/common/content/history.js
--- a/common/content/history.js
+++ b/common/content/history.js
@@ -1,88 +1,143 @@
// Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
// Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
var History = Module("history", {
+ SORT_DEFAULT: "-date",
+
get format() bookmarks.format,
get service() services.history,
- get: function get(filter, maxItems, order) {
+ get: function get(filter, maxItems, sort) {
+ sort = sort || this.SORT_DEFAULT;
+
+ if (isString(filter))
+ filter = { searchTerms: filter };
+
// no query parameters will get all history
let query = services.history.getNewQuery();
let options = services.history.getNewQueryOptions();
- if (typeof filter == "string")
- filter = { searchTerms: filter };
for (let [k, v] in Iterator(filter))
query[k] = v;
- order = order || "+date";
- dactyl.assert((order = /^([+-])(.+)/.exec(order)) &&
- (order = "SORT_BY_" + order[2].toUpperCase() + "_" +
- (order[1] == "+" ? "ASCENDING" : "DESCENDING")) &&
- order in options,
- _("error.invalidSort", order));
-
- options.sortingMode = options[order];
+ let res = /^([+-])(.+)/.exec(sort);
+ dactyl.assert(res, _("error.invalidSort", sort));
+
+ let [, dir, field] = res;
+ let _sort = "SORT_BY_" + field.toUpperCase() + "_" +
+ { "+": "ASCENDING", "-": "DESCENDING" }[dir];
+
+ dactyl.assert(_sort in options,
+ _("error.invalidSort", sort));
+
+ options.sortingMode = options[_sort];
options.resultType = options.RESULTS_AS_URI;
if (maxItems > 0)
options.maxResults = maxItems;
- // execute the query
let root = services.history.executeQuery(query, options).root;
root.containerOpen = true;
- let items = iter(util.range(0, root.childCount)).map(function (i) {
+ try {
+ var items = iter(util.range(0, root.childCount)).map(function (i) {
let node = root.getChild(i);
return {
url: node.uri,
title: node.title,
- icon: node.icon ? node.icon : DEFAULT_FAVICON
+ icon: node.icon ? node.icon : BookmarkCache.DEFAULT_FAVICON
};
}).toArray();
- root.containerOpen = false; // close a container after using it!
+ }
+ finally {
+ root.containerOpen = false;
+ }
return items;
},
get session() {
- let sh = window.getWebNavigation().sessionHistory;
+ let webNav = window.getWebNavigation()
+ let sh = webNav.sessionHistory;
+
let obj = [];
- obj.index = sh.index;
+ obj.__defineGetter__("index", function () sh.index);
+ obj.__defineSetter__("index", function (val) { webNav.gotoIndex(val) });
obj.__iterator__ = function () array.iterItems(this);
- for (let i in util.range(0, sh.count)) {
- obj[i] = update(Object.create(sh.getEntryAtIndex(i, false)),
- { index: i });
- memoize(obj[i], "icon",
- function () services.favicon.getFaviconImageForPage(this.URI).spec);
- }
+
+ for (let item in iter(sh.SHistoryEnumerator, Ci.nsIHistoryEntry))
+ obj.push(update(Object.create(item), {
+ index: obj.length,
+ icon: Class.Memoize(function () services.favicon.getFaviconImageForPage(this.URI).spec)
+ }));
return obj;
},
- 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)
- dactyl.beep();
- else {
- let index = Math.constrain(current + steps, start, end);
+ /**
+ * Step to the given offset in the history stack.
+ *
+ * @param {number} steps The possibly negative number of steps to
+ * step.
+ * @param {boolean} jumps If true, take into account jumps in the
+ * marks stack. @optional
+ */
+ stepTo: function stepTo(steps, jumps) {
+ if (dactyl.forceOpen.target == dactyl.NEW_TAB)
+ tabs.cloneTab(tabs.getTab(), true);
+
+ if (jumps)
+ steps -= marks.jump(steps);
+ if (steps == 0)
+ return;
+
+ let sh = this.session;
+ dactyl.assert(steps > 0 && sh.index < sh.length - 1 || steps < 0 && sh.index > 0);
+
try {
- window.getWebNavigation().gotoIndex(index);
- }
- catch (e) {} // We get NS_ERROR_FILE_NOT_FOUND if files in history don't exist
- }
- },
+ sh.index = Math.constrain(sh.index + steps, 0, sh.length - 1);
+ }
+ catch (e if e.result == Cr.NS_ERROR_FILE_NOT_FOUND) {}
+ },
+
+ /**
+ * Search for the *steps*th next *item* in the history list.
+ *
+ * @param {string} item The nebulously defined item to search for.
+ * @param {number} steps The number of steps to step.
+ */
+ search: function search(item, steps) {
+ var ctxt;
+ var filter = function (item) true;
+ if (item == "domain")
+ var filter = function (item) {
+ let res = item.URI.hostPort != ctxt;
+ ctxt = item.URI.hostPort;
+ return res;
+ };
+
+ let sh = this.session;
+ let idx;
+ let sign = steps / Math.abs(steps);
+
+ filter(sh[sh.index]);
+ for (let i = sh.index + sign; steps && i >= 0 && i < sh.length; i += sign)
+ if (filter(sh[i])) {
+ idx = i;
+ steps -= sign;
+ }
+
+ dactyl.assert(idx != null);
+ sh.index = idx;
+ },
goToStart: function goToStart() {
let index = window.getWebNavigation().sessionHistory.index;
if (index > 0)
window.getWebNavigation().gotoIndex(0);
else
dactyl.beep();
@@ -233,16 +288,43 @@ var History = Module("history", {
["+" + order.replace(" ", ""), /*L*/"Sort by " + order + " ascending"],
["-" + order.replace(" ", ""), /*L*/"Sort by " + order + " descending"]
]));
}
}
],
privateData: true
});
+
+ commands.add(["ju[mps]"],
+ "Show jumplist",
+ function () {
+ let sh = history.session;
+ let index = sh.index;
+
+ let jumps = marks.jumps;
+ if (jumps.index < 0)
+ jumps = [sh[sh.index]];
+ else {
+ index += jumps.index;
+ jumps = jumps.locations.map(function (l) ({
+ __proto__: l,
+ title: buffer.title,
+ get URI() util.newURI(this.location)
+ }));
+ }
+
+ let list = sh.slice(0, sh.index)
+ .concat(jumps)
+ .concat(sh.slice(sh.index + 1));
+
+ commandline.commandOutput(template.jumps(index, list));
+ },
+ { argCount: "0" });
+
},
completion: function () {
completion.domain = function (context) {
context.anchored = false;
context.compare = function (a, b) String.localeCompare(a.key, b.key);
context.keys = { text: util.identity, description: util.identity,
key: function (host) host.split(".").reverse().join(".") };
@@ -262,36 +344,40 @@ var History = Module("history", {
if (maxItems == null)
context.maxItems = maxItems;
if (maxItems && context.maxItems == null)
context.maxItems = 100;
context.regenerate = true;
context.generate = function () history.get(context.filter, this.maxItems, sort);
};
- completion.addUrlCompleter("h", "History", completion.history);
+ completion.addUrlCompleter("history", "History", completion.history);
},
mappings: function () {
- var myModes = config.browserModes;
-
- mappings.add(myModes,
- ["<C-o>"], "Go to an older position in the jump list",
- function (args) { history.stepTo(-Math.max(args.count, 1)); },
- { count: true });
-
- mappings.add(myModes,
- ["<C-i>"], "Go to a newer position in the jump list",
- function (args) { history.stepTo(Math.max(args.count, 1)); },
- { count: true });
-
- mappings.add(myModes,
- ["H", "<A-Left>", "<M-Left>"], "Go back in the browser history",
- function (args) { history.stepTo(-Math.max(args.count, 1)); },
- { count: true });
-
- mappings.add(myModes,
- ["L", "<A-Right>", "<M-Right>"], "Go forward in the browser history",
- function (args) { history.stepTo(Math.max(args.count, 1)); },
+ function bind() mappings.add.apply(mappings, [config.browserModes].concat(Array.slice(arguments)));
+
+ bind(["<C-o>"], "Go to an older position in the jump list",
+ function ({ count }) { history.stepTo(-Math.max(count, 1), true); },
+ { count: true });
+
+ bind(["<C-i>"], "Go to a newer position in the jump list",
+ function ({ count }) { history.stepTo(Math.max(count, 1), true); },
+ { count: true });
+
+ bind(["H", "<A-Left>", "<M-Left>"], "Go back in the browser history",
+ function ({ count }) { history.stepTo(-Math.max(count, 1)); },
+ { count: true });
+
+ bind(["L", "<A-Right>", "<M-Right>"], "Go forward in the browser history",
+ function ({ count }) { history.stepTo(Math.max(count, 1)); },
+ { count: true });
+
+ bind(["[d"], "Go back to the previous domain in the browser history",
+ function ({ count }) { history.search("domain", -Math.max(count, 1)) },
+ { count: true });
+
+ bind(["]d"], "Go forward to the next domain in the browser history",
+ function ({ count }) { history.search("domain", Math.max(count, 1)) },
{ count: true });
}
});
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/key-processors.js b/common/content/key-processors.js
new file mode 100644
--- /dev/null
+++ b/common/content/key-processors.js
@@ -0,0 +1,324 @@
+// Copyright (c) 2008-2011 by Kris Maglione <maglione.k at Gmail>
+//
+// This work is licensed for reuse under an MIT license. Details are
+// given in the LICENSE.txt file included with this file.
+/* use strict */
+
+/** @scope modules */
+
+var ProcessorStack = Class("ProcessorStack", {
+ init: function (mode, hives, builtin) {
+ this.main = mode.main;
+ this._actions = [];
+ this.actions = [];
+ this.buffer = "";
+ this.events = [];
+
+ events.dbg("STACK " + mode);
+
+ let main = { __proto__: mode.main, params: mode.params };
+ this.modes = array([mode.params.keyModes, main, mode.main.allBases.slice(1)]).flatten().compact();
+
+ if (builtin)
+ hives = hives.filter(function (h) h.name === "builtin");
+
+ this.processors = this.modes.map(function (m) hives.map(function (h) KeyProcessor(m, h)))
+ .flatten().array;
+ this.ownsBuffer = !this.processors.some(function (p) p.main.ownsBuffer);
+
+ for (let [i, input] in Iterator(this.processors)) {
+ let params = input.main.params;
+
+ if (params.preExecute)
+ input.preExecute = params.preExecute;
+
+ if (params.postExecute)
+ input.postExecute = params.postExecute;
+
+ if (params.onKeyPress && input.hive === mappings.builtin)
+ input.fallthrough = function fallthrough(events) {
+ return params.onKeyPress(events) === false ? Events.KILL : Events.PASS;
+ };
+ }
+
+ let hive = options.get("passkeys")[this.main.input ? "inputHive" : "commandHive"];
+ if (!builtin && hive.active && (!dactyl.focusedElement || events.isContentNode(dactyl.focusedElement)))
+ this.processors.unshift(KeyProcessor(modes.BASE, hive));
+ },
+
+ passUnknown: Class.Memoize(function () options.get("passunknown").getKey(this.modes)),
+
+ notify: function () {
+ events.dbg("NOTIFY()");
+ events.keyEvents = [];
+ events.processor = null;
+ if (!this.execute(undefined, true)) {
+ events.processor = this;
+ events.keyEvents = this.keyEvents;
+ }
+ },
+
+ _result: function (result) (result === Events.KILL ? "KILL" :
+ result === Events.PASS ? "PASS" :
+ result === Events.PASS_THROUGH ? "PASS_THROUGH" :
+ result === Events.ABORT ? "ABORT" :
+ callable(result) ? result.toSource().substr(0, 50) : result),
+
+ execute: function execute(result, force) {
+ events.dbg("EXECUTE(" + this._result(result) + ", " + force + ") events:" + this.events.length
+ + " processors:" + this.processors.length + " actions:" + this.actions.length);
+
+ let processors = this.processors;
+ let length = 1;
+
+ if (force)
+ this.processors = [];
+
+ if (this.ownsBuffer)
+ statusline.inputBuffer = this.processors.length ? this.buffer : "";
+
+ if (!this.processors.some(function (p) !p.extended) && this.actions.length) {
+ // We have matching actions and no processors other than
+ // those waiting on further arguments. Execute actions as
+ // long as they continue to return PASS.
+
+ for (var action in values(this.actions)) {
+ while (callable(action)) {
+ length = action.eventLength;
+ action = dactyl.trapErrors(action);
+ events.dbg("ACTION RES: " + length + " " + this._result(action));
+ }
+ if (action !== Events.PASS)
+ break;
+ }
+
+ // Result is the result of the last action. Unless it's
+ // PASS, kill any remaining argument processors.
+ result = action !== undefined ? action : Events.KILL;
+ if (action !== Events.PASS)
+ this.processors.length = 0;
+ }
+ else if (this.processors.length) {
+ // We're still waiting on the longest matching processor.
+ // Kill the event, set a timeout to give up waiting if applicable.
+
+ result = Events.KILL;
+ if (options["timeout"] && (this.actions.length || events.hasNativeKey(this.events[0], this.main, this.passUnknown)))
+ this.timer = services.Timer(this, options["timeoutlen"], services.Timer.TYPE_ONE_SHOT);
+ }
+ else if (result !== Events.KILL && !this.actions.length &&
+ !(this.events[0].isReplay || this.passUnknown
+ || this.modes.some(function (m) m.passEvent(this), this.events[0]))) {
+ // No patching processors, this isn't a fake, pass-through
+ // event, we're not in pass-through mode, and we're not
+ // choosing to pass unknown keys. Kill the event and beep.
+
+ result = Events.ABORT;
+ if (!Events.isEscape(this.events.slice(-1)[0]))
+ dactyl.beep();
+ events.feedingKeys = false;
+ }
+ else if (result === undefined)
+ // No matching processors, we're willing to pass this event,
+ // and we don't have a default action from a processor. Just
+ // pass the event.
+ result = Events.PASS;
+
+ events.dbg("RESULT: " + length + " " + this._result(result) + "\n\n");
+
+ if (result !== Events.PASS || this.events.length > 1)
+ if (result !== Events.ABORT || !this.events[0].isReplay)
+ Events.kill(this.events[this.events.length - 1]);
+
+ if (result === Events.PASS_THROUGH || result === Events.PASS && this.passUnknown)
+ events.passing = true;
+
+ if (result === Events.PASS_THROUGH && this.keyEvents.length)
+ events.dbg("PASS_THROUGH:\n\t" + this.keyEvents.map(function (e) [e.type, DOM.Event.stringify(e)]).join("\n\t"));
+
+ if (result === Events.PASS_THROUGH)
+ events.feedevents(null, this.keyEvents, { skipmap: true, isMacro: true, isReplay: true });
+ else {
+ let list = this.events.filter(function (e) e.getPreventDefault() && !e.dactylDefaultPrevented);
+
+ if (result === Events.PASS)
+ events.dbg("PASS THROUGH: " + list.slice(0, length).filter(function (e) e.type === "keypress").map(DOM.Event.closure.stringify));
+ if (list.length > length)
+ events.dbg("REFEED: " + list.slice(length).filter(function (e) e.type === "keypress").map(DOM.Event.closure.stringify));
+
+ if (result === Events.PASS)
+ events.feedevents(null, list.slice(0, length), { skipmap: true, isMacro: true, isReplay: true });
+ if (list.length > length && this.processors.length === 0)
+ events.feedevents(null, list.slice(length));
+ }
+
+ return this.processors.length === 0;
+ },
+
+ process: function process(event) {
+ if (this.timer)
+ this.timer.cancel();
+
+ let key = DOM.Event.stringify(event);
+ this.events.push(event);
+ if (this.keyEvents)
+ this.keyEvents.push(event);
+
+ this.buffer += key;
+
+ let actions = [];
+ let processors = [];
+
+ events.dbg("PROCESS(" + key + ") skipmap: " + event.skipmap + " macro: " + event.isMacro + " replay: " + event.isReplay);
+
+ for (let [i, input] in Iterator(this.processors)) {
+ let res = input.process(event);
+ if (res !== Events.ABORT)
+ var result = res;
+
+ events.dbg("RES: " + input + " " + this._result(res));
+
+ if (res === Events.KILL)
+ break;
+
+ if (callable(res))
+ actions.push(res);
+
+ if (res === Events.WAIT || input.waiting)
+ processors.push(input);
+ if (isinstance(res, KeyProcessor))
+ processors.push(res);
+ }
+
+ events.dbg("RESULT: " + event.getPreventDefault() + " " + this._result(result));
+ events.dbg("ACTIONS: " + actions.length + " " + this.actions.length);
+ events.dbg("PROCESSORS:", processors, "\n");
+
+ this._actions = actions;
+ this.actions = actions.concat(this.actions);
+
+ for (let action in values(actions))
+ if (!("eventLength" in action))
+ action.eventLength = this.events.length;
+
+ if (result === Events.KILL)
+ this.actions = [];
+ else if (!this.actions.length && !processors.length)
+ for (let input in values(this.processors))
+ if (input.fallthrough) {
+ if (result === Events.KILL)
+ break;
+ result = dactyl.trapErrors(input.fallthrough, input, this.events);
+ }
+
+ this.processors = processors;
+
+ return this.execute(result, options["timeout"] && options["timeoutlen"] === 0);
+ }
+});
+
+var KeyProcessor = Class("KeyProcessor", {
+ init: function init(main, hive) {
+ this.main = main;
+ this.events = [];
+ this.hive = hive;
+ this.wantCount = this.main.count;
+ },
+
+ get toStringParams() [this.main.name, this.hive.name],
+
+ countStr: "",
+ command: "",
+ get count() this.countStr ? Number(this.countStr) : this.main.params.count || null,
+
+ append: function append(event) {
+ this.events.push(event);
+ let key = DOM.Event.stringify(event);
+
+ if (this.wantCount && !this.command &&
+ (this.countStr ? /^[0-9]$/ : /^[1-9]$/).test(key))
+ this.countStr += key;
+ else
+ this.command += key;
+ return this.events;
+ },
+
+ process: function process(event) {
+ this.append(event);
+ this.waiting = false;
+ return this.onKeyPress(event);
+ },
+
+ execute: function execute(map, args)
+ let (self = this)
+ function execute() {
+ if (self.preExecute)
+ self.preExecute.apply(self, args);
+
+ args.self = self.main.params.mappingSelf || self.main.mappingSelf || map;
+ let res = map.execute.call(map, args);
+
+ if (self.postExecute)
+ self.postExecute.apply(self, args);
+ return res;
+ },
+
+ onKeyPress: function onKeyPress(event) {
+ if (event.skipmap)
+ return Events.ABORT;
+
+ if (!this.command)
+ return Events.WAIT;
+
+ var map = this.hive.get(this.main, this.command);
+ this.waiting = this.hive.getCandidates(this.main, this.command);
+ if (map) {
+ if (map.arg)
+ return KeyArgProcessor(this, map, false, "arg");
+ else if (map.motion)
+ return KeyArgProcessor(this, map, true, "motion");
+
+ return this.execute(map, {
+ command: this.command,
+ count: this.count,
+ keyEvents: events.keyEvents,
+ keypressEvents: this.events
+ });
+ }
+
+ if (!this.waiting)
+ return this.main.insert ? Events.PASS : Events.ABORT;
+
+ return Events.WAIT;
+ }
+});
+
+var KeyArgProcessor = Class("KeyArgProcessor", KeyProcessor, {
+ init: function init(input, map, wantCount, argName) {
+ init.supercall(this, input.main, input.hive);
+ this.map = map;
+ this.parent = input;
+ this.argName = argName;
+ this.wantCount = wantCount;
+ },
+
+ extended: true,
+
+ onKeyPress: function onKeyPress(event) {
+ if (Events.isEscape(event))
+ return Events.KILL;
+ if (!this.command)
+ return Events.WAIT;
+
+ let args = {
+ command: this.parent.command,
+ count: this.count || this.parent.count,
+ keyEvents: events.keyEvents,
+ keypressEvents: this.parent.events.concat(this.events)
+ };
+ args[this.argName] = this.command;
+
+ return this.execute(this.map, args);
+ }
+});
+
diff --git a/common/content/mappings.js b/common/content/mappings.js
--- a/common/content/mappings.js
+++ b/common/content/mappings.js
@@ -1,56 +1,59 @@
// Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
// Copyright (c) 2008-2011 by Kris Maglione <maglione.k at Gmail>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
/** @scope modules */
/**
* A class representing key mappings. Instances are created by the
* {@link Mappings} class.
*
* @param {[Modes.Mode]} modes The modes in which this mapping is active.
* @param {[string]} keys The key sequences which are bound to
* *action*.
* @param {string} description A short one line description of the key mapping.
* @param {function} action The action invoked by each key sequence.
- * @param {Object} extraInfo An optional extra configuration hash. The
+ * @param {Object} info An optional extra configuration hash. The
* following properties are supported.
* arg - see {@link Map#arg}
* count - see {@link Map#count}
* motion - see {@link Map#motion}
* noremap - see {@link Map#noremap}
* rhs - see {@link Map#rhs}
* silent - see {@link Map#silent}
* @optional
* @private
*/
var Map = Class("Map", {
- init: function (modes, keys, description, action, extraInfo) {
+ init: function (modes, keys, description, action, info) {
this.id = ++Map.id;
this.modes = modes;
this._keys = keys;
this.action = action;
this.description = description;
Object.freeze(this.modes);
- if (extraInfo)
- this.update(extraInfo);
- },
-
- name: Class.memoize(function () this.names[0]),
+ if (info) {
+ if (Set.has(Map.types, info.type))
+ this.update(Map.types[info.type]);
+ this.update(info);
+ }
+ },
+
+ name: Class.Memoize(function () this.names[0]),
/** @property {[string]} All of this mapping's names (key sequences). */
- names: Class.memoize(function () this._keys.map(function (k) events.canonicalKeys(k))),
+ names: Class.Memoize(function () this._keys.map(function (k) DOM.Event.canonicalKeys(k))),
get toStringParams() [this.modes.map(function (m) m.name), this.names.map(String.quote)],
get identifier() [this.modes[0].name, this.hive.prefix + this.names[0]].join("."),
/** @property {number} A unique ID for this mapping. */
id: null,
/** @property {[Modes.Mode]} All of the modes for which this mapping applies. */
@@ -64,22 +67,34 @@ var Map = Class("Map", {
arg: false,
/** @property {boolean} Whether this mapping accepts a count. */
count: false,
/**
* @property {boolean} Whether the mapping accepts a motion mapping
* as an argument.
*/
motion: false,
+
/** @property {boolean} Whether the RHS of the mapping should expand mappings recursively. */
noremap: false,
+
+ /** @property {function(object)} A function to be executed before this mapping. */
+ preExecute: function preExecute(args) {},
+ /** @property {function(object)} A function to be executed after this mapping. */
+ postExecute: function postExecute(args) {},
+
/** @property {boolean} Whether any output from the mapping should be echoed on the command line. */
silent: false,
+
/** @property {string} The literal RHS expansion of this mapping. */
rhs: null,
+
+ /** @property {string} The type of this mapping. */
+ type: "",
+
/**
* @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.
*/
user: false,
/**
@@ -99,46 +114,55 @@ var Map = Class("Map", {
* @param {object} args The arguments object for the given mapping.
*/
execute: function (args) {
if (!isObject(args)) // Backwards compatibility :(
args = iter(["motion", "count", "arg", "command"])
.map(function ([i, prop]) [prop, this[i]], arguments)
.toObject();
- args = update({ context: contexts.context },
- this.hive.argsExtra(args),
+ args = this.hive.makeArgs(this.hive.group.lastDocument,
+ contexts.context,
args);
let self = this;
function repeat() self.action(args)
if (this.names[0] != ".") // FIXME: Kludge.
mappings.repeat = repeat;
if (this.executing)
- util.dumpStack(_("map.recursive", args.command));
- dactyl.assert(!this.executing, _("map.recursive", args.command));
+ util.assert(!args.keypressEvents[0].isMacro,
+ _("map.recursive", args.command),
+ false);
try {
+ dactyl.triggerObserver("mappings.willExecute", this, args);
+ mappings.pushCommand();
+ this.preExecute(args);
this.executing = true;
var res = repeat();
}
catch (e) {
events.feedingKeys = false;
dactyl.reportError(e, true);
}
finally {
this.executing = false;
+ mappings.popCommand();
+ this.postExecute(args);
+ dactyl.triggerObserver("mappings.executed", this, args);
}
return res;
}
}, {
- id: 0
-});
+ id: 0,
+
+ types: {}
+});
var MapHive = Class("MapHive", Contexts.Hive, {
init: function init(group) {
init.supercall(this, group);
this.stacks = {};
},
/**
@@ -267,70 +291,100 @@ var MapHive = Class("MapHive", Contexts.
get candidates() this.states.candidates,
get mappings() this.states.mappings,
add: function (map) {
this.push(map);
delete this.states;
},
- states: Class.memoize(function () {
+ states: Class.Memoize(function () {
var states = {
candidates: {},
mappings: {}
};
for (let map in this)
for (let name in values(map.keys)) {
states.mappings[name] = map;
let state = "";
- for (let key in events.iterKeys(name)) {
+ for (let key in DOM.Event.iterKeys(name)) {
state += key;
if (state !== name)
states.candidates[state] = (states.candidates[state] || 0) + 1;
}
}
return states;
})
})
});
/**
* @instance mappings
*/
var Mappings = Module("mappings", {
init: function () {
- },
+ this.watches = [];
+ this._watchStack = 0;
+ this._yielders = 0;
+ },
+
+ afterCommands: function afterCommands(count, cmd, self) {
+ this.watches.push([cmd, self, Math.max(this._watchStack - 1, 0), count || 1]);
+ },
+
+ pushCommand: function pushCommand(cmd) {
+ this._watchStack++;
+ this._yielders = util.yielders;
+ },
+ popCommand: function popCommand(cmd) {
+ this._watchStack = Math.max(this._watchStack - 1, 0);
+ if (util.yielders > this._yielders)
+ this._watchStack = 0;
+
+ this.watches = this.watches.filter(function (elem) {
+ if (this._watchStack <= elem[2])
+ elem[3]--;
+ if (elem[3] <= 0)
+ elem[0].call(elem[1] || null);
+ return elem[3] > 0;
+ }, this);
+ },
repeat: Modes.boundProperty(),
get allHives() contexts.allGroups.mappings,
get userHives() this.allHives.filter(function (h) h !== this.builtin, this),
expandLeader: function expandLeader(keyString) keyString.replace(/<Leader>/i, function () options["mapleader"]),
- prefixes: Class.memoize(function () {
+ prefixes: Class.Memoize(function () {
let list = Array.map("CASM", function (s) s + "-");
return iter(util.range(0, 1 << list.length)).map(function (mask)
list.filter(function (p, i) mask & (1 << i)).join("")).toArray().concat("*-");
}),
expand: function expand(keys) {
keys = keys.replace(/<leader>/i, options["mapleader"]);
+
if (!/<\*-/.test(keys))
- return keys;
-
- return util.debrace(events.iterKeys(keys).map(function (key) {
+ var res = keys;
+ else
+ res = util.debrace(DOM.Event.iterKeys(keys).map(function (key) {
if (/^<\*-/.test(key))
return ["<", this.prefixes, key.slice(3)];
return key;
- }, this).flatten().array).map(function (k) events.canonicalKeys(k));
- },
+ }, this).flatten().array).map(function (k) DOM.Event.canonicalKeys(k));
+
+ if (keys != arguments[0])
+ return [arguments[0]].concat(keys);
+ return keys;
+ },
iterate: function (mode) {
let seen = {};
for (let hive in this.hives.iterValues())
for (let map in array(hive.getStack(mode)).iterValues())
if (!Set.add(seen, map.name))
yield map;
},
@@ -389,26 +443,26 @@ var Mappings = Module("mappings", {
*
* @param {Modes.Mode} mode The mode to search.
* @param {string} cmd The map name to match.
* @returns {Map}
*/
get: function get(mode, cmd) this.hives.map(function (h) h.get(mode, cmd)).compact()[0] || null,
/**
- * Returns an array of maps with names starting with but not equal to
+ * Returns a count of maps with names starting with but not equal to
* *prefix*.
*
* @param {Modes.Mode} mode The mode to search.
* @param {string} prefix The map prefix string to match.
* @returns {[Map]}
*/
getCandidates: function (mode, prefix)
this.hives.map(function (h) h.getCandidates(mode, prefix))
- .flatten(),
+ .reduce(function (a, b) a + b, 0),
/**
* Lists all user-defined mappings matching *filter* for the specified
* *modes* in the specified *hives*.
*
* @param {[Modes.Mode]} modes An array of modes to search.
* @param {string} filter The filter string to match. @optional
* @param {[MapHive]} hives The map hives to list. @optional
@@ -566,24 +620,23 @@ var Mappings = Module("mappings", {
],
serialize: function () {
return this.name != "map" ? [] :
array(mappings.userHives)
.filter(function (h) h.persist)
.map(function (hive) [
{
command: "map",
- options: array([
- hive.name !== "user" && ["-group", hive.name],
- ["-modes", uniqueModes(map.modes)],
- ["-description", map.description],
- map.silent && ["-silent"]])
-
- .filter(util.identity)
- .toObject(),
+ options: {
+ "-count": map.count ? null : undefined,
+ "-description": map.description,
+ "-group": hive.name == "user" ? undefined : hive.name,
+ "-modes": uniqueModes(map.modes),
+ "-silent": map.silent ? null : undefined
+ },
arguments: [map.names[0]],
literalArg: map.rhs,
ignoreDefaults: true
}
for (map in userMappings(hive))
if (map.persist)
])
.flatten().array;
@@ -744,37 +797,37 @@ var Mappings = Module("mappings", {
iter.forEach(modes.mainModes, function (mode) {
if (mode.char && !commands.get(mode.char + "listkeys", true))
dactyl.addUsageCommand({
__proto__: args,
name: [mode.char + "listk[eys]", mode.char + "lk"],
iterateIndex: function (args)
let (self = this, prefix = /^[bCmn]$/.test(mode.char) ? "" : mode.char + "_",
- tags = services["dactyl:"].HELP_TAGS)
+ haveTag = Set.has(help.tags))
({ helpTag: prefix + map.name, __proto__: map }
for (map in self.iterate(args, true))
- if (map.hive === mappings.builtin || Set.has(tags, prefix + map.name))),
- description: "List all " + mode.name + " mode mappings along with their short descriptions",
+ if (map.hive === mappings.builtin || haveTag(prefix + map.name))),
+ description: "List all " + mode.displayName + " mode mappings along with their short descriptions",
index: mode.char + "-map",
getMode: function (args) mode,
options: []
});
});
},
completion: function initCompletion(dactyl, modules, window) {
completion.userMapping = function userMapping(context, modes_, hive) {
hive = hive || mappings.user;
modes_ = modes_ || [modes.NORMAL];
context.keys = { text: function (m) m.names[0], description: function (m) m.description + ": " + m.action };
context.completions = hive.iterate(modes_);
};
},
javascript: function initJavascript(dactyl, modules, window) {
- JavaScript.setCompleter([mappings.get, mappings.builtin.get],
+ JavaScript.setCompleter([Mappings.prototype.get, MapHive.prototype.get],
[
null,
function (context, obj, args) [[m.names, m.description] for (m in this.iterate(args[0]))]
]);
},
options: function initOptions(dactyl, modules, window) {
options.add(["mapleader", "ml"],
"Define the replacement keys for the <Leader> pseudo-key",
diff --git a/common/content/marks.js b/common/content/marks.js
--- a/common/content/marks.js
+++ b/common/content/marks.js
@@ -1,15 +1,15 @@
// Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
// Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
/**
* @scope modules
* @instance marks
*/
var Marks = Module("marks", {
init: function init() {
this._localMarks = storage.newMap("local-marks", { privateData: true, replacer: Storage.Replacer.skipXpcom, store: true });
@@ -27,47 +27,124 @@ var Marks = Module("marks", {
/**
* @property {Array} Returns all marks, both local and URL, in a sorted
* array.
*/
get all() iter(this._localMarks.get(this.localURI) || {},
this._urlMarks
).sort(function (a, b) String.localeCompare(a[0], b[0])),
- get localURI() buffer.focusedFrame.document.documentURI,
+ get localURI() buffer.focusedFrame.document.documentURI.replace(/#.*/, ""),
+
+ Mark: function Mark(params) {
+ let win = buffer.focusedFrame;
+ let doc = win.document;
+
+ params = params || {};
+
+ params.location = doc.documentURI.replace(/#.*/, ""),
+ params.offset = buffer.scrollPosition;
+ params.path = DOM(buffer.findScrollable(0, false)).xpath;
+ params.timestamp = Date.now() * 1000;
+ params.equals = function (m) this.location == m.location
+ && this.offset.x == m.offset.x
+ && this.offset.y == m.offset.y
+ && this.path == m.path;
+ return params;
+ },
/**
* 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 {string} name The mark name.
* @param {boolean} silent Whether to output error messages.
*/
- add: function (mark, silent) {
- let win = buffer.focusedFrame;
- let doc = win.document;
-
- let position = { x: buffer.scrollXPercent / 100, y: buffer.scrollYPercent / 100 };
-
- if (Marks.isURLMark(mark)) {
- let res = this._urlMarks.set(mark, { location: doc.documentURI, position: position, tab: Cu.getWeakReference(tabs.getTab()), timestamp: Date.now()*1000 });
+ add: function (name, silent) {
+ let mark = this.Mark();
+
+ if (Marks.isURLMark(name)) {
+ mark.tab = util.weakReference(tabs.getTab());
+ this._urlMarks.set(name, mark);
+ var message = "mark.addURL";
+ }
+ else if (Marks.isLocalMark(name)) {
+ this._localMarks.get(mark.location, {})[name] = mark;
+ this._localMarks.changed();
+ message = "mark.addLocal";
+ }
+
if (!silent)
- dactyl.log(_("mark.addURL", Marks.markToString(mark, res)), 5);
- }
- else if (Marks.isLocalMark(mark)) {
- let marks = this._localMarks.get(doc.documentURI, {});
- marks[mark] = { location: doc.documentURI, position: position, timestamp: Date.now()*1000 };
- this._localMarks.changed();
- if (!silent)
- dactyl.log(_("mark.addLocal", Marks.markToString(mark, marks[mark])), 5);
- }
- },
+ dactyl.log(_(message, Marks.markToString(name, mark)), 5);
+ return mark;
+ },
+
+ /**
+ * Push the current buffer position onto the jump stack.
+ *
+ * @param {string} reason The reason for this scroll event. Multiple
+ * scroll events for the same reason are coalesced. @optional
+ */
+ push: function push(reason) {
+ let store = buffer.localStore;
+ let jump = store.jumps[store.jumpsIndex];
+
+ if (reason && jump && jump.reason == reason)
+ return;
+
+ let mark = this.add("'");
+ if (jump && mark.equals(jump.mark))
+ return;
+
+ if (!this.jumping) {
+ store.jumps[++store.jumpsIndex] = { mark: mark, reason: reason };
+ store.jumps.length = store.jumpsIndex + 1;
+
+ if (store.jumps.length > this.maxJumps) {
+ store.jumps = store.jumps.slice(-this.maxJumps);
+ store.jumpsIndex = store.jumps.length - 1;
+ }
+ }
+ },
+
+ maxJumps: 200,
+
+ /**
+ * Jump to the given offset in the jump stack.
+ *
+ * @param {number} offset The offset from the current position in
+ * the jump stack to jump to.
+ * @returns {number} The actual change in offset.
+ */
+ jump: function jump(offset) {
+ let store = buffer.localStore;
+ if (offset < 0 && store.jumpsIndex == store.jumps.length - 1)
+ this.push();
+
+ return this.withSavedValues(["jumping"], function _jump() {
+ this.jumping = true;
+ let idx = Math.constrain(store.jumpsIndex + offset, 0, store.jumps.length - 1);
+ let orig = store.jumpsIndex;
+
+ if (idx in store.jumps && !dactyl.trapErrors("_scrollTo", this, store.jumps[idx].mark))
+ store.jumpsIndex = idx;
+ return store.jumpsIndex - orig;
+ });
+ },
+
+ get jumps() {
+ let store = buffer.localStore;
+ return {
+ index: store.jumpsIndex,
+ locations: store.jumps.map(function (j) j.mark)
+ };
+ },
/**
* Remove all marks matching *filter*. If *special* is given, removes all
* local marks.
*
* @param {string} filter The list of marks to delete, e.g. "aA b C-I"
* @param {boolean} special Whether to delete all local marks.
*/
@@ -101,27 +178,27 @@ var Marks = Module("marks", {
jumpTo: function (char) {
if (Marks.isURLMark(char)) {
let mark = this._urlMarks.get(char);
dactyl.assert(mark, _("mark.unset", char));
let tab = mark.tab && mark.tab.get();
if (!tab || !tab.linkedBrowser || tabs.allTabs.indexOf(tab) == -1)
for ([, tab] in iter(tabs.visibleTabs, tabs.allTabs)) {
- if (tab.linkedBrowser.contentDocument.documentURI === mark.location)
+ if (tab.linkedBrowser.contentDocument.documentURI.replace(/#.*/, "") === mark.location)
break;
tab = null;
}
if (tab) {
tabs.select(tab);
let doc = tab.linkedBrowser.contentDocument;
- if (doc.documentURI == mark.location) {
+ if (doc.documentURI.replace(/#.*/, "") == mark.location) {
dactyl.log(_("mark.jumpingToURL", Marks.markToString(char, mark)), 5);
- buffer.scrollToPercent(mark.position.x * 100, mark.position.y * 100);
+ this._scrollTo(mark);
}
else {
this._pendingJumps.push(mark);
let sh = tab.linkedBrowser.sessionHistory;
let items = array(util.range(0, sh.count));
let a = items.slice(0, sh.index).reverse();
@@ -142,23 +219,40 @@ var Marks = Module("marks", {
dactyl.open(mark.location, dactyl.NEW_TAB);
}
}
else if (Marks.isLocalMark(char)) {
let mark = (this._localMarks.get(this.localURI) || {})[char];
dactyl.assert(mark, _("mark.unset", char));
dactyl.log(_("mark.jumpingToLocal", Marks.markToString(char, mark)), 5);
- buffer.scrollToPercent(mark.position.x * 100, mark.position.y * 100);
+ this._scrollTo(mark);
}
else
dactyl.echoerr(_("mark.invalid"));
},
+ _scrollTo: function _scrollTo(mark) {
+ if (!mark.path)
+ var node = buffer.findScrollable(0, (mark.offset || mark.position).x)
+ else
+ for (node in DOM.XPath(mark.path, buffer.focusedFrame.document))
+ break;
+
+ util.assert(node);
+ if (node instanceof Element)
+ DOM(node).scrollIntoView();
+
+ if (mark.offset)
+ Buffer.scrollToPosition(node, mark.offset.x, mark.offset.y);
+ else if (mark.position)
+ Buffer.scrollToPercent(node, mark.position.x * 100, mark.position.y * 100);
+ },
+
/**
* List all marks matching *filter*.
*
* @param {string} filter List of marks to show, e.g. "ab A-I".
*/
list: function (filter) {
let marks = this.all;
@@ -169,45 +263,57 @@ var Marks = Module("marks", {
marks = marks.filter(function ([k, ]) pattern.test(k));
dactyl.assert(marks.length > 0, _("mark.noMatching", filter.quote()));
}
commandline.commandOutput(
template.tabular(
["Mark", "HPos", "VPos", "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)))));
- },
+ ([name,
+ mark.offset ? Math.round(mark.offset.x)
+ : Math.round(mark.position.x * 100) + "%",
+ mark.offset ? Math.round(mark.offset.y)
+ : Math.round(mark.position.y * 100) + "%",
+ mark.location]
+ for ([, [name, mark]] in Iterator(marks)))));
+ },
_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._scrollTo(mark);
this._pendingJumps.splice(i, 1);
return;
}
}
},
}, {
markToString: function markToString(name, mark) {
let tab = mark.tab && mark.tab.get();
- return name + ", " + mark.location +
- ", (" + Math.round(mark.position.x * 100) +
- "%, " + Math.round(mark.position.y * 100) + "%)" +
- (tab ? ", tab: " + tabs.index(tab) : "");
- },
-
- isLocalMark: function isLocalMark(mark) /^[a-z`']$/.test(mark),
-
- isURLMark: function isURLMark(mark) /^[A-Z]$/.test(mark)
+ if (mark.offset)
+ return [name, mark.location,
+ "(" + Math.round(mark.offset.x * 100),
+ Math.round(mark.offset.y * 100) + ")",
+ (tab && "tab: " + tabs.index(tab))
+ ].filter(util.identity).join(", ");
+
+ if (mark.position)
+ return [name, mark.location,
+ "(" + Math.round(mark.position.x * 100) + "%",
+ Math.round(mark.position.y * 100) + "%)",
+ (tab && "tab: " + tabs.index(tab))
+ ].filter(util.identity).join(", ");
+
+ },
+
+ isLocalMark: bind("test", /^[a-z`']$/),
+
+ isURLMark: bind("test", /^[A-Z]$/)
}, {
events: function () {
let appContent = document.getElementById("appcontent");
if (appContent)
events.listen(appContent, "load", marks.closure._onPageLoad, true);
},
mappings: function () {
var myModes = config.browserModes;
@@ -266,17 +372,19 @@ var Marks = Module("marks", {
});
},
completion: function () {
completion.mark = function mark(context) {
function percent(i) Math.round(i * 100);
context.title = ["Mark", "HPos VPos File"];
- context.keys.description = function ([, m]) percent(m.position.x) + "% " + percent(m.position.y) + "% " + m.location;
+ context.keys.description = function ([, m]) (m.offset ? Math.round(m.offset.x) + " " + Math.round(m.offset.y)
+ : percent(m.position.x) + "% " + percent(m.position.y) + "%"
+ ) + " " + m.location;
context.completions = marks.all;
};
},
sanitizer: function () {
sanitizer.addItem("marks", {
description: "Local and URL marks",
persistent: true,
contains: ["history"],
diff --git a/common/content/modes.js b/common/content/modes.js
--- a/common/content/modes.js
+++ b/common/content/modes.js
@@ -1,15 +1,15 @@
// Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
// Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
/** @scope modules */
var Modes = Module("modes", {
init: function init() {
this.modeChars = {};
this._main = 1; // NORMAL
this._extended = 0; // NONE
@@ -52,95 +52,50 @@ var Modes = Module("modes", {
description: "The base mode for most modes which accept commands rather than input"
});
this.addMode("NORMAL", {
char: "n",
description: "Active when nothing is focused",
bases: [this.COMMAND]
});
- this.addMode("VISUAL", {
- char: "v",
- description: "Active when text is selected",
- display: function () "VISUAL" + (this._extended & modes.LINE ? " LINE" : ""),
- bases: [this.COMMAND],
- ownsFocus: true
- }, {
- leave: function (stack, newMode) {
- if (newMode.main == modes.CARET) {
- let selection = content.getSelection();
- if (selection && !selection.isCollapsed)
- selection.collapseToStart();
- }
- else if (stack.pop)
- editor.unselectText();
- }
- });
this.addMode("CARET", {
+ char: "caret",
description: "Active when the caret is visible in the web content",
bases: [this.NORMAL]
}, {
get pref() prefs.get("accessibility.browsewithcaret"),
set pref(val) prefs.set("accessibility.browsewithcaret", val),
enter: function (stack) {
if (stack.pop && !this.pref)
modes.pop();
else if (!stack.pop && !this.pref)
this.pref = true;
- },
+ if (!stack.pop)
+ buffer.resetCaret();
+ },
leave: function (stack) {
if (!stack.push && this.pref)
this.pref = false;
}
});
- this.addMode("TEXT_EDIT", {
- char: "t",
- description: "Vim-like editing of input elements",
- bases: [this.COMMAND],
- ownsFocus: true
- }, {
- onKeyPress: function (eventList) {
- const KILL = false, PASS = true;
-
- // Hack, really.
- if (eventList[0].charCode || /^<(?:.-)*(?:BS|Del|C-h|C-w|C-u|C-k)>$/.test(events.toString(eventList[0]))) {
- dactyl.beep();
- return KILL;
- }
- return PASS;
- }
- });
- this.addMode("OUTPUT_MULTILINE", {
- description: "Active when the multi-line output buffer is open",
- bases: [this.NORMAL]
- });
this.addMode("INPUT", {
char: "I",
description: "The base mode for input modes, including Insert and Command Line",
bases: [this.MAIN],
insert: true
});
- this.addMode("INSERT", {
- char: "i",
- description: "Active when an input element is focused",
- insert: true,
- ownsFocus: true
- });
- this.addMode("AUTOCOMPLETE", {
- description: "Active when an input autocomplete pop-up is active",
- display: function () "AUTOCOMPLETE (insert)",
- bases: [this.INSERT]
- });
this.addMode("EMBED", {
description: "Active when an <embed> or <object> element is focused",
+ bases: [modes.MAIN],
insert: true,
ownsFocus: true,
passthrough: true
});
this.addMode("PASS_THROUGH", {
description: "All keys but <C-v> are ignored by " + config.appName,
bases: [this.BASE],
@@ -202,80 +157,47 @@ var Modes = Module("modes", {
let selection = frame.getSelection();
if (selection && !selection.isCollapsed)
selection.collapseToStart();
}
}
}
});
-
- function makeTree() {
- let list = modes.all.filter(function (m) m.name !== m.description);
-
- let tree = {};
-
- for (let mode in values(list))
- tree[mode.name] = {};
-
- for (let mode in values(list))
- for (let base in values(mode.bases))
- tree[base.name][mode.name] = tree[mode.name];
-
- let roots = iter([m.name, tree[m.name]] for (m in values(list)) if (!m.bases.length)).toObject();
-
- default xml namespace = NS;
- function rec(obj) {
- XML.ignoreWhitespace = XML.prettyPrinting = false;
-
- let res = <ul dactyl:highlight="Dense" xmlns:dactyl={NS}/>;
- Object.keys(obj).sort().forEach(function (name) {
- let mode = modes.getMode(name);
- res.* += <li><em>{mode.displayName}</em>: {mode.description}{
- rec(obj[name])
- }</li>;
- });
-
- if (res.*.length())
- return res;
- return <></>;
- }
-
- return rec(roots);
- }
-
- util.timeout(function () {
- // Waits for the add-on to become available, if necessary.
- config.addon;
- config.version;
-
- services["dactyl:"].pages["modes.dtd"] = services["dactyl:"].pages["modes.dtd"]();
- });
-
- services["dactyl:"].pages["modes.dtd"] = function () [null,
- util.makeDTD(iter({ "modes.tree": makeTree() },
- config.dtd))];
- },
+ },
+
cleanup: function cleanup() {
modes.reset();
},
+ signals: {
+ "io.source": function ioSource(context, file, modTime) {
+ cache.flushEntry("modes.dtd", modTime);
+ }
+ },
+
_getModeMessage: function _getModeMessage() {
// when recording a macro
let macromode = "";
if (this.recording)
- macromode = "recording";
+ macromode = "recording " + this.recording + " ";
else if (this.replaying)
macromode = "replaying";
- let val = this._modeMap[this._main].display();
- if (val)
- return "-- " + val + " --" + macromode;
+ if (!options.get("showmode").getKey(this.main.allBases, false))
return macromode;
- },
+
+ let modeName = this._modeMap[this._main].display();
+ if (!modeName)
+ return macromode;
+
+ if (macromode)
+ macromode = " " + macromode;
+ return "-- " + modeName + " --" + macromode;
+ },
NONE: 0,
__iterator__: function __iterator__() array.iterValues(this.all),
get all() this._modes.slice(),
get mainModes() (mode for ([k, mode] in Iterator(modes._modeMap)) if (!mode.extended && mode.name == k)),
@@ -297,16 +219,28 @@ var Modes = Module("modes", {
this._modes.push(mode);
if (!mode.extended)
this._mainModes.push(mode);
dactyl.triggerObserver("modes.add", mode);
},
+ removeMode: function removeMode(mode) {
+ this.remove(mode);
+ if (this[mode.name] == mode)
+ delete this[mode.name];
+ if (this._modeMap[mode.name] == mode)
+ delete this._modeMap[mode.name];
+ if (this._modeMap[mode.mode] == mode)
+ delete this._modeMap[mode.mode];
+
+ this._mainModes = this._mainModes.filter(function (m) m != mode);
+ },
+
dumpStack: function dumpStack() {
util.dump("Mode stack:");
for (let [i, mode] in array.iterItems(this._modeStack))
util.dump(" " + i + ": " + mode);
},
getMode: function getMode(name) this._modeMap[name],
@@ -322,19 +256,17 @@ var Modes = Module("modes", {
this._modes.filter(function (mode) Object.keys(obj)
.every(function (k) obj[k] == (mode[k] || false))),
// show the current mode string in the command line
show: function show() {
if (!loaded.modes)
return;
- let msg = null;
- if (options.get("showmode").getKey(this.main.allBases, false))
- msg = this._getModeMessage();
+ let msg = this._getModeMessage();
if (msg || loaded.commandline)
commandline.widgets.mode = msg || null;
},
remove: function remove(mode, covert) {
if (covert && this.topOfStack.main != mode) {
util.assert(mode != this.NORMAL);
@@ -349,22 +281,21 @@ var Modes = Module("modes", {
delayed: [],
delay: function delay(callback, self) { this.delayed.push([callback, self]); },
save: function save(id, obj, prop, test) {
if (!(id in this.boundProperties))
for (let elem in array.iterValues(this._modeStack))
elem.saved[id] = { obj: obj, prop: prop, value: obj[prop], test: test };
- this.boundProperties[id] = { obj: Cu.getWeakReference(obj), prop: prop, test: test };
- },
+ this.boundProperties[id] = { obj: util.weakReference(obj), prop: prop, test: test };
+ },
inSet: false,
- // helper function to set both modes in one go
set: function set(mainMode, extendedMode, params, stack) {
var delayed, oldExtended, oldMain, prev, push;
if (this.inSet) {
dactyl.reportError(Error(_("mode.recursiveSet")), true);
return;
}
@@ -435,25 +366,27 @@ var Modes = Module("modes", {
onCaretChange: function onPrefChange(value) {
if (!value && modes.main == modes.CARET)
modes.pop();
if (value && modes.main == modes.NORMAL)
modes.push(modes.CARET);
},
push: function push(mainMode, extendedMode, params) {
+ if (this.main == this.IGNORE)
+ this.pop();
+
this.set(mainMode, extendedMode, params, { push: this.topOfStack });
},
pop: function pop(mode, args) {
while (this._modeStack.length > 1 && this.main != mode) {
let a = this._modeStack.pop();
this.set(this.topOfStack.main, this.topOfStack.extended, this.topOfStack.params,
- update({ pop: a },
- args || {}));
+ update({ pop: a }, args));
if (mode == null)
return;
}
},
replace: function replace(mode, oldMode, args) {
while (oldMode && this._modeStack.length > 1 && this.main != oldMode)
@@ -484,64 +417,64 @@ var Modes = Module("modes", {
get extended() this._extended,
set extended(value) { this.set(null, value); }
}, {
Mode: Class("Mode", {
init: function init(name, options, params) {
if (options.bases)
util.assert(options.bases.every(function (m) m instanceof this, this.constructor),
- _("mode.invalidBases"), true);
+ _("mode.invalidBases"), false);
this.update({
id: 1 << Modes.Mode._id++,
description: name,
name: name,
params: params || {}
}, options);
},
description: Messages.Localized(""),
- displayName: Class.memoize(function () this.name.split("_").map(util.capitalize).join(" ")),
+ displayName: Class.Memoize(function () this.name.split("_").map(util.capitalize).join(" ")),
isinstance: function isinstance(obj)
this.allBases.indexOf(obj) >= 0 || callable(obj) && this instanceof obj,
- allBases: Class.memoize(function () {
+ allBases: Class.Memoize(function () {
let seen = {}, res = [], queue = [this].concat(this.bases);
for (let mode in array.iterValues(queue))
if (!Set.add(seen, mode)) {
res.push(mode);
queue.push.apply(queue, mode.bases);
}
return res;
}),
get bases() this.input ? [modes.INPUT] : [modes.MAIN],
get count() !this.insert,
- _display: Class.memoize(function _display() this.name.replace("_", " ", "g")),
+ _display: Class.Memoize(function _display() this.name.replace("_", " ", "g")),
display: function display() this._display,
extended: false,
hidden: false,
- input: Class.memoize(function input() this.insert || this.bases.length && this.bases.some(function (b) b.input)),
-
- insert: Class.memoize(function insert() this.bases.length && this.bases.some(function (b) b.insert)),
-
- ownsFocus: Class.memoize(function ownsFocus() this.bases.length && this.bases.some(function (b) b.ownsFocus)),
+ input: Class.Memoize(function input() this.insert || this.bases.length && this.bases.some(function (b) b.input)),
+
+ insert: Class.Memoize(function insert() this.bases.length && this.bases.some(function (b) b.insert)),
+
+ ownsFocus: Class.Memoize(function ownsFocus() this.bases.length && this.bases.some(function (b) b.ownsFocus)),
passEvent: function passEvent(event) this.input && event.charCode && !(event.ctrlKey || event.altKey || event.metaKey),
- passUnknown: Class.memoize(function () options.get("passunknown").getKey(this.name)),
+ passUnknown: Class.Memoize(function () options.get("passunknown").getKey(this.name)),
get mask() this,
get toStringParams() [this.name],
valueOf: function valueOf() this.id
}, {
_id: 0
@@ -579,34 +512,77 @@ var Modes = Module("modes", {
if (desc.set)
value = desc.set.call(this, val);
value = !desc.set || value === undefined ? val : value;
}
})
}, desc));
}
}, {
+ cache: function initCache() {
+ function makeTree() {
+ let list = modes.all.filter(function (m) m.name !== m.description);
+
+ let tree = {};
+
+ for (let mode in values(list))
+ tree[mode.name] = {};
+
+ for (let mode in values(list))
+ for (let base in values(mode.bases))
+ tree[base.name][mode.name] = tree[mode.name];
+
+ let roots = iter([m.name, tree[m.name]] for (m in values(list)) if (!m.bases.length)).toObject();
+
+ default xml namespace = NS;
+ function rec(obj) {
+ XML.ignoreWhitespace = XML.prettyPrinting = false;
+
+ let res = <ul dactyl:highlight="Dense" xmlns:dactyl={NS}/>;
+ Object.keys(obj).sort().forEach(function (name) {
+ let mode = modes.getMode(name);
+ res.* += <li><em>{mode.displayName}</em>: {mode.description}{
+ rec(obj[name])
+ }</li>;
+ });
+
+ if (res.*.length())
+ return res;
+ return <></>;
+ }
+
+ return rec(roots);
+ }
+
+ cache.register("modes.dtd", function ()
+ util.makeDTD(iter({ "modes.tree": makeTree() },
+ config.dtd)));
+ },
mappings: function initMappings() {
mappings.add([modes.BASE, modes.NORMAL],
["<Esc>", "<C-[>"],
"Return to Normal mode",
function () { modes.reset(); });
- mappings.add([modes.INPUT, modes.COMMAND, modes.PASS_THROUGH, modes.QUOTE],
+ mappings.add([modes.INPUT, modes.COMMAND, modes.OPERATOR, modes.PASS_THROUGH, modes.QUOTE],
["<Esc>", "<C-[>"],
"Return to the previous mode",
+ function () { modes.pop(null, { fromEscape: true }); });
+
+ mappings.add([modes.AUTOCOMPLETE, modes.MENU], ["<C-c>"],
+ "Leave Autocomplete or Menu mode",
function () { modes.pop(); });
- mappings.add([modes.MENU], ["<C-c>"],
- "Leave Menu mode",
- function () { modes.pop(); });
-
mappings.add([modes.MENU], ["<Esc>"],
"Close the current popup",
- function () { return Events.PASS_THROUGH; });
+ function () {
+ if (events.popups.active.length)
+ return Events.PASS_THROUGH;
+ modes.pop();
+ });
mappings.add([modes.MENU], ["<C-[>"],
"Close the current popup",
function () { events.feedkeys("<Esc>"); });
},
options: function initOptions() {
let opts = {
completer: function completer(context, extra) {
@@ -637,22 +613,23 @@ var Modes = Module("modes", {
validator: function validator(vals) vals.map(function (v) v.replace(/^!/, "")).every(Set.has(this.values)),
get values() array.toObject([[m.name.toLowerCase(), m.description] for (m in values(modes._modes)) if (!m.hidden)])
};
options.add(["passunknown", "pu"],
"Pass through unknown keys in these modes",
- "stringlist", "!text_edit,base",
+ "stringlist", "!text_edit,!visual,base",
opts);
options.add(["showmode", "smd"],
"Show the current mode in the command line when it matches this expression",
- "stringlist", "caret,output_multiline,!normal,base",
+ "stringlist", "caret,output_multiline,!normal,base,operator",
opts);
},
prefs: function initPrefs() {
- prefs.watch("accessibility.browsewithcaret", function () modes.onCaretChange.apply(modes, arguments));
- }
-});
+ prefs.watch("accessibility.browsewithcaret",
+ function () { modes.onCaretChange.apply(modes, arguments) });
+ }
+});
// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/mow.js b/common/content/mow.js
--- a/common/content/mow.js
+++ b/common/content/mow.js
@@ -1,55 +1,53 @@
// Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
// Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
var MOW = Module("mow", {
init: function init() {
this._resize = Timer(20, 400, function _resize() {
if (this.visible)
this.resize(false);
if (this.visible && isinstance(modes.main, modes.OUTPUT_MULTILINE))
this.updateMorePrompt();
}, this);
this._timer = Timer(20, 400, function _timer() {
if (modes.have(modes.OUTPUT_MULTILINE)) {
this.resize(true);
- if (options["more"] && this.isScrollable(1)) {
+ if (options["more"] && this.canScroll(1))
// start the last executed command's output at the top of the screen
- let elements = this.document.getElementsByClassName("ex-command-output");
- elements[elements.length - 1].scrollIntoView(true);
- }
+ DOM(this.document.body.lastElementChild).scrollIntoView(true);
else
this.body.scrollTop = this.body.scrollHeight;
dactyl.focus(this.window);
this.updateMorePrompt();
}
}, this);
events.listen(window, this, "windowEvents");
modules.mow = this;
- let fontSize = util.computedStyle(document.documentElement).fontSize;
+ let fontSize = DOM(document.documentElement).style.fontSize;
styles.system.add("font-size", "dactyl://content/buffer.xhtml",
"body { font-size: " + fontSize + "; } \
html|html > xul|scrollbar { visibility: collapse !important; }",
true);
XML.ignoreWhitespace = true;
- util.overlayWindow(window, {
+ overlay.overlayWindow(window, {
objects: {
eventTarget: this
},
append: <e4x xmlns={XUL} xmlns:dactyl={NS}>
<window id={document.documentElement.id}>
<popupset>
<menupopup id="dactyl-contextmenu" highlight="Events" events="contextEvents">
<menuitem id="dactyl-context-copylink"
@@ -62,102 +60,107 @@ var MOW = Module("mow", {
label={_("mow.contextMenu.copy")} dactyl:group="selection"
command="cmd_copy"/>
<menuitem id="dactyl-context-selectall"
label={_("mow.contextMenu.selectAll")}
command="cmd_selectAll"/>
</menupopup>
</popupset>
</window>
- <vbox id={config.commandContainer}>
+ <vbox id={config.ids.commandContainer}>
<vbox class="dactyl-container" id="dactyl-multiline-output-container" hidden="false" collapsed="true">
<iframe id="dactyl-multiline-output" src="dactyl://content/buffer.xhtml"
flex="1" hidden="false" collapsed="false" contextmenu="dactyl-contextmenu"
highlight="Events" />
</vbox>
</vbox>
</e4x>
});
},
__noSuchMethod__: function (meth, args) Buffer[meth].apply(Buffer, [this.body].concat(args)),
get widget() this.widgets.multilineOutput,
- widgets: Class.memoize(function widgets() commandline.widgets),
-
- body: Class.memoize(function body() this.widget.contentDocument.documentElement),
+ widgets: Class.Memoize(function widgets() commandline.widgets),
+
+ body: Class.Memoize(function body() this.widget.contentDocument.documentElement),
get document() this.widget.contentDocument,
get window() this.widget.contentWindow,
/**
* Display a multi-line message.
*
* @param {string} data
* @param {string} highlightGroup
*/
echo: function echo(data, highlightGroup, silent) {
- let body = this.document.body;
+ let body = DOM(this.document.body);
this.widgets.message = null;
if (!commandline.commandVisible)
commandline.hide();
if (modes.main != modes.OUTPUT_MULTILINE) {
modes.push(modes.OUTPUT_MULTILINE, null, {
onKeyPress: this.closure.onKeyPress,
+
leave: this.closure(function leave(stack) {
if (stack.pop)
for (let message in values(this.messages))
if (message.leave)
message.leave(stack);
- })
+ }),
+
+ window: this.window
});
this.messages = [];
}
// 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 = XML.prettyPrinting = false;
+ highlightGroup = "CommandOutput " + (highlightGroup || "");
+
if (isObject(data) && !isinstance(data, _)) {
this.lastOutput = null;
- var output = util.xmlToDom(<div class="ex-command-output" style="white-space: nowrap" highlight={highlightGroup}/>,
+ var output = DOM(<div style="white-space: nowrap" highlight={highlightGroup}/>,
this.document);
data.document = this.document;
try {
- output.appendChild(data.message);
+ output.append(data.message);
}
catch (e) {
util.reportError(e);
util.dump(data);
}
this.messages.push(data);
}
else {
- let style = isString(data) ? "pre" : "nowrap";
- this.lastOutput = <div class="ex-command-output" style={"white-space: " + style} highlight={highlightGroup}>{data}</div>;
-
- var output = util.xmlToDom(this.lastOutput, this.document);
- }
+ let style = isString(data) ? "pre-wrap" : "nowrap";
+ this.lastOutput = <div style={"white-space: " + style} highlight={highlightGroup}>{data}</div>;
+
+ var output = DOM(this.lastOutput, this.document);
+ }
// FIXME: need to make sure an open MOW is closed when commands
// that don't generate output are executed
if (!this.visible) {
this.body.scrollTop = 0;
- body.textContent = "";
- }
-
- body.appendChild(output);
+ body.empty();
+ }
+
+ body.append(output);
let str = typeof data !== "xml" && data.message || data;
if (!silent)
- dactyl.triggerObserver("echoMultiline", data, highlightGroup, output);
+ dactyl.triggerObserver("echoMultiline", data, highlightGroup, output[0]);
this._timer.tell();
if (!this.visible)
this._timer.flush();
},
events: {
click: function onClick(event) {
@@ -165,17 +168,17 @@ var MOW = Module("mow", {
return;
const openLink = function openLink(where) {
event.preventDefault();
dactyl.open(event.target.href, where);
};
if (event.target instanceof HTMLAnchorElement)
- switch (events.toString(event)) {
+ switch (DOM.Event.stringify(event)) {
case "<LeftMouse>":
openLink(dactyl.CURRENT_TAB);
break;
case "<MiddleMouse>":
case "<C-LeftMouse>":
case "<C-M-LeftMouse>":
openLink({ where: dactyl.NEW_TAB, background: true });
break;
@@ -214,17 +217,17 @@ var MOW = Module("mow", {
node.hidden = group && !group.split(/\s+/).every(function (g) enabled[g]);
}
}
},
onKeyPress: function onKeyPress(eventList) {
const KILL = false, PASS = true;
- if (options["more"] && mow.isScrollable(1))
+ if (options["more"] && mow.canScroll(1))
this.updateMorePrompt(false, true);
else {
modes.pop();
events.feedevents(null, eventList);
return KILL;
}
return PASS;
},
@@ -236,37 +239,43 @@ var MOW = Module("mow", {
* already so.
*/
resize: function resize(open, extra) {
if (!(open || this.visible))
return;
let doc = this.widget.contentDocument;
- let availableHeight = config.outputHeight;
+ let trim = this.spaceNeeded;
+ let availableHeight = config.outputHeight - trim;
if (this.visible)
availableHeight += parseFloat(this.widgets.mowContainer.height || 0);
availableHeight -= extra || 0;
doc.body.style.minWidth = this.widgets.commandbar.commandline.scrollWidth + "px";
- this.widgets.mowContainer.height = Math.min(doc.body.clientHeight, availableHeight) + "px";
- this.timeout(function ()
- this.widgets.mowContainer.height = Math.min(doc.body.clientHeight, availableHeight) + "px",
- 0);
+
+ function adjust() {
+ let wantedHeight = doc.body.clientHeight;
+ this.widgets.mowContainer.height = Math.min(wantedHeight, availableHeight) + "px",
+ this.wantedHeight = Math.max(0, wantedHeight - availableHeight);
+ }
+ adjust.call(this);
+ this.timeout(adjust);
doc.body.style.minWidth = "";
this.visible = true;
},
get spaceNeeded() {
- let rect = this.widgets.commandbar.commandline.getBoundingClientRect();
- let offset = rect.bottom - window.innerHeight;
- return Math.max(0, offset);
- },
+ if (DOM("#dactyl-bell", document).isVisible)
+ return 0;
+ return Math.max(0, DOM("#" + config.ids.commandContainer, document).rect.bottom
+ - window.innerHeight);
+ },
/**
* Update or remove the multi-line 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.
@@ -274,52 +283,63 @@ var MOW = Module("mow", {
updateMorePrompt: function updateMorePrompt(force, showHelp) {
if (!this.visible || !isinstance(modes.main, modes.OUTPUT_MULTILINE))
return this.widgets.message = null;
let elem = this.widget.contentDocument.documentElement;
if (showHelp)
this.widgets.message = ["MoreMsg", _("mow.moreHelp")];
- else if (force || (options["more"] && Buffer.isScrollable(elem, 1)))
+ else if (force || (options["more"] && Buffer.canScroll(elem, 1)))
this.widgets.message = ["MoreMsg", _("mow.more")];
else
this.widgets.message = ["Question", _("mow.continue")];
},
visible: Modes.boundProperty({
get: function get_mowVisible() !this.widgets.mowContainer.collapsed,
set: function set_mowVisible(value) {
this.widgets.mowContainer.collapsed = !value;
let elem = this.widget;
if (!value && elem && elem.contentWindow == document.commandDispatcher.focusedWindow) {
let focused = content.document.activeElement;
- if (Events.isInputElement(focused))
+ if (focused && Events.isInputElement(focused))
focused.blur();
document.commandDispatcher.focusedWindow = content;
}
}
})
}, {
}, {
+ modes: function initModes() {
+ modes.addMode("OUTPUT_MULTILINE", {
+ description: "Active when the multi-line output buffer is open",
+ bases: [modes.NORMAL]
+ });
+ },
mappings: function initMappings() {
const PASS = true;
const DROP = false;
const BEEP = {};
mappings.add([modes.COMMAND],
["g<lt>"], "Redisplay the last command output",
function () {
dactyl.assert(mow.lastOutput, _("mow.noPreviousOutput"));
mow.echo(mow.lastOutput, "Normal");
});
+ mappings.add([modes.OUTPUT_MULTILINE],
+ ["<Esc>", "<C-[>"],
+ "Return to the previous mode",
+ function () { modes.pop(null, { fromEscape: true }); });
+
let bind = function bind(keys, description, action, test, default_) {
mappings.add([modes.OUTPUT_MULTILINE],
keys, description,
function (args) {
if (!options["more"])
var res = PASS;
else if (test && !test(args))
res = default_;
@@ -336,46 +356,46 @@ var MOW = Module("mow", {
events.feedkeys(args.command);
}, {
count: action.length > 0
});
};
bind(["j", "<C-e>", "<Down>"], "Scroll down one line",
function ({ count }) { mow.scrollVertical("lines", 1 * (count || 1)); },
- function () mow.isScrollable(1), BEEP);
+ function () mow.canScroll(1), BEEP);
bind(["k", "<C-y>", "<Up>"], "Scroll up one line",
function ({ count }) { mow.scrollVertical("lines", -1 * (count || 1)); },
- function () mow.isScrollable(-1), BEEP);
+ function () mow.canScroll(-1), BEEP);
bind(["<C-j>", "<C-m>", "<Return>"], "Scroll down one line, exit on last line",
function ({ count }) { mow.scrollVertical("lines", 1 * (count || 1)); },
- function () mow.isScrollable(1), DROP);
+ function () mow.canScroll(1), DROP);
// half page down
bind(["<C-d>"], "Scroll down half a page",
function ({ count }) { mow.scrollVertical("pages", .5 * (count || 1)); },
- function () mow.isScrollable(1), BEEP);
+ function () mow.canScroll(1), BEEP);
bind(["<C-f>", "<PageDown>"], "Scroll down one page",
function ({ count }) { mow.scrollVertical("pages", 1 * (count || 1)); },
- function () mow.isScrollable(1), BEEP);
+ function () mow.canScroll(1), BEEP);
bind(["<Space>"], "Scroll down one page",
function ({ count }) { mow.scrollVertical("pages", 1 * (count || 1)); },
- function () mow.isScrollable(1), DROP);
+ function () mow.canScroll(1), DROP);
bind(["<C-u>"], "Scroll up half a page",
function ({ count }) { mow.scrollVertical("pages", -.5 * (count || 1)); },
- function () mow.isScrollable(-1), BEEP);
+ function () mow.canScroll(-1), BEEP);
bind(["<C-b>", "<PageUp>"], "Scroll up half a page",
function ({ count }) { mow.scrollVertical("pages", -1 * (count || 1)); },
- function () mow.isScrollable(-1), BEEP);
+ function () mow.canScroll(-1), BEEP);
bind(["gg"], "Scroll to the beginning of output",
function () { mow.scrollToPercent(null, 0); });
bind(["G"], "Scroll to the end of output",
function ({ count }) { mow.scrollToPercent(null, count || 100); });
// copy text to clipboard
diff --git a/common/content/quickmarks.js b/common/content/quickmarks.js
--- a/common/content/quickmarks.js
+++ b/common/content/quickmarks.js
@@ -1,15 +1,15 @@
// Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
// Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
/** @scope modules */
/**
* @instance quickmarks
*/
var QuickMarks = Module("quickmarks", {
init: function () {
@@ -94,19 +94,19 @@ var QuickMarks = Module("quickmarks", {
/**
* Lists all quickmarks matching *filter* in the message window.
*
* @param {string} filter The list of quickmarks to display, e.g. "a-c i O-X".
*/
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();
+ let lowercaseMarks = marks.filter(bind("test", /[a-z]/)).sort();
+ let uppercaseMarks = marks.filter(bind("test", /[A-Z]/)).sort();
+ let numberMarks = marks.filter(bind("test", /[0-9]/)).sort();
marks = Array.concat(lowercaseMarks, uppercaseMarks, numberMarks);
dactyl.assert(marks.length > 0, _("quickmark.none"));
if (filter.length > 0) {
let pattern = util.charListToRegexp(filter, "a-zA-Z0-9");
marks = marks.filter(function (qmark) pattern.test(qmark));
diff --git a/common/content/statusline.js b/common/content/statusline.js
--- a/common/content/statusline.js
+++ b/common/content/statusline.js
@@ -1,34 +1,33 @@
// Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
// Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
/** @scope modules */
var StatusLine = Module("statusline", {
init: function init() {
this._statusLine = document.getElementById("status-bar");
this.statusBar = document.getElementById("addon-bar") || this._statusLine;
- this.statusBar.collapsed = true;
this.baseGroup = this.statusBar == this._statusLine ? "StatusLine " : "";
if (this.statusBar.localName == "toolbar") {
styles.system.add("addon-bar", config.styleableChrome, <css><![CDATA[
#status-bar { margin-top: 0 !important; }
#addon-bar > statusbar { -moz-box-flex: 1 }
#addon-bar > #addonbar-closebutton { visibility: collapse; }
#addon-bar > xul|toolbarspring { visibility: collapse; }
]]></css>);
- util.overlayWindow(window, { append: <><statusbar id="status-bar" ordinal="0"/></> });
+ overlay.overlayWindow(window, { append: <><statusbar id="status-bar" ordinal="0"/></> });
highlight.loadCSS(util.compileMacro(<![CDATA[
!AddonBar;#addon-bar {
/* The Add-on Bar */
padding-left: 0 !important;
min-height: 18px !important;
-moz-appearance: none !important;
<padding>
@@ -37,17 +36,17 @@ var StatusLine = Module("statusline", {
/* An Add-on Bar button */
-moz-appearance: none !important;
padding: 0 !important;
border-width: 0px !important;
min-width: 0 !important;
color: inherit !important;
}
AddonButton:not(:hover) background: transparent;
- ]]>)({ padding: util.OS.isMacOSX ? "padding-right: 10px !important;" : "" }));
+ ]]>)({ padding: config.OS.isMacOSX ? "padding-right: 10px !important;" : "" }));
if (document.getElementById("appmenu-button"))
highlight.loadCSS(<![CDATA[
AppmenuButton /* The app-menu button */ \
min-width: 0 !important; padding: 0 .5em !important;
]]>);
}
@@ -78,17 +77,17 @@ var StatusLine = Module("statusline", {
<statusbarpanel id="statusbar-display" hidden="true"/>
<statusbarpanel id="statusbar-progresspanel" hidden="true"/>
</statusbar>
</e4x>;
for each (let attr in prepend..@key)
attr.parent().@id = "dactyl-statusline-field-" + attr;
- util.overlayWindow(window, {
+ overlay.overlayWindow(window, {
objects: this.widgets = { get status() this.container },
prepend: prepend.elements()
});
try {
this.security = content.document.dactylSecurity || "insecure";
}
catch (e) {}
@@ -136,22 +135,26 @@ var StatusLine = Module("statusline", {
this.security = "secure";
else // if (state & Ci.nsIWebProgressListener.STATE_IS_INSECURE)
this.security = "insecure";
if (webProgress && webProgress.DOMWindow)
webProgress.DOMWindow.document.dactylSecurity = this.security;
},
"browser.stateChange": function onStateChange(webProgress, request, flags, status) {
- if (flags & Ci.nsIWebProgressListener.STATE_START)
+ const L = Ci.nsIWebProgressListener;
+
+ if (flags & (L.STATE_IS_DOCUMENT | L.STATE_IS_WINDOW))
+ if (flags & L.STATE_START)
this.progress = 0;
- if (flags & Ci.nsIWebProgressListener.STATE_STOP) {
+ else if (flags & L.STATE_STOP)
this.progress = "";
+
+ if (flags & L.STATE_STOP)
this.updateStatus();
- }
},
"browser.statusChange": function onStatusChange(webProgress, request, status, message) {
this.timeout(function () {
this.status = message || buffer.uri;
});
}
},
diff --git a/common/content/tabs.js b/common/content/tabs.js
--- a/common/content/tabs.js
+++ b/common/content/tabs.js
@@ -1,15 +1,15 @@
// Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
// Copyright (c) 2008-2011 by Kris Maglione <maglione.k at Gmail>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
/** @scope modules */
// TODO: many methods do not work with Thunderbird correctly yet
/**
* @instance tabs
*/
@@ -18,31 +18,31 @@ var Tabs = Module("tabs", {
// used for the "gb" and "gB" mappings to remember the last :buffer[!] command
this._lastBufferSwitchArgs = "";
this._lastBufferSwitchSpecial = true;
this.xulTabs = document.getElementById("tabbrowser-tabs");
// hide tabs initially to prevent flickering when 'stal' would hide them
// on startup
- if (config.hasTabbrowser)
+ if (config.has("tabbrowser"))
config.tabStrip.collapsed = true;
this.tabStyle = styles.system.add("tab-strip-hiding", config.styleableChrome,
(config.tabStrip.id ? "#" + config.tabStrip.id : ".tabbrowser-strip") +
"{ visibility: collapse; }",
false, true);
dactyl.commands["tabs.select"] = function (event) {
- tabs.select(event.originalTarget.getAttribute("identifier"));
+ tabs.switchTo(event.originalTarget.getAttribute("identifier"));
};
this.tabBinding = styles.system.add("tab-binding", "chrome://browser/content/browser.xul", String.replace(<><![CDATA[
xul|tab { -moz-binding: url(chrome://dactyl/content/bindings.xml#tab) !important; }
- ]]></>, /tab-./g, function (m) util.OS.isMacOSX ? "tab-mac" : m),
+ ]]></>, /tab-./g, function (m) config.OS.isMacOSX ? "tab-mac" : m),
false, true);
this.timeout(function () {
for (let { linkedBrowser: { contentDocument } } in values(this.allTabs))
if (contentDocument.readyState === "complete")
dactyl.initDocument(contentDocument);
}, 1000);
@@ -52,46 +52,52 @@ var Tabs = Module("tabs", {
signals: {
enter: function enter() {
if (window.TabsInTitlebar)
window.TabsInTitlebar.allowedBy("dactyl", true);
}
},
- _alternates: Class.memoize(function () [config.tabbrowser.mCurrentTab, null]),
+ _alternates: Class.Memoize(function () [config.tabbrowser.mCurrentTab, null]),
cleanup: function cleanup() {
for (let [i, tab] in Iterator(this.allTabs)) {
let node = function node(class_) document.getAnonymousElementByAttribute(tab, "class", class_);
for (let elem in values(["dactyl-tab-icon-number", "dactyl-tab-number"].map(node)))
if (elem)
elem.parentNode.parentNode.removeChild(elem.parentNode);
- }
- },
+
+ delete tab.dactylOrdinal;
+ tab.removeAttribute("dactylOrdinal");
+ }
+ },
updateTabCount: function updateTabCount() {
for (let [i, tab] in Iterator(this.visibleTabs)) {
if (dactyl.has("Gecko2")) {
let node = function node(class_) document.getAnonymousElementByAttribute(tab, "class", class_);
if (!node("dactyl-tab-number")) {
let img = node("tab-icon-image");
if (img) {
- let nodes = {};
- let dom = util.xmlToDom(<xul xmlns:xul={XUL} xmlns:html={XHTML}
- ><xul:hbox highlight="tab-number"><xul:label key="icon" align="center" highlight="TabIconNumber" class="dactyl-tab-icon-number"/></xul:hbox
- ><xul:hbox highlight="tab-number"><html:div key="label" highlight="TabNumber" class="dactyl-tab-number"/></xul:hbox
- ></xul>.*, document, nodes);
- img.parentNode.appendChild(dom);
- tab.__defineGetter__("dactylOrdinal", function () Number(nodes.icon.value));
- tab.__defineSetter__("dactylOrdinal", function (i) nodes.icon.value = nodes.label.textContent = i);
- }
- }
- }
- tab.setAttribute("dactylOrdinal", i + 1);
+ let dom = DOM(<xul xmlns:xul={XUL} xmlns:html={XHTML}>
+ <xul:hbox highlight="tab-number"><xul:label key="icon" align="center" highlight="TabIconNumber" class="dactyl-tab-icon-number"/></xul:hbox>
+ <xul:hbox highlight="tab-number"><html:div key="label" highlight="TabNumber" class="dactyl-tab-number"/></xul:hbox>
+ </xul>.elements(), document).appendTo(img.parentNode);
+
+ update(tab, {
+ get dactylOrdinal() Number(dom.nodes.icon.value),
+ set dactylOrdinal(i) {
+ dom.nodes.icon.value = dom.nodes.label.textContent = i;
+ this.setAttribute("dactylOrdinal", i);
+ }
+ });
+ }
+ }
+ }
tab.dactylOrdinal = i + 1;
}
statusline.updateTabCount(true);
},
_onTabSelect: function _onTabSelect() {
// TODO: is all of that necessary?
// I vote no. --Kris
@@ -122,22 +128,17 @@ var Tabs = Module("tabs", {
/**
* @property {number} The number of tabs in the current window.
*/
get count() config.tabbrowser.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;
- },
+ get options() this.localStore.options,
get visibleTabs() config.tabbrowser.visibleTabs || this.allTabs.filter(function (tab) !tab.hidden),
/**
* Returns the local state store for the tab at the specified *tabIndex*.
* If *tabIndex* is not specified then the current tab is used.
*
* @param {number} tabIndex
@@ -146,26 +147,31 @@ var Tabs = Module("tabs", {
// FIXME: why not a tab arg? Why this and the property?
// : To the latter question, because this works for any tab, the
// property doesn't. And the property is so oft-used that it's
// convenient. To the former question, because I think this is mainly
// useful for autocommands, and they get index arguments. --Kris
getLocalStore: function getLocalStore(tabIndex) {
let tab = this.getTab(tabIndex);
if (!tab.dactylStore)
- tab.dactylStore = {};
- return tab.dactylStore;
- },
+ tab.dactylStore = Object.create(this.localStorePrototype);
+ return tab.dactylStore.instance = tab.dactylStore;
+ },
/**
* @property {Object} The local state store for the currently selected
* tab.
*/
get localStore() this.getLocalStore(),
+ localStorePrototype: memoize({
+ instance: {},
+ get options() ({})
+ }),
+
/**
* @property {[Object]} The array of closed tabs for the current
* session.
*/
get closedTabs() JSON.parse(services.sessionStore.getClosedTabData(window)),
/**
* Clones the specified *tab* and append it to the tab list.
@@ -214,26 +220,31 @@ var Tabs = Module("tabs", {
/**
* If TabView exists, returns the Panorama window. If the Panorama
* is has not yet initialized, this function will not return until
* it has.
*
* @returns {Window}
*/
- getGroups: function getGroups() {
- if ("_groups" in this)
- return this._groups;
-
- if (window.TabView && TabView._initFrame)
- TabView._initFrame();
-
+ getGroups: function getGroups(func) {
let iframe = document.getElementById("tab-view");
this._groups = iframe ? iframe.contentWindow : null;
- if (this._groups)
+
+ if ("_groups" in this && !func)
+ return this._groups;
+
+ if (func)
+ func = bind(function (func) { func(this._groups) }, this, func);
+
+ if (window.TabView && TabView._initFrame)
+ TabView._initFrame(func);
+
+ this._groups = iframe ? iframe.contentWindow : null;
+ if (this._groups && !func)
util.waitFor(function () this._groups.TabItems, this);
return this._groups;
},
/**
* Returns the tab at the specified *index* or the currently selected tab
* if *index* is not specified. This is a 0-based index.
*
@@ -315,16 +326,70 @@ var Tabs = Module("tabs", {
*
* @param {string} filter A filter matching a substring of the tab's
* document title or URL.
*/
list: function list(filter) {
completion.listCompleter("buffer", filter);
},
+
+ /**
+ * Return an iterator of tabs matching the given filter. If no
+ * *filter* or *count* is provided, returns the currently selected
+ * tab. If *filter* is a number or begins with a number followed
+ * by a colon, the tab of that ordinal is returned. Otherwise,
+ * tabs matching the filter as below are returned.
+ *
+ * @param {string} filter The filter. If *regexp*, this is a
+ * regular expression against which the tab's URL or title
+ * must match. Otherwise, it is a site filter.
+ * @optional
+ * @param {number|null} count If non-null, return only the
+ * *count*th matching tab.
+ * @optional
+ * @param {boolean} regexp Whether to interpret *filter* as a
+ * regular expression.
+ * @param {boolean} all If true, match against all tabs. If
+ * false, match only tabs in the current tab group.
+ */
+ match: function match(filter, count, regexp, all) {
+ if (!filter && count == null)
+ yield tabs.getTab();
+ else if (!filter)
+ yield dactyl.assert(tabs.getTab(count - 1));
+ else {
+ let matches = /^(\d+)(?:$|:)/.exec(filter);
+ if (matches)
+ yield dactyl.assert(count == null &&
+ tabs.getTab(parseInt(matches[1], 10) - 1, !all));
+ else {
+ if (regexp)
+ regexp = util.regexp(filter, "i");
+ else
+ var matcher = Styles.matchFilter(filter);
+
+ for (let tab in values(tabs[all ? "allTabs" : "visibleTabs"])) {
+ let browser = tab.linkedBrowser;
+ let uri = browser.currentURI;
+ let title;
+ if (uri.spec == "about:blank")
+ title = "(Untitled)";
+ else
+ title = browser.contentTitle;
+
+ if (matcher && matcher(uri)
+ || regexp && (regexp.test(title) || regexp.test(uri.spec)))
+ if (count == null || --count == 0)
+ yield tab;
+ }
+ }
+ }
+ },
+
/**
* 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 *spec* causes the
* destination position to wrap around the start/end of the tab list.
*/
@@ -526,102 +591,76 @@ var Tabs = Module("tabs", {
copyTab: function (to, from) {
if (!from)
from = config.tabbrowser.mTabContainer.selectedItem;
let tabState = services.sessionStore.getTabState(from);
services.sessionStore.setTabState(to, tabState);
}
}, {
- load: function () {
+ load: function init_load() {
tabs.updateTabCount();
},
- commands: function () {
- commands.add(["bd[elete]", "bw[ipeout]", "bun[load]", "tabc[lose]"],
- "Delete current buffer",
+ commands: function init_commands() {
+ [
+ {
+ name: ["bd[elete]"],
+ description: "Delete matching buffers",
+ visible: false
+ },
+ {
+ name: ["tabc[lose]"],
+ description: "Delete matching tabs",
+ visible: true
+ }
+ ].forEach(function (params) {
+ commands.add(params.name, params.description,
function (args) {
let removed = 0;
- for (let tab in matchTabs(args, args.bang, true)) {
+ for (let tab in tabs.match(args[0], args.count, args.bang, !params.visible)) {
config.removeTab(tab);
removed++;
}
if (args[0])
if (removed > 0)
dactyl.echomsg(_("buffer.fewerTab" + (removed == 1 ? "" : "s"), removed), 9);
else
- dactyl.echoerr(_("buffer.noMatching", arg));
+ dactyl.echoerr(_("buffer.noMatching", args[0]));
}, {
argCount: "?",
bang: true,
count: true,
completer: function (context) completion.buffer(context),
literal: 0,
privateData: true
});
-
- function matchTabs(args, substr, all) {
- let filter = args[0];
-
- if (!filter && args.count == null)
- yield tabs.getTab();
- else if (!filter)
- yield dactyl.assert(tabs.getTab(args.count - 1));
- else {
- let matches = /^(\d+)(?:$|:)/.exec(filter);
- if (matches)
- yield dactyl.assert(args.count == null &&
- tabs.getTab(parseInt(matches[1], 10) - 1, !all));
- else {
- let str = filter.toLowerCase();
- for (let tab in values(tabs[all ? "allTabs" : "visibleTabs"])) {
- let host, title;
- let browser = tab.linkedBrowser;
- let uri = browser.currentURI.spec;
- if (browser.currentURI.schemeIs("about")) {
- host = "";
- title = "(Untitled)";
- }
- else {
- host = browser.currentURI.host;
- title = browser.contentTitle;
- }
-
- [host, title, uri] = [host, title, uri].map(String.toLowerCase);
-
- if (host.indexOf(str) >= 0 || uri == str ||
- (substr && (title.indexOf(str) >= 0 || uri.indexOf(str) >= 0)))
- if (args.count == null || --args.count == 0)
- yield tab;
- }
- }
- }
- }
+ });
commands.add(["pin[tab]"],
"Pin tab as an application tab",
function (args) {
- for (let tab in matchTabs(args))
+ for (let tab in tabs.match(args[0], args.count))
config.browser[!args.bang || !tab.pinned ? "pinTab" : "unpinTab"](tab);
},
{
argCount: "?",
bang: true,
count: true,
completer: function (context, args) {
if (!args.bang)
context.filters.push(function ({ item }) !item.tab.pinned);
completion.buffer(context);
}
});
commands.add(["unpin[tab]"],
"Unpin tab as an application tab",
function (args) {
- for (let tab in matchTabs(args))
+ for (let tab in tabs.match(args[0], args.count))
config.browser.unpinTab(tab);
},
{
argCount: "?",
count: true,
completer: function (context, args) {
context.filters.push(function ({ item }) item.tab.pinned);
completion.buffer(context);
@@ -642,41 +681,55 @@ var Tabs = Module("tabs", {
completer: function (context) completion.ex(context),
literal: 0,
subCommand: 0
});
commands.add(["tab"],
"Execute a command and tell it to output in a new tab",
function (args) {
- dactyl.withSavedValues(["forceNewTab"], function () {
- this.forceNewTab = true;
+ dactyl.withSavedValues(["forceTarget"], function () {
+ this.forceTarget = dactyl.NEW_TAB;
dactyl.execute(args[0], null, true);
});
}, {
argCount: "1",
completer: function (context) completion.ex(context),
literal: 0,
subCommand: 0
});
- commands.add(["tabd[o]", "bufd[o]"],
- "Execute a command in each tab",
- function (args) {
- for (let tab in values(tabs.visibleTabs)) {
- tabs.select(tab);
+ commands.add(["background", "bg"],
+ "Execute a command opening any new tabs in the background",
+ function (args) {
+ dactyl.withSavedValues(["forceBackground"], function () {
+ this.forceBackground = true;
dactyl.execute(args[0], null, true);
- }
+ });
}, {
argCount: "1",
completer: function (context) completion.ex(context),
literal: 0,
subCommand: 0
});
+ commands.add(["tabd[o]", "bufd[o]"],
+ "Execute a command in each tab",
+ function (args) {
+ for (let tab in values(tabs.visibleTabs)) {
+ tabs.select(tab);
+ dactyl.execute(args[0], null, true);
+ }
+ }, {
+ argCount: "1",
+ completer: function (context) completion.ex(context),
+ literal: 0,
+ subCommand: 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",
@@ -730,17 +783,17 @@ var Tabs = Module("tabs", {
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) {
+ if (config.has("tabbrowser")) {
commands.add(["b[uffer]"],
"Switch to a buffer",
function (args) { tabs.switchTo(args[0], args.bang, args.count); }, {
argCount: "?",
bang: true,
count: true,
completer: function (context) completion.buffer(context),
literal: 0,
@@ -773,20 +826,20 @@ var Tabs = Module("tabs", {
// TODO: add count and bang multimatch support - unify with :buffer nonsense
commands.add(["tabm[ove]"],
"Move the current tab to the position of tab N",
function (args) {
let arg = args[0];
if (tabs.indexFromSpec(arg) == -1) {
- let tabs = [tab for (tab in matchTabs(args, true))];
- dactyl.assert(tabs.length, _("error.invalidArgument", arg));
- dactyl.assert(tabs.length == 1, _("buffer.multipleMatching", arg));
- arg = tabs[0];
+ let list = [tab for (tab in tabs.match(args[0], args.count, true))];
+ dactyl.assert(list.length, _("error.invalidArgument", arg));
+ dactyl.assert(list.length == 1, _("buffer.multipleMatching", arg));
+ arg = list[0];
}
tabs.move(tabs.getTab(), arg, args.bang);
}, {
argCount: "1",
bang: true,
completer: function (context, args) completion.buffer(context, true),
literal: 0
});
@@ -794,17 +847,18 @@ var Tabs = Module("tabs", {
commands.add(["tabo[nly]"],
"Close all other tabs",
function () { tabs.keepOnly(tabs.getTab()); },
{ argCount: "0" });
commands.add(["tabopen", "t[open]", "tabnew"],
"Open one or more URLs in a new tab",
function (args) {
- dactyl.open(args[0] || "about:blank", { from: "tabopen", where: dactyl.NEW_TAB, background: args.bang });
+ dactyl.open(args[0] || "about:blank",
+ { from: "tabopen", where: dactyl.NEW_TAB, background: args.bang });
}, {
bang: true,
completer: function (context) completion.url(context),
domains: function (args) commands.get("open").domains(args),
literal: 0,
privateData: true
});
@@ -835,62 +889,83 @@ var Tabs = Module("tabs", {
// : better name or merge with :tabmove?
commands.add(["taba[ttach]"],
"Attach the current tab to another window",
function (args) {
dactyl.assert(args.length <= 2 && !args.some(function (i) !/^\d+(?:$|:)/.test(i)),
_("error.trailingCharacters"));
let [winIndex, tabIndex] = args.map(function (arg) parseInt(arg));
+ if (args["-group"]) {
+ util.assert(args.length == 1);
+ window.TabView.moveTabTo(tabs.getTab(), winIndex);
+ return;
+ }
+
let win = dactyl.windows[winIndex - 1];
+ let sourceTab = tabs.getTab();
dactyl.assert(win, _("window.noIndex", winIndex));
dactyl.assert(win != window, _("window.cantAttachSame"));
- let browser = win.getBrowser();
+ let modules = win.dactyl.modules;
+ let { browser } = modules.config;
if (args[1]) {
- let tabList = browser.visibleTabs || browser.mTabs;
+ let tabList = modules.tabs.visibleTabs;
let target = dactyl.assert(tabList[tabIndex]);
- tabIndex = Array.indexOf(browser.mTabs, target) - 1;
- }
-
- let dummy = browser.addTab("about:blank");
+ tabIndex = Array.indexOf(tabs.allTabs, target) - 1;
+ }
+
+ let newTab = 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;
+ let last = modules.tabs.allTabs.length - 1;
if (args[1])
- browser.moveTabTo(dummy, tabIndex);
- browser.selectedTab = dummy; // required
- browser.swapBrowsersAndCloseOther(dummy, config.tabbrowser.mCurrentTab);
+ browser.moveTabTo(newTab, tabIndex);
+ browser.selectedTab = newTab; // required
+ browser.swapBrowsersAndCloseOther(newTab, sourceTab);
}, {
argCount: "+",
literal: 1,
completer: function (context, args) {
switch (args.completeArg) {
case 0:
+ if (args["-group"])
+ completion.tabGroup(context);
+ else {
context.filters.push(function ({ item }) item != window);
completion.window(context);
+ }
break;
case 1:
+ if (!args["-group"]) {
let win = dactyl.windows[Number(args[0]) - 1];
if (!win || !win.dactyl)
context.message = _("Error", _("window.noIndex", winIndex));
else
win.dactyl.modules.commands.get("tabmove").completer(context);
+ }
break;
}
- }
- });
- }
+ },
+ options: [
+ {
+ names: ["-group", "-g"],
+ description: "Attach to a group rather than a window",
+ type: CommandOption.NOARG
+ }
+ ]
+ });
+ }
if (dactyl.has("tabs_undo")) {
commands.add(["u[ndo]"],
"Undo closing of a tab",
function (args) {
if (args.length)
args = args[0];
else
@@ -935,26 +1010,118 @@ var Tabs = Module("tabs", {
if (dactyl.has("session")) {
commands.add(["wqa[ll]", "wq", "xa[ll]"],
"Save the session and quit",
function () { dactyl.quit(true); },
{ argCount: "0" });
}
},
- events: function () {
+ completion: function init_completion() {
+
+ completion.buffer = function buffer(context, visible) {
+ let { tabs } = modules;
+
+ let filter = context.filter.toLowerCase();
+
+ let defItem = { parent: { getTitle: function () "" } };
+
+ let tabGroups = {};
+ tabs.getGroups();
+ tabs[visible ? "visibleTabs" : "allTabs"].forEach(function (tab, i) {
+ let group = (tab.tabItem || tab._tabViewTabItem || defItem).parent || defItem.parent;
+ if (!Set.has(tabGroups, group.id))
+ tabGroups[group.id] = [group.getTitle(), []];
+
+ group = tabGroups[group.id];
+ group[1].push([i, tab.linkedBrowser]);
+ });
+
+ context.pushProcessor(0, function (item, text, next) <>
+ <span highlight="Indicator" style="display: inline-block;">{item.indicator}</span>
+ { next.call(this, item, text) }
+ </>);
+ context.process[1] = function (item, text) template.bookmarkDescription(item, template.highlightFilter(text, this.filter));
+
+ context.anchored = false;
+ context.keys = {
+ text: "text",
+ description: "url",
+ indicator: function (item) item.tab === tabs.getTab() ? "%" :
+ item.tab === tabs.alternate ? "#" : " ",
+ icon: "icon",
+ id: "id",
+ command: function () "tabs.select"
+ };
+ context.compare = CompletionContext.Sort.number;
+ context.filters[0] = CompletionContext.Filter.textDescription;
+
+ for (let [id, vals] in Iterator(tabGroups))
+ context.fork(id, 0, this, function (context, [name, browsers]) {
+ context.title = [name || "Buffers"];
+ context.generate = function ()
+ Array.map(browsers, function ([i, browser]) {
+ let indicator = " ";
+ if (i == tabs.index())
+ indicator = "%";
+ else if (i == tabs.index(tabs.alternate))
+ indicator = "#";
+
+ let tab = tabs.getTab(i, visible);
+ let url = browser.contentDocument.location.href;
+ i = i + 1;
+
+ return {
+ text: [i + ": " + (tab.label || /*L*/"(Untitled)"), i + ": " + url],
+ tab: tab,
+ id: i,
+ url: url,
+ icon: tab.image || BookmarkCache.DEFAULT_FAVICON
+ };
+ });
+ }, vals);
+ };
+
+ completion.tabGroup = function tabGroup(context) {
+ context.title = ["Tab Groups"];
+ context.keys = {
+ text: "id",
+ description: function (group) group.getTitle() ||
+ group.getChildren().map(function (t) t.tab.label).join(", ")
+ };
+ context.generate = function () {
+ context.incomplete = true;
+ tabs.getGroups(function ({ GroupItems }) {
+ context.incomplete = false;
+ context.completions = GroupItems.groupItems;
+ });
+ };
+ };
+ },
+ events: function init_events() {
let tabContainer = config.tabbrowser.mTabContainer;
function callback() {
tabs.timeout(function () { this.updateTabCount(); });
}
for (let event in values(["TabMove", "TabOpen", "TabClose"]))
events.listen(tabContainer, event, callback, false);
events.listen(tabContainer, "TabSelect", tabs.closure._onTabSelect, false);
},
- mappings: function () {
+ mappings: function init_mappings() {
+
+ mappings.add([modes.COMMAND], ["<C-t>", "<new-tab-next>"],
+ "Execute the next mapping in a new tab",
+ function ({ count }) {
+ dactyl.forceTarget = dactyl.NEW_TAB;
+ mappings.afterCommands((count || 1) + 1, function () {
+ dactyl.forceTarget = null;
+ });
+ },
+ { count: true });
+
mappings.add([modes.NORMAL], ["g0", "g^"],
"Go to the first tab",
function () { tabs.select(0); });
mappings.add([modes.NORMAL], ["g$"],
"Go to the last tab",
function () { tabs.select("$"); });
@@ -973,17 +1140,17 @@ var Tabs = Module("tabs", {
function ({ count }) { tabs.select("+" + (count || 1), 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), true); },
{ count: true });
- if (config.hasTabbrowser) {
+ if (config.has("tabbrowser")) {
mappings.add([modes.NORMAL], ["b"],
"Open a prompt to switch buffers",
function ({ count }) {
if (count != null)
tabs.switchTo(String(count));
else
CommandExMode().open("buffer! ");
},
@@ -999,22 +1166,22 @@ var Tabs = Module("tabs", {
{ count: true });
mappings.add([modes.NORMAL], ["D"],
"Delete current buffer, focus tab to the left",
function ({ count }) { tabs.remove(tabs.getTab(), count, true); },
{ count: true });
mappings.add([modes.NORMAL], ["gb"],
- "Repeat last :buffer[!] command",
+ "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",
+ "Repeat last :buffer command in reverse direction",
function ({ count }) { tabs.switchTo(null, null, count, true); },
{ count: true });
// TODO: feature dependencies - implies "session"?
if (dactyl.has("tabs_undo")) {
mappings.add([modes.NORMAL], ["u"],
"Undo closing of a tab",
function ({ count }) { ex.undo({ "#": count }); },
@@ -1027,27 +1194,27 @@ var Tabs = Module("tabs", {
if (count != null)
tabs.switchTo(String(count), false);
else
tabs.selectAlternateTab();
},
{ count: true });
}
},
- options: function () {
+ options: function init_options() {
options.add(["showtabline", "stal"],
"Define when the tab bar is visible",
- "string", config.defaults["showtabline"],
+ "string", true,
{
setter: function (value) {
if (value === "never")
tabs.tabStyle.enabled = true;
else {
prefs.safeSet("browser.tabs.autoHide", value === "multitab",
- _("option.showtabline.safeSet"));
+ _("option.safeSet", "showtabline"));
tabs.tabStyle.enabled = false;
}
if (value !== "multitab" || !dactyl.has("Gecko2"))
if (tabs.xulTabs)
tabs.xulTabs.visible = value !== "never";
else
config.tabStrip.collapsed = false;
@@ -1058,17 +1225,17 @@ var Tabs = Module("tabs", {
},
values: {
"never": "Never show the tab bar",
"multitab": "Show the tab bar when there are multiple tabs",
"always": "Always show the tab bar"
}
});
- if (config.hasTabbrowser) {
+ if (config.has("tabbrowser")) {
let activateGroups = [
["all", "Activate everything"],
["addons", ":addo[ns] command"],
["bookmarks", "Tabs loaded from bookmarks", "loadBookmarksInBackground"],
["diverted", "Links with targets set to new tabs", "loadDivertedInBackground"],
["downloads", ":downl[oads] command"],
["extoptions", ":exto[ptions] command"],
["help", ":h[elp] command"],
@@ -1085,17 +1252,17 @@ var Tabs = Module("tabs", {
values: activateGroups,
has: Option.has.toggleAll,
setter: function (newValues) {
let valueSet = Set(newValues);
for (let group in values(activateGroups))
if (group[2])
prefs.safeSet("browser.tabs." + group[2],
!(valueSet["all"] ^ valueSet[group[0]]),
- _("option.activate.safeSet"));
+ _("option.safeSet", "activate"));
return newValues;
}
});
options.add(["newtab"],
"Define which commands should output in a new tab by default",
"stringlist", "",
{
@@ -1120,19 +1287,19 @@ var Tabs = Module("tabs", {
open = 3;
else if (opt == "window")
open = 2;
else if (opt == "resized")
restriction = 2;
}
prefs.safeSet("browser.link.open_newwindow", open,
- _("option.popups.safeSet"));
+ _("option.safeSet", "popups"));
prefs.safeSet("browser.link.open_newwindow.restriction", restriction,
- _("option.popups.safeSet"));
+ _("option.safeSet", "popups"));
return values;
},
values: {
"tab": "Open popups in a new tab",
"window": "Open popups in a new window",
"resized": "Open resized popups in a new window"
}
});
diff --git a/common/modules/addons.jsm b/common/modules/addons.jsm
--- a/common/modules/addons.jsm
+++ b/common/modules/addons.jsm
@@ -1,22 +1,21 @@
// Copyright (c) 2009-2011 by Kris Maglione <maglione.k@gmail.com>
// Copyright (c) 2009-2010 by Doug Kearns <dougkearns@gmail.com>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
try {
Components.utils.import("resource://dactyl/bootstrap.jsm");
defineModule("addons", {
exports: ["AddonManager", "Addons", "Addon", "addons"],
- require: ["services"],
- use: ["completion", "config", "io", "messages", "prefs", "template", "util"]
+ require: ["services"]
}, this);
var callResult = function callResult(method) {
let args = Array.slice(arguments, 1);
return function (result) { result[method].apply(result, args); };
}
var listener = function listener(action, event)
@@ -54,34 +53,33 @@ var updateAddons = Class("UpgradeListene
this.remaining = addons;
this.upgrade = [];
this.dactyl.echomsg(_("addon.check", addons.map(function (a) a.name).join(", ")));
for (let addon in values(addons))
addon.findUpdates(this, AddonManager.UPDATE_WHEN_USER_REQUESTED, null, null);
},
onUpdateAvailable: function (addon, install) {
- util.dump("onUpdateAvailable");
this.upgrade.push(addon);
install.addListener(this);
install.install();
},
onUpdateFinished: function (addon, error) {
this.remaining = this.remaining.filter(function (a) a.type != addon.type || a.id != addon.id);
if (!this.remaining.length)
this.dactyl.echomsg(
this.upgrade.length
? _("addon.installingUpdates", this.upgrade.map(function (i) i.name).join(", "))
: _("addon.noUpdates"));
}
});
var actions = {
delete: {
- name: "extde[lete]",
+ name: ["extde[lete]", "extrm"],
description: "Uninstall an extension",
action: callResult("uninstall"),
perm: "uninstall"
},
enable: {
name: "exte[nable]",
description: "Enable an extension",
action: function (addon) { addon.userDisabled = false; },
@@ -106,25 +104,26 @@ var actions = {
this.dactyl.open(addon.optionsURL, { from: "extoptions" });
},
filter: function ({ item }) item.isActive && item.optionsURL
},
rehash: {
name: "extr[ehash]",
description: "Reload an extension",
action: function (addon) {
- util.assert(util.haveGecko("2b"), _("command.notUseful", config.host));
+ util.assert(config.haveGecko("2b"), _("command.notUseful", config.host));
+ util.flushCache();
util.timeout(function () {
addon.userDisabled = true;
addon.userDisabled = false;
});
},
get filter() {
- let ids = Set(keys(JSON.parse(prefs.get("extensions.bootstrappedAddons", "{}"))));
- return function ({ item }) !item.userDisabled && Set.has(ids, item.id);
+ return function ({ item }) !item.userDisabled &&
+ !(item.operationsRequiringRestart & (AddonManager.OP_NEEDS_RESTART_ENABLE | AddonManager.OP_NEEDS_RESTART_DISABLE))
},
perm: "disable"
},
toggle: {
name: "extt[oggle]",
description: "Toggle an extension's enabled status",
action: function (addon) { addon.userDisabled = !addon.userDisabled; }
},
@@ -145,24 +144,24 @@ var Addon = Class("Addon", {
this.nodes = {
commandTarget: this
};
XML.ignoreWhitespace = true;
util.xmlToDom(
<tr highlight="Addon" key="row" xmlns:dactyl={NS} xmlns={XHTML}>
<td highlight="AddonName" key="name"/>
<td highlight="AddonVersion" key="version"/>
- <td highlight="AddonStatus" key="status"/>
<td highlight="AddonButtons Buttons">
<a highlight="Button" href="javascript:0" key="enable">{_("addon.action.On")}</a>
<a highlight="Button" href="javascript:0" key="disable">{_("addon.action.Off")}</a>
<a highlight="Button" href="javascript:0" key="delete">{_("addon.action.Delete")}</a>
<a highlight="Button" href="javascript:0" key="update">{_("addon.action.Update")}</a>
<a highlight="Button" href="javascript:0" key="options">{_("addon.action.Options")}</a>
</td>
+ <td highlight="AddonStatus" key="status"/>
<td highlight="AddonDescription" key="description"/>
</tr>,
this.list.document, this.nodes);
this.update();
},
commandAllowed: function commandAllowed(cmd) {
@@ -220,16 +219,17 @@ var Addon = Class("Addon", {
node.removeChild(node.firstChild);
node.appendChild(util.xmlToDom(<>{xml}</>, self.list.document));
}
update("name", template.icon({ icon: this.iconURL }, this.name));
this.nodes.version.textContent = this.version;
update("status", this.statusInfo);
this.nodes.description.textContent = this.description;
+ DOM(this.nodes.row).attr("active", this.isActive || null);
for (let node in values(this.nodes))
if (node.update && node.update !== callee)
node.update();
let event = this.list.document.createEvent("Events");
event.initEvent("dactyl-commandupdate", true, false);
this.list.document.dispatchEvent(event);
@@ -273,25 +273,25 @@ var AddonList = Class("AddonList", {
},
_init: function _init() {
this._addons.forEach(this.closure.addAddon);
this.ready = true;
this.update();
},
- message: Class.memoize(function () {
+ message: Class.Memoize(function () {
XML.ignoreWhitespace = true;
util.xmlToDom(<table highlight="Addons" key="list" xmlns={XHTML}>
<tr highlight="AddonHead">
<td>{_("title.Name")}</td>
<td>{_("title.Version")}</td>
+ <td/>
<td>{_("title.Status")}</td>
- <td/>
<td>{_("title.Description")}</td>
</tr>
</table>, this.document, this.nodes);
if (this._addons)
this._init();
return this.nodes.list;
@@ -343,17 +343,17 @@ var AddonList = Class("AddonList", {
onInstalling: function (addon) { this.update(addon); },
onUninstalled: function (addon) { this.removeAddon(addon); },
onUninstalling: function (addon) { this.update(addon); },
onOperationCancelled: function (addon) { this.update(addon); },
onPropertyChanged: function onPropertyChanged(addon, properties) {}
});
var Addons = Module("addons", {
- errors: Class.memoize(function ()
+ errors: Class.Memoize(function ()
array(["ERROR_NETWORK_FAILURE", "ERROR_INCORRECT_HASH",
"ERROR_CORRUPT_FILE", "ERROR_FILE_ACCESS"])
.map(function (e) [AddonManager[e], _("AddonManager." + e)])
.toObject())
}, {
}, {
commands: function (dactyl, modules, window) {
const { CommandOption, commands, completion } = modules;
@@ -420,32 +420,31 @@ var Addons = Module("addons", {
let name = args[0];
if (args.bang && !command.bang)
dactyl.assert(!name, _("error.trailingCharacters"));
else
dactyl.assert(name, _("error.argumentRequired"));
AddonManager.getAddonsByTypes(args["-types"], dactyl.wrapCallback(function (list) {
if (!args.bang || command.bang) {
- list = list.filter(function (extension) extension.name == name);
- if (list.length == 0)
- return void dactyl.echoerr(_("error.invalidArgument", name));
- if (!list.every(ok))
- return void dactyl.echoerr(_("error.invalidOperation"));
+ list = list.filter(function (addon) addon.id == name || addon.name == name);
+ dactyl.assert(list.length, _("error.invalidArgument", name));
+ dactyl.assert(list.some(ok), _("error.invalidOperation"));
+ list = list.filter(ok);
}
if (command.actions)
command.actions(list, this.modules);
else
list.forEach(function (addon) command.action.call(this.modules, addon, args.bang), this);
}));
}, {
argCount: "?", // FIXME: should be "1"
bang: true,
completer: function (context, args) {
- completion.extension(context, args["-types"]);
+ completion.addon(context, args["-types"]);
context.filters.push(function ({ item }) ok(item));
if (command.filter)
context.filters.push(command.filter);
},
literal: 0,
options: [
{
names: ["-types", "-type", "-t"],
@@ -473,20 +472,24 @@ var Addons = Module("addons", {
context.incomplete = false;
update(array.uniq(base.concat(addons.map(function (a) a.type)),
true));
});
}
};
};
- completion.extension = function extension(context, types) {
- context.title = ["Extension"];
+ completion.addon = function addon(context, types) {
+ context.title = ["Add-on"];
context.anchored = false;
- context.keys = { text: "name", description: "description", icon: "iconURL" },
+ context.keys = {
+ text: function (addon) [addon.name, addon.id],
+ description: "description",
+ icon: "iconURL"
+ };
context.generate = function () {
context.incomplete = true;
AddonManager.getAddonsByTypes(types || ["extension"], function (addons) {
context.incomplete = false;
context.completions = addons;
});
};
};
@@ -534,17 +537,17 @@ else
if (target && target instanceof Ci.nsIRDFLiteral)
return target.Value;
}
return "";
},
- installLocation: Class.memoize(function () services.extensionManager.getInstallLocation(this.id)),
+ installLocation: Class.Memoize(function () services.extensionManager.getInstallLocation(this.id)),
getResourceURI: function getResourceURI(path) {
let file = this.installLocation.getItemFile(this.id, path);
return services.io.newFileURI(file);
},
get isActive() this.getProperty("isDisabled") != "true",
uninstall: function uninstall() {
diff --git a/common/modules/base.jsm b/common/modules/base.jsm
--- a/common/modules/base.jsm
+++ b/common/modules/base.jsm
@@ -1,28 +1,27 @@
// Copyright (c) 2009-2011 by Kris Maglione <maglione.k@gmail.com>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
-
-var Cc = Components.classes;
-var Ci = Components.interfaces;
-var Cr = Components.results;
-var Cu = Components.utils;
-
+/* use strict */
+
+var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://dactyl/bootstrap.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
try {
var ctypes;
- Components.utils.import("resource://gre/modules/ctypes.jsm");
+ Cu.import("resource://gre/modules/ctypes.jsm");
}
catch (e) {}
let objproto = Object.prototype;
-let { __lookupGetter__, __lookupSetter__, hasOwnProperty, propertyIsEnumerable } = objproto;
+let { __lookupGetter__, __lookupSetter__, __defineGetter__, __defineSetter__,
+ hasOwnProperty, propertyIsEnumerable } = objproto;
if (typeof XPCSafeJSObjectWrapper === "undefined")
this.XPCSafeJSObjectWrapper = XPCNativeWrapper;
if (!XPCNativeWrapper.unwrap)
XPCNativeWrapper.unwrap = function unwrap(obj) {
if (hasOwnProperty.call(obj, "wrappedJSObject"))
return obj.wrappedJSObject;
@@ -42,25 +41,25 @@ if (!Object.defineProperty)
if ("value" in desc)
if (desc.writable && !__lookupGetter__.call(obj, prop)
&& !__lookupSetter__.call(obj, prop))
try {
obj[prop] = value;
}
catch (e if e instanceof TypeError) {}
else {
- objproto.__defineGetter__.call(obj, prop, function () value);
+ __defineGetter__.call(obj, prop, function () value);
if (desc.writable)
- objproto.__defineSetter__.call(obj, prop, function (val) { value = val; });
- }
+ __defineSetter__.call(obj, prop, function (val) { value = val; });
+ }
if ("get" in desc)
- objproto.__defineGetter__.call(obj, prop, desc.get);
+ __defineGetter__.call(obj, prop, desc.get);
if ("set" in desc)
- objproto.__defineSetter__.call(obj, prop, desc.set);
+ __defineSetter__.call(obj, prop, desc.set);
}
catch (e) {
throw e.stack ? e : Error(e);
}
};
if (!Object.defineProperties)
Object.defineProperties = function defineProperties(obj, props) {
for (let [k, v] in Iterator(props))
@@ -118,58 +117,67 @@ if (!Object.getOwnPropertyNames)
if (!Object.getPrototypeOf)
Object.getPrototypeOf = function getPrototypeOf(obj) obj.__proto__;
if (!Object.keys)
Object.keys = function keys(obj)
Object.getOwnPropertyNames(obj).filter(function (k) propertyIsEnumerable.call(obj, k));
let getGlobalForObject = Cu.getGlobalForObject || function (obj) obj.__parent__;
+let jsmodules = {
+ lazyRequire: function lazyRequire(module, names, target) {
+ for each (let name in names)
+ memoize(target || this, name, function (name) require(module)[name]);
+ }
+};
+jsmodules.jsmodules = jsmodules;
+
let use = {};
let loaded = {};
let currentModule;
let global = this;
function defineModule(name, params, module) {
if (!module)
module = getGlobalForObject(params);
module.NAME = name;
module.EXPORTED_SYMBOLS = params.exports || [];
- defineModule.loadLog.push("defineModule " + name);
+ if (!~module.EXPORTED_SYMBOLS.indexOf("File"))
+ delete module.File;
+
+ defineModule.loadLog.push("[Begin " + name + "]");
+ defineModule.prefix += " ";
+
for (let [, mod] in Iterator(params.require || []))
- require(module, mod);
-
- for (let [, mod] in Iterator(params.use || []))
- if (loaded.hasOwnProperty(mod))
- require(module, mod, "use");
- else {
- use[mod] = use[mod] || [];
- use[mod].push(module);
- }
+ require(module, mod, null, name);
+ module.__proto__ = jsmodules;
+
+ module._lastModule = currentModule;
currentModule = module;
module.startTime = Date.now();
}
defineModule.loadLog = [];
Object.defineProperty(defineModule.loadLog, "push", {
value: function (val) {
+ val = defineModule.prefix + val;
if (false)
defineModule.dump(val + "\n");
this[this.length] = Date.now() + " " + val;
}
});
+defineModule.prefix = "";
defineModule.dump = function dump_() {
let msg = Array.map(arguments, function (msg) {
if (loaded.util && typeof msg == "object")
msg = util.objectToString(msg);
return msg;
}).join(", ");
- let name = loaded.config ? config.name : "dactyl";
dump(String.replace(msg, /\n?$/, "\n")
- .replace(/^./gm, name + ": $&"));
+ .replace(/^./gm, JSMLoader.name + ": $&"));
}
defineModule.modules = [];
defineModule.time = function time(major, minor, func, self) {
let time = Date.now();
if (typeof func !== "function")
func = self[func];
try {
@@ -179,66 +187,66 @@ defineModule.time = function time(major,
loaded.util && util.reportError(e);
}
JSMLoader.times.add(major, minor, Date.now() - time);
return res;
}
function endModule() {
- defineModule.loadLog.push("endModule " + currentModule.NAME);
-
- for (let [, mod] in Iterator(use[currentModule.NAME] || []))
- require(mod, currentModule.NAME, "use");
+ defineModule.prefix = defineModule.prefix.slice(0, -2);
+ defineModule.loadLog.push("(End " + currentModule.NAME + ")");
loaded[currentModule.NAME] = 1;
-}
-
-function require(obj, name, from) {
+ require(jsmodules, currentModule.NAME);
+ currentModule = currentModule._lastModule;
+}
+
+function require(obj, name, from, targetName) {
try {
if (arguments.length === 1)
[obj, name] = [{}, obj];
let caller = Components.stack.caller;
if (!loaded[name])
- defineModule.loadLog.push((from || "require") + ": loading " + name + " into " + (obj.NAME || caller.filename + ":" + caller.lineNumber));
+ defineModule.loadLog.push((from || "require") + ": loading " + name +
+ " into " + (targetName || obj.NAME || caller.filename + ":" + caller.lineNumber));
JSMLoader.load(name + ".jsm", obj);
+
+ if (!loaded[name] && obj != jsmodules)
+ JSMLoader.load(name + ".jsm", jsmodules);
+
return obj;
}
catch (e) {
defineModule.dump("loading " + String.quote(name + ".jsm") + "\n");
if (loaded.util)
util.reportError(e);
else
defineModule.dump(" " + (e.filename || e.fileName) + ":" + e.lineNumber + ": " + e + "\n");
}
}
defineModule("base", {
- // sed -n 's/^(const|function) ([a-zA-Z0-9_]+).*/ "\2",/p' base.jsm | sort | fmt
+ // sed -n 's/^(const|var|function) ([a-zA-Z0-9_]+).*/ "\2",/p' base.jsm | sort | fmt
exports: [
- "ErrorBase", "Cc", "Ci", "Class", "Cr", "Cu", "Module", "JSMLoader", "Object", "Runnable",
- "Set", "Struct", "StructBase", "Timer", "UTF8", "XPCOM", "XPCOMUtils", "XPCSafeJSObjectWrapper",
- "array", "bind", "call", "callable", "ctypes", "curry", "debuggerProperties", "defineModule",
- "deprecated", "endModule", "forEach", "isArray", "isGenerator", "isinstance", "isObject",
- "isString", "isSubclass", "iter", "iterAll", "iterOwnProperties", "keys", "memoize", "octal",
- "properties", "require", "set", "update", "values", "withCallerGlobal"
- ],
- use: ["config", "services", "util"]
+ "ErrorBase", "Cc", "Ci", "Class", "Cr", "Cu", "Module", "JSMLoader", "Object",
+ "Set", "Struct", "StructBase", "Timer", "UTF8", "XPCOM", "XPCOMShim", "XPCOMUtils",
+ "XPCSafeJSObjectWrapper", "array", "bind", "call", "callable", "ctypes", "curry",
+ "debuggerProperties", "defineModule", "deprecated", "endModule", "forEach", "isArray",
+ "isGenerator", "isinstance", "isObject", "isString", "isSubclass", "iter", "iterAll",
+ "iterOwnProperties", "keys", "memoize", "octal", "properties", "require", "set", "update",
+ "values", "withCallerGlobal"
+ ]
}, this);
-function Runnable(self, func, args) {
- return {
- __proto__: Runnable.prototype,
- run: function () { func.apply(self, args || []); }
- };
-}
-Runnable.prototype.QueryInterface = XPCOMUtils.generateQI([Ci.nsIRunnable]);
+this.lazyRequire("messages", ["_", "Messages"]);
+this.lazyRequire("util", ["util"]);
/**
* Returns a list of all of the top-level properties of an object, by
* way of the debugger.
*
* @param {object} obj
* @returns [jsdIProperty]
*/
@@ -326,31 +334,32 @@ deprecated.warn = function warn(func, na
"resource://dactyl" + JSMLoader.suffix + "/javascript.jsm",
"resource://dactyl" + JSMLoader.suffix + "/util.jsm"
]);
frame = frame || Components.stack.caller.caller;
let filename = util.fixURI(frame.filename || "unknown");
if (!Set.add(func.seenCaller, filename))
util.dactyl(func).warn([util.urlPath(filename), frame.lineNumber, " "].join(":")
- + require("messages")._("warn.deprecated", name, alternative));
-}
+ + _("warn.deprecated", name, alternative));
+}
/**
* Iterates over all of the top-level, iterable property names of an
* object.
*
* @param {object} obj The object to inspect.
* @returns {Generator}
*/
function keys(obj) iter(function keys() {
for (var k in obj)
if (hasOwnProperty.call(obj, k))
yield k;
}());
+
/**
* Iterates over all of the top-level, iterable property values of an
* object.
*
* @param {object} obj The object to inspect.
* @returns {Generator}
*/
function values(obj) iter(function values() {
@@ -589,50 +598,56 @@ function isGenerator(val) objproto.toStr
* using (obj instanceof String) or (typeof obj == "string").
*/
function isString(val) objproto.toString.call(val) == "[object String]";
/**
* Returns true if and only if its sole argument may be called
* as a function. This includes classes and function objects.
*/
-function callable(val) typeof val === "function";
+function callable(val) typeof val === "function" && !(val instanceof Ci.nsIDOMElement);
function call(fn) {
fn.apply(arguments[1], Array.slice(arguments, 2));
return fn;
}
/**
* Memoizes an object property value.
*
* @param {object} obj The object to add the property to.
* @param {string} key The property name.
* @param {function} getter The function which will return the initial
* value of the property.
*/
function memoize(obj, key, getter) {
if (arguments.length == 1) {
- obj = update({ __proto__: obj.__proto__ }, obj);
- for (let prop in Object.getOwnPropertyNames(obj)) {
+ let res = update(Object.create(obj), obj);
+ for each (let prop in Object.getOwnPropertyNames(obj)) {
let get = __lookupGetter__.call(obj, prop);
if (get)
- memoize(obj, prop, get);
- }
- return obj;
- }
+ memoize(res, prop, get);
+ }
+ return res;
+ }
try {
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
- get: function g_replaceProperty() (
- Class.replaceProperty(this.instance || this, key, null),
- Class.replaceProperty(this.instance || this, key, getter.call(this, key))),
+ get: function g_replaceProperty() {
+ try {
+ Class.replaceProperty(this.instance || this, key, null);
+ return Class.replaceProperty(this.instance || this, key, getter.call(this, key));
+ }
+ catch (e) {
+ util.reportError(e);
+ }
+ },
set: function s_replaceProperty(val)
Class.replaceProperty(this.instance || this, key, val)
});
}
catch (e) {
obj[key] = getter.call(obj, key);
}
@@ -675,28 +690,28 @@ var withCallerGlobal = Cu.evalInSandbox(
function update(target) {
for (let i = 1; i < arguments.length; i++) {
let src = arguments[i];
Object.getOwnPropertyNames(src || {}).forEach(function (k) {
let desc = Object.getOwnPropertyDescriptor(src, k);
if (desc.value instanceof Class.Property)
desc = desc.value.init(k, target) || desc.value;
- if (typeof desc.value === "function" && target.__proto__) {
+ try {
+ if (typeof desc.value === "function" && target.__proto__ && !(desc.value instanceof Ci.nsIDOMElement /* wtf? */)) {
let func = desc.value.wrapped || desc.value;
if (!func.superapply) {
func.__defineGetter__("super", function () Object.getPrototypeOf(target)[k]);
func.superapply = function superapply(self, args)
let (meth = Object.getPrototypeOf(target)[k])
meth && meth.apply(self, args);
func.supercall = function supercall(self)
func.superapply(self, Array.slice(arguments, 1));
}
}
- try {
Object.defineProperty(target, k, desc);
}
catch (e) {}
});
}
return target;
}
@@ -727,32 +742,36 @@ function Class() {
var args = Array.slice(arguments);
if (isString(args[0]))
var name = args.shift();
var superclass = Class;
if (callable(args[0]))
superclass = args.shift();
- if (loaded.util && util.haveGecko("6.0a1")) // Bug 657418.
+ if (loaded.config && (config.haveGecko("5.*", "6.0") || config.haveGecko("6.*"))) // Bug 657418.
var Constructor = function Constructor() {
- var self = Object.create(Constructor.prototype, {
- constructor: { value: Constructor },
- });
+ var self = Object.create(Constructor.prototype);
self.instance = self;
+
+ if ("_metaInit_" in self && self._metaInit_)
+ self._metaInit_.apply(self, arguments);
+
var res = self.init.apply(self, arguments);
return res !== undefined ? res : self;
};
else
var Constructor = eval(String.replace(<![CDATA[
(function constructor(PARAMS) {
- var self = Object.create(Constructor.prototype, {
- constructor: { value: Constructor },
- });
+ var self = Object.create(Constructor.prototype);
self.instance = self;
+
+ if ("_metaInit_" in self && self._metaInit_)
+ self._metaInit_.apply(self, arguments);
+
var res = self.init.apply(self, arguments);
return res !== undefined ? res : self;
})]]>,
"constructor", (name || superclass.className).replace(/\W/g, "_"))
.replace("PARAMS", /^function .*?\((.*?)\)/.exec(args[0] && args[0].init || Class.prototype.init)[1]
.replace(/\b(self|res|Constructor)\b/g, "$1_")));
Constructor.className = name || superclass.className || superclass.name;
@@ -764,20 +783,22 @@ function Class() {
superclass = function Shim() {};
Class.extend(superclass, superc, {
init: superc
});
superclass.__proto__ = superc;
}
Class.extend(Constructor, superclass, args[0]);
+ memoize(Constructor, "closure", Class.makeClosure);
update(Constructor, args[1]);
+
Constructor.__proto__ = superclass;
- args = args.slice(2);
- Array.forEach(args, function (obj) {
+
+ args.slice(2).forEach(function (obj) {
if (callable(obj))
obj = obj.prototype;
update(Constructor.prototype, obj);
});
return Constructor;
}
if (Cu.getGlobalForObject)
@@ -834,24 +855,25 @@ Class.extend = function extend(subclass,
/**
* Memoizes the value of a class property to the value returned by
* the passed function the first time the property is accessed.
*
* @param {function(string)} getter The function which returns the
* property's value.
* @returns {Class.Property}
*/
-Class.memoize = function memoize(getter, wait)
+Class.Memoize = function Memoize(getter, wait)
Class.Property({
configurable: true,
enumerable: true,
init: function (key) {
let done = false;
if (wait)
+ // Crazy, yeah, I know. -- Kris
this.get = function replace() {
let obj = this.instance || this;
Object.defineProperty(obj, key, {
configurable: true, enumerable: false,
get: function get() {
util.waitFor(function () done);
return this[key];
}
@@ -866,38 +888,62 @@ Class.memoize = function memoize(getter,
}
Class.replaceProperty(obj, key, res);
done = true;
})();
return this[key];
};
else
- this.get = function replace() {
+ this.get = function g_Memoize() {
let obj = this.instance || this;
+ try {
Class.replaceProperty(obj, key, null);
return Class.replaceProperty(obj, key, getter.call(this, key));
- };
-
- this.set = function replace(val) Class.replaceProperty(this.instance || this, val);
- }
- });
+ }
+ catch (e) {
+ util.reportError(e);
+ }
+ };
+
+ this.set = function s_Memoize(val) Class.replaceProperty(this.instance || this, key, val);
+ }
+ });
+
+Class.memoize = deprecated("Class.Memoize", function memoize() Class.Memoize.apply(this, arguments));
+
+/**
+ * Updates the given object with the object in the target class's
+ * prototype.
+ */
+Class.Update = function Update(obj)
+ Class.Property({
+ configurable: true,
+ enumerable: true,
+ writable: true,
+ init: function (key, target) {
+ this.value = update({}, target[key], obj);
+ }
+ });
Class.replaceProperty = function replaceProperty(obj, prop, value) {
Object.defineProperty(obj, prop, { configurable: true, enumerable: true, value: value, writable: true });
return value;
};
Class.toString = function toString() "[class " + this.className + "]";
Class.prototype = {
/**
* Initializes new instances of this class. Called automatically
* when new instances are created.
*/
init: function c_init() {},
+ get instance() ({}),
+ set instance(val) Class.replaceProperty(this, "instance", val),
+
withSavedValues: function withSavedValues(names, callback, self) {
let vals = names.map(function (name) this[name], this);
try {
return callback.call(self || this);
}
finally {
names.forEach(function (name, i) this[name] = vals[i], this);
}
@@ -921,20 +967,24 @@ Class.prototype = {
* @returns {nsITimer} The timer which backs this timeout.
*/
timeout: function timeout(callback, timeout) {
const self = this;
function timeout_notify(timer) {
if (self.stale ||
util.rehashing && !isinstance(Cu.getGlobalForObject(callback), ["BackstagePass"]))
return;
+ self.timeouts.splice(self.timeouts.indexOf(timer), 1);
util.trapErrors(callback, self);
}
- return services.Timer(timeout_notify, timeout || 0, services.Timer.TYPE_ONE_SHOT);
- },
+ let timer = services.Timer(timeout_notify, timeout || 0, services.Timer.TYPE_ONE_SHOT);
+ this.timeouts.push(timer);
+ return timer;
+ },
+ timeouts: [],
/**
* Updates this instance with the properties of the given objects.
* Like the update function, but with special semantics for
* localized properties.
*/
update: function update() {
let self = this;
@@ -963,20 +1013,28 @@ Class.prototype = {
if ("value" in desc && (k in this.localizedProperties || k in this.magicalProperties))
this[k] = desc.value;
else
Object.defineProperty(this, k, desc);
}
catch (e) {}
}, this);
}
- },
-
+ return this;
+ },
+
+ localizedProperties: {},
magicalProperties: {}
};
+for (let name in properties(Class.prototype)) {
+ let desc = Object.getOwnPropertyDescriptor(Class.prototype, name);
+ desc.enumerable = false;
+ Object.defineProperty(Class.prototype, name, desc);
+}
+
Class.makeClosure = function makeClosure() {
const self = this;
function closure(fn) {
function _closure() {
try {
return fn.apply(self, arguments);
}
catch (e if !(e instanceof FailedAssertion)) {
@@ -1010,26 +1068,45 @@ memoize(Class.prototype, "closure", Clas
* @param {nsIIID|[nsIJSIID]} interfaces The interfaces which the class
* implements.
* @param {Class} superClass A super class. @optional
* @returns {Class}
*/
function XPCOM(interfaces, superClass) {
interfaces = Array.concat(interfaces);
- let shim = interfaces.reduce(function (shim, iface) shim.QueryInterface(iface),
- Cc["@dactyl.googlecode.com/base/xpc-interface-shim"].createInstance());
-
- let res = Class("XPCOM(" + interfaces + ")", superClass || Class, update(
- iter.toObject([k, v === undefined || callable(v) ? function stub() null : v]
- for ([k, v] in Iterator(shim))),
+ let shim = XPCOMShim(interfaces);
+
+ let res = Class("XPCOM(" + interfaces + ")", superClass || Class,
+ update(iter([k,
+ v === undefined || callable(v) ? stub : v]
+ for ([k, v] in Iterator(shim))).toObject(),
{ QueryInterface: XPCOMUtils.generateQI(interfaces) }));
- shim = interfaces = null;
+
return res;
}
+function XPCOMShim(interfaces) {
+ let ip = services.InterfacePointer({
+ QueryInterface: function (iid) {
+ if (iid.equals(Ci.nsISecurityCheckedComponent))
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ return this;
+ },
+ getHelperForLanguage: function () null,
+ getInterfaces: function (count) { count.value = 0; }
+ });
+ return (interfaces || []).reduce(function (shim, iface) shim.QueryInterface(Ci[iface]),
+ ip.data)
+};
+let stub = Class.Property({
+ configurable: true,
+ enumerable: false,
+ value: function stub() null,
+ writable: true
+});
/**
* An abstract base class for classes that wish to inherit from Error.
*/
var ErrorBase = Class("ErrorBase", Error, {
level: 2,
init: function EB_init(message, level) {
level = level || 0;
@@ -1054,28 +1131,43 @@ var ErrorBase = Class("ErrorBase", Error
* module global object.
*
* @param {string} name The name of the instance.
* @param {Object} prototype The instance prototype.
* @param {Object} classProperties Properties to be applied to the class constructor.
* @returns {Class}
*/
function Module(name, prototype) {
+ try {
let init = callable(prototype) ? 4 : 3;
+ let proto = arguments[callable(prototype) ? 2 : 1];
+
+ proto._metaInit_ = function () {
+ delete module.prototype._metaInit_;
+ currentModule[name.toLowerCase()] = this;
+ };
+
const module = Class.apply(Class, Array.slice(arguments, 0, init));
let instance = module();
module.className = name.toLowerCase();
instance.INIT = update(Object.create(Module.INIT),
arguments[init] || {});
currentModule[module.className] = instance;
defineModule.modules.push(instance);
return module;
}
+ catch (e) {
+ if (typeof e === "string")
+ e = Error(e);
+
+ dump(e.fileName + ":" + e.lineNumber + ": " + e + "\n" + (e.stack || Error().stack));
+ }
+}
Module.INIT = {
init: function Module_INIT_init(dactyl, modules, window) {
let args = arguments;
let locals = [];
for (let local = this.Local; local; local = local.super)
locals.push(local);
@@ -1128,23 +1220,25 @@ function Struct() {
members: array.toObject(args.map(function (v, k) [v, k]))
});
args.forEach(function (name, i) {
Struct.prototype.__defineGetter__(name, function () this[i]);
Struct.prototype.__defineSetter__(name, function (val) { this[i] = val; });
});
return Struct;
}
-let StructBase = Class("StructBase", Array, {
+var StructBase = Class("StructBase", Array, {
init: function struct_init() {
for (let i = 0; i < arguments.length; i++)
if (arguments[i] != undefined)
this[i] = arguments[i];
},
+ get toStringParams() this,
+
clone: function struct_clone() this.constructor.apply(null, this.slice()),
closure: Class.Property(Object.getOwnPropertyDescriptor(Class.prototype, "closure")),
get: function struct_get(key, val) this[this.members[key]],
set: function struct_set(key, val) this[this.members[key]] = val,
toString: function struct_toString() Class.prototype.toString.apply(this, arguments),
@@ -1176,17 +1270,17 @@ let StructBase = Class("StructBase", Arr
this.prototype.__defineGetter__(i, function () (this[i] = val.call(this)));
this.prototype.__defineSetter__(i, function (value)
Class.replaceProperty(this, i, value));
return this;
},
localize: function localize(key, defaultValue) {
let i = this.prototype.members[key];
- Object.defineProperty(this.prototype, i, require("messages").Messages.Localized(defaultValue).init(key, this.prototype));
+ Object.defineProperty(this.prototype, i, Messages.Localized(defaultValue).init(key, this.prototype));
return this;
}
});
var Timer = Class("Timer", {
init: function init(minInterval, maxInterval, callback, self) {
this._timer = services.Timer();
this.callback = callback;
@@ -1206,17 +1300,17 @@ var Timer = Class("Timer", {
this.latest = 0;
// minInterval is the time between the completion of the command and the next firing
this.doneAt = Date.now() + this.minInterval;
this.callback.call(this.self, this.arg);
}
catch (e) {
if (typeof util === "undefined")
- dump("dactyl: " + e + "\n" + (e.stack || Error().stack));
+ dump(JSMLoader.name + ": " + e + "\n" + (e.stack || Error().stack));
else
util.reportError(e);
}
finally {
this.doneAt = Date.now() + this.minInterval;
}
},
@@ -1292,19 +1386,23 @@ function octal(decimal) parseInt(decimal
* property is a function, it is called first. If it contains the
* property "getNext" along with either "hasMoreItems" or "hasMore", it
* is iterated over appropriately.
*
* For all other cases, this function behaves exactly like the Iterator
* function.
*
* @param {object} obj
+ * @param {nsIJSIID} iface The interface to which to query all elements.
* @returns {Generator}
*/
-function iter(obj) {
+function iter(obj, iface) {
+ if (arguments.length == 2 && iface instanceof Ci.nsIJSIID)
+ return iter(obj).map(function (item) item.QueryInterface(iface));
+
let args = arguments;
let res = Iterator(obj);
if (args.length > 1)
res = (function () {
for (let i = 0; i < args.length; i++)
for (let j in iter(args[i]))
yield j;
diff --git a/common/modules/bookmarkcache.jsm b/common/modules/bookmarkcache.jsm
--- a/common/modules/bookmarkcache.jsm
+++ b/common/modules/bookmarkcache.jsm
@@ -1,47 +1,58 @@
// Copyright ©2008-2010 Kris Maglione <maglione.k at Gmail>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
Components.utils.import("resource://dactyl/bootstrap.jsm");
defineModule("bookmarkcache", {
exports: ["Bookmark", "BookmarkCache", "Keyword", "bookmarkcache"],
- require: ["services", "storage", "util"]
+ require: ["services", "util"]
}, this);
+this.lazyRequire("storage", ["storage"]);
+
+function newURI(url, charset, base) {
+ try {
+ return services.io.newURI(url, charset, base);
+ }
+ catch (e) {
+ throw Error(e);
+ }
+}
+
var Bookmark = Struct("url", "title", "icon", "post", "keyword", "tags", "charset", "id");
var Keyword = Struct("keyword", "title", "icon", "url");
Bookmark.defaultValue("icon", function () BookmarkCache.getFavicon(this.url));
update(Bookmark.prototype, {
get extra() [
["keyword", this.keyword, "Keyword"],
["tags", this.tags.join(", "), "Tag"]
].filter(function (item) item[1]),
- get uri() util.newURI(this.url),
+ get uri() newURI(this.url),
+ set uri(uri) {
+ let tags = this.tags;
+ this.tags = null;
+ services.bookmarks.changeBookmarkURI(this.id, uri);
+ this.tags = tags;
+ },
encodeURIComponent: function _encodeURIComponent(str) {
if (!this.charset || this.charset === "UTF-8")
return encodeURIComponent(str);
let conv = services.CharsetConv(this.charset);
return escape(conv.ConvertFromUnicode(str) + conv.Finish());
}
})
+Bookmark.prototype.members.uri = Bookmark.prototype.members.url;
Bookmark.setter = function (key, func) this.prototype.__defineSetter__(key, func);
-Bookmark.setter("url", function (val) {
- if (isString(val))
- val = util.newURI(val);
- let tags = this.tags;
- this.tags = null;
- services.bookmarks.changeBookmarkURI(this.id, val);
- this.tags = tags;
-});
+Bookmark.setter("url", function (val) { this.uri = isString(val) ? newURI(val) : val; });
Bookmark.setter("title", function (val) { services.bookmarks.setItemTitle(this.id, val); });
Bookmark.setter("post", function (val) { bookmarkcache.annotate(this.id, bookmarkcache.POST, val); });
Bookmark.setter("charset", function (val) { bookmarkcache.annotate(this.id, bookmarkcache.CHARSET, val); });
Bookmark.setter("keyword", function (val) { services.bookmarks.setKeywordForBookmark(this.id, val); });
Bookmark.setter("tags", function (val) {
services.tagging.untagURI(this.uri, null);
if (val)
services.tagging.tagURI(this.uri, val);
@@ -58,50 +69,54 @@ var BookmarkCache = Module("BookmarkCach
},
cleanup: function cleanup() {
services.bookmarks.removeObserver(this);
},
__iterator__: function () (val for ([, val] in Iterator(bookmarkcache.bookmarks))),
- get bookmarks() Class.replaceProperty(this, "bookmarks", this.load()),
-
- keywords: Class.memoize(function () array.toObject([[b.keyword, b] for (b in this) if (b.keyword)])),
+ bookmarks: Class.Memoize(function () this.load()),
+
+ keywords: Class.Memoize(function () array.toObject([[b.keyword, b] for (b in this) if (b.keyword)])),
rootFolders: ["toolbarFolder", "bookmarksMenuFolder", "unfiledBookmarksFolder"]
.map(function (s) services.bookmarks[s]),
_deleteBookmark: function deleteBookmark(id) {
let result = this.bookmarks[id] || null;
delete this.bookmarks[id];
return result;
},
_loadBookmark: function loadBookmark(node) {
if (node.uri == null) // How does this happen?
return false;
- let uri = util.newURI(node.uri);
+
+ let uri = newURI(node.uri);
let keyword = services.bookmarks.getKeywordForBookmark(node.itemId);
- let tags = services.tagging.getTagsForURI(uri, {}) || [];
+
+ let tags = tags in node ? (node.tags ? node.tags.split(/, /g) : [])
+ : services.tagging.getTagsForURI(uri, {}) || [];
+
let post = BookmarkCache.getAnnotation(node.itemId, this.POST);
let charset = BookmarkCache.getAnnotation(node.itemId, this.CHARSET);
return Bookmark(node.uri, node.title, node.icon && node.icon.spec, post, keyword, tags, charset, node.itemId);
},
annotate: function (id, key, val, timespan) {
if (val)
services.annotation.setItemAnnotation(id, key, val, 0,
timespan || services.annotation.EXPIRE_NEVER);
else if (services.annotation.itemHasAnnotation(id, key))
services.annotation.removeItemAnnotation(id, key);
},
get: function (url) {
- let ids = services.bookmarks.getBookmarkIdsForURI(util.newURI(url), {});
+ let ids = services.bookmarks.getBookmarkIdsForURI(newURI(url), {});
for (let id in values(ids))
if (id in this.bookmarks)
return this.bookmarks[id];
return null;
},
readBookmark: function readBookmark(id) ({
itemId: id,
@@ -124,17 +139,17 @@ var BookmarkCache = Module("BookmarkCach
* not a Live Bookmark.
*
* @param {nsIURI|string} url The URL of which to check the bookmarked
* state.
* @returns {boolean}
*/
isBookmarked: function isBookmarked(uri) {
if (isString(uri))
- uri = util.newURI(uri);
+ uri = newURI(uri);
try {
return services.bookmarks
.getBookmarkIdsForURI(uri, {})
.some(this.closure.isRegularBookmark);
}
catch (e) {
return false;
@@ -149,38 +164,34 @@ var BookmarkCache = Module("BookmarkCach
id = services.bookmarks.getFolderIdForItem(id);
} while (id != services.bookmarks.placesRoot && id != root);
return this.rootFolders.indexOf(root) >= 0;
},
load: function load() {
let bookmarks = {};
- let folders = this.rootFolders.slice();
let query = services.history.getNewQuery();
let options = services.history.getNewQueryOptions();
- while (folders.length > 0) {
- query.setFolders(folders, 1);
- folders.shift();
- let result = services.history.executeQuery(query, options);
- let folder = result.root;
- folder.containerOpen = true;
-
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ options.excludeItemIfParentHasAnnotation = "livemark/feedURI";
+
+ let { root } = services.history.executeQuery(query, options);
+ root.containerOpen = true;
+ try {
// 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
+ for (let i = 0; i < root.childCount; i++) {
+ let node = root.getChild(i);
+ if (node.type == node.RESULT_TYPE_URI) // bookmark
bookmarks[node.itemId] = this._loadBookmark(node);
}
-
- // close a container after using it!
- folder.containerOpen = false;
- }
+ }
+ finally {
+ root.containerOpen = false;
+ }
return bookmarks;
},
onItemAdded: function onItemAdded(itemId, folder, index) {
if (services.bookmarks.getItemType(itemId) == services.bookmarks.TYPE_BOOKMARK) {
if (this.isBookmark(itemId)) {
let bmark = this._loadBookmark(this.readBookmark(itemId));
@@ -213,22 +224,25 @@ var BookmarkCache = Module("BookmarkCach
value = services.tagging.getTagsForURI(bookmark.uri, {});
if (property in bookmark) {
bookmark[bookmark.members[property]] = value;
storage.fireEvent(name, "change", { __proto__: bookmark, changed: property });
}
}
}
}, {
+ DEFAULT_FAVICON: "chrome://mozapps/skin/places/defaultFavicon.png",
+
getAnnotation: function getAnnotation(item, anno)
services.annotation.itemHasAnnotation(item, anno) ?
services.annotation.getItemAnnotation(item, anno) : null,
+
getFavicon: function getFavicon(uri) {
try {
- return services.favicon.getFaviconImageForPage(util.newURI(uri)).spec;
+ return services.favicon.getFaviconImageForPage(newURI(uri)).spec;
}
catch (e) {
return "";
}
}
});
endModule();
diff --git a/common/modules/bootstrap.jsm b/common/modules/bootstrap.jsm
--- a/common/modules/bootstrap.jsm
+++ b/common/modules/bootstrap.jsm
@@ -1,52 +1,49 @@
// Copyright (c) 2011 by Kris Maglione <maglione.k@gmail.com>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
try {
let { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
var EXPORTED_SYMBOLS = ["JSMLoader"];
var BOOTSTRAP_CONTRACT = "@dactyl.googlecode.com/base/bootstrap";
var JSMLoader = BOOTSTRAP_CONTRACT in Components.classes &&
Components.classes[BOOTSTRAP_CONTRACT].getService().wrappedJSObject.loader;
-if (!JSMLoader && "@mozilla.org/fuel/application;1" in Components.classes)
- JSMLoader = Components.classes["@mozilla.org/fuel/application;1"]
- .getService(Components.interfaces.extIApplication)
- .storage.get("dactyl.JSMLoader", null);
-
-if (JSMLoader && JSMLoader.bump === 4)
+if (JSMLoader && JSMLoader.bump === 6)
JSMLoader.global = this;
else
JSMLoader = {
- bump: 4,
+ bump: 6,
builtin: Cu.Sandbox(this),
canonical: {},
factories: [],
+ name: "dactyl",
+
global: this,
globals: JSMLoader ? JSMLoader.globals : {},
io: Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService),
loader: Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader),
manager: Components.manager.QueryInterface(Ci.nsIComponentRegistrar),
- modules: JSMLoader ? JSMLoader.modules : {},
+ modules: JSMLoader && JSMLoader.modules || {},
stale: JSMLoader ? JSMLoader.stale : {},
suffix: "",
times: {
all: 0,
add: function add(major, minor, delta) {
@@ -128,16 +125,18 @@ else
cleanup: function unregister() {
for each (let factory in this.factories.splice(0))
this.manager.unregisterFactory(factory.classID, factory);
},
purge: function purge() {
dump("dactyl: JSMLoader: purge\n");
+ this.bootstrap = null;
+
if (Cu.unload) {
Object.keys(this.modules).reverse().forEach(function (url) {
try {
Cu.unload(url);
}
catch (e) {
Cu.reportError(e);
}
@@ -162,16 +161,34 @@ else
catch (e) {
dump("Deleting property " + prop + " on " + url + ":\n " + e + "\n");
Cu.reportError(e);
}
}
}
},
+ Factory: function Factory(clas) ({
+ __proto__: clas.prototype,
+
+ createInstance: function (outer, iid) {
+ try {
+ if (outer != null)
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ if (!clas.instance)
+ clas.instance = new clas();
+ return clas.instance.QueryInterface(iid);
+ }
+ catch (e) {
+ Cu.reportError(e);
+ throw e;
+ }
+ }
+ }),
+
registerFactory: function registerFactory(factory) {
this.manager.registerFactory(factory.classID,
String(factory.classID),
factory.contractID,
factory);
this.factories.push(factory);
}
};
diff --git a/common/content/buffer.js b/common/modules/buffer.jsm
rename from common/content/buffer.js
rename to common/modules/buffer.jsm
--- a/common/content/buffer.js
+++ b/common/modules/buffer.jsm
@@ -1,401 +1,258 @@
// Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
// Copyright (c) 2008-2011 by Kris Maglione <maglione.k at Gmail>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
-
-/** @scope modules */
+try {"use strict";
+
+Components.utils.import("resource://dactyl/bootstrap.jsm");
+defineModule("buffer", {
+ exports: ["Buffer", "buffer"],
+ require: ["prefs", "services", "util"]
+}, this);
+
+this.lazyRequire("finder", ["RangeFind"]);
+this.lazyRequire("overlay", ["overlay"]);
+this.lazyRequire("storage", ["storage"]);
+this.lazyRequire("template", ["template"]);
/**
* A class to manage the primary web content buffer. The name comes
* from Vim's term, 'buffer', which signifies instances of open
* files.
* @instance buffer
*/
-var Buffer = Module("buffer", {
- init: function init() {
- this.evaluateXPath = util.evaluateXPath;
- this.pageInfo = {};
-
- this.addPageInfoSection("e", "Search Engines", function (verbose) {
-
- let n = 1;
- let nEngines = 0;
- for (let { document: doc } in values(buffer.allFrames())) {
- let engines = util.evaluateXPath(["link[@href and @rel='search' and @type='application/opensearchdescription+xml']"], doc);
- nEngines += engines.snapshotLength;
-
- if (verbose)
- for (let link in engines)
- yield [link.title || /*L*/ "Engine " + n++,
- <a xmlns={XHTML} href={link.href} onclick="if (event.button == 0) { window.external.AddSearchProvider(this.href); return false; }" highlight="URL">{link.href}</a>];
- }
-
- if (!verbose && nEngines)
- yield nEngines + /*L*/" engine" + (nEngines > 1 ? "s" : "");
- });
-
- this.addPageInfoSection("f", "Feeds", function (verbose) {
- 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 [i, win] in Iterator(buffer.allFrames())) {
- let doc = win.document;
-
- 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 + /*L*/" feed" + (nFeed > 1 ? "s" : "");
- });
-
- this.addPageInfoSection("g", "General Info", function (verbose) {
- let doc = buffer.focusedFrame.document;
-
- // get file size
- const ACCESS_READ = Ci.nsICache.ACCESS_READ;
- let cacheKey = doc.documentURI;
-
- for (let proto in array.iterValues(["HTTP", "FTP"])) {
- try {
- var cacheEntryDescriptor = services.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]) + /*L*/" bytes";
- yield lastMod;
- return;
- }
-
- yield ["Title", doc.title];
- yield ["URL", template.highlightURL(doc.location.href, 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) {
- if (!verbose)
- return [];
-
- // get meta tag data, sort and put into pageMeta[]
- let metaNodes = buffer.focusedFrame.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]));
- });
-
- let identity = window.gIdentityHandler;
- this.addPageInfoSection("s", "Security", function (verbose) {
- if (!verbose || !identity)
- return; // For now
-
- // Modified from Firefox
- function location(data) array.compact([
- data.city, data.state, data.country
- ]).join(", ");
-
- switch (statusline.security) {
- case "secure":
- case "extended":
- var data = identity.getIdentityData();
-
- yield ["Host", identity.getEffectiveHost()];
-
- if (statusline.security === "extended")
- yield ["Owner", data.subjectOrg];
- else
- yield ["Owner", _("pageinfo.s.ownerUnverified", data.subjectOrg)];
-
- if (location(data).length)
- yield ["Location", location(data)];
-
- yield ["Verified by", data.caOrg];
-
- if (identity._overrideService.hasMatchingOverride(identity._lastLocation.hostname,
- (identity._lastLocation.port || 443),
- data.cert, {}, {}))
- yield ["User exception", /*L*/"true"];
- break;
- }
- });
-
- dactyl.commands["buffer.viewSource"] = function (event) {
- let elem = event.originalTarget;
- let obj = { url: elem.getAttribute("href"), line: Number(elem.getAttribute("line")) };
- if (elem.hasAttribute("column"))
- obj.column = elem.getAttribute("column");
-
- buffer.viewSource(obj);
- };
- },
+var Buffer = Module("Buffer", {
+ Local: function Local(dactyl, modules, window) ({
+ get win() {
+ return window.content;
+
+ let win = services.focus.focusedWindow;
+ if (!win || win == window || util.topWindow(win) != window)
+ return window.content
+ if (win.top == window)
+ return win;
+ return win.top;
+ }
+ }),
+
+ init: function init(win) {
+ if (win)
+ this.win = win;
+ },
+
+ get addPageInfoSection() Buffer.closure.addPageInfoSection,
+
+ get pageInfo() Buffer.pageInfo,
// called when the active document is scrolled
_updateBufferPosition: function _updateBufferPosition() {
- statusline.updateBufferPosition();
- commandline.clear(true);
- },
+ this.modules.statusline.updateBufferPosition();
+ this.modules.commandline.clear(true);
+ },
/**
* @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(this.focusedFrame);
+ let stylesheets = array.flatten(
+ this.allFrames().map(function (w) Array.slice(w.document.styleSheets)));
return stylesheets.filter(
function (stylesheet) /^(screen|all|)$/i.test(stylesheet.media.mediaText) && !/^\s*$/.test(stylesheet.title)
);
},
climbUrlPath: function climbUrlPath(count) {
- let url = buffer.documentURI.clone();
+ let { dactyl } = this.modules;
+
+ let url = this.documentURI.clone();
dactyl.assert(url instanceof Ci.nsIURL);
while (count-- && url.path != "/")
url.path = url.path.replace(/[^\/]+\/*$/, "");
- dactyl.assert(!url.equals(buffer.documentURI));
+ dactyl.assert(!url.equals(this.documentURI));
dactyl.open(url.spec);
},
incrementURL: function incrementURL(count) {
- let matches = buffer.uri.spec.match(/(.*?)(\d+)(\D*)$/);
+ let { dactyl } = this.modules;
+
+ let matches = this.uri.spec.match(/(.*?)(\d+)(\D*)$/);
dactyl.assert(matches);
let oldNum = matches[2];
// disallow negative numbers as trailing numbers are often proceeded by hyphens
let newNum = String(Math.max(parseInt(oldNum, 10) + count, 0));
if (/^0/.test(oldNum))
while (newNum.length < oldNum.length)
newNum = "0" + newNum;
matches[2] = newNum;
dactyl.open(matches.slice(1).join(""));
},
/**
- * @property {Object} A map of page info sections to their
- * content generating functions.
- */
- pageInfo: null,
-
- /**
* @property {number} True when the buffer is fully loaded.
*/
get loaded() Math.min.apply(null,
this.allFrames()
.map(function (frame) ["loading", "interactive", "complete"]
.indexOf(frame.document.readyState))),
/**
* @property {Object} The local state store for the currently selected
* tab.
*/
get localStore() {
- if (!content.document.dactylStore)
- content.document.dactylStore = {};
- return content.document.dactylStore;
- },
+ let { doc } = this;
+
+ let store = overlay.getData(doc, "buffer", null);
+ if (!store || !this.localStorePrototype.isPrototypeOf(store))
+ store = overlay.setData(doc, "buffer", Object.create(this.localStorePrototype));
+ return store.instance = store;
+ },
+
+ localStorePrototype: memoize({
+ instance: {},
+ get jumps() [],
+ jumpsIndex: -1
+ }),
/**
* @property {Node} The last focused input field in the buffer. Used
* by the "gi" key binding.
*/
get lastInputField() {
let field = this.localStore.lastInputField && this.localStore.lastInputField.get();
+
let doc = field && field.ownerDocument;
let win = doc && doc.defaultView;
return win && doc === win.document ? field : null;
},
- set lastInputField(value) { this.localStore.lastInputField = value && Cu.getWeakReference(value); },
+ set lastInputField(value) { this.localStore.lastInputField = util.weakReference(value); },
/**
* @property {nsIURI} The current top-level document.
*/
- get doc() window.content.document,
+ get doc() this.win.document,
+
+ get docShell() util.docShell(this.win),
+
+ get modules() this.topWindow.dactyl.modules,
+ set modules(val) {},
+
+ topWindow: Class.Memoize(function () util.topWindow(this.win)),
/**
* @property {nsIURI} The current top-level document's URI.
*/
- get uri() util.newURI(content.location.href),
+ get uri() util.newURI(this.win.location.href),
/**
* @property {nsIURI} The current top-level document's URI, sans any
* fragment identifier.
*/
- get documentURI() let (doc = content.document) doc.documentURIObject || util.newURI(doc.documentURI),
+ get documentURI() this.doc.documentURIObject || util.newURI(this.doc.documentURI),
/**
* @property {string} The current top-level document's URL.
*/
- get URL() update(new String(content.location.href), util.newURI(content.location.href)),
+ get URL() update(new String(this.win.location.href), util.newURI(this.win.location.href)),
/**
* @property {number} The buffer's height in pixels.
*/
- get pageHeight() content.innerHeight,
+ get pageHeight() this.win.innerHeight,
+
+ get contentViewer() this.docShell.contentViewer
+ .QueryInterface(Components.interfaces.nsIMarkupDocumentViewer),
/**
* @property {number} The current browser's zoom level, as a
* percentage with 100 as 'normal'.
*/
- get zoomLevel() config.browser.markupDocumentViewer[this.fullZoom ? "fullZoom" : "textZoom"] * 100,
+ get zoomLevel() {
+ let v = this.contentViewer;
+ return v[v.textZoom == 1 ? "fullZoom" : "textZoom"] * 100
+ },
set zoomLevel(value) { this.setZoom(value, this.fullZoom); },
/**
* @property {boolean} Whether the current browser is using full
* zoom, as opposed to text zoom.
*/
- get fullZoom() ZoomManager.useFullZoom,
+ get fullZoom() this.ZoomManager.useFullZoom,
set fullZoom(value) { this.setZoom(this.zoomLevel, value); },
+ get ZoomManager() this.topWindow.ZoomManager,
+
/**
* @property {string} The current document's title.
*/
- get title() content.document.title,
+ get title() this.doc.title,
/**
* @property {number} The buffer's horizontal scroll percentile.
*/
get scrollXPercent() {
- let elem = this.findScrollable(0, true);
+ let elem = Buffer.Scrollable(this.findScrollable(0, true));
if (elem.scrollWidth - elem.clientWidth === 0)
return 0;
return elem.scrollLeft * 100 / (elem.scrollWidth - elem.clientWidth);
},
/**
* @property {number} The buffer's vertical scroll percentile.
*/
get scrollYPercent() {
- let elem = this.findScrollable(0, false);
+ let elem = Buffer.Scrollable(this.findScrollable(0, false));
if (elem.scrollHeight - elem.clientHeight === 0)
return 0;
return elem.scrollTop * 100 / (elem.scrollHeight - elem.clientHeight);
},
/**
- * 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] = Buffer.PageInfo(option, title, func);
- },
+ * @property {{ x: number, y: number }} The buffer's current scroll position
+ * as reported by {@link Buffer.getScrollPosition}.
+ */
+ get scrollPosition() Buffer.getScrollPosition(this.findScrollable(0, false)),
/**
* Returns a list of all frames in the given window or current buffer.
*/
allFrames: function allFrames(win, focusedFirst) {
let frames = [];
(function rec(frame) {
- if (true || frame.document.body instanceof HTMLBodyElement)
+ if (true || frame.document.body instanceof Ci.nsIDOMHTMLBodyElement)
frames.push(frame);
Array.forEach(frame.frames, rec);
- })(win || content);
+ })(win || this.win);
+
if (focusedFirst)
- return frames.filter(function (f) f === buffer.focusedFrame).concat(
- frames.filter(function (f) f !== buffer.focusedFrame));
+ return frames.filter(function (f) f === this.focusedFrame).concat(
+ frames.filter(function (f) f !== this.focusedFrame));
return frames;
},
/**
* @property {Window} Returns the currently focused frame.
*/
get focusedFrame() {
let frame = this.localStore.focusedFrame;
- return frame && frame.get() || content;
+ return frame && frame.get() || this.win;
},
set focusedFrame(frame) {
- this.localStore.focusedFrame = Cu.getWeakReference(frame);
- },
+ this.localStore.focusedFrame = util.weakReference(frame);
+ },
/**
* Returns the currently selected word. If the selection is
* null, it tries to guess the word that the caret is
* positioned in.
*
* @returns {string}
*/
@@ -405,59 +262,76 @@ var Buffer = Module("buffer", {
/**
* Returns true if a scripts are allowed to focus the given input
* element or input elements in the given window.
*
* @param {Node|Window}
* @returns {boolean}
*/
focusAllowed: function focusAllowed(elem) {
- if (elem instanceof Window && !Editor.getEditor(elem))
+ if (elem instanceof Ci.nsIDOMWindow && !DOM(elem).isEditable)
return true;
+ let { options } = this.modules;
+
let doc = elem.ownerDocument || elem.document || elem;
switch (options.get("strictfocus").getKey(doc.documentURIObject || util.newURI(doc.documentURI), "moderate")) {
case "despotic":
- return elem.dactylFocusAllowed || elem.frameElement && elem.frameElement.dactylFocusAllowed;
+ return overlay.getData(elem)["focus-allowed"]
+ || elem.frameElement && overlay.getData(elem.frameElement)["focus-allowed"];
case "moderate":
- return doc.dactylFocusAllowed || elem.frameElement && elem.frameElement.ownerDocument.dactylFocusAllowed;
+ return overlay.getData(doc, "focus-allowed")
+ || elem.frameElement && overlay.getData(elem.frameElement.ownerDocument)["focus-allowed"];
default:
return true;
}
},
/**
* 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 focusElement(elem) {
let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem;
- elem.dactylFocusAllowed = true;
- win.document.dactylFocusAllowed = true;
-
- if (isinstance(elem, [HTMLFrameElement, HTMLIFrameElement]))
+ overlay.setData(elem, "focus-allowed", true);
+ overlay.setData(win.document, "focus-allowed", true);
+
+ if (isinstance(elem, [Ci.nsIDOMHTMLFrameElement,
+ Ci.nsIDOMHTMLIFrameElement]))
elem = elem.contentWindow;
+
if (elem.document)
- elem.document.dactylFocusAllowed = true;
-
- if (elem instanceof HTMLInputElement && elem.type == "file") {
+ overlay.setData(elem.document, "focus-allowed", true);
+
+ if (elem instanceof Ci.nsIDOMHTMLInputElement && elem.type == "file") {
Buffer.openUploadPrompt(elem);
this.lastInputField = elem;
}
else {
- if (isinstance(elem, [HTMLInputElement, XULTextBoxElement]))
+ if (isinstance(elem, [Ci.nsIDOMHTMLInputElement,
+ Ci.nsIDOMXULTextBoxElement]))
var flags = services.focus.FLAG_BYMOUSE;
else
flags = services.focus.FLAG_SHOWRING;
- dactyl.focus(elem, flags);
-
- if (elem instanceof Window) {
+
+ // Hack to deal with current versions of Firefox misplacing
+ // the caret
+ if (!overlay.getData(elem, "had-focus", false) && elem.value &&
+ elem instanceof Ci.nsIDOMHTMLInputElement &&
+ DOM(elem).isEditable &&
+ elem.selectionStart != null &&
+ elem.selectionStart == elem.selectionEnd)
+ elem.selectionStart = elem.selectionEnd = elem.value.length;
+
+ DOM(elem).focus(flags);
+
+ if (elem instanceof Ci.nsIDOMWindow) {
let sel = elem.getSelection();
if (sel && !sel.rangeCount)
sel.addRange(RangeFind.endpoint(
RangeFind.nodeRange(elem.document.body || elem.document.documentElement),
true));
}
else {
let range = RangeFind.nodeRange(elem);
@@ -465,21 +339,21 @@ var Buffer = Module("buffer", {
if (!sel.rangeCount || !RangeFind.intersects(range, sel.getRangeAt(0))) {
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
}
// for imagemap
- if (elem instanceof HTMLAreaElement) {
+ if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
try {
let [x, y] = elem.getAttribute("coords").split(",").map(parseFloat);
- events.dispatch(elem, events.create(elem.ownerDocument, "mouseover", { screenX: x, screenY: y }));
+ DOM(elem).mouseover({ screenX: x, screenY: y });
}
catch (e) {}
}
}
},
/**
* Find the *count*th last link on a page matching one of the given
@@ -493,21 +367,19 @@ var Buffer = Module("buffer", {
* If follow is true, the link is followed.
*
* @param {string} rel The relationship to look for.
* @param {[RegExp]} regexps The regular expressions to search for.
* @param {number} count The nth matching link to follow.
* @param {bool} follow Whether to follow the matching link.
* @param {string} path The CSS to use for the search. @optional
*/
- followDocumentRelationship: deprecated("buffer.findLink",
- function followDocumentRelationship(rel) {
- this.findLink(rel, options[rel + "pattern"], 0, true);
- }),
findLink: function findLink(rel, regexps, count, follow, path) {
+ let { Hints, dactyl, options } = this.modules;
+
let selector = path || options.get("hinttags").stringDefaultValue;
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];
}
@@ -515,142 +387,236 @@ var Buffer = Module("buffer", {
let elems = frame.document.getElementsByTagName("link");
for (let elem in iter(elems))
yield elem;
elems = frame.document.getElementsByTagName("a");
for (let elem in iter(elems))
yield elem;
- let res = frame.document.querySelectorAll(selector);
- for (let regexp in values(regexps)) {
- for (let i in util.range(res.length, 0, -1)) {
- let elem = res[i];
- if (regexp.test(elem.textContent) === regexp.result || regexp.test(elem.title) === regexp.result ||
- Array.some(elem.childNodes, function (child) regexp.test(child.alt) === regexp.result))
- yield elem;
- }
- }
- }
+ function a(regexp, elem) regexp.test(elem.textContent) === regexp.result ||
+ Array.some(elem.childNodes, function (child) regexp.test(child.alt) === regexp.result);
+ function b(regexp, elem) regexp.test(elem.title);
+
+ let res = Array.filter(frame.document.querySelectorAll(selector), Hints.isVisible);
+ for (let test in values([a, b]))
+ for (let regexp in values(regexps))
+ for (let i in util.range(res.length, 0, -1))
+ if (test(regexp, res[i]))
+ yield res[i];
+ }
for (let frame in values(this.allFrames(null, true)))
for (let elem in followFrame(frame))
if (count-- === 0) {
if (follow)
this.followLink(elem, dactyl.CURRENT_TAB);
return elem;
}
if (follow)
dactyl.beep();
},
+ followDocumentRelationship: deprecated("buffer.findLink",
+ function followDocumentRelationship(rel) {
+ let { options } = this.modules;
+
+ this.findLink(rel, options[rel + "pattern"], 0, true);
+ }),
/**
* Fakes a click on a link.
*
* @param {Node} elem The element to click.
* @param {number} where Where to open the link. See
* {@link dactyl.open}.
*/
followLink: function followLink(elem, where) {
+ let { dactyl } = this.modules;
+
let doc = elem.ownerDocument;
let win = doc.defaultView;
let { left: offsetX, top: offsetY } = elem.getBoundingClientRect();
- if (isinstance(elem, [HTMLFrameElement, HTMLIFrameElement]))
+ if (isinstance(elem, [Ci.nsIDOMHTMLFrameElement,
+ Ci.nsIDOMHTMLIFrameElement]))
return this.focusElement(elem);
- if (isinstance(elem, HTMLLinkElement))
+
+ if (isinstance(elem, Ci.nsIDOMHTMLLinkElement))
return dactyl.open(elem.href, where);
- if (elem instanceof HTMLAreaElement) { // for imagemap
+ if (elem instanceof Ci.nsIDOMHTMLAreaElement) { // 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") {
+ else if (elem instanceof Ci.nsIDOMHTMLInputElement && elem.type == "file") {
Buffer.openUploadPrompt(elem);
return;
}
+ let { dactyl } = this.modules;
+
let ctrlKey = false, shiftKey = false;
- switch (where) {
+ switch (dactyl.forceTarget || where) {
case dactyl.NEW_TAB:
case dactyl.NEW_BACKGROUND_TAB:
ctrlKey = true;
- shiftKey = (where != dactyl.NEW_BACKGROUND_TAB);
+ shiftKey = dactyl.forceBackground != null ? dactyl.forceBackground
+ : where != dactyl.NEW_BACKGROUND_TAB;
break;
case dactyl.NEW_WINDOW:
shiftKey = true;
break;
case dactyl.CURRENT_TAB:
break;
}
this.focusElement(elem);
prefs.withContext(function () {
prefs.set("browser.tabs.loadInBackground", true);
- ["mousedown", "mouseup", "click"].slice(0, util.haveGecko("2b") ? 2 : 3)
- .forEach(function (event) {
- events.dispatch(elem, events.create(doc, event, {
+ let params = {
screenX: offsetX, screenY: offsetY,
ctrlKey: ctrlKey, shiftKey: shiftKey, metaKey: ctrlKey
- }));
- });
+ };
+
+ DOM(elem).mousedown(params).mouseup(params);
+ if (!config.haveGecko("2b"))
+ DOM(elem).click(params);
+
let sel = util.selectionController(win);
sel.getSelection(sel.SELECTION_FOCUS_REGION).collapseToStart();
});
},
/**
+ * Resets the caret position so that it resides within the current
+ * viewport.
+ */
+ resetCaret: function resetCaret() {
+ function visible(range) util.intersection(DOM(range).rect, viewport);
+
+ function getRanges(rect) {
+ let nodes = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
+ .nodesFromRect(rect.x, rect.y, 0, rect.width, rect.height, 0, false, false);
+ return Array.filter(nodes, function (n) n instanceof Ci.nsIDOMText)
+ .map(RangeFind.nodeContents);
+ }
+
+ let win = this.focusedFrame;
+ let doc = win.document;
+ let sel = win.getSelection();
+ let { viewport } = DOM(win);
+
+ if (sel.rangeCount) {
+ var range = sel.getRangeAt(0);
+ if (visible(range).height > 0)
+ return;
+
+ var { rect } = DOM(range);
+ var reverse = rect.bottom > viewport.bottom;
+
+ rect = { x: rect.left, y: 0, width: rect.width, height: win.innerHeight };
+ }
+ else {
+ let w = win.innerWidth;
+ rect = { x: w / 3, y: 0, width: w / 3, height: win.innerHeight };
+ }
+
+ var reduce = function (a, b) DOM(a).rect.top < DOM(b).rect.top ? a : b;
+ var dir = "forward";
+ var y = 0;
+ if (reverse) {
+ reduce = function (a, b) DOM(b).rect.bottom > DOM(a).rect.bottom ? b : a;
+ dir = "backward";
+ y = win.innerHeight - 1;
+ }
+
+ let ranges = getRanges(rect);
+ if (!ranges.length)
+ ranges = getRanges({ x: 0, y: y, width: win.innerWidth, height: 0 });
+
+ if (ranges.length) {
+ range = ranges.reduce(reduce);
+
+ if (range) {
+ range.collapse(!reverse);
+ sel.removeAllRanges();
+ sel.addRange(range);
+ do {
+ if (visible(range).height > 0)
+ break;
+
+ var { startContainer, startOffset } = range;
+ sel.modify("move", dir, "line");
+ range = sel.getRangeAt(0);
+ }
+ while (startContainer != range.startContainer || startOffset != range.startOffset);
+
+ sel.modify("move", reverse ? "forward" : "backward", "lineboundary");
+ }
+ }
+
+ if (!sel.rangeCount)
+ sel.collapse(doc.body || doc.querySelector("body") || doc.documentElement,
+ 0);
+ },
+
+ /**
+ * @property {nsISelection} The current document's normal selection.
+ */
+ get selection() this.win.getSelection(),
+
+ /**
* @property {nsISelectionController} The current document's selection
* controller.
*/
get selectionController() util.selectionController(this.focusedFrame),
/**
* Opens the appropriate context menu for *elem*.
*
* @param {Node} elem The context element.
*/
- openContextMenu: function openContextMenu(elem) {
- document.popupNode = elem;
- let menu = document.getElementById("contentAreaContextMenu");
- menu.showPopup(elem, -1, -1, "context", "bottomleft", "topleft");
- },
+ openContextMenu: deprecated("DOM#contextmenu", function openContextMenu(elem) DOM(elem).contextmenu()),
/**
* Saves a page link to disk.
*
* @param {HTMLAnchorElement} elem The page link to save.
*/
saveLink: function saveLink(elem) {
+ let { completion, dactyl, io } = this.modules;
+
+ let self = this;
let doc = elem.ownerDocument;
let uri = util.newURI(elem.href || elem.src, null, util.newURI(elem.baseURI));
let referrer = util.newURI(doc.documentURI, doc.characterSet);
try {
- window.urlSecurityCheck(uri.spec, doc.nodePrincipal);
+ services.security.checkLoadURIWithPrincipal(doc.nodePrincipal, uri,
+ services.security.STANDARD);
io.CommandFileMode(_("buffer.prompt.saveLink") + " ", {
onSubmit: function (path) {
let file = io.File(path);
if (file.exists() && file.isDirectory())
file.append(Buffer.getDefaultNames(elem)[0][0]);
try {
if (!file.exists())
file.create(File.NORMAL_FILE_TYPE, octal(644));
}
catch (e) {
util.assert(false, _("save.invalidDestination", e.name));
}
- buffer.saveURI(uri, file);
- },
+ self.saveURI(uri, file);
+ },
completer: function (context) completion.savePage(context, elem)
}).open();
}
catch (e) {
dactyl.echoerr(e);
}
},
@@ -661,24 +627,25 @@ var Buffer = Module("buffer", {
* @param {nsIURI} uri The URI to save
* @param {nsIFile} file The file into which to write the result.
*/
saveURI: function saveURI(uri, file, callback, self) {
var persist = services.Persist();
persist.persistFlags = persist.PERSIST_FLAGS_FROM_CACHE
| persist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
+ let window = this.topWindow;
let downloadListener = new window.DownloadListener(window,
services.Transfer(uri, File(file).URI, "",
null, null, null, persist));
persist.progressListener = update(Object.create(downloadListener), {
onStateChange: util.wrapCallback(function onStateChange(progress, request, flags, status) {
if (callback && (flags & Ci.nsIWebProgressListener.STATE_STOP) && status == 0)
- dactyl.trapErrors(callback, self, uri, file, progress, request, flags, status);
+ util.trapErrors(callback, self, uri, file, progress, request, flags, status);
return onStateChange.superapply(this, arguments);
})
});
persist.saveURI(uri, null, null, null, null, file);
},
@@ -699,31 +666,43 @@ var Buffer = Module("buffer", {
/**
* Scrolls the currently active element to the given horizontal and
* vertical percentages. See {@link Buffer.scrollToPercent} for
* parameters.
*/
scrollToPercent: function scrollToPercent(horizontal, vertical)
Buffer.scrollToPercent(this.findScrollable(0, vertical == null), horizontal, vertical),
+ /**
+ * Scrolls the currently active element to the given horizontal and
+ * vertical positions. See {@link Buffer.scrollToPosition} for
+ * parameters.
+ */
+ scrollToPosition: function scrollToPosition(horizontal, vertical)
+ Buffer.scrollToPosition(this.findScrollable(0, vertical == null), horizontal, vertical),
+
_scrollByScrollSize: function _scrollByScrollSize(count, direction) {
+ let { options } = this.modules;
+
if (count > 0)
options["scroll"] = count;
this.scrollByScrollSize(direction);
},
/**
* 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 scrollByScrollSize(direction, count) {
+ let { options } = this.modules;
+
direction = direction ? 1 : -1;
count = count || 1;
if (options["scroll"] > 0)
this.scrollVertical("lines", options["scroll"] * direction);
else
this.scrollVertical("pages", direction / 2);
},
@@ -736,21 +715,22 @@ var Buffer = Module("buffer", {
* able to scroll. Negative numbers represent up or left, while
* positive numbers represent down or right.
* @param {boolean} horizontal If true, look for horizontally
* scrollable elements, otherwise look for vertically scrollable
* elements.
*/
findScrollable: function findScrollable(dir, horizontal) {
function find(elem) {
- while (elem && !(elem instanceof Element) && elem.parentNode)
+ while (elem && !(elem instanceof Ci.nsIDOMElement) && elem.parentNode)
elem = elem.parentNode;
- for (; elem && elem.parentNode instanceof Element; elem = elem.parentNode)
+ for (; elem instanceof Ci.nsIDOMElement; elem = elem.parentNode)
if (Buffer.isScrollable(elem, dir, horizontal))
break;
+
return elem;
}
try {
var elem = this.focusedFrame.document.activeElement;
if (elem == elem.ownerDocument.body)
elem = null;
}
@@ -760,38 +740,40 @@ var Buffer = Module("buffer", {
var sel = this.focusedFrame.getSelection();
}
catch (e) {}
if (!elem && sel && sel.rangeCount)
elem = sel.getRangeAt(0).startContainer;
if (elem)
elem = find(elem);
- if (!(elem instanceof Element)) {
+ if (!(elem instanceof Ci.nsIDOMElement)) {
let doc = this.findScrollableWindow().document;
elem = find(doc.body || doc.getElementsByTagName("body")[0] ||
doc.documentElement);
}
let doc = this.focusedFrame.document;
- return dactyl.assert(elem || doc.body || doc.documentElement);
- },
+ return util.assert(elem || doc.body || doc.documentElement);
+ },
/**
* Find the best candidate scrollable frame in the current buffer.
*/
findScrollableWindow: function findScrollableWindow() {
- win = window.document.commandDispatcher.focusedWindow;
+ let { document } = this.topWindow;
+
+ let win = document.commandDispatcher.focusedWindow;
if (win && (win.scrollMaxX > 0 || win.scrollMaxY > 0))
return win;
let win = this.focusedFrame;
if (win && (win.scrollMaxX > 0 || win.scrollMaxY > 0))
return win;
- win = content;
+ win = this.win;
if (win.scrollMaxX > 0 || win.scrollMaxY > 0)
return win;
for (let frame in array.iterValues(win.frames))
if (frame.scrollMaxX > 0 || frame.scrollMaxY > 0)
return frame;
return win;
@@ -800,30 +782,38 @@ var Buffer = Module("buffer", {
/**
* Finds the next visible element for the node path in 'jumptags'
* for *arg*.
*
* @param {string} arg The element in 'jumptags' to use for the search.
* @param {number} count The number of elements to jump.
* @optional
* @param {boolean} reverse If true, search backwards. @optional
- */
- findJump: function findJump(arg, count, reverse) {
+ * @param {boolean} offScreen If true, include only off-screen elements. @optional
+ */
+ findJump: function findJump(arg, count, reverse, offScreen) {
+ let { marks, options } = this.modules;
+
const FUDGE = 10;
+ marks.push();
+
let path = options["jumptags"][arg];
- dactyl.assert(path, _("error.invalidArgument", arg));
+ util.assert(path, _("error.invalidArgument", arg));
let distance = reverse ? function (rect) -rect.top : function (rect) rect.top;
let elems = [[e, distance(e.getBoundingClientRect())] for (e in path.matcher(this.focusedFrame.document))]
.filter(function (e) e[1] > FUDGE)
.sort(function (a, b) a[1] - b[1])
+ if (offScreen && !reverse)
+ elems = elems.filter(function (e) e[1] > this, this.topWindow.innerHeight);
+
let idx = Math.min((count || 1) - 1, elems.length);
- dactyl.assert(idx in elems);
+ util.assert(idx in elems);
let elem = elems[idx][0];
elem.scrollIntoView(true);
let sel = elem.ownerDocument.defaultView.getSelection();
sel.removeAllRanges();
sel.addRange(RangeFind.endpoint(RangeFind.nodeRange(elem), true));
},
@@ -832,117 +822,131 @@ var Buffer = Module("buffer", {
/**
* 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. A negative
* count skips backwards.
*/
shiftFrameFocus: function shiftFrameFocus(count) {
- if (!(content.document instanceof HTMLDocument))
+ if (!(this.doc instanceof Ci.nsIDOMHTMLDocument))
return;
let frames = this.allFrames();
if (frames.length == 0) // currently top is always included
return;
// remove all hidden frames
- frames = frames.filter(function (frame) !(frame.document.body instanceof HTMLFrameSetElement))
+ frames = frames.filter(function (frame) !(frame.document.body instanceof Ci.nsIDOMHTMLFrameSetElement))
.filter(function (frame) !frame.frameElement ||
let (rect = frame.frameElement.getBoundingClientRect())
rect.width && rect.height);
// find the currently focused frame index
let current = Math.max(0, frames.indexOf(this.focusedFrame));
// calculate the next frame to focus
let next = current + count;
if (next < 0 || next >= frames.length)
- dactyl.beep();
+ util.dactyl.beep();
next = Math.constrain(next, 0, frames.length - 1);
// focus next frame and scroll into view
- dactyl.focus(frames[next]);
- if (frames[next] != content)
- frames[next].frameElement.scrollIntoView(false);
+ DOM(frames[next]).focus();
+ if (frames[next] != this.win)
+ DOM(frames[next].frameElement).scrollIntoView();
// add the frame indicator
let doc = frames[next].document;
- let indicator = util.xmlToDom(<div highlight="FrameIndicator"/>, doc);
- (doc.body || doc.documentElement || doc).appendChild(indicator);
-
- util.timeout(function () { doc.body.removeChild(indicator); }, 500);
+ let indicator = DOM(<div highlight="FrameIndicator"/>, doc)
+ .appendTo(doc.body || doc.documentElement || doc);
+
+ util.timeout(function () { indicator.remove(); }, 500);
// Doesn't unattach
//doc.body.setAttributeNS(NS.uri, "activeframe", "true");
//util.timeout(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 showElementInfo(elem) {
- dactyl.echo(<><!--L-->Element:<br/>{util.objectToString(elem, true)}</>, commandline.FORCE_MULTILINE);
- },
+ let { dactyl } = this.modules;
+
+ XML.ignoreWhitespace = XML.prettyPrinting = false;
+ dactyl.echo(<><!--L-->Element:<br/>{util.objectToString(elem, true)}</>);
+ },
/**
* 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 showPageInfo(verbose, sections) {
+ let { commandline, dactyl, options } = this.modules;
+
+ let self = this;
+
// Ctrl-g single line output
if (!verbose) {
- let file = content.location.pathname.split("/").pop() || _("buffer.noName");
- let title = content.document.title || _("buffer.noTitle");
-
- let info = template.map(sections || options["pageinfo"],
- function (opt) template.map(buffer.pageInfo[opt].action(), util.identity, ", "),
+ let file = this.win.location.pathname.split("/").pop() || _("buffer.noName");
+ let title = this.win.document.title || _("buffer.noTitle");
+
+ let info = template.map(
+ (sections || options["pageinfo"])
+ .map(function (opt) Buffer.pageInfo[opt].action.call(self)),
+ function (res) res && iter(res).join(", ") || undefined,
", ");
if (bookmarkcache.isBookmarked(this.URL))
info += ", " + _("buffer.bookmarked");
let pageInfoText = <>{file.quote()} [{info}] {title}</>;
dactyl.echo(pageInfoText, commandline.FORCE_SINGLELINE);
return;
}
let list = template.map(sections || options["pageinfo"], function (option) {
- let { action, title } = buffer.pageInfo[option];
- return template.table(title, action(true));
+ let { action, title } = Buffer.pageInfo[option];
+ return template.table(title, action.call(self, true));
}, <br/>);
- dactyl.echo(list, commandline.FORCE_MULTILINE);
- },
+
+ commandline.commandOutput(list);
+ },
/**
* Stops loading and animations in the current content.
*/
stop: function stop() {
+ let { config } = this.modules;
+
if (config.stop)
config.stop();
else
- config.browser.mCurrentBrowser.stop();
- },
+ this.docShell.stop(this.docShell.STOP_ALL);
+ },
/**
* Opens a viewer to inspect the source of the currently selected
* range.
*/
viewSelectionSource: function viewSelectionSource() {
// copied (and tuned somewhat) from browser.jar -> nsContextMenu.js
+ let { document, window } = this.topWindow;
+
let win = document.commandDispatcher.focusedWindow;
- if (win == window)
+ if (win == this.topWindow)
win = this.focusedFrame;
let charset = win ? "charset=" + win.document.characterSet : null;
window.openDialog("chrome://global/content/viewPartialSource.xul",
"_blank", "scrollbars,resizable,chrome,dialog=no",
null, charset, win.getSelection(), "selection");
},
@@ -960,16 +964,20 @@ var Buffer = Module("buffer", {
* line: The line to select.
* column: The column to select.
*
* If no URL is provided, the current document is used.
* @default The current buffer.
* @param {boolean} useExternalEditor View the source in the external editor.
*/
viewSource: function viewSource(loc, useExternalEditor) {
+ let { dactyl, editor, history, options } = this.modules;
+
+ let window = this.topWindow;
+
let doc = this.focusedFrame.document;
if (isObject(loc)) {
if (options.get("editor").has("line") || !loc.url)
this.viewSourceExternally(loc.doc || loc.url || doc, loc);
else
window.openDialog("chrome://global/content/viewSource.xul",
"_blank", "all,dialog=no",
@@ -983,17 +991,17 @@ var Buffer = Module("buffer", {
const PREFIX = "view-source:";
if (url.indexOf(PREFIX) == 0)
url = url.substr(PREFIX.length);
else
url = PREFIX + url;
let sh = history.session;
if (sh[sh.index].URI.spec == url)
- window.getWebNavigation().gotoIndex(sh.index);
+ this.docShell.gotoIndex(sh.index);
else
dactyl.open(url, { hide: true });
}
}
},
/**
* Launches an editor to view the source of the given document. The
@@ -1009,30 +1017,35 @@ var Buffer = Module("buffer", {
* source.
* @optional
*/
viewSourceExternally: Class("viewSourceExternally",
XPCOM([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]), {
init: function init(doc, callback) {
this.callback = callable(callback) ? callback :
function (file, temp) {
+ let { editor } = overlay.activeModules;
+
editor.editFileExternally(update({ file: file.path }, callback || {}),
function () { temp && file.remove(false); });
return true;
};
let uri = isString(doc) ? util.newURI(doc) : util.newURI(doc.location.href);
+ let ext = uri.fileExtension || "txt";
+ if (doc.contentType)
+ ext = services.mime.getPrimaryExtension(doc.contentType, ext);
if (!isString(doc))
return io.withTempFiles(function (temp) {
let encoder = services.HtmlEncoder();
encoder.init(doc, "text/unicode", encoder.OutputRaw|encoder.OutputPreformatted);
temp.write(encoder.encodeToString(), ">");
return this.callback(temp, true);
- }, this, true);
+ }, this, true, ext);
let file = util.getFile(uri);
if (file)
this.callback(file, false);
else {
this.file = io.createTempFile();
var persist = services.Persist();
persist.persistFlags = persist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
@@ -1084,290 +1097,492 @@ var Buffer = Module("buffer", {
* percentage of the page's natural size.
* @param {boolean} fullZoom If true, zoom all content of the page,
* including raster images. If false, zoom only text. If omitted,
* use the current zoom function. @optional
* @throws {FailedAssertion} if the given *value* is not within the
* closed range [Buffer.ZOOM_MIN, Buffer.ZOOM_MAX].
*/
setZoom: function setZoom(value, fullZoom) {
- dactyl.assert(value >= Buffer.ZOOM_MIN || value <= Buffer.ZOOM_MAX,
- _("zoom.outOfRange", Buffer.ZOOM_MIN, Buffer.ZOOM_MAX));
-
- if (fullZoom !== undefined)
+ let { dactyl, statusline } = this.modules;
+ let { ZoomManager } = this;
+
+ if (fullZoom === undefined)
+ fullZoom = ZoomManager.useFullZoom;
+ else
ZoomManager.useFullZoom = fullZoom;
+
+ value /= 100;
try {
- ZoomManager.zoom = value / 100;
+ this.contentViewer.textZoom = fullZoom ? 1 : value;
+ this.contentViewer.fullZoom = !fullZoom ? 1 : value;
}
catch (e if e == Cr.NS_ERROR_ILLEGAL_VALUE) {
return dactyl.echoerr(_("zoom.illegal"));
}
- if ("FullZoom" in window)
- FullZoom._applySettingToPref();
-
- statusline.updateZoomLevel(value, ZoomManager.useFullZoom);
- },
+ if (services.has("contentPrefs") && !storage.privateMode
+ && prefs.get("browser.zoom.siteSpecific")) {
+ services.contentPrefs[value != 1 ? "setPref" : "removePref"]
+ (this.uri, "browser.content.full-zoom", value);
+ services.contentPrefs[value != 1 ? "setPref" : "removePref"]
+ (this.uri, "dactyl.content.full-zoom", fullZoom);
+ }
+
+ statusline.updateZoomLevel();
+ },
+
+ /**
+ * Updates the zoom level of this buffer from a content preference.
+ */
+ updateZoom: util.wrapCallback(function updateZoom() {
+ let self = this;
+ let uri = this.uri;
+
+ if (services.has("contentPrefs") && prefs.get("browser.zoom.siteSpecific"))
+ services.contentPrefs.getPref(uri, "dactyl.content.full-zoom", function (val) {
+ if (val != null && uri.equals(self.uri) && val != prefs.get("browser.zoom.full"))
+ [self.contentViewer.textZoom, self.contentViewer.fullZoom] =
+ [self.contentViewer.fullZoom, self.contentViewer.textZoom];
+ });
+ }),
/**
* Adjusts the page zoom of the current buffer relative to the
* current zoom level.
*
* @param {number} steps The integral number of natural fractions by which
* to adjust the current page zoom. If positive, the zoom level is
* increased, if negative it is decreased.
* @param {boolean} fullZoom If true, zoom all content of the page,
* including raster images. If false, zoom only text. If omitted, use
* the current zoom function. @optional
* @throws {FailedAssertion} if the buffer's zoom level is already at its
* extreme in the given direction.
*/
bumpZoomLevel: function bumpZoomLevel(steps, fullZoom) {
+ let { ZoomManager } = this;
+
if (fullZoom === undefined)
fullZoom = ZoomManager.useFullZoom;
let values = ZoomManager.zoomValues;
- let cur = values.indexOf(ZoomManager.snap(ZoomManager.zoom));
+ let cur = values.indexOf(ZoomManager.snap(this.zoomLevel / 100));
let i = Math.constrain(cur + steps, 0, values.length - 1);
- dactyl.assert(i != cur || fullZoom != ZoomManager.useFullZoom);
+ util.assert(i != cur || fullZoom != ZoomManager.useFullZoom);
this.setZoom(Math.round(values[i] * 100), fullZoom);
},
getAllFrames: deprecated("buffer.allFrames", "allFrames"),
- scrollTop: deprecated("buffer.scrollToPercent", function scrollTop() buffer.scrollToPercent(null, 0)),
- scrollBottom: deprecated("buffer.scrollToPercent", function scrollBottom() buffer.scrollToPercent(null, 100)),
- scrollStart: deprecated("buffer.scrollToPercent", function scrollStart() buffer.scrollToPercent(0, null)),
- scrollEnd: deprecated("buffer.scrollToPercent", function scrollEnd() buffer.scrollToPercent(100, null)),
- scrollColumns: deprecated("buffer.scrollHorizontal", function scrollColumns(cols) buffer.scrollHorizontal("columns", cols)),
- scrollPages: deprecated("buffer.scrollHorizontal", function scrollPages(pages) buffer.scrollVertical("pages", pages)),
- scrollTo: deprecated("Buffer.scrollTo", function scrollTo(x, y) content.scrollTo(x, y)),
- textZoom: deprecated("buffer.zoomValue/buffer.fullZoom", function textZoom() config.browser.markupDocumentViewer.textZoom * 100)
+ scrollTop: deprecated("buffer.scrollToPercent", function scrollTop() this.scrollToPercent(null, 0)),
+ scrollBottom: deprecated("buffer.scrollToPercent", function scrollBottom() this.scrollToPercent(null, 100)),
+ scrollStart: deprecated("buffer.scrollToPercent", function scrollStart() this.scrollToPercent(0, null)),
+ scrollEnd: deprecated("buffer.scrollToPercent", function scrollEnd() this.scrollToPercent(100, null)),
+ scrollColumns: deprecated("buffer.scrollHorizontal", function scrollColumns(cols) this.scrollHorizontal("columns", cols)),
+ scrollPages: deprecated("buffer.scrollHorizontal", function scrollPages(pages) this.scrollVertical("pages", pages)),
+ scrollTo: deprecated("Buffer.scrollTo", function scrollTo(x, y) this.win.scrollTo(x, y)),
+ textZoom: deprecated("buffer.zoomValue/buffer.fullZoom", function textZoom() this.contentViewer.markupDocumentViewer.textZoom * 100)
}, {
PageInfo: Struct("PageInfo", "name", "title", "action")
.localize("title"),
- ZOOM_MIN: Class.memoize(function () prefs.get("zoom.minPercent")),
- ZOOM_MAX: Class.memoize(function () prefs.get("zoom.maxPercent")),
-
- setZoom: deprecated("buffer.setZoom", function setZoom() buffer.setZoom.apply(buffer, arguments)),
- bumpZoomLevel: deprecated("buffer.bumpZoomLevel", function bumpZoomLevel() buffer.bumpZoomLevel.apply(buffer, arguments)),
+ pageInfo: {},
+
+ /**
+ * 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] = Buffer.PageInfo(option, title, func);
+ },
+
+ Scrollable: function Scrollable(elem) {
+ if (elem instanceof Ci.nsIDOMElement)
+ return elem;
+ if (isinstance(elem, [Ci.nsIDOMWindow, Ci.nsIDOMDocument]))
+ return {
+ __proto__: elem.documentElement || elem.ownerDocument.documentElement,
+
+ win: elem.defaultView || elem.ownerDocument.defaultView,
+
+ get clientWidth() this.win.innerWidth,
+ get clientHeight() this.win.innerHeight,
+
+ get scrollWidth() this.win.scrollMaxX + this.win.innerWidth,
+ get scrollHeight() this.win.scrollMaxY + this.win.innerHeight,
+
+ get scrollLeft() this.win.scrollX,
+ set scrollLeft(val) { this.win.scrollTo(val, this.win.scrollY) },
+
+ get scrollTop() this.win.scrollY,
+ set scrollTop(val) { this.win.scrollTo(this.win.scrollX, val) }
+ };
+ return elem;
+ },
+
+ get ZOOM_MIN() prefs.get("zoom.minPercent"),
+ get ZOOM_MAX() prefs.get("zoom.maxPercent"),
+
+ setZoom: deprecated("buffer.setZoom", function setZoom()
+ let ({ buffer } = overlay.activeModules) buffer.setZoom.apply(buffer, arguments)),
+ bumpZoomLevel: deprecated("buffer.bumpZoomLevel", function bumpZoomLevel()
+ let ({ buffer } = overlay.activeModules) buffer.bumpZoomLevel.apply(buffer, arguments)),
/**
* Returns the currently selected word in *win*. If the selection is
* null, it tries to guess the word that the caret is positioned in.
*
* @returns {string}
*/
currentWord: function currentWord(win, select) {
+ let { Editor, options } = Buffer(win).modules;
+
let selection = win.getSelection();
if (selection.rangeCount == 0)
return "";
let range = selection.getRangeAt(0).cloneRange();
- if (range.collapsed && range.startContainer instanceof Text) {
+ if (range.collapsed) {
let re = options.get("iskeyword").regexp;
Editor.extendRange(range, true, re, true);
Editor.extendRange(range, false, re, true);
}
if (select) {
selection.removeAllRanges();
selection.addRange(range);
}
- return util.domToString(range);
- },
+ return DOM.stringify(range);
+ },
getDefaultNames: function getDefaultNames(node) {
let url = node.href || node.src || node.documentURI;
let currExt = url.replace(/^.*?(?:\.([a-z0-9]+))?$/i, "$1").toLowerCase();
- if (isinstance(node, [Document, HTMLImageElement])) {
+ let ext = "";
+ if (isinstance(node, [Ci.nsIDOMDocument,
+ Ci.nsIDOMHTMLImageElement])) {
let type = node.contentType || node.QueryInterface(Ci.nsIImageLoadingContent)
.getRequest(0).mimeType;
if (type === "text/plain")
- var ext = "." + (currExt || "txt");
+ ext = "." + (currExt || "txt");
else
ext = "." + services.mime.getPrimaryExtension(type, currExt);
}
else if (currExt)
ext = "." + currExt;
- else
- ext = "";
+
let re = ext ? RegExp("(\\." + currExt + ")?$", "i") : /$/;
var names = [];
if (node.title)
- names.push([node.title, /*L*/"Page Name"]);
+ names.push([node.title,
+ _("buffer.save.pageName")]);
if (node.alt)
- names.push([node.alt, /*L*/"Alternate Text"]);
-
- if (!isinstance(node, Document) && node.textContent)
- names.push([node.textContent, /*L*/"Link Text"]);
-
- names.push([decodeURIComponent(url.replace(/.*?([^\/]*)\/*$/, "$1")), "File Name"]);
+ names.push([node.alt,
+ _("buffer.save.altText")]);
+
+ if (!isinstance(node, Ci.nsIDOMDocument) && node.textContent)
+ names.push([node.textContent,
+ _("buffer.save.linkText")]);
+
+ names.push([decodeURIComponent(url.replace(/.*?([^\/]*)\/*$/, "$1")),
+ _("buffer.save.filename")]);
return names.filter(function ([leaf, title]) leaf)
- .map(function ([leaf, title]) [leaf.replace(util.OS.illegalCharacters, encodeURIComponent)
+ .map(function ([leaf, title]) [leaf.replace(config.OS.illegalCharacters, encodeURIComponent)
.replace(re, ext), title]);
},
- findScrollableWindow: deprecated("buffer.findScrollableWindow", function findScrollableWindow() buffer.findScrollableWindow.apply(buffer, arguments)),
- findScrollable: deprecated("buffer.findScrollable", function findScrollable() buffer.findScrollable.apply(buffer, arguments)),
+ findScrollableWindow: deprecated("buffer.findScrollableWindow", function findScrollableWindow()
+ let ({ buffer } = overlay.activeModules) buffer.findScrollableWindow.apply(buffer, arguments)),
+ findScrollable: deprecated("buffer.findScrollable", function findScrollable()
+ let ({ buffer } = overlay.activeModules) buffer.findScrollable.apply(buffer, arguments)),
isScrollable: function isScrollable(elem, dir, horizontal) {
+ if (!DOM(elem).isScrollable(horizontal ? "horizontal" : "vertical"))
+ return false;
+
+ return this.canScroll(elem, dir, horizontal);
+ },
+
+ canScroll: function canScroll(elem, 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";
- let style = util.computedStyle(elem);
+ let style = DOM(elem).style;
let borderSize = Math.round(parseFloat(style[border1]) + parseFloat(style[border2]));
let realSize = elem[size];
+
// Stupid Gecko eccentricities. May fail for quirks mode documents.
if (elem[size] + borderSize == elem[max] || elem[size] == 0) // Stupid, fallible heuristic.
return false;
+
if (style[overflow] == "hidden")
realSize += borderSize;
return dir < 0 && elem[pos] > 0 || dir > 0 && elem[pos] + realSize < elem[max] || !dir && realSize < elem[max];
},
/**
* Scroll the contents of the given element to the absolute *left*
* and *top* pixel offsets.
*
* @param {Element} elem The element to scroll.
* @param {number|null} left The left absolute pixel offset. If
* null, to not alter the horizontal scroll offset.
* @param {number|null} top The top absolute pixel offset. If
* null, to not alter the vertical scroll offset.
- */
- scrollTo: function scrollTo(elem, left, top) {
- // Temporary hack. Should be done better.
- if (elem.ownerDocument == buffer.focusedFrame.document)
- marks.add("'");
+ * @param {string} reason The reason for the scroll event. See
+ * {@link marks.push}. @optional
+ */
+ scrollTo: function scrollTo(elem, left, top, reason) {
+ let doc = elem.ownerDocument || elem.document || elem;
+
+ let { buffer, marks, options } = util.topWindow(doc.defaultView).dactyl.modules;
+
+ if (~[elem, elem.document, elem.ownerDocument].indexOf(buffer.focusedFrame.document))
+ marks.push(reason);
+
+ if (options["scrollsteps"] > 1)
+ return this.smoothScrollTo(elem, left, top);
+
+ elem = Buffer.Scrollable(elem);
if (left != null)
elem.scrollLeft = left;
if (top != null)
elem.scrollTop = top;
-
- if (util.haveGecko("2.0") && !util.haveGecko("7.*"))
- elem.ownerDocument.defaultView
- .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
- .redraw();
- },
+ },
+
+ /**
+ * Like scrollTo, but scrolls more smoothly and does not update
+ * marks.
+ */
+ smoothScrollTo: function smoothScrollTo(node, x, y) {
+ let { options } = overlay.activeModules;
+
+ let time = options["scrolltime"];
+ let steps = options["scrollsteps"];
+
+ let elem = Buffer.Scrollable(node);
+
+ if (node.dactylScrollTimer)
+ node.dactylScrollTimer.cancel();
+
+ if (x == null)
+ x = elem.scrollLeft;
+ if (y == null)
+ y = elem.scrollTop;
+
+ x = node.dactylScrollDestX = Math.min(x, elem.scrollWidth - elem.clientWidth);
+ y = node.dactylScrollDestY = Math.min(y, elem.scrollHeight - elem.clientHeight);
+ let [startX, startY] = [elem.scrollLeft, elem.scrollTop];
+ let n = 0;
+ (function next() {
+ if (n++ === steps) {
+ elem.scrollLeft = x;
+ elem.scrollTop = y;
+ delete node.dactylScrollDestX;
+ delete node.dactylScrollDestY;
+ }
+ else {
+ elem.scrollLeft = startX + (x - startX) / steps * n;
+ elem.scrollTop = startY + (y - startY) / steps * n;
+ node.dactylScrollTimer = util.timeout(next, time / steps);
+ }
+ }).call(this);
+ },
/**
* Scrolls the currently given element horizontally.
*
* @param {Element} elem The element to scroll.
- * @param {string} increment The increment by which to scroll.
+ * @param {string} unit The increment by which to scroll.
* Possible values are: "columns", "pages"
* @param {number} number The possibly fractional number of
* increments to scroll. Positive values scroll to the right while
* negative values scroll to the left.
* @throws {FailedAssertion} if scrolling is not possible in the
* given direction.
*/
- scrollHorizontal: function scrollHorizontal(elem, increment, number) {
- let fontSize = parseInt(util.computedStyle(elem).fontSize);
- if (increment == "columns")
+ scrollHorizontal: function scrollHorizontal(node, unit, number) {
+ let fontSize = parseInt(DOM(node).style.fontSize);
+
+ let elem = Buffer.Scrollable(node);
+ let increment;
+ if (unit == "columns")
increment = fontSize; // Good enough, I suppose.
- else if (increment == "pages")
+ else if (unit == "pages")
increment = elem.clientWidth - fontSize;
else
throw Error();
- dactyl.assert(number < 0 ? elem.scrollLeft > 0 : elem.scrollLeft < elem.scrollWidth - elem.clientWidth);
-
- let left = elem.dactylScrollDestX !== undefined ? elem.dactylScrollDestX : elem.scrollLeft;
- elem.dactylScrollDestX = undefined;
-
- Buffer.scrollTo(elem, left + number * increment, null);
- },
-
- /**
- * Scrolls the currently given element vertically.
+ util.assert(number < 0 ? elem.scrollLeft > 0 : elem.scrollLeft < elem.scrollWidth - elem.clientWidth);
+
+ let left = node.dactylScrollDestX !== undefined ? node.dactylScrollDestX : elem.scrollLeft;
+ node.dactylScrollDestX = undefined;
+
+ Buffer.scrollTo(node, left + number * increment, null, "h-" + unit);
+ },
+
+ /**
+ * Scrolls the given element vertically.
*
* @param {Element} elem The element to scroll.
- * @param {string} increment The increment by which to scroll.
+ * @param {string} unit The increment by which to scroll.
* Possible values are: "lines", "pages"
* @param {number} number The possibly fractional number of
* increments to scroll. Positive values scroll upward while
* negative values scroll downward.
* @throws {FailedAssertion} if scrolling is not possible in the
* given direction.
*/
- scrollVertical: function scrollVertical(elem, increment, number) {
- let fontSize = parseInt(util.computedStyle(elem).fontSize);
- if (increment == "lines")
+ scrollVertical: function scrollVertical(node, unit, number) {
+ let fontSize = parseInt(DOM(node).style.lineHeight);
+
+ let elem = Buffer.Scrollable(node);
+ let increment;
+ if (unit == "lines")
increment = fontSize;
- else if (increment == "pages")
+ else if (unit == "pages")
increment = elem.clientHeight - fontSize;
else
throw Error();
- dactyl.assert(number < 0 ? elem.scrollTop > 0 : elem.scrollTop < elem.scrollHeight - elem.clientHeight);
-
- let top = elem.dactylScrollDestY !== undefined ? elem.dactylScrollDestY : elem.scrollTop;
- elem.dactylScrollDestY = undefined;
-
- Buffer.scrollTo(elem, null, top + number * increment);
- },
+ util.assert(number < 0 ? elem.scrollTop > 0 : elem.scrollTop < elem.scrollHeight - elem.clientHeight);
+
+ let top = node.dactylScrollDestY !== undefined ? node.dactylScrollDestY : elem.scrollTop;
+ node.dactylScrollDestY = undefined;
+
+ Buffer.scrollTo(node, null, top + number * increment, "v-" + unit);
+ },
/**
* Scrolls the currently active element to the given horizontal and
* vertical percentages.
*
* @param {Element} elem The element to scroll.
* @param {number|null} horizontal The possibly fractional
* percentage of the current viewport width to scroll to. If null,
* do not scroll horizontally.
* @param {number|null} vertical The possibly fractional percentage
* of the current viewport height to scroll to. If null, do not
* scroll vertically.
*/
- scrollToPercent: function scrollToPercent(elem, horizontal, vertical) {
- Buffer.scrollTo(elem,
+ scrollToPercent: function scrollToPercent(node, horizontal, vertical) {
+ let elem = Buffer.Scrollable(node);
+ Buffer.scrollTo(node,
horizontal == null ? null
: (elem.scrollWidth - elem.clientWidth) * (horizontal / 100),
vertical == null ? null
: (elem.scrollHeight - elem.clientHeight) * (vertical / 100));
},
+ /**
+ * Scrolls the currently active element to the given horizontal and
+ * vertical position.
+ *
+ * @param {Element} elem The element to scroll.
+ * @param {number|null} horizontal The possibly fractional
+ * line ordinal to scroll to.
+ * @param {number|null} vertical The possibly fractional
+ * column ordinal to scroll to.
+ */
+ scrollToPosition: function scrollToPosition(elem, horizontal, vertical) {
+ let style = DOM(elem.body || elem).style;
+ Buffer.scrollTo(elem,
+ horizontal == null ? null :
+ horizontal == 0 ? 0 : this._exWidth(elem) * horizontal,
+ vertical == null ? null : parseFloat(style.lineHeight) * vertical);
+ },
+
+ /**
+ * Returns the current scroll position as understood by
+ * {@link #scrollToPosition}.
+ *
+ * @param {Element} elem The element to scroll.
+ */
+ getScrollPosition: function getPosition(node) {
+ let style = DOM(node.body || node).style;
+
+ let elem = Buffer.Scrollable(node);
+ return {
+ x: elem.scrollLeft && elem.scrollLeft / this._exWidth(node),
+ y: elem.scrollTop / parseFloat(style.lineHeight)
+ }
+ },
+
+ _exWidth: function _exWidth(elem) {
+ try {
+ let div = DOM(<elem style="width: 1ex !important; position: absolute !important; padding: 0 !important; display: block;"/>,
+ elem.ownerDocument).appendTo(elem.body || elem);
+ try {
+ return parseFloat(div.style.width);
+ }
+ finally {
+ div.remove();
+ }
+ }
+ catch (e) {
+ return parseFloat(DOM(elem).fontSize) / 1.618;
+ }
+ },
+
openUploadPrompt: function openUploadPrompt(elem) {
+ let { io } = overlay.activeModules;
+
io.CommandFileMode(_("buffer.prompt.uploadFile") + " ", {
onSubmit: function onSubmit(path) {
let file = io.File(path);
- dactyl.assert(file.exists());
-
- elem.value = file.path;
- events.dispatch(elem, events.create(elem.ownerDocument, "change", {}));
+ util.assert(file.exists());
+
+ DOM(elem).val(file.path).change();
}
}).open(elem.value);
}
}, {
+ init: function init(dactyl, modules, window) {
+ init.superapply(this, arguments);
+
+ dactyl.commands["buffer.viewSource"] = function (event) {
+ let elem = event.originalTarget;
+ let obj = { url: elem.getAttribute("href"), line: Number(elem.getAttribute("line")) };
+ if (elem.hasAttribute("column"))
+ obj.column = elem.getAttribute("column");
+
+ modules.buffer.viewSource(obj);
+ };
+ },
commands: function initCommands(dactyl, modules, window) {
+ let { buffer, commands, config, options } = modules;
+
commands.add(["frameo[nly]"],
"Show only the current frame's page",
function (args) {
dactyl.open(buffer.focusedFrame.location.href);
},
{ 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
- dactyl.assert(!arg || arg[0] == ">" && !util.OS.isWindows,
+ dactyl.assert(!arg || arg[0] == ">" && !config.OS.isWindows,
_("error.trailingCharacters"));
const PRINTER = "PostScript/default";
const BRANCH = "print.printer_" + PRINTER + ".";
prefs.withContext(function () {
if (arg) {
prefs.set("print.print_printer", PRINTER);
@@ -1408,17 +1623,17 @@ var Buffer = Module("buffer", {
dactyl.assert(!arg || opt.validator(opt.parse(arg)),
_("error.invalidArgument", arg));
buffer.showPageInfo(true, arg);
},
{
argCount: "?",
completer: function (context) {
- completion.optionValue(context, "pageinfo", "+", "");
+ modules.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[0] || "";
@@ -1430,33 +1645,35 @@ var Buffer = Module("buffer", {
if (options["usermode"])
options["usermode"] = false;
window.stylesheetSwitchAll(buffer.focusedFrame, arg);
},
{
argCount: "?",
- completer: function (context) completion.alternateStyleSheet(context),
+ completer: function (context) modules.completion.alternateStyleSheet(context),
literal: 0
});
commands.add(["re[load]"],
"Reload the current web page",
- function (args) { tabs.reload(config.browser.mCurrentTab, args.bang); },
+ function (args) { modules.tabs.reload(config.browser.mCurrentTab, args.bang); },
{
argCount: "0",
bang: true
});
// 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 = content.document;
+ let { commandline, io } = modules;
+ let { doc, win } = buffer;
+
let chosenData = null;
let filename = args[0];
let command = commandline.command;
if (filename) {
if (filename[0] == "!")
return buffer.viewSourceExternally(buffer.focusedFrame.document,
function (file) {
@@ -1464,196 +1681,176 @@ var Buffer = Module("buffer", {
commandline.command = command;
commandline.commandOutput(<span highlight="CmdOutput">{output}</span>);
});
if (/^>>/.test(filename)) {
let file = io.File(filename.replace(/^>>\s*/, ""));
dactyl.assert(args.bang || file.exists() && file.isWritable(),
_("io.notWriteable", file.path.quote()));
+
return buffer.viewSourceExternally(buffer.focusedFrame.document,
function (tmpFile) {
try {
file.write(tmpFile, ">>");
}
catch (e) {
dactyl.echoerr(_("io.notWriteable", file.path.quote()));
}
});
}
- let file = io.File(filename.replace(RegExp(File.PATH_SEP + "*$"), ""));
+ let file = io.File(filename);
if (filename.substr(-1) === File.PATH_SEP || file.exists() && file.isDirectory())
file.append(Buffer.getDefaultNames(doc)[0][0]);
dactyl.assert(args.bang || !file.exists(), _("io.exists"));
chosenData = { file: file, uri: util.newURI(doc.location.href) };
}
// 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?
prefs.set("browser.download.lastDir", io.cwd.path);
try {
- var contentDisposition = content.QueryInterface(Ci.nsIInterfaceRequestor)
+ var contentDisposition = win.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) {
+ let { buffer, completion } = modules;
+
if (context.filter[0] == "!")
return;
if (/^>>/.test(context.filter))
context.advance(/^>>\s*/.exec(context.filter)[0].length);
- completion.savePage(context, content.document);
+ completion.savePage(context, buffer.doc);
context.fork("file", 0, completion, "file");
},
literal: 0
});
commands.add(["st[op]"],
"Stop loading the current web page",
function () { buffer.stop(); },
{ 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")
- });
+ completer: function (context) modules.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)) {
+ else if (/^[+-]\d+$/.test(arg))
level = Math.round(buffer.zoomLevel + parseInt(arg, 10));
- level = Math.constrain(level, Buffer.ZOOM_MIN, Buffer.ZOOM_MAX);
- }
else
dactyl.assert(false, _("error.trailingCharacters"));
buffer.setZoom(level, args.bang);
},
{
argCount: "?",
bang: true
});
},
completion: function initCompletion(dactyl, modules, window) {
+ let { CompletionContext, buffer, completion } = modules;
+
completion.alternateStyleSheet = function alternateStylesheet(context) {
context.title = ["Stylesheet", "Location"];
// unify split style sheets
let styles = iter([s.title, []] for (s in values(buffer.alternateStyleSheets))).toObject();
buffer.alternateStyleSheets.forEach(function (style) {
styles[style.title].push(style.href || _("style.inline"));
});
context.completions = [[title, href.join(", ")] for ([title, href] in Iterator(styles))];
};
- completion.buffer = function buffer(context, visible) {
- let filter = context.filter.toLowerCase();
-
- let defItem = { parent: { getTitle: function () "" } };
-
- let tabGroups = {};
- tabs.getGroups();
- tabs[visible ? "visibleTabs" : "allTabs"].forEach(function (tab, i) {
- let group = (tab.tabItem || tab._tabViewTabItem || defItem).parent || defItem.parent;
- if (!Set.has(tabGroups, group.id))
- tabGroups[group.id] = [group.getTitle(), []];
-
- group = tabGroups[group.id];
- group[1].push([i, tab.linkedBrowser]);
- });
-
- context.pushProcessor(0, function (item, text, next) <>
- <span highlight="Indicator" style="display: inline-block;">{item.indicator}</span>
- { next.call(this, item, text) }
- </>);
- context.process[1] = function (item, text) template.bookmarkDescription(item, template.highlightFilter(text, this.filter));
-
- context.anchored = false;
- context.keys = {
- text: "text",
- description: "url",
- indicator: function (item) item.tab === tabs.getTab() ? "%" :
- item.tab === tabs.alternate ? "#" : " ",
- icon: "icon",
- id: "id",
- command: function () "tabs.select"
- };
- context.compare = CompletionContext.Sort.number;
- context.filters[0] = CompletionContext.Filter.textDescription;
-
- for (let [id, vals] in Iterator(tabGroups))
- context.fork(id, 0, this, function (context, [name, browsers]) {
- context.title = [name || "Buffers"];
- context.generate = function ()
- Array.map(browsers, function ([i, browser]) {
- let indicator = " ";
- if (i == tabs.index())
- indicator = "%";
- else if (i == tabs.index(tabs.alternate))
- indicator = "#";
-
- let tab = tabs.getTab(i, visible);
- let url = browser.contentDocument.location.href;
- i = i + 1;
-
- return {
- text: [i + ": " + (tab.label || /*L*/"(Untitled)"), i + ": " + url],
- tab: tab,
- id: i - 1,
- url: url,
- icon: tab.image || DEFAULT_FAVICON
- };
- });
- }, vals);
- };
-
completion.savePage = function savePage(context, node) {
context.fork("generated", context.filter.replace(/[^/]*$/, "").length,
this, function (context) {
- context.completions = Buffer.getDefaultNames(node);
+ context.generate = function () {
+ this.incomplete = true;
+ this.completions = Buffer.getDefaultNames(node);
+ util.httpGet(node.href || node.src || node.documentURI, {
+ method: "HEAD",
+ callback: function callback(xhr) {
+ context.incomplete = false;
+ try {
+ if (/filename="(.*?)"/.test(xhr.getResponseHeader("Content-Disposition")))
+ context.completions.push([decodeURIComponent(RegExp.$1), _("buffer.save.suggested")]);
+ }
+ finally {
+ context.completions = context.completions.slice();
+ }
+ },
+ notificationCallbacks: Class(XPCOM([Ci.nsIChannelEventSink, Ci.nsIInterfaceRequestor]), {
+ getInterface: function getInterface(iid) this.QueryInterface(iid),
+
+ asyncOnChannelRedirect: function (oldChannel, newChannel, flags, callback) {
+ if (newChannel instanceof Ci.nsIHttpChannel)
+ newChannel.requestMethod = "HEAD";
+ callback.onRedirectVerifyCallback(Cr.NS_OK);
+ }
+ })()
});
};
+ });
+ };
},
events: function initEvents(dactyl, modules, window) {
+ let { buffer, config, events } = modules;
+
events.listen(config.browser, "scroll", buffer.closure._updateBufferPosition, false);
},
mappings: function initMappings(dactyl, modules, window) {
+ let { Editor, Events, buffer, editor, events, ex, mappings, modes, options, tabs } = modules;
+
mappings.add([modes.NORMAL],
["y", "<yank-location>"], "Yank current location to the clipboard",
- function () { dactyl.clipboardWrite(buffer.uri.spec, true); });
+ function () {
+ let { doc, uri } = buffer;
+ if (uri instanceof Ci.nsIURL)
+ uri.query = uri.query.replace(/(?:^|&)utm_[^&]+/g, "")
+ .replace(/^&/, "");
+
+ let link = DOM("link[href][rev=canonical], link[href][rel=shortlink]", doc);
+ let url = link.length && options.get("yankshort").getKey(uri) ? link.attr("href") : uri.spec;
+ dactyl.clipboardWrite(url, true);
+ });
mappings.add([modes.NORMAL],
["<C-a>", "<increment-url-path>"], "Increment last number in URL",
function (args) { buffer.incrementURL(Math.max(args.count, 1)); },
{ count: true });
mappings.add([modes.NORMAL],
["<C-x>", "<decrement-url-path>"], "Decrement last number in URL",
@@ -1683,87 +1880,102 @@ var Buffer = Module("buffer", {
"Start Caret mode",
function () { modes.push(modes.CARET); });
mappings.add([modes.NORMAL], ["<C-c>", "<stop-load>"],
"Stop loading the current web page",
function () { ex.stop(); });
// scrolling
- mappings.add([modes.COMMAND], ["j", "<Down>", "<C-e>", "<scroll-down-line>"],
+ mappings.add([modes.NORMAL], ["j", "<Down>", "<C-e>", "<scroll-down-line>"],
"Scroll document down",
function (args) { buffer.scrollVertical("lines", Math.max(args.count, 1)); },
{ count: true });
- mappings.add([modes.COMMAND], ["k", "<Up>", "<C-y>", "<scroll-up-line>"],
+ mappings.add([modes.NORMAL], ["k", "<Up>", "<C-y>", "<scroll-up-line>"],
"Scroll document up",
function (args) { buffer.scrollVertical("lines", -Math.max(args.count, 1)); },
{ count: true });
mappings.add([modes.COMMAND], dactyl.has("mail") ? ["h", "<scroll-left-column>"] : ["h", "<Left>", "<scroll-left-column>"],
"Scroll document to the left",
function (args) { buffer.scrollHorizontal("columns", -Math.max(args.count, 1)); },
{ count: true });
- mappings.add([modes.COMMAND], dactyl.has("mail") ? ["l", "<scroll-right-column>"] : ["l", "<Right>", "<scroll-right-column>"],
+ mappings.add([modes.NORMAL], dactyl.has("mail") ? ["l", "<scroll-right-column>"] : ["l", "<Right>", "<scroll-right-column>"],
"Scroll document to the right",
function (args) { buffer.scrollHorizontal("columns", Math.max(args.count, 1)); },
{ count: true });
- mappings.add([modes.COMMAND], ["0", "^", "<scroll-begin>"],
+ mappings.add([modes.NORMAL], ["0", "^", "<scroll-begin>"],
"Scroll to the absolute left of the document",
function () { buffer.scrollToPercent(0, null); });
- mappings.add([modes.COMMAND], ["$", "<scroll-end>"],
+ mappings.add([modes.NORMAL], ["$", "<scroll-end>"],
"Scroll to the absolute right of the document",
function () { buffer.scrollToPercent(100, null); });
- mappings.add([modes.COMMAND], ["gg", "<Home>", "<scroll-top>"],
+ mappings.add([modes.NORMAL], ["gg", "<Home>", "<scroll-top>"],
"Go to the top of the document",
function (args) { buffer.scrollToPercent(null, args.count != null ? args.count : 0); },
{ count: true });
- mappings.add([modes.COMMAND], ["G", "<End>", "<scroll-bottom>"],
+ mappings.add([modes.NORMAL], ["G", "<End>", "<scroll-bottom>"],
"Go to the end of the document",
- function (args) { buffer.scrollToPercent(null, args.count != null ? args.count : 100); },
- { count: true });
-
- mappings.add([modes.COMMAND], ["%", "<scroll-percent>"],
+ function (args) {
+ if (args.count)
+ var elem = options.get("linenumbers")
+ .getLine(buffer.focusedFrame.document,
+ args.count);
+ if (elem)
+ elem.scrollIntoView(true);
+ else if (args.count)
+ buffer.scrollToPosition(null, args.count);
+ else
+ buffer.scrollToPercent(null, 100);
+ },
+ { count: true });
+
+ mappings.add([modes.NORMAL], ["%", "<scroll-percent>"],
"Scroll to {count} percent of the document",
function (args) {
dactyl.assert(args.count > 0 && args.count <= 100);
buffer.scrollToPercent(null, args.count);
},
{ count: true });
- mappings.add([modes.COMMAND], ["<C-d>", "<scroll-down>"],
+ mappings.add([modes.NORMAL], ["<C-d>", "<scroll-down>"],
"Scroll window downwards in the buffer",
function (args) { buffer._scrollByScrollSize(args.count, true); },
{ count: true });
- mappings.add([modes.COMMAND], ["<C-u>", "<scroll-up>"],
+ mappings.add([modes.NORMAL], ["<C-u>", "<scroll-up>"],
"Scroll window upwards in the buffer",
function (args) { buffer._scrollByScrollSize(args.count, false); },
{ count: true });
- mappings.add([modes.COMMAND], ["<C-b>", "<PageUp>", "<S-Space>", "<scroll-up-page>"],
+ mappings.add([modes.NORMAL], ["<C-b>", "<PageUp>", "<S-Space>", "<scroll-up-page>"],
"Scroll up a full page",
function (args) { buffer.scrollVertical("pages", -Math.max(args.count, 1)); },
{ count: true });
- mappings.add([modes.COMMAND], ["<Space>"],
+ mappings.add([modes.NORMAL], ["<Space>"],
"Scroll down a full page",
function (args) {
- if (isinstance(content.document.activeElement, [HTMLInputElement, HTMLButtonElement]))
+ if (isinstance((services.focus.focusedWindow || buffer.win).document.activeElement,
+ [Ci.nsIDOMHTMLInputElement,
+ Ci.nsIDOMHTMLButtonElement,
+ Ci.nsIDOMXULButtonElement]))
return Events.PASS;
+
buffer.scrollVertical("pages", Math.max(args.count, 1));
},
{ count: true });
- mappings.add([modes.COMMAND], ["<C-f>", "<PageDown>", "<scroll-down-page>"],
+ mappings.add([modes.NORMAL], ["<C-f>", "<PageDown>", "<scroll-down-page>"],
"Scroll down a full page",
function (args) { buffer.scrollVertical("pages", Math.max(args.count, 1)); },
{ count: true });
mappings.add([modes.NORMAL], ["]f", "<previous-frame>"],
"Focus next frame",
function (args) { buffer.shiftFrameFocus(Math.max(args.count, 1)); },
{ count: true });
@@ -1773,16 +1985,21 @@ var Buffer = Module("buffer", {
function (args) { buffer.shiftFrameFocus(-Math.max(args.count, 1)); },
{ count: true });
mappings.add([modes.NORMAL], ["["],
"Jump to the previous element as defined by 'jumptags'",
function (args) { buffer.findJump(args.arg, args.count, true); },
{ arg: true, count: true });
+ mappings.add([modes.NORMAL], ["g]"],
+ "Jump to the next off-screen element as defined by 'jumptags'",
+ function (args) { buffer.findJump(args.arg, args.count, false, true); },
+ { arg: true, count: true });
+
mappings.add([modes.NORMAL], ["]"],
"Jump to the next element as defined by 'jumptags'",
function (args) { buffer.findJump(args.arg, args.count, false); },
{ arg: true, count: true });
mappings.add([modes.NORMAL], ["{"],
"Jump to the previous paragraph",
function (args) { buffer.findJump("p", args.count, true); },
@@ -1820,45 +2037,48 @@ var Buffer = Module("buffer", {
function (args) {
let elem = buffer.lastInputField;
if (args.count >= 1 || !elem || !events.isContentNode(elem)) {
let xpath = ["frame", "iframe", "input", "xul:textbox", "textarea[not(@disabled) and not(@readonly)]"];
let frames = buffer.allFrames(null, true);
- let elements = array.flatten(frames.map(function (win) [m for (m in util.evaluateXPath(xpath, win.document))]))
+ let elements = array.flatten(frames.map(function (win) [m for (m in DOM.XPath(xpath, win.document))]))
.filter(function (elem) {
- if (isinstance(elem, [HTMLFrameElement, HTMLIFrameElement]))
+ if (isinstance(elem, [Ci.nsIDOMHTMLFrameElement,
+ Ci.nsIDOMHTMLIFrameElement]))
return Editor.getEditor(elem.contentWindow);
- if (elem.readOnly || elem instanceof HTMLInputElement && !Set.has(util.editableInputs, elem.type))
+ elem = DOM(elem);
+
+ if (elem[0].readOnly || !DOM(elem).isEditable)
return false;
- let computedStyle = util.computedStyle(elem);
- let rect = elem.getBoundingClientRect();
- return computedStyle.visibility != "hidden" && computedStyle.display != "none" &&
- (elem instanceof Ci.nsIDOMXULTextBoxElement || computedStyle.MozUserFocus != "ignore") &&
+ let style = elem.style;
+ let rect = elem.rect;
+ return elem.isVisible &&
+ (elem[0] instanceof Ci.nsIDOMXULTextBoxElement || style.MozUserFocus != "ignore") &&
rect.width && rect.height;
});
dactyl.assert(elements.length > 0);
elem = elements[Math.constrain(args.count, 1, elements.length) - 1];
}
buffer.focusElement(elem);
- util.scrollIntoView(elem);
- },
- { count: true });
+ DOM(elem).scrollIntoView();
+ },
+ { count: true });
function url() {
let url = dactyl.clipboardRead();
dactyl.assert(url, _("error.clipboardEmpty"));
let proto = /^([-\w]+):/.exec(url);
- if (proto && "@mozilla.org/network/protocol;1?name=" + proto[1] in Cc && !RegExp(options["urlseparator"]).test(url))
+ if (proto && services.PROTOCOL + proto[1] in Cc && !RegExp(options["urlseparator"]).test(url))
return url.replace(/\s+/g, "");
return url;
}
mappings.add([modes.NORMAL], ["gP"],
"Open (put) a URL based on the current clipboard contents in a new background buffer",
function () {
dactyl.open(url(), { from: "paste", where: dactyl.NEW_TAB, background: true });
@@ -1881,23 +2101,23 @@ var Buffer = Module("buffer", {
"Reload the current web page",
function () { tabs.reload(tabs.getTab(), false); });
mappings.add([modes.NORMAL], ["R", "<full-reload>"],
"Reload while skipping the cache",
function () { tabs.reload(tabs.getTab(), true); });
// yanking
- mappings.add([modes.COMMAND], ["Y", "<yank-word>"],
+ mappings.add([modes.NORMAL], ["Y", "<yank-selection>"],
"Copy selected text or current word",
function () {
let sel = buffer.currentWord;
dactyl.assert(sel);
- dactyl.clipboardWrite(sel, true);
- });
+ editor.setRegister(null, sel, true);
+ });
// zooming
mappings.add([modes.NORMAL], ["zi", "+", "<text-zoom-in>"],
"Enlarge text zoom of current web page",
function (args) { buffer.zoomIn(Math.max(args.count, 1), false); },
{ count: true });
mappings.add([modes.NORMAL], ["zm", "<text-zoom-more>"],
@@ -1950,40 +2170,42 @@ var Buffer = Module("buffer", {
"Print the current file name",
function () { buffer.showPageInfo(false); });
mappings.add([modes.NORMAL], ["g<C-g>", "<more-page-info>"],
"Print file information",
function () { buffer.showPageInfo(true); });
},
options: function initOptions(dactyl, modules, window) {
+ let { Option, buffer, completion, config, options } = modules;
+
options.add(["encoding", "enc"],
"The current buffer's character encoding",
"string", "UTF-8",
{
scope: Option.SCOPE_LOCAL,
- getter: function () config.browser.docShell.QueryInterface(Ci.nsIDocCharset).charset,
+ getter: function () buffer.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 {
- config.browser.docShell.QueryInterface(Ci.nsIDocCharset).charset = val;
- PlacesUtils.history.setCharsetForURI(getWebNavigation().currentURI, val);
- getWebNavigation().reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
+ buffer.docShell.QueryInterface(Ci.nsIDocCharset).charset = val;
+ window.PlacesUtils.history.setCharsetForURI(buffer.uri, val);
+ buffer.docShell.reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
}
catch (e) { dactyl.reportError(e); }
return null;
},
completer: function (context) completion.charset(context)
});
options.add(["iskeyword", "isk"],
- "Regular expression defining which characters constitute word characters",
+ "Regular expression defining which characters constitute words",
"string", '[^\\s.,!?:;/"\'^$%&?()[\\]{}<>#*+|=~_-]',
{
setter: function (value) {
this.regexp = util.regexp(value);
return value;
},
validator: function (value) RegExp(value)
});
@@ -1993,37 +2215,85 @@ var Buffer = Module("buffer", {
"stringmap", {
"p": "p,table,ul,ol,blockquote",
"h": "h1,h2,h3,h4,h5,h6"
},
{
keepQuotes: true,
setter: function (vals) {
for (let [k, v] in Iterator(vals))
- vals[k] = update(new String(v), { matcher: util.compileMatcher(Option.splitList(v)) });
+ vals[k] = update(new String(v), { matcher: DOM.compileMatcher(Option.splitList(v)) });
return vals;
},
- validator: function (value) util.validateMatcher.call(this, value)
+ validator: function (value) DOM.validateMatcher.call(this, value)
&& Object.keys(value).every(function (v) v.length == 1)
});
+ options.add(["linenumbers", "ln"],
+ "Patterns used to determine line numbers used by G",
+ "sitemap", {
+ "code.google.com": '#nums [id^="nums_table"] a[href^="#"]',
+ "github.com": '.line_numbers>*',
+ "mxr.mozilla.org": 'a.l',
+ "pastebin.com": '#code_frame>div>ol>li',
+ "addons.mozilla.org": '.gutter>.line>a',
+ "*": '/* Hgweb/Gitweb */ .completecodeline a.codeline, a.linenr'
+ },
+ {
+ getLine: function getLine(doc, line) {
+ let uri = util.newURI(doc.documentURI);
+ for (let filter in values(this.value))
+ if (filter(uri, doc)) {
+ if (/^func:/.test(filter.result))
+ var res = dactyl.userEval("(" + Option.dequote(filter.result.substr(5)) + ")")(doc, line);
+ else
+ res = iter.nth(filter.matcher(doc),
+ function (elem) (elem.nodeValue || elem.textContent).trim() == line && DOM(elem).display != "none",
+ 0)
+ || iter.nth(filter.matcher(doc), util.identity, line - 1);
+ if (res)
+ break;
+ }
+
+ return res;
+ },
+
+ keepQuotes: true,
+
+ setter: function (vals) {
+ for (let value in values(vals))
+ if (!/^func:/.test(value.result))
+ value.matcher = DOM.compileMatcher(Option.splitList(value.result));
+ return vals;
+ },
+
+ validator: function validate(values) {
+ return this.testValues(values, function (value) {
+ if (/^func:/.test(value))
+ return callable(dactyl.userEval("(" + Option.dequote(value.substr(5)) + ")"));
+ else
+ return DOM.testMatcher(Option.dequote(value));
+ });
+ }
+ });
+
options.add(["nextpattern"],
"Patterns to use when guessing the next page in a document sequence",
- "regexplist", UTF8("'\\bnext\\b',^>$,^(>>|»)$,^(>|»),(>|»)$,'\\bmore\\b'"),
+ "regexplist", UTF8(/'\bnext\b',^>$,^(>>|»)$,^(>|»),(>|»)$,'\bmore\b'/.source),
{ regexpFlags: "i" });
options.add(["previouspattern"],
"Patterns to use when guessing the previous page in a document sequence",
- "regexplist", UTF8("'\\bprev|previous\\b',^<$,^(<<|«)$,^(<|«),(<|«)$"),
+ "regexplist", UTF8(/'\bprev|previous\b',^<$,^(<<|«)$,^(<|«),(<|«)$/.source),
{ regexpFlags: "i" });
options.add(["pageinfo", "pa"],
"Define which sections are shown by the :pageinfo command",
"charlist", "gesfm",
- { get values() values(buffer.pageInfo).toObject() });
+ { get values() values(Buffer.pageInfo).toObject() });
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"],
"Where to show the destination of the link under the cursor",
@@ -2031,19 +2301,233 @@ var Buffer = Module("buffer", {
{
values: {
"": "Don't show link destinations",
"status": "Show link destinations in the status line",
"command": "Show link destinations in the command line"
}
});
+ options.add(["scrolltime", "sct"],
+ "The time, in milliseconds, in which to smooth scroll to a new position",
+ "number", 100);
+
+ options.add(["scrollsteps", "scs"],
+ "The number of steps in which to smooth scroll to a new position",
+ "number", 5,
+ {
+ PREF: "general.smoothScroll",
+
+ initValue: function () {},
+
+ getter: function getter(value) !prefs.get(this.PREF) ? 1 : value,
+
+ setter: function setter(value) {
+ prefs.set(this.PREF, value > 1);
+ if (value > 1)
+ return value;
+ },
+
+ validator: function (value) value > 0
+ });
+
options.add(["usermode", "um"],
"Show current website without styling defined by the author",
"boolean", false,
{
- setter: function (value) config.browser.markupDocumentViewer.authorStyleDisabled = value,
- getter: function () config.browser.markupDocumentViewer.authorStyleDisabled
- });
- }
-});
-
-// vim: set fdm=marker sw=4 ts=4 et:
+ setter: function (value) buffer.contentViewer.authorStyleDisabled = value,
+ getter: function () buffer.contentViewer.authorStyleDisabled
+ });
+
+ options.add(["yankshort", "ys"],
+ "Yank the canonical short URL of a web page where provided",
+ "sitelist", ["youtube.com", "bugzilla.mozilla.org"]);
+ }
+});
+
+Buffer.addPageInfoSection("e", "Search Engines", function (verbose) {
+ let n = 1;
+ let nEngines = 0;
+
+ for (let { document: doc } in values(this.allFrames())) {
+ let engines = DOM("link[href][rel=search][type='application/opensearchdescription+xml']", doc);
+ nEngines += engines.length;
+
+ if (verbose)
+ for (let link in engines)
+ yield [link.title || /*L*/ "Engine " + n++,
+ <a xmlns={XHTML} href={link.href}
+ onclick="if (event.button == 0) { window.external.AddSearchProvider(this.href); return false; }"
+ highlight="URL">{link.href}</a>];
+ }
+
+ if (!verbose && nEngines)
+ yield nEngines + /*L*/" engine" + (nEngines > 1 ? "s" : "");
+});
+
+Buffer.addPageInfoSection("f", "Feeds", function (verbose) {
+ 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 {
+ services.security.checkLoadURIStrWithPrincipal(principal, data.href,
+ services.security.DISALLOW_INHERIT_PRINCIPAL);
+ }
+ catch (e) {
+ isFeed = false;
+ }
+ }
+
+ if (type)
+ data.type = type;
+
+ return isFeed;
+ }
+
+ let nFeed = 0;
+ for (let [i, win] in Iterator(this.allFrames())) {
+ let doc = win.document;
+
+ for (let link in DOM("link[href][rel=feed], link[href][rel=alternate][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 + /*L*/" feed" + (nFeed > 1 ? "s" : "");
+});
+
+Buffer.addPageInfoSection("g", "General Info", function (verbose) {
+ let doc = this.focusedFrame.document;
+
+ // get file size
+ const ACCESS_READ = Ci.nsICache.ACCESS_READ;
+ let cacheKey = doc.documentURI;
+
+ for (let proto in array.iterValues(["HTTP", "FTP"])) {
+ try {
+ var cacheEntryDescriptor = services.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]) + /*L*/" bytes";
+ yield lastMod;
+ return;
+ }
+
+ yield ["Title", doc.title];
+ yield ["URL", template.highlightURL(doc.location.href, 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];
+});
+
+Buffer.addPageInfoSection("m", "Meta Tags", function (verbose) {
+ if (!verbose)
+ return [];
+
+ // get meta tag data, sort and put into pageMeta[]
+ let metaNodes = this.focusedFrame.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]));
+});
+
+Buffer.addPageInfoSection("s", "Security", function (verbose) {
+ let { statusline } = this.modules
+
+ let identity = this.topWindow.gIdentityHandler;
+
+ if (!verbose || !identity)
+ return; // For now
+
+ // Modified from Firefox
+ function location(data) array.compact([
+ data.city, data.state, data.country
+ ]).join(", ");
+
+ switch (statusline.security) {
+ case "secure":
+ case "extended":
+ var data = identity.getIdentityData();
+
+ yield ["Host", identity.getEffectiveHost()];
+
+ if (statusline.security === "extended")
+ yield ["Owner", data.subjectOrg];
+ else
+ yield ["Owner", _("pageinfo.s.ownerUnverified", data.subjectOrg)];
+
+ if (location(data).length)
+ yield ["Location", location(data)];
+
+ yield ["Verified by", data.caOrg];
+
+ if (identity._overrideService.hasMatchingOverride(identity._lastLocation.hostname,
+ (identity._lastLocation.port || 443),
+ data.cert, {}, {}))
+ yield ["User exception", /*L*/"true"];
+ break;
+ }
+});
+
+} catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
+
+endModule();
+
+// vim: set fdm=marker sw=4 ts=4 et ft=javascript:
diff --git a/common/modules/cache.jsm b/common/modules/cache.jsm
new file mode 100644
--- /dev/null
+++ b/common/modules/cache.jsm
@@ -0,0 +1,262 @@
+// Copyright (c) 2011 by Kris Maglione <maglione.k@gmail.com>
+//
+// This work is licensed for reuse under an MIT license. Details are
+// given in the LICENSE.txt file included with this file.
+/* use strict */
+
+Components.utils.import("resource://dactyl/bootstrap.jsm");
+defineModule("cache", {
+ exports: ["Cache", "cache"],
+ require: ["config", "services", "util"]
+}, this);
+
+var Cache = Module("Cache", XPCOM(Ci.nsIRequestObserver), {
+ init: function init() {
+ this.queue = [];
+ this.cache = {};
+ this.providers = {};
+ this.globalProviders = this.providers;
+ this.providing = {};
+ this.localProviders = {};
+
+ if (JSMLoader.cacheFlush)
+ this.flush();
+
+ update(services["dactyl:"].providers, {
+ "cache": function (uri, path) {
+ let contentType = "text/plain";
+ try {
+ contentType = services.mime.getTypeFromURI(uri)
+ }
+ catch (e) {}
+
+ if (!cache.cacheReader || !cache.cacheReader.hasEntry(path))
+ return [contentType, cache.force(path)];
+
+ let channel = services.StreamChannel(uri);
+ channel.contentStream = cache.cacheReader.getInputStream(path);
+ channel.contentType = contentType;
+ channel.contentCharset = "UTF-8";
+ return channel;
+ }
+ });
+ },
+
+ Local: function Local(dactyl, modules, window) ({
+ init: function init() {
+ delete this.instance;
+ this.providers = {};
+ },
+
+ isLocal: true
+ }),
+
+ parse: function parse(str) {
+ if (~'{['.indexOf(str[0]))
+ return JSON.parse(str);
+ return str;
+ },
+
+ stringify: function stringify(obj) {
+ if (isString(obj))
+ return obj;
+ return JSON.stringify(obj);
+ },
+
+ compression: 9,
+
+ cacheFile: Class.Memoize(function () {
+ let dir = File(services.directory.get("ProfD", Ci.nsIFile))
+ .child("dactyl");
+ if (!dir.exists())
+ dir.create(dir.DIRECTORY_TYPE, octal(777));
+ return dir.child("cache.zip");
+ }),
+
+ get cacheReader() {
+ if (!this._cacheReader && this.cacheFile.exists()
+ && !this.inQueue)
+ try {
+ this._cacheReader = services.ZipReader(this.cacheFile);
+ }
+ catch (e if e.result == Cr.NS_ERROR_FILE_CORRUPTED) {
+ util.reportError(e);
+ this.closeWriter();
+ this.cacheFile.remove(false);
+ }
+
+ return this._cacheReader;
+ },
+
+ get inQueue() this._cacheWriter && this._cacheWriter.inQueue,
+
+ getCacheWriter: function () {
+ if (!this._cacheWriter)
+ try {
+ let mode = File.MODE_RDWR;
+ if (!this.cacheFile.exists())
+ mode |= File.MODE_CREATE;
+
+ cache._cacheWriter = services.ZipWriter(this.cacheFile, mode);
+ }
+ catch (e if e.result == Cr.NS_ERROR_FILE_CORRUPTED) {
+ util.reportError(e);
+ this.cacheFile.remove(false);
+
+ mode |= File.MODE_CREATE;
+ cache._cacheWriter = services.ZipWriter(this.cacheFile, mode);
+ }
+ return this._cacheWriter;
+ },
+
+ closeReader: function closeReader() {
+ if (cache._cacheReader) {
+ this.cacheReader.close();
+ delete cache._cacheReader;
+ }
+ },
+
+ closeWriter: function closeWriter() {
+ this.closeReader();
+
+ if (this._cacheWriter) {
+ this._cacheWriter.close();
+ delete cache._cacheWriter;
+
+ // ZipWriter bug.
+ if (this.cacheFile.fileSize <= 22)
+ this.cacheFile.remove(false);
+ }
+ },
+
+ flush: function flush() {
+ cache.cache = {};
+ if (this.cacheFile.exists()) {
+ this.closeReader();
+
+ this.flushJAR(this.cacheFile);
+ this.cacheFile.remove(false);
+ }
+ },
+
+ flushAll: function flushAll(file) {
+ this.flushStartup();
+ this.flush();
+ },
+
+ flushEntry: function flushEntry(name, time) {
+ if (this.cacheReader && this.cacheReader.hasEntry(name)) {
+ if (time && this.cacheReader.getEntry(name).lastModifiedTime / 1000 >= time)
+ return;
+
+ this.queue.push([null, name]);
+ cache.processQueue();
+ }
+
+ delete this.cache[name];
+ },
+
+ flushJAR: function flushJAR(file) {
+ services.observer.notifyObservers(file, "flush-cache-entry", "");
+ },
+
+ flushStartup: function flushStartup() {
+ services.observer.notifyObservers(null, "startupcache-invalidate", "");
+ },
+
+ force: function force(name, localOnly) {
+ util.waitFor(function () !this.inQueue, this);
+
+ if (this.cacheReader && this.cacheReader.hasEntry(name)) {
+ return this.parse(File.readStream(
+ this.cacheReader.getInputStream(name)));
+ }
+
+ if (Set.has(this.localProviders, name) && !this.isLocal) {
+ for each (let { cache } in overlay.modules)
+ if (cache._has(name))
+ return cache.force(name, true);
+ }
+
+ if (Set.has(this.providers, name)) {
+ util.assert(!Set.add(this.providing, name),
+ "Already generating cache for " + name,
+ false);
+ try {
+ let [func, self] = this.providers[name];
+ this.cache[name] = func.call(self || this, name);
+ }
+ finally {
+ delete this.providing[name];
+ }
+
+ cache.queue.push([Date.now(), name]);
+ cache.processQueue();
+
+ return this.cache[name];
+ }
+
+ if (this.isLocal && !localOnly)
+ return cache.force(name);
+ },
+
+ get: function get(name) {
+ if (!Set.has(this.cache, name)) {
+ this.cache[name] = this.force(name);
+ util.assert(this.cache[name] !== undefined,
+ "No such cache key", false);
+ }
+
+ return this.cache[name];
+ },
+
+ _has: function _has(name) Set.has(this.providers, name) || set.has(this.cache, name),
+
+ has: function has(name) [this.globalProviders, this.cache, this.localProviders]
+ .some(function (obj) Set.has(obj, name)),
+
+ register: function register(name, callback, self) {
+ if (this.isLocal)
+ Set.add(this.localProviders, name);
+
+ this.providers[name] = [callback, self];
+ },
+
+ processQueue: function processQueue() {
+ this.closeReader();
+ this.closeWriter();
+
+ if (this.queue.length && !this.inQueue) {
+ // removeEntry does not work properly with queues.
+ for each (let [, entry] in this.queue)
+ if (this.getCacheWriter().hasEntry(entry)) {
+ this.getCacheWriter().removeEntry(entry, false);
+ this.closeWriter();
+ }
+
+ this.queue.splice(0).forEach(function ([time, entry]) {
+ if (time && Set.has(this.cache, entry)) {
+ let stream = services.CharsetConv("UTF-8")
+ .convertToInputStream(this.stringify(this.cache[entry]));
+
+ this.getCacheWriter().addEntryStream(entry, time * 1000,
+ this.compression, stream,
+ true);
+ }
+ }, this);
+
+ if (this._cacheWriter)
+ this.getCacheWriter().processQueue(this, null);
+ }
+ },
+
+ onStopRequest: function onStopRequest() {
+ this.processQueue();
+ }
+});
+
+endModule();
+
+// catch(e){ if (typeof e === "string") e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
+
+// vim: set fdm=marker sw=4 sts=4 et ft=javascript:
diff --git a/common/modules/commands.jsm b/common/modules/commands.jsm
--- a/common/modules/commands.jsm
+++ b/common/modules/commands.jsm
@@ -1,36 +1,36 @@
// Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
// Copyright (c) 2008-2011 by Kris Maglione <maglione.k at Gmail>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
try {
Components.utils.import("resource://dactyl/bootstrap.jsm");
defineModule("commands", {
exports: ["ArgType", "Command", "Commands", "CommandOption", "Ex", "commands"],
- require: ["contexts", "messages", "util"],
- use: ["config", "options", "services", "template"]
+ require: ["contexts", "messages", "util"]
}, this);
/**
* A structure representing the options available for a command.
*
* Do NOT create instances of this class yourself, use the helper method
* {@see Commands#add} instead
*
* @property {[string]} names An array of option names. The first name
* is the canonical option name.
* @property {number} type The option's value type. This is one of:
* (@link CommandOption.NOARG),
* (@link CommandOption.STRING),
+ * (@link CommandOption.STRINGMAP),
* (@link CommandOption.BOOL),
* (@link CommandOption.INT),
* (@link CommandOption.FLOAT),
* (@link CommandOption.LIST),
* (@link CommandOption.ANY)
* @property {function} validator A validator function
* @property {function (CompletionContext, object)} completer A list of
* completions, or a completion function which will be passed a
@@ -68,16 +68,21 @@ update(CommandOption, {
*/
BOOL: ArgType("boolean", function parseBoolArg(val) Commands.parseBool(val)),
/**
* @property {object} The option accepts a string argument.
* @final
*/
STRING: ArgType("string", function (val) val),
/**
+ * @property {object} The option accepts a stringmap argument.
+ * @final
+ */
+ STRINGMAP: ArgType("stringmap", function (val, quoted) Option.parse.stringmap(quoted)),
+ /**
* @property {object} The option accepts an integer argument.
* @final
*/
INT: ArgType("int", function parseIntArg(val) parseInt(val)),
/**
* @property {object} The option accepts a float argument.
* @final
*/
@@ -113,33 +118,28 @@ update(CommandOption, {
* serialize - see {@link Command#serialize}
* subCommand - see {@link Command#subCommand}
* @optional
* @private
*/
var Command = Class("Command", {
init: function init(specs, description, action, extraInfo) {
specs = Array.concat(specs); // XXX
- let parsedSpecs = extraInfo.parsedSpecs || Command.parseSpecs(specs);
this.specs = specs;
- this.shortNames = array.compact(parsedSpecs.map(function (n) n[1]));
- this.longNames = parsedSpecs.map(function (n) n[0]);
- this.name = this.longNames[0];
- this.names = array.flatten(parsedSpecs);
this.description = description;
this.action = action;
+ if (extraInfo.options)
+ this._options = extraInfo.options;
+ delete extraInfo.options;
+
if (extraInfo)
this.update(extraInfo);
- if (this.options)
- this.options = this.options.map(CommandOption.fromArray, CommandOption);
- for each (let option in this.options)
- option.localeName = ["command", this.name, option.names[0]];
- },
+ },
get toStringParams() [this.name, this.hive.name],
get identifier() this.hive.prefix + this.name,
get helpTag() ":" + this.name,
get lastCommand() this._lastCommand || this.modules.commandline.command,
@@ -160,18 +160,21 @@ var Command = Class("Command", {
modifiers = modifiers || {};
if (args.count != null && !this.count)
throw FailedAssertion(_("command.noCount"));
if (args.bang && !this.bang)
throw FailedAssertion(_("command.noBang"));
+ args.doc = this.hive.group.lastDocument;
+
return !dactyl.trapErrors(function exec() {
let extra = this.hive.argsExtra(args);
+
for (let k in properties(extra))
if (!(k in args))
Object.defineProperty(args, k, Object.getOwnPropertyDescriptor(extra, k));
if (this.always)
this.always(args, modifiers);
if (!context || !context.noExecute)
@@ -180,18 +183,17 @@ var Command = Class("Command", {
},
/**
* Returns whether this command may be invoked via *name*.
*
* @param {string} name The candidate name.
* @returns {boolean}
*/
- hasName: function hasName(name) this.parsedSpecs.some(
- function ([long, short]) name.indexOf(short) == 0 && long.indexOf(name) == 0),
+ hasName: function hasName(name) Command.hasName(this.parsedSpecs, name),
/**
* A helper function to parse an argument string.
*
* @param {string} args The argument string to parse.
* @param {CompletionContext} complete A completion context.
* Non-null when the arguments are being parsed for completion
* purposes.
@@ -201,33 +203,37 @@ var Command = Class("Command", {
* @see Commands#parseArgs
*/
parseArgs: function parseArgs(args, complete, extra) this.modules.commands.parseArgs(args, {
__proto__: this,
complete: complete,
extra: extra
}),
- complained: Class.memoize(function () ({})),
+ complained: Class.Memoize(function () ({})),
/**
* @property {[string]} All of this command's name specs. e.g., "com[mand]"
*/
specs: null,
+ parsedSpecs: Class.Memoize(function () Command.parseSpecs(this.specs)),
+
/** @property {[string]} All of this command's short names, e.g., "com" */
- shortNames: null,
+ shortNames: Class.Memoize(function () array.compact(this.parsedSpecs.map(function (n) n[1]))),
+
/**
* @property {[string]} All of this command's long names, e.g., "command"
*/
- longNames: null,
+ longNames: Class.Memoize(function () this.parsedSpecs.map(function (n) n[0])),
/** @property {string} The command's canonical name. */
- name: null,
+ name: Class.Memoize(function () this.longNames[0]),
+
/** @property {[string]} All of this command's long and short names. */
- names: null,
+ names: Class.Memoize(function () this.names = array.flatten(this.parsedSpecs)),
/** @property {string} This command's description, as shown in :listcommands */
description: Messages.Localized(""),
/** @property {string|null} If set, the deprecation message for this command. */
deprecated: Messages.Localized(null),
/**
@@ -270,61 +276,71 @@ var Command = Class("Command", {
* passed literally. This is especially useful for commands which take
* key mappings or Ex command lines as arguments.
*/
literal: null,
/**
* @property {Array} The options this command takes.
* @see Commands@parseArguments
*/
- options: [],
-
- optionMap: Class.memoize(function () array(this.options)
+ options: Class.Memoize(function ()
+ this._options.map(function (opt) {
+ let option = CommandOption.fromArray(opt);
+ option.localeName = ["command", this.name, option.names[0]];
+ return option;
+ }, this)),
+ _options: [],
+
+ optionMap: Class.Memoize(function () array(this.options)
.map(function (opt) opt.names.map(function (name) [name, opt]))
.flatten().toObject()),
newArgs: function newArgs(base) {
let res = [];
update(res, base);
res.__proto__ = this.argsPrototype;
return res;
},
- argsPrototype: Class.memoize(function argsPrototype() {
+ argsPrototype: Class.Memoize(function argsPrototype() {
let res = update([], {
__iterator__: function AP__iterator__() array.iterItems(this),
command: this,
- explicitOpts: Class.memoize(function () ({})),
+ explicitOpts: Class.Memoize(function () ({})),
has: function AP_has(opt) Set.has(this.explicitOpts, opt) || typeof opt === "number" && Set.has(this, opt),
get literalArg() this.command.literal != null && this[this.command.literal] || "",
- // TODO: string: Class.memoize(function () { ... }),
+ // TODO: string: Class.Memoize(function () { ... }),
verify: function verify() {
if (this.command.argCount) {
util.assert((this.length > 0 || !/^[1+]$/.test(this.command.argCount)) &&
(this.literal == null || !/[1+]/.test(this.command.argCount) || /\S/.test(this.literalArg || "")),
_("error.argumentRequired"));
util.assert((this.length == 0 || this.command.argCount !== "0") &&
(this.length <= 1 || !/^[01?]$/.test(this.command.argCount)),
_("error.trailingCharacters"));
}
}
});
this.options.forEach(function (opt) {
- if (opt.default !== undefined)
- Object.defineProperty(res, opt.names[0],
- Object.getOwnPropertyDescriptor(opt, "default") ||
- { configurable: true, enumerable: true, get: function () opt.default });
+ if (opt.default !== undefined) {
+ let prop = Object.getOwnPropertyDescriptor(opt, "default") ||
+ { configurable: true, enumerable: true, get: function () opt.default };
+
+ if (prop.get && !prop.set)
+ prop.set = function (val) { Class.replaceProperty(this, opt.names[0], val) };
+ Object.defineProperty(res, opt.names[0], prop);
+ }
});
return res;
}),
/**
* @property {boolean|function(args)} When true, invocations of this
* command may contain private data which should be purged from
@@ -368,16 +384,20 @@ var Command = Class("Command", {
*/
warn: function warn(context, type, message) {
let loc = !context ? "" : [context.file, context.line, " "].join(":");
if (!Set.add(this.complained, type + ":" + (context ? context.file : "[Command Line]")))
this.modules.dactyl.warn(loc + message);
}
}, {
+ hasName: function hasName(specs, name)
+ specs.some(function ([long, short])
+ name.indexOf(short) == 0 && long.indexOf(name) == 0),
+
// TODO: do we really need more than longNames as a convenience anyway?
/**
* Converts command name abbreviation specs of the form
* 'shortname[optional-tail]' to short and long versions:
* ["abc[def]", "ghijkl"] -> [["abcdef", "abc"], ["ghijlk"]]
*
* @param {Array} specs An array of command name specs to parse.
* @returns {Array}
@@ -445,22 +465,65 @@ var Ex = Module("Ex", {
},
__noSuchMethod__: function __noSuchMethod__(meth, args) this._run(meth).apply(this, args)
});
var CommandHive = Class("CommandHive", Contexts.Hive, {
init: function init(group) {
init.supercall(this, group);
+
this._map = {};
this._list = [];
- },
+ this._specs = [];
+ },
+
+ /**
+ * Caches this command hive.
+ */
+
+ cache: function cache() {
+ let self = this;
+ let { cache } = this.modules;
+ this.cached = true;
+
+ cache.register(this.cacheKey, function () {
+ self.cached = false;
+ this.modules.moduleManager.initDependencies("commands");
+
+ let map = {};
+ for (let [name, cmd] in Iterator(self._map))
+ if (cmd.sourceModule)
+ map[name] = { sourceModule: cmd.sourceModule, isPlaceholder: true };
+
+ let specs = [];
+ for (let cmd in values(self._list))
+ for each (let spec in cmd.parsedSpecs)
+ specs.push(spec.concat(cmd.name));
+
+ return { map: map, specs: specs };
+ });
+
+ let cached = cache.get(this.cacheKey);
+ if (this.cached) {
+ this._specs = cached.specs;
+ for (let [k, v] in Iterator(cached.map))
+ this._map[k] = v;
+ }
+ },
+
+ get cacheKey() "commands/hives/" + this.name + ".json",
/** @property {Iterator(Command)} @private */
- __iterator__: function __iterator__() array.iterValues(this._list.sort(function (a, b) a.name > b.name)),
+ __iterator__: function __iterator__() {
+ if (this.cached)
+ this.modules.initDependencies("commands");
+ this.cached = false;
+ return array.iterValues(this._list.sort(function (a, b) a.name > b.name))
+ },
/** @property {string} The last executed Ex command line. */
repeat: null,
/**
* Adds a new command to the builtin hive. Accessible only to core
* dactyl code. Plugins should use group.commands.add instead.
*
@@ -473,39 +536,44 @@ var CommandHive = Class("CommandHive", C
* @optional
*/
add: function add(specs, description, action, extra, replace) {
const { commands, contexts } = this.modules;
extra = extra || {};
if (!extra.definedAt)
extra.definedAt = contexts.getCaller(Components.stack.caller);
+ if (!extra.sourceModule)
+ extra.sourceModule = commands.currentDependency;
extra.hive = this;
extra.parsedSpecs = Command.parseSpecs(specs);
let names = array.flatten(extra.parsedSpecs);
let name = names[0];
+ if (this.name != "builtin") {
util.assert(!names.some(function (name) name in commands.builtin._map),
_("command.cantReplace", name));
util.assert(replace || names.every(function (name) !(name in this._map), this),
_("command.wontReplace", name));
+ }
for (let name in values(names)) {
ex.__defineGetter__(name, function () this._run(name));
- if (name in this._map)
+ if (name in this._map && !this._map[name].isPlaceholder)
this.remove(name);
}
let self = this;
let closure = function () self._map[name];
memoize(this._map, name, function () commands.Command(specs, description, action, extra));
+ if (!extra.hidden)
memoize(this._list, this._list.length, closure);
for (let alias in values(names.slice(1)))
memoize(this._map, alias, closure);
return name;
},
_add: function _add(names, description, action, extra, replace) {
@@ -530,19 +598,32 @@ var CommandHive = Class("CommandHive", C
* Returns the command with matching *name*.
*
* @param {string} name The name of the command to return. This can be
* any of the command's names.
* @param {boolean} full If true, only return a command if one of
* its names matches *name* exactly.
* @returns {Command}
*/
- get: function get(name, full) this._map[name]
+ get: function get(name, full) {
+ let cmd = this._map[name]
|| !full && array.nth(this._list, function (cmd) cmd.hasName(name), 0)
- || null,
+ || null;
+
+ if (!cmd && full) {
+ let name = array.nth(this.specs, function (spec) Command.hasName(spec, name), 0);
+ return name && this.get(name);
+ }
+
+ if (cmd && cmd.isPlaceholder) {
+ this.modules.moduleManager.initDependencies("commands", [cmd.sourceModule]);
+ cmd = this._map[name];
+ }
+ return cmd;
+ },
/**
* Remove the user-defined command with matching *name*.
*
* @param {string} name The name of the command to remove. This can be
* any of the command's names.
*/
remove: function remove(name) {
@@ -555,27 +636,35 @@ var CommandHive = Class("CommandHive", C
}
});
/**
* @instance commands
*/
var Commands = Module("commands", {
lazyInit: true,
+ lazyDepends: true,
Local: function Local(dactyl, modules, window) let ({ Group, contexts } = modules) ({
init: function init() {
this.Command = Class("Command", Command, { modules: modules });
update(this, {
hives: contexts.Hives("commands", Class("CommandHive", CommandHive, { modules: modules })),
user: contexts.hives.commands.user,
builtin: contexts.hives.commands.builtin
});
},
+ reallyInit: function reallyInit() {
+ if (false)
+ this.builtin.cache();
+ else
+ this.modules.moduleManager.initDependencies("commands");
+ },
+
get context() contexts.context,
get readHeredoc() modules.io.readHeredoc,
get allHives() contexts.allGroups.commands,
get userHives() this.allHives.filter(function (h) h !== this.builtin, this),
@@ -742,32 +831,36 @@ var Commands = Module("commands", {
let res = [args.command + (args.bang ? "!" : "")];
let defaults = {};
if (args.ignoreDefaults)
defaults = array(this.options).map(function (opt) [opt.names[0], opt.default])
.toObject();
for (let [opt, val] in Iterator(args.options || {})) {
+ if (val === undefined)
+ continue;
if (val != null && defaults[opt] === val)
continue;
+
let chr = /^-.$/.test(opt) ? " " : "=";
if (isArray(val))
opt += chr + Option.stringify.stringlist(val);
else if (val != null)
opt += chr + Commands.quote(val);
res.push(opt);
}
+
for (let [, arg] in Iterator(args.arguments || []))
res.push(Commands.quote(arg));
let str = args.literalArg;
if (str)
res.push(!/\n/.test(str) ? str :
- this.hereDoc && false ? "<<EOF\n" + String.replace(str, /\n$/, "") + "\nEOF"
+ this.serializeHereDoc ? "<<EOF\n" + String.replace(str, /\n$/, "") + "\nEOF"
: String.replace(str, /\n/g, "\n" + res[0].replace(/./g, " ").replace(/.$/, "\\")));
return res.join(" ");
},
/**
* Returns the command with matching *name*.
*
* @param {string} name The name of the command to return. This can be
@@ -1161,26 +1254,26 @@ var Commands = Module("commands", {
Ufe30-Ufe4f // CJK Compatibility Forms
Ufe50-Ufe6f // Small Form Variants
Ufe70-Ufeff // Arabic Presentation Forms-B
Uff00-Uffef // Halfwidth and Fullwidth Forms
Ufff0-Uffff // Specials
]]>, /U/g, "\\u"), "x")
}),
- validName: Class.memoize(function validName() util.regexp("^" + this.nameRegexp.source + "$")),
-
- commandRegexp: Class.memoize(function commandRegexp() util.regexp(<![CDATA[
+ validName: Class.Memoize(function validName() util.regexp("^" + this.nameRegexp.source + "$")),
+
+ commandRegexp: Class.Memoize(function commandRegexp() util.regexp(<![CDATA[
^
(?P<spec>
(?P<prespace> [:\s]*)
(?P<count> (?:\d+ | %)? )
(?P<fullCmd>
(?: (?P<group> <name>) : )?
- (?P<cmd> (?:<name> | !)? ))
+ (?P<cmd> (?:-? [()] | <name> | !)? ))
(?P<bang> !?)
(?P<space> \s*)
)
(?P<args>
(?:. | \n)*?
)?
$
]]>, "x", {
@@ -1235,20 +1328,18 @@ var Commands = Module("commands", {
else if (group = contexts.getGroup(group, "commands"))
command = group.get(cmd || "");
if (command == null) {
yield [null, { commandString: str }];
return;
}
- if (complete) {
- complete.fork(command.name);
- var context = complete.fork("args", len);
- }
+ if (complete)
+ var context = complete.fork(command.name).fork("opts", len);;
if (!complete || /(\w|^)[!\s]/.test(str))
args = command.parseArgs(args, context, { count: count, bang: bang });
else
args = this.parseArgs(args, { extra: { count: count, bang: bang } });
args.context = this.context;
args.commandName = cmd;
args.commandString = str.substr(0, len) + args.string;
@@ -1363,59 +1454,100 @@ var Commands = Module("commands", {
// dynamically get completions as specified with the command's completer function
context.highlight();
if (!command) {
context.message = _("command.noSuch", match.cmd);
context.highlight(0, match.cmd.length, "SPELLCHECK");
return;
}
- let cmdContext = context.fork(command.name, match.fullCmd.length + match.bang.length + match.space.length);
+ let cmdContext = context.fork(command.name + "/args", match.fullCmd.length + match.bang.length + match.space.length);
try {
if (!cmdContext.waitingForTab) {
if (!args.completeOpt && command.completer && args.completeStart != null) {
cmdContext.advance(args.completeStart);
cmdContext.quote = args.quote;
cmdContext.filter = args.completeFilter;
command.completer.call(command, cmdContext, args);
}
}
}
catch (e) {
util.reportError(e);
cmdContext.message = _("error.error", e);
}
};
+ completion.exMacro = function exMacro(context, args, cmd) {
+ if (!cmd.action.macro)
+ return;
+ let { macro } = cmd.action;
+
+ let start = "«%-d-]'", end = "'[-d-%»";
+
+ let n = /^\d+$/.test(cmd.argCount) ? parseInt(cmd.argCount) : 12;
+ for (let i = args.completeArg; i < n; i++)
+ args[i] = start + i + end;
+
+ let params = {
+ args: { __proto__: args, toString: function () this.join(" ") },
+ bang: args.bang ? "!" : "",
+ count: args.count
+ };
+
+ if (!macro.valid(params))
+ return;
+
+ let str = macro(params);
+ let idx = str.indexOf(start);
+ if (!~idx || !/^(')?(\d+)'/.test(str.substr(idx + start.length))
+ || RegExp.$2 != args.completeArg)
+ return;
+
+ let quote = RegExp.$2;
+ context.quote = null;
+ context.offset -= idx;
+ context.filter = str.substr(0, idx) + (quote ? Option.quote : util.identity)(context.filter);
+
+ context.fork("ex", 0, completion, "ex");
+ };
+
completion.userCommand = function userCommand(context, group) {
context.title = ["User Command", "Definition"];
context.keys = { text: "name", description: "replacementText" };
context.completions = group || modules.commands.user;
};
},
commands: function initCommands(dactyl, modules, window) {
const { commands, contexts } = modules;
+ commands.add(["(", "-("], "",
+ function (args) { dactyl.echoerr(_("dactyl.cheerUp")); },
+ { hidden: true });
+ commands.add([")", "-)"], "",
+ function (args) { dactyl.echoerr(_("dactyl.somberDown")); },
+ { hidden: true });
+
commands.add(["com[mand]"],
"List or define commands",
function (args) {
let cmd = args[0];
util.assert(!cmd || cmd.split(",").every(commands.validName.closure.test),
_("command.invalidName", cmd));
if (args.length <= 1)
commands.list(cmd, args.explicitOpts["-group"] ? [args["-group"]] : null);
else {
util.assert(args["-group"].modifiable,
_("group.cantChangeBuiltin", _("command.commands")));
let completer = args["-complete"];
- let completerFunc = null; // default to no completion for user commands
+ let completerFunc = function (context, args) modules.completion.exMacro(context, args, this);
if (completer) {
if (/^custom,/.test(completer)) {
completer = completer.substr(7);
if (contexts.context)
var ctxt = update({}, contexts.context || {});
completerFunc = function (context) {
@@ -1434,17 +1566,17 @@ var Commands = Module("commands", {
}
let added = args["-group"].add(cmd.split(","),
args["-description"],
contexts.bindMacro(args, "-ex",
function makeParams(args, modifiers) ({
args: {
__proto__: args,
- toString: function () this.string,
+ toString: function () this.string
},
bang: this.bang && args.bang ? "!" : "",
count: this.count && args.count
})),
{
argCount: args["-nargs"],
bang: args["-bang"],
count: args["-count"],
@@ -1500,17 +1632,17 @@ var Commands = Module("commands", {
description: "The allowed number of arguments",
completer: [["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 are allowed"]],
default: "0",
type: CommandOption.STRING,
- validator: function (arg) /^[01*?+]$/.test(arg)
+ validator: bind("test", /^[01*?+]$/)
},
{
names: ["-nopersist", "-n"],
description: "Do not save this command to an auto-generated RC file"
}
],
literal: 1,
@@ -1571,17 +1703,17 @@ var Commands = Module("commands", {
description: "List all Ex commands along with their short descriptions",
index: "ex-cmd",
iterate: function (args) commands.iterator().map(function (cmd) ({
__proto__: cmd,
columns: [
cmd.hive == commands.builtin ? "" : <span highlight="Object" style="padding-right: 1em;">{cmd.hive.name}</span>
]
})),
- iterateIndex: function (args) let (tags = services["dactyl:"].HELP_TAGS)
+ iterateIndex: function (args) let (tags = help.tags)
this.iterate(args).filter(function (cmd) cmd.hive === commands.builtin || Set.has(tags, cmd.helpTag)),
format: {
headings: ["Command", "Group", "Description"],
description: function (cmd) template.linkifyHelp(cmd.description + (cmd.replacementText ? ": " + cmd.action : "")),
help: function (cmd) ":" + cmd.name
}
});
@@ -1601,19 +1733,19 @@ var Commands = Module("commands", {
argCount: "1",
completer: function (context) modules.completion[/^:/.test(context.filter) ? "ex" : "javascript"](context),
literal: 0
});
},
javascript: function initJavascript(dactyl, modules, window) {
const { JavaScript, commands } = modules;
- JavaScript.setCompleter([commands.user.get, commands.user.remove],
+ JavaScript.setCompleter([CommandHive.prototype.get, CommandHive.prototype.remove],
[function () [[c.names, c.description] for (c in this)]]);
- JavaScript.setCompleter([commands.get],
+ JavaScript.setCompleter([Commands.prototype.get],
[function () [[c.names, c.description] for (c in this.iterator())]]);
},
mappings: function initMappings(dactyl, modules, window) {
const { commands, mappings, modes } = modules;
mappings.add([modes.COMMAND],
["@:"], "Repeat the last Ex command",
function (args) {
diff --git a/common/modules/completion.jsm b/common/modules/completion.jsm
--- a/common/modules/completion.jsm
+++ b/common/modules/completion.jsm
@@ -1,22 +1,19 @@
// Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
// Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
-
-try {
+/* use strict */
Components.utils.import("resource://dactyl/bootstrap.jsm");
defineModule("completion", {
- exports: ["CompletionContext", "Completion", "completion"],
- use: ["config", "messages", "template", "util"]
+ exports: ["CompletionContext", "Completion", "completion"]
}, this);
/**
* Creates a new completion context.
*
* @class A class to provide contexts for command completion.
* Manages the filtering and formatting of completions, and keeps
* track of the positions and quoting of replacement text. Allows for
@@ -207,58 +204,76 @@ var CompletionContext = Class("Completio
* @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;
},
+ __title: Class.Memoize(function () this._title.map(function (s)
+ typeof s == "string" ? messages.get("completion.title." + s, s)
+ : s)),
+
+ set title(val) {
+ delete this.__title;
+ return this._title = val;
+ },
+ get title() this.__title,
+
+ get activeContexts() this.contextList.filter(function (c) c.items.length),
+
// Temporary
/**
* @property {Object}
*
* An object describing the results from all sub-contexts. Results are
* adjusted so that all have the same starting offset.
*
* @deprecated
*/
get allItems() {
+ let self = this;
+
try {
- let self = this;
- let allItems = this.contextList.map(function (context) context.hasItems && context.items);
+ let allItems = this.contextList.map(function (context) context.hasItems && context.items.length);
if (this.cache.allItems && array.equals(this.cache.allItems, allItems))
return this.cache.allItemsResult;
this.cache.allItems = allItems;
- let minStart = Math.min.apply(Math, [context.offset for ([k, context] in Iterator(this.contexts)) if (context.hasItems && context.items.length)]);
+ let minStart = Math.min.apply(Math, this.activeContexts.map(function (c) c.offset));
if (minStart == Infinity)
minStart = 0;
- let items = this.contextList.map(function (context) {
- if (!context.hasItems)
- return [];
+
+ this.cache.allItemsResult = memoize({
+ start: minStart,
+
+ get longestSubstring() self.longestAllSubstring,
+
+ get items() array.flatten(self.activeContexts.map(function (context) {
let prefix = self.value.substring(minStart, context.offset);
+
return context.items.map(function (item) ({
text: prefix + item.text,
result: prefix + item.result,
__proto__: item
}));
+ }))
});
- this.cache.allItemsResult = { start: minStart, items: array.flatten(items) };
- memoize(this.cache.allItemsResult, "longestSubstring", function () self.longestAllSubstring);
+
return this.cache.allItemsResult;
}
catch (e) {
util.reportError(e);
return { start: 0, items: [], longestAllSubstring: "" };
}
},
// Temporary
get allSubstrings() {
- let contexts = this.contextList.filter(function (c) c.hasItems && c.items.length);
+ let contexts = this.activeContexts;
let minStart = Math.min.apply(Math, contexts.map(function (c) c.offset));
let lists = contexts.map(function (context) {
let prefix = context.value.substring(minStart, context.offset);
return context.substrings.map(function (s) prefix + s);
});
/* TODO: Deal with sub-substrings for multiple contexts again.
* Possibly.
@@ -290,21 +305,23 @@ var CompletionContext = Class("Completio
items = [x for (x in Iterator(items || []))];
if (this._completions !== items) {
delete this.cache.filtered;
delete this.cache.filter;
this.cache.rows = [];
this._completions = items;
this.itemCache[this.key] = items;
}
+
if (this._completions)
this.hasItems = this._completions.length > 0;
+
if (this.updateAsync && !this.noUpdate)
- this.onUpdate();
- },
+ util.trapErrors("onUpdate", this);
+ },
get createRow() this._createRow || template.completionRow, // XXX
set createRow(createRow) this._createRow = createRow,
get filterFunc() this._filterFunc || util.identity,
set filterFunc(val) this._filterFunc = val,
get filter() this._filter != null ? this._filter : this.value.substr(this.offset, this.caret),
@@ -333,29 +350,34 @@ var CompletionContext = Class("Completio
*/
get message() this._message || (this.waitingForTab && this.hasItems !== false ? _("completion.waitingFor", "<Tab>") : null),
set message(val) this._message = val,
/**
* The prototype object for items returned by {@link items}.
*/
get itemPrototype() {
+ let self = this;
let res = { highlight: "" };
+
function result(quote) {
+ yield ["context", function () self];
yield ["result", quote ? function () quote[0] + util.trapErrors(1, quote, this.text) + quote[2]
: function () this.text];
+ yield ["texts", function () Array.concat(this.text)];
};
+
for (let i in iter(this.keys, result(this.quote))) {
let [k, v] = i;
if (typeof v == "string" && /^[.[]/.test(v))
// This is only allowed to be a simple accessor, and shouldn't
// reference any variables. Don't bother with eval context.
v = Function("i", "return i" + v);
if (typeof v == "function")
- res.__defineGetter__(k, function () Class.replaceProperty(this, k, v.call(this, this.item)));
+ res.__defineGetter__(k, function () Class.replaceProperty(this, k, v.call(this, this.item, self)));
else
res.__defineGetter__(k, function () Class.replaceProperty(this, k, this.item[v]));
res.__defineSetter__(k, function (val) Class.replaceProperty(this, k, val));
}
return res;
},
/**
@@ -400,17 +422,17 @@ var CompletionContext = Class("Completio
}
}
// XXX
this.noUpdate = true;
this.completions = this.itemCache[this.key];
this.noUpdate = false;
},
- ignoreCase: Class.memoize(function () {
+ ignoreCase: Class.Memoize(function () {
let mode = this.wildcase;
if (mode == "match")
return false;
else if (mode == "ignore")
return true;
else
return !/[A-Z]/.test(this.filter);
}),
@@ -464,17 +486,17 @@ var CompletionContext = Class("Completio
this.matchString = this.anchored ?
function (filter, str) String.indexOf(str, filter) == 0 :
function (filter, str) String.indexOf(str, filter) >= 0;
// Item formatters
this.processor = Array.slice(this.process);
if (!this.anchored)
this.processor[0] = function (item, text) self.process[0].call(self, item,
- template.highlightFilter(item.text, self.filter));
+ template.highlightFilter(item.text, self.filter, null, item.isURI));
try {
// Item prototypes
if (!this._cache.constructed) {
let proto = this.itemPrototype;
this._cache.constructed = items.map(function (item) ({ __proto__: proto, item: item }));
}
@@ -537,20 +559,21 @@ var CompletionContext = Class("Completio
}
substrings = items.reduce(function (res, item)
res.map(function (substring) {
// A simple binary search to find the longest substring
// of the given string which also matches the current
// item's text.
let len = substring.length;
- let i = 0, n = len;
+ let i = 0, n = len + 1;
+ let result = n && fixCase(item.result);
while (n) {
let m = Math.floor(n / 2);
- let keep = compare(fixCase(item.text), substring.substring(0, i + m));
+ let keep = compare(result, substring.substring(0, i + m));
if (!keep)
len = i + m - 1;
if (!keep || m == 0)
n = m;
else {
i += m;
n = n - m;
}
@@ -584,18 +607,18 @@ var CompletionContext = Class("Completio
if (this.quote && count) {
advance = this.quote[1](this.filter.substr(0, count)).length;
count = this.quote[0].length + advance;
this.quote[0] = "";
this.quote[2] = "";
}
this.offset += count;
if (this._filter)
- this._filter = this._filter.substr(advance);
- },
+ this._filter = this._filter.substr(arguments[0] || 0);
+ },
/**
* Calls the {@link #cancel} method of all currently active
* sub-contexts.
*/
cancelAll: function () {
for (let [, context] in Iterator(this.contextList)) {
if (context.cancel)
@@ -619,26 +642,49 @@ var CompletionContext = Class("Completio
getItems: function getItems(start, end) {
let items = this.items;
let step = start > end ? -1 : 1;
start = Math.max(0, start || 0);
end = Math.min(items.length, end ? end : items.length);
return iter.map(util.range(start, end, step), function (i) items[i]);
},
+ getRow: function getRow(idx, doc) {
+ let cache = this.cache.rows;
+ if (cache) {
+ if (idx in this.items && !(idx in this.cache.rows))
+ try {
+ cache[idx] = util.xmlToDom(this.createRow(this.items[idx]),
+ doc || this.doc);
+ }
+ catch (e) {
+ util.reportError(e);
+ cache[idx] = util.xmlToDom(
+ <div highlight="CompItem" style="white-space: nowrap">
+ <li highlight="CompResult">{this.text}&#xa0;</li>
+ <li highlight="CompDesc ErrorMsg">{e}&#xa0;</li>
+ </div>, doc || this.doc);
+ }
+ return cache[idx];
+ }
+ },
+
getRows: function getRows(start, end, doc) {
let self = this;
let items = this.items;
let cache = this.cache.rows;
let step = start > end ? -1 : 1;
+
start = Math.max(0, start || 0);
end = Math.min(items.length, end != null ? end : items.length);
+
+ this.doc = doc;
for (let i in util.range(start, end, step))
- yield [i, cache[i] = cache[i] || util.xmlToDom(self.createRow(items[i]), doc)];
- },
+ yield [i, this.getRow(i)];
+ },
/**
* Forks this completion context to create a new sub-context named
* as {this.name}/{name}. The new context is automatically advanced
* *offset* characters. If *completer* is provided, it is called
* with *self* as its 'this' object, the new context as its first
* argument, and any subsequent arguments after *completer* as its
* following arguments.
@@ -818,17 +864,17 @@ var CompletionContext = Class("Completio
}, {
Sort: {
number: function (a, b) parseInt(a.text) - parseInt(b.text) || String.localeCompare(a.text, b.text),
unsorted: null
},
Filter: {
text: function (item) {
- let text = Array.concat(item.text);
+ let text = item.texts;
for (let [i, str] in Iterator(text)) {
if (this.match(String(str))) {
item.text = String(text[i]);
return true;
}
}
return false;
},
@@ -845,16 +891,17 @@ var Completion = Module("completion", {
init: function () {
},
get setFunctionCompleter() JavaScript.setCompleter, // Backward compatibility
Local: function (dactyl, modules, window) ({
urlCompleters: {},
+ get modules() modules,
get options() modules.options,
// FIXME
_runCompleter: function _runCompleter(name, filter, maxItems) {
let context = modules.CompletionContext(filter);
context.maxItems = maxItems;
let res = context.fork.apply(context, ["run", 0, this, name].concat(Array.slice(arguments, 3)));
if (res) {
@@ -873,17 +920,17 @@ var Completion = Module("completion", {
listCompleter: function listCompleter(name, filter, maxItems) {
let context = modules.CompletionContext(filter || "");
context.maxItems = maxItems;
context.fork.apply(context, ["list", 0, this, name].concat(Array.slice(arguments, 3)));
context = context.contexts["/list"];
context.wait(null, true);
- let contexts = context.contextList.filter(function (c) c.hasItems && c.items.length);
+ let contexts = context.activeContexts;
if (!contexts.length)
contexts = context.contextList.filter(function (c) c.hasItems).slice(0, 1);
if (!contexts.length)
contexts = context.contextList.slice(-1);
modules.commandline.commandOutput(
<div highlight="Completions">
{ template.map(contexts, function (context)
@@ -915,38 +962,98 @@ var Completion = Module("completion", {
var skip = util.regexp("^.*" + this.options["urlseparator"] + "\\s*")
.exec(context.filter);
if (skip)
context.advance(skip[0].length);
if (/^about:/.test(context.filter))
context.fork("about", 6, this, function (context) {
+ context.title = ["about:"];
context.generate = function () {
- const PREFIX = "@mozilla.org/network/protocol/about;1?what=";
- return [[k.substr(PREFIX.length), ""] for (k in Cc) if (k.indexOf(PREFIX) == 0)];
+ return [[k.substr(services.ABOUT.length), ""]
+ for (k in Cc)
+ if (k.indexOf(services.ABOUT) == 0)];
};
});
if (complete == null)
complete = this.options["complete"];
// Will, and should, throw an error if !(c in opts)
Array.forEach(complete, function (c) {
- let completer = this.urlCompleters[c];
+ let completer = this.urlCompleters[c] || { args: [], completer: this.autocomplete(c.replace(/^native:/, "")) };
context.forkapply(c, 0, this, completer.completer, completer.args);
}, this);
},
addUrlCompleter: function addUrlCompleter(opt) {
let completer = Completion.UrlCompleter.apply(null, Array.slice(arguments));
completer.args = Array.slice(arguments, completer.length);
this.urlCompleters[opt] = completer;
},
+ autocomplete: curry(function autocomplete(provider, context) {
+ let running = context.getCache("autocomplete-search-running", Object);
+
+ let name = "autocomplete:" + provider;
+ if (!services.has(name))
+ services.add(name, services.AUTOCOMPLETE + provider, "nsIAutoCompleteSearch");
+ let service = services[name];
+
+ util.assert(service, _("autocomplete.noSuchProvider", provider), false);
+
+ if (running[provider]) {
+ this.completions = this.completions;
+ this.cancel();
+ }
+
+ context.anchored = false;
+ context.compare = CompletionContext.Sort.unsorted;
+ context.filterFunc = null;
+
+ let words = context.filter.toLowerCase().split(/\s+/g);
+ context.hasItems = true;
+ context.completions = context.completions.filter(function ({ url, title })
+ words.every(function (w) (url + " " + title).toLowerCase().indexOf(w) >= 0))
+ context.incomplete = true;
+
+ context.format = this.modules.bookmarks.format;
+ context.keys.extra = function (item) {
+ try {
+ return bookmarkcache.get(item.url).extra;
+ }
+ catch (e) {}
+ return null;
+ };
+ context.title = [_("autocomplete.title", provider)];
+
+ context.cancel = function () {
+ this.incomplete = false;
+ if (running[provider])
+ service.stopSearch();
+ running[provider] = false;
+ };
+
+ service.startSearch(context.filter, "", context.result, {
+ onSearchResult: util.wrapCallback(function onSearchResult(search, result) {
+ if (result.searchResult <= result.RESULT_SUCCESS)
+ running[provider] = null;
+
+ context.incomplete = result.searchResult >= result.RESULT_NOMATCH_ONGOING;
+ context.completions = [
+ { url: result.getValueAt(i), title: result.getCommentAt(i), icon: result.getImageAt(i) }
+ for (i in util.range(0, result.matchCount))
+ ];
+ }),
+ get onUpdateSearchResult() this.onSearchResult
+ });
+ running[provider] = true;
+ }),
+
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;
}
@@ -958,21 +1065,23 @@ var Completion = Module("completion", {
context.anchored = false;
if (!context.title)
context.title = ["URL", "Title"];
context.fork("additional", 0, this, function (context) {
context.title[0] += " " + _("completion.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.regexp.escape).join("|"), "g");
function highlight(item, text, i) process[i].call(this, item, template.highlightRegexp(text, re));
@@ -1049,18 +1158,42 @@ var Completion = Module("completion", {
wildmode);
options.add(["autocomplete", "au"],
"Automatically update the completion list on any key press",
"regexplist", ".*");
options.add(["complete", "cpt"],
"Items which are completed at the :open prompts",
- "charlist", config.defaults.complete == null ? "slf" : config.defaults.complete,
- { get values() values(completion.urlCompleters).toArray() });
+ "stringlist", "slf",
+ {
+ valueMap: {
+ S: "suggestion",
+ b: "bookmark",
+ f: "file",
+ h: "history",
+ l: "location",
+ s: "search"
+ },
+
+ get values() values(completion.urlCompleters).toArray()
+ .concat([let (name = k.substr(services.AUTOCOMPLETE.length))
+ ["native:" + name, _("autocomplete.description", name)]
+ for (k in Cc)
+ if (k.indexOf(services.AUTOCOMPLETE) == 0)]),
+
+ setter: function setter(values) {
+ if (values.length == 1 && !Set.has(values[0], this.values)
+ && Array.every(values[0], Set.has(this.valueMap)))
+ return Array.map(values[0], function (v) this[v], this.valueMap);
+ return values;
+ },
+
+ validator: function validator(values) validator.supercall(this, this.setter(values))
+ });
options.add(["wildanchor", "wia"],
"Define which completion groups only match at the beginning of their text",
"regexplist", "!/ex/(back|buffer|ext|forward|help|undo)");
options.add(["wildcase", "wic"],
"Completion case matching mode",
"regexpmap", ".?:smart",
@@ -1080,11 +1213,11 @@ var Completion = Module("completion", {
options.add(["wildsort", "wis"],
"Define which completion groups are sorted",
"regexplist", ".*");
}
});
endModule();
-} catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
+// catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
// vim: set fdm=marker sw=4 ts=4 et ft=javascript:
diff --git a/common/modules/config.jsm b/common/modules/config.jsm
--- a/common/modules/config.jsm
+++ b/common/modules/config.jsm
@@ -1,51 +1,165 @@
// Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
// Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
-
-try {
+/* use strict */
let global = this;
Components.utils.import("resource://dactyl/bootstrap.jsm");
defineModule("config", {
exports: ["ConfigBase", "Config", "config"],
- require: ["services", "storage", "util", "template"],
- use: ["io", "messages", "prefs", "styles"]
+ require: ["dom", "io", "protocol", "services", "util", "template"]
}, this);
+this.lazyRequire("addons", ["AddonManager"]);
+this.lazyRequire("cache", ["cache"]);
+this.lazyRequire("highlight", ["highlight"]);
+this.lazyRequire("messages", ["_"]);
+this.lazyRequire("prefs", ["localPrefs", "prefs"]);
+this.lazyRequire("storage", ["storage", "File"]);
+
+function AboutHandler() {}
+AboutHandler.prototype = {
+ get classDescription() "About " + config.appName + " Page",
+
+ classID: Components.ID("81495d80-89ee-4c36-a88d-ea7c4e5ac63f"),
+
+ get contractID() services.ABOUT + config.name,
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]),
+
+ newChannel: function (uri) {
+ let channel = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService)
+ .newChannel("dactyl://content/about.xul", null, null);
+ channel.originalURI = uri;
+ return channel;
+ },
+
+ getURIFlags: function (uri) Ci.nsIAboutModule.ALLOW_SCRIPT,
+};
var ConfigBase = Class("ConfigBase", {
/**
* Called on dactyl startup to allow for any arbitrary application-specific
* initialization code. Must call superclass's init function.
*/
init: function init() {
+ this.loadConfig();
+
this.features.push = deprecated("Set.add", function push(feature) Set.add(this, feature));
- if (util.haveGecko("2b"))
+ if (this.haveGecko("2b"))
Set.add(this.features, "Gecko2");
+ JSMLoader.registerFactory(JSMLoader.Factory(AboutHandler));
+ JSMLoader.registerFactory(JSMLoader.Factory(
+ Protocol("dactyl", "{9c8f2530-51c8-4d41-b356-319e0b155c44}",
+ "resource://dactyl-content/")));
+
this.timeout(function () {
- services["dactyl:"].pages.dtd = function () [null, util.makeDTD(config.dtd)];
+ cache.register("config.dtd", function () util.makeDTD(config.dtd));
});
- },
+
+ services["dactyl:"].pages["dtd"] = function () [null, cache.get("config.dtd")];
+
+ update(services["dactyl:"].providers, {
+ "locale": function (uri, path) LocaleChannel("dactyl-locale", config.locale, path, uri),
+ "locale-local": function (uri, path) LocaleChannel("dactyl-local-locale", config.locale, path, uri)
+ });
+ },
+
+ get prefs() localPrefs,
+
+ get has() Set.has(this.features),
+
+ configFiles: [
+ "resource://dactyl-common/config.json",
+ "resource://dactyl-local/config.json"
+ ],
+
+ configs: Class.Memoize(function () this.configFiles.map(function (url) JSON.parse(File.readURL(url)))),
+
+ loadConfig: function loadConfig(documentURL) {
+
+ for each (let config in this.configs) {
+ if (documentURL)
+ config = config.overlays && config.overlays[documentURL] || {};
+
+ for (let [name, value] in Iterator(config)) {
+ let prop = util.camelCase(name);
+
+ if (isArray(this[prop]))
+ this[prop] = [].concat(this[prop], value);
+ else if (isObject(this[prop])) {
+ if (isArray(value))
+ value = Set(value);
+
+ this[prop] = update({}, this[prop],
+ iter([util.camelCase(k), value[k]]
+ for (k in value)).toObject());
+ }
+ else
+ this[prop] = value;
+ }
+ }
+ },
+
+ modules: {
+ global: ["addons",
+ "base",
+ "io",
+ "buffer",
+ "cache",
+ "commands",
+ "completion",
+ "config",
+ "contexts",
+ "dom",
+ "downloads",
+ "finder",
+ "help",
+ "highlight",
+ "javascript",
+ "main",
+ "messages",
+ "options",
+ "overlay",
+ "prefs",
+ "protocol",
+ "sanitizer",
+ "services",
+ "storage",
+ "styles",
+ "template",
+ "util"],
+
+ window: ["dactyl",
+ "modes",
+ "commandline",
+ "abbreviations",
+ "autocommands",
+ "editor",
+ "events",
+ "hints",
+ "key-processors",
+ "mappings",
+ "marks",
+ "mow",
+ "statusline"]
+ },
loadStyles: function loadStyles(force) {
- const { highlight } = require("highlight");
- const { _ } = require("messages");
-
highlight.styleableChrome = this.styleableChrome;
highlight.loadCSS(this.CSS.replace(/__MSG_(.*?)__/g, function (m0, m1) _(m1)));
highlight.loadCSS(this.helpCSS.replace(/__MSG_(.*?)__/g, function (m0, m1) _(m1)));
- if (!util.haveGecko("2b"))
+ if (!this.haveGecko("2b"))
highlight.loadCSS(<![CDATA[
!TabNumber font-weight: bold; margin: 0px; padding-right: .8ex;
!TabIconNumber {
font-weight: bold;
color: white;
text-align: center;
text-shadow: black -1px 0 1px, black 0 1px 1px, black 1px 0 1px, black 0 -1px 1px;
}
@@ -55,58 +169,50 @@ var ConfigBase = Class("ConfigBase", {
hl.onChange = function () {
function hex(val) ("#" + util.regexp.iterate(/\d+/g, val)
.map(function (num) ("0" + Number(num).toString(16)).slice(-2))
.join("")
).slice(0, 7);
let elem = services.appShell.hiddenDOMWindow.document.createElement("div");
elem.style.cssText = this.cssText;
- let style = util.computedStyle(elem);
let keys = iter(Styles.propertyIter(this.cssText)).map(function (p) p.name).toArray();
- let bg = keys.some(function (k) /^background/.test(k));
+ let bg = keys.some(bind("test", /^background/));
let fg = keys.indexOf("color") >= 0;
+ let style = DOM(elem).style;
prefs[bg ? "safeSet" : "safeReset"]("ui.textHighlightBackground", hex(style.backgroundColor));
prefs[fg ? "safeSet" : "safeReset"]("ui.textHighlightForeground", hex(style.color));
};
},
get addonID() this.name + "@dactyl.googlecode.com",
- addon: Class.memoize(function () {
- let addon;
- do {
- addon = (JSMLoader.bootstrap || {}).addon;
- if (addon && !addon.getResourceURI) {
- util.reportError(Error(_("addon.unavailable")));
- yield 10;
- }
- }
- while (addon && !addon.getResourceURI);
-
- if (!addon)
- addon = require("addons").AddonManager.getAddonByID(this.addonID);
- yield addon;
- }, true),
+
+ addon: Class.Memoize(function () {
+ return (JSMLoader.bootstrap || {}).addon ||
+ AddonManager.getAddonByID(this.addonID);
+ }),
+
+ get styleableChrome() Object.keys(this.overlays),
/**
* The current application locale.
*/
- appLocale: Class.memoize(function () services.chromeRegistry.getSelectedLocale("global")),
+ appLocale: Class.Memoize(function () services.chromeRegistry.getSelectedLocale("global")),
/**
* The current dactyl locale.
*/
- locale: Class.memoize(function () this.bestLocale(this.locales)),
+ locale: Class.Memoize(function () this.bestLocale(this.locales)),
/**
* The current application locale.
*/
- locales: Class.memoize(function () {
+ locales: Class.Memoize(function () {
// TODO: Merge with completion.file code.
function getDir(str) str.match(/^(?:.*[\/\\])?/)[0];
let uri = "resource://dactyl-locale/";
let jar = io.isJarURL(uri);
if (jar) {
let prefix = getDir(jar.JAREntry);
var res = iter(s.slice(prefix.length).replace(/\/.*/, "") for (s in io.listJar(jar.JARFile, prefix)))
@@ -114,104 +220,204 @@ var ConfigBase = Class("ConfigBase", {
}
else {
res = array(f.leafName
// Fails on FF3: for (f in util.getFile(uri).iterDirectory())
for (f in values(util.getFile(uri).readDirectory()))
if (f.isDirectory())).array;
}
- function exists(pkg) {
- try {
- services["resource:"].getSubstitution(pkg);
- return true;
- }
- catch (e) {
- return false;
- }
- }
+ function exists(pkg) services["resource:"].hasSubstitution("dactyl-locale-" + pkg);
return array.uniq([this.appLocale, this.appLocale.replace(/-.*/, "")]
- .filter(function (locale) exists("dactyl-locale-" + locale))
+ .filter(exists)
.concat(res));
}),
/**
* Returns the best locale match to the current locale from a list
* of available locales.
*
* @param {[string]} list A list of available locales
* @returns {string}
*/
bestLocale: function (list) {
- let langs = Set(list);
return values([this.appLocale, this.appLocale.replace(/-.*/, ""),
- "en", "en-US", iter(langs).next()])
- .nth(function (l) Set.has(langs, l), 0);
- },
+ "en", "en-US", list[0]])
+ .nth(Set.has(Set(list)), 0);
+ },
+
+ /**
+ * A list of all known registered chrome and resource packages.
+ */
+ get chromePackages() {
+ // Horrible hack.
+ let res = {};
+ function process(manifest) {
+ for each (let line in manifest.split(/\n+/)) {
+ let match = /^\s*(content|skin|locale|resource)\s+([^\s#]+)\s/.exec(line);
+ if (match)
+ res[match[2]] = true;
+ }
+ }
+ function processJar(file) {
+ let jar = services.ZipReader(file);
+ if (jar)
+ try {
+ if (jar.hasEntry("chrome.manifest"))
+ process(File.readStream(jar.getInputStream("chrome.manifest")));
+ }
+ finally {
+ jar.close();
+ }
+ }
+
+ for each (let dir in ["UChrm", "AChrom"]) {
+ dir = File(services.directory.get(dir, Ci.nsIFile));
+ if (dir.exists() && dir.isDirectory())
+ for (let file in dir.iterDirectory())
+ if (/\.manifest$/.test(file.leafName))
+ process(file.read());
+
+ dir = File(dir.parent);
+ if (dir.exists() && dir.isDirectory())
+ for (let file in dir.iterDirectory())
+ if (/\.jar$/.test(file.leafName))
+ processJar(file);
+
+ dir = dir.child("extensions");
+ if (dir.exists() && dir.isDirectory())
+ for (let ext in dir.iterDirectory()) {
+ if (/\.xpi$/.test(ext.leafName))
+ processJar(ext);
+ else {
+ if (ext.isFile())
+ ext = File(ext.read().replace(/\n*$/, ""));
+ let mf = ext.child("chrome.manifest");
+ if (mf.exists())
+ process(mf.read());
+ }
+ }
+ }
+ return Object.keys(res).sort();
+ },
+
+ /**
+ * Returns true if the current Gecko runtime is of the given version
+ * or greater.
+ *
+ * @param {string} min The minimum required version. @optional
+ * @param {string} max The maximum required version. @optional
+ * @returns {boolean}
+ */
+ haveGecko: function (min, max) let ({ compare } = services.versionCompare,
+ { platformVersion } = services.runtime)
+ (min == null || compare(platformVersion, min) >= 0) &&
+ (max == null || compare(platformVersion, max) < 0),
+
+ /** Dactyl's notion of the current operating system platform. */
+ OS: memoize({
+ _arch: services.runtime.OS,
+ /**
+ * @property {string} The normalised name of the OS. This is one of
+ * "Windows", "Mac OS X" or "Unix".
+ */
+ get name() this.isWindows ? "Windows" : this.isMacOSX ? "Mac OS X" : "Unix",
+ /** @property {boolean} True if the OS is Windows. */
+ get isWindows() this._arch == "WINNT",
+ /** @property {boolean} True if the OS is Mac OS X. */
+ get isMacOSX() this._arch == "Darwin",
+ /** @property {boolean} True if the OS is some other *nix variant. */
+ get isUnix() !this.isWindows,
+ /** @property {RegExp} A RegExp which matches illegal characters in path components. */
+ get illegalCharacters() this.isWindows ? /[<>:"/\\|?*\x00-\x1f]/g : /[\/\x00]/g,
+
+ get pathListSep() this.isWindows ? ";" : ":"
+ }),
/**
* @property {string} The pathname of the VCS repository clone's root
* directory if the application is running from one via an extension
* proxy file.
*/
- VCSPath: Class.memoize(function () {
+ VCSPath: Class.Memoize(function () {
if (/pre$/.test(this.addon.version)) {
let uri = util.newURI(this.addon.getResourceURI("").spec + "../.hg");
if (uri instanceof Ci.nsIFileURL &&
uri.file.exists() &&
io.pathSearch("hg"))
return uri.file.parent.path;
}
return null;
}),
/**
* @property {string} The name of the VCS branch that the application is
* running from if using an extension proxy file or was built from if
* installed as an XPI.
*/
- branch: Class.memoize(function () {
+ branch: Class.Memoize(function () {
if (this.VCSPath)
return io.system(["hg", "-R", this.VCSPath, "branch"]).output;
return (/pre-hg\d+-(\S*)/.exec(this.version) || [])[1];
}),
+ /** @property {string} The name of the current user profile. */
+ profileName: Class.Memoize(function () {
+ // NOTE: services.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.
+
+ let dir = services.directory.get("ProfD", Ci.nsIFile);
+ for (let prof in iter(services.profile.profiles))
+ if (prof.QueryInterface(Ci.nsIToolkitProfile).rootDir.path === dir.path)
+ return prof.name;
+ return "unknown";
+ }),
+
/** @property {string} The Dactyl version string. */
- version: Class.memoize(function () {
+ version: Class.Memoize(function () {
if (this.VCSPath)
return io.system(["hg", "-R", this.VCSPath, "log", "-r.",
- "--template=hg{rev}-" + this.branch + " ({date|isodate})"]).output;
- let version = this.addon.version;
+ "--template=hg{rev}-{branch}"]).output;
+
+ return this.addon.version;
+ }),
+
+ buildDate: Class.Memoize(function () {
+ if (this.VCSPath)
+ return io.system(["hg", "-R", this.VCSPath, "log", "-r.",
+ "--template={date|isodate}"]).output;
if ("@DATE@" !== "@" + "DATE@")
- version += " " + _("dactyl.created", "@DATE@");
- return version;
- }),
+ return _("dactyl.created", "@DATE@");
+ }),
get fileExt() this.name.slice(0, -6),
- dtd: Class.memoize(function ()
+ dtd: Class.Memoize(function ()
iter(this.dtdExtra,
(["dactyl." + k, v] for ([k, v] in iter(config.dtdDactyl))),
(["dactyl." + s, config[s]] for each (s in config.dtdStrings)))
.toObject()),
dtdDactyl: memoize({
get name() config.name,
get home() "http://dactyl.sourceforge.net/",
get apphome() this.home + this.name,
code: "http://code.google.com/p/dactyl/",
get issues() this.home + "bug/" + this.name,
get plugins() "http://dactyl.sf.net/" + this.name + "/plugins",
get faq() this.home + this.name + "/faq",
- "list.mailto": Class.memoize(function () config.name + "@googlegroups.com"),
- "list.href": Class.memoize(function () "http://groups.google.com/group/" + config.name),
-
- "hg.latest": Class.memoize(function () this.code + "source/browse/"), // XXX
+ "list.mailto": Class.Memoize(function () config.name + "@googlegroups.com"),
+ "list.href": Class.Memoize(function () "http://groups.google.com/group/" + config.name),
+
+ "hg.latest": Class.Memoize(function () this.code + "source/browse/"), // XXX
"irc": "irc://irc.oftc.net/#pentadactyl",
}),
dtdExtra: {
"xmlns.dactyl": "http://vimperator.org/namespaces/liberator",
"xmlns.html": "http://www.w3.org/1999/xhtml",
"xmlns.xul": "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
@@ -228,26 +434,26 @@ var ConfigBase = Class("ConfigBase", {
"idName",
"name",
"version"
],
helpStyles: /^(Help|StatusLine|REPL)|^(Boolean|Dense|Indicator|MoreMsg|Number|Object|Logo|Key(word)?|String)$/,
styleHelp: function styleHelp() {
if (!this.helpStyled) {
- const { highlight } = require("highlight");
for (let k in keys(highlight.loaded))
if (this.helpStyles.test(k))
highlight.loaded[k] = true;
}
this.helpCSS = true;
},
- Local: function Local(dactyl, modules, window) ({
+ Local: function Local(dactyl, modules, { document, window }) ({
init: function init() {
+ this.loadConfig(document.documentURI);
let append = <e4x xmlns={XUL} xmlns:dactyl={NS}>
<menupopup id="viewSidebarMenu"/>
<broadcasterset id="mainBroadcasterSet"/>
</e4x>;
for each (let [id, [name, key, uri]] in Iterator(this.sidebars)) {
append.XUL::menupopup[0].* +=
<menuitem observes={"pentadactyl-" + id + "Sidebar"} label={name} accesskey={key} xmlns={XUL}/>;
@@ -256,85 +462,63 @@ var ConfigBase = Class("ConfigBase", {
autoCheck="false" type="checkbox" group="sidebar"
sidebartitle={name} sidebarurl={uri}
oncommand="toggleSidebar(this.id || this.observes);" xmlns={XUL}/>;
}
util.overlayWindow(window, { append: append.elements() });
},
- browser: Class.memoize(function () window.gBrowser),
- tabbrowser: Class.memoize(function () window.gBrowser),
+ get window() window,
+
+ get document() document,
+
+ ids: Class.Update({
+ get commandContainer() document.documentElement.id
+ }),
+
+ browser: Class.Memoize(function () window.gBrowser),
+ tabbrowser: Class.Memoize(function () window.gBrowser),
get browserModes() [modules.modes.NORMAL],
/**
* @property {string} The ID of the application's main XUL window.
*/
- mainWindowId: window.document.documentElement.id,
+ mainWindowId: document.documentElement.id,
/**
* @property {number} The height (px) that is available to the output
* window.
*/
get outputHeight() this.browser.mPanelContainer.boxObject.height,
- tabStrip: Class.memoize(function () window.document.getElementById("TabsToolbar") || this.tabbrowser.mTabContainer),
- }),
+ tabStrip: Class.Memoize(function () document.getElementById("TabsToolbar") || this.tabbrowser.mTabContainer),
+ }),
/**
* @property {Object} A mapping of names and descriptions
* of the autocommands available in this application. Primarily used
* for completion results.
*/
autocommands: {},
/**
* @property {Object} A map of :command-complete option values to completer
* function names.
*/
- completers: {
- abbreviation: "abbreviation",
- altstyle: "alternateStyleSheet",
- bookmark: "bookmark",
- buffer: "buffer",
- charset: "charset",
- color: "colorScheme",
- command: "command",
- dialog: "dialog",
- dir: "directory",
- environment: "environment",
- event: "autocmdEvent",
- extension: "extension",
- file: "file",
- help: "help",
- highlight: "highlightGroup",
- history: "history",
- javascript: "javascript",
- macro: "macro",
- mapping: "userMapping",
- mark: "mark",
- menu: "menuItem",
- option: "option",
- preference: "preference",
- qmark: "quickmark",
- runtime: "runtime",
- search: "search",
- shellcmd: "shellCommand",
- toolbar: "toolbar",
- url: "url",
- usercommand: "userCommand"
- },
+ completers: {},
/**
* @property {Object} Application specific defaults for option values. The
* property names must be the options' canonical names, and the values
* must be strings as entered via :set.
*/
- defaults: { guioptions: "rb" },
+ optionDefaults: {},
+
cleanups: {},
/**
* @property {Object} A map of dialogs available via the
* :dialog command. Property names map dialog names to an array
* with the following elements:
* [0] description - A description of the dialog, used in
* command completion results for :dialog.
@@ -355,32 +539,23 @@ var ConfigBase = Class("ConfigBase", {
/**
* @property {string} The file extension used for command script files.
* This is the name string sans "dactyl".
*/
get fileExtension() this.name.slice(0, -6),
guioptions: {},
- hasTabbrowser: false,
-
/**
* @property {string} The name of the application that hosts the
* extension. E.g., "Firefox" or "XULRunner".
*/
host: null,
/**
- * @property {[[]]} An array of application specific mode specifications.
- * The values of each mode are passed to modes.addMode during
- * dactyl startup.
- */
- modes: [],
-
- /**
* @property {string} The name of the extension.
* Required.
*/
name: null,
/**
* @property {[string]} A list of extra scripts in the dactyl or
* application namespaces which should be loaded before dactyl
@@ -389,545 +564,37 @@ var ConfigBase = Class("ConfigBase", {
scripts: [],
sidebars: {},
/**
* @property {string} The leaf name of any temp files created by
* {@link io.createTempFile}.
*/
- get tempFile() this.name + ".tmp",
+ get tempFile() this.name + ".txt",
/**
* @constant
* @property {string} The default highlighting rules.
* See {@link Highlights#loadCSS} for details.
*/
- CSS: UTF8(String.replace(<><![CDATA[
- // <css>
- Boolean /* JavaScript booleans */ color: red;
- Function /* JavaScript functions */ color: navy;
- Null /* JavaScript null values */ color: blue;
- Number /* JavaScript numbers */ color: blue;
- Object /* JavaScript objects */ color: maroon;
- String /* String values */ color: green; white-space: pre;
- Comment /* JavaScriptor CSS comments */ color: gray;
-
- Key /* Keywords */ font-weight: bold;
-
- Enabled /* Enabled item indicator text */ color: blue;
- Disabled /* Disabled item indicator text */ color: red;
-
- FontFixed /* The font used for fixed-width text */ \
- font-family: monospace !important;
- FontCode /* The font used for code listings */ \
- font-size: 9pt; font-family: monospace !important;
- FontProportional /* The font used for proportionally spaced text */ \
- font-size: 10pt; font-family: "Droid Sans", "Helvetica LT Std", Helvetica, "DejaVu Sans", Verdana, sans-serif !important;
-
- // Hack to give these groups slightly higher precedence
- // than their unadorned variants.
- CmdCmdLine;[dactyl|highlight]>* &#x0d; StatusCmdLine;[dactyl|highlight]>*
- CmdNormal;[dactyl|highlight] &#x0d; StatusNormal;[dactyl|highlight]
- CmdErrorMsg;[dactyl|highlight] &#x0d; StatusErrorMsg;[dactyl|highlight]
- CmdInfoMsg;[dactyl|highlight] &#x0d; StatusInfoMsg;[dactyl|highlight]
- CmdModeMsg;[dactyl|highlight] &#x0d; StatusModeMsg;[dactyl|highlight]
- CmdMoreMsg;[dactyl|highlight] &#x0d; StatusMoreMsg;[dactyl|highlight]
- CmdQuestion;[dactyl|highlight] &#x0d; StatusQuestion;[dactyl|highlight]
- CmdWarningMsg;[dactyl|highlight] &#x0d; StatusWarningMsg;[dactyl|highlight]
-
- Normal /* Normal text */ \
- color: black !important; background: white !important; font-weight: normal !important;
- StatusNormal /* Normal text in the status line */ \
- color: inherit !important; background: transparent !important;
- ErrorMsg /* Error messages */ \
- color: white !important; background: red !important; font-weight: bold !important;
- InfoMsg /* Information messages */ \
- color: black !important; background: white !important;
- StatusInfoMsg /* Information messages in the status line */ \
- color: inherit !important; background: transparent !important;
- LineNr /* The line number of an error */ \
- color: orange !important; background: white !important;
- ModeMsg /* The mode indicator */ \
- color: black !important; background: white !important;
- StatusModeMsg /* The mode indicator in the status line */ \
- color: inherit !important; background: transparent !important; padding-right: 1em;
- MoreMsg /* The indicator that there is more text to view */ \
- color: green !important; background: white !important;
- StatusMoreMsg background: transparent !important;
- Message /* A message as displayed in <ex>:messages</ex> */ \
- white-space: pre-wrap !important; min-width: 100%; width: 100%; padding-left: 4em; text-indent: -4em; display: block;
- Message String /* A message as displayed in <ex>:messages</ex> */ \
- white-space: pre-wrap;
- NonText /* The <em>~</em> indicators which mark blank lines in the completion list */ \
- color: blue; background: transparent !important;
- *Preview /* The completion preview displayed in the &tag.command-line; */ \
- color: gray;
- Question /* A prompt for a decision */ \
- color: green !important; background: white !important; font-weight: bold !important;
- StatusQuestion /* A prompt for a decision in the status line */ \
- color: green !important; background: transparent !important;
- WarningMsg /* A warning message */ \
- color: red !important; background: white !important;
- StatusWarningMsg /* A warning message in the status line */ \
- color: red !important; background: transparent !important;
- Disabled /* Disabled items */ \
- color: gray !important;
-
- CmdLine;>*;;FontFixed /* The command line */ \
- padding: 1px !important;
- CmdPrompt;.dactyl-commandline-prompt /* The default styling form the command prompt */
- CmdInput;.dactyl-commandline-command
- CmdOutput /* The output of commands executed by <ex>:run</ex> */ \
- white-space: pre;
-
- CompGroup /* Item group in completion output */
- CompGroup:not(:first-of-type) margin-top: .5em;
- CompGroup:last-of-type padding-bottom: 1.5ex;
-
- CompTitle /* Completion row titles */ \
- color: magenta; background: white; font-weight: bold;
- CompTitle>* padding: 0 .5ex;
- CompTitleSep /* The element which separates the completion title from its results */ \
- height: 1px; background: magenta; background: -moz-linear-gradient(60deg, magenta, white);
-
- CompMsg /* The message which may appear at the top of a group of completion results */ \
- font-style: italic; margin-left: 16px;
-
- CompItem /* A single row of output in the completion list */
- CompItem:nth-child(2n+1) background: rgba(0, 0, 0, .04);
- CompItem[selected] /* A selected row of completion list */ \
- background: yellow;
- CompItem>* padding: 0 .5ex;
-
- CompIcon /* The favicon of a completion row */ \
- width: 16px; min-width: 16px; display: inline-block; margin-right: .5ex;
- CompIcon>img max-width: 16px; max-height: 16px; vertical-align: middle;
-
- CompResult /* The result column of the completion list */ \
- width: 36%; padding-right: 1%; overflow: hidden;
- CompDesc /* The description column of the completion list */ \
- color: gray; width: 62%; padding-left: 1em;
-
- CompLess /* The indicator shown when completions may be scrolled up */ \
- text-align: center; height: 0; line-height: .5ex; padding-top: 1ex;
- CompLess::after /* The character of indicator shown when completions may be scrolled up */ \
- content: "⌃";
-
- CompMore /* The indicator shown when completions may be scrolled down */ \
- text-align: center; height: .5ex; line-height: .5ex; margin-bottom: -.5ex;
- CompMore::after /* The character of indicator shown when completions may be scrolled down */ \
- content: "⌄";
-
- Dense /* Arbitrary elements which should be packed densely together */\
- margin-top: 0; margin-bottom: 0;
-
- EditorEditing;;* /* Text fields for which an external editor is open */ \
- background-color: #bbb !important; -moz-user-input: none !important; -moz-user-modify: read-only !important;
- EditorError;;* /* Text fields briefly after an error has occurred running the external editor */ \
- background: red !important;
- EditorBlink1;;* /* Text fields briefly after successfully running the external editor, alternated with EditorBlink2 */ \
- background: yellow !important;
- EditorBlink2;;* /* Text fields briefly after successfully running the external editor, alternated with EditorBlink1 */
-
- REPL /* Read-Eval-Print-Loop output */ \
- overflow: auto; max-height: 40em;
- REPL-R;;;Question /* Prompts in REPL mode */
- REPL-E /* Evaled input in REPL mode */ \
- white-space: pre-wrap;
- REPL-P /* Evaled output in REPL mode */ \
- white-space: pre-wrap; margin-bottom: 1em;
-
- Usage /* Output from the :*usage commands */ \
- width: 100%;
- UsageHead /* Headings in output from the :*usage commands */
- UsageBody /* The body of listings in output from the :*usage commands */
- UsageItem /* Individual items in output from the :*usage commands */
- UsageItem:nth-of-type(2n) background: rgba(0, 0, 0, .04);
-
- Indicator /* The <em>#</em> and <em>%</em> in the <ex>:buffers</ex> list */ \
- color: blue; width: 1.5em; text-align: center;
- Filter /* The matching text in a completion list */ \
- font-weight: bold;
-
- Keyword /* A bookmark keyword for a URL */ \
- color: red;
- Tag /* A bookmark tag for a URL */ \
- color: blue;
-
- Link /* A link with additional information shown on hover */ \
- position: relative; padding-right: 2em;
- Link:not(:hover)>LinkInfo opacity: 0; left: 0; width: 1px; height: 1px; overflow: hidden;
- LinkInfo {
- /* Information shown when hovering over a link */
- color: black;
- position: absolute;
- left: 100%;
- padding: 1ex;
- margin: -1ex -1em;
- background: rgba(255, 255, 255, .8);
- border-radius: 1ex;
- }
-
- StatusLine;;;FontFixed {
- /* The status bar */
- -moz-appearance: none !important;
- font-weight: bold;
- background: transparent !important;
- border: 0px !important;
- padding-right: 0px !important;
- min-height: 18px !important;
- text-shadow: none !important;
- }
- StatusLineNormal;[dactyl|highlight] /* The status bar for an ordinary web page */ \
- color: white !important; background: black !important;
- StatusLineBroken;[dactyl|highlight] /* The status bar for a broken web page */ \
- color: black !important; background: #FFa0a0 !important; /* light-red */
- StatusLineSecure;[dactyl|highlight] /* The status bar for a secure web page */ \
- color: black !important; background: #a0a0FF !important; /* light-blue */
- StatusLineExtended;[dactyl|highlight] /* The status bar for a secure web page with an Extended Validation (EV) certificate */ \
- color: black !important; background: #a0FFa0 !important; /* light-green */
-
- !TabClose;.tab-close-button /* The close button of a browser tab */ \
- /* The close button of a browser tab */
- !TabIcon;.tab-icon,.tab-icon-image /* The icon of a browser tab */ \
- /* The icon of a browser tab */
- !TabText;.tab-text /* The text of a browser tab */
- TabNumber /* The number of a browser tab, next to its icon */ \
- font-weight: bold; margin: 0px; padding-right: .8ex; cursor: default;
- TabIconNumber {
- /* The number of a browser tab, over its icon */
- cursor: default;
- width: 16px;
- margin: 0 2px 0 -18px !important;
- font-weight: bold;
- color: white;
- text-align: center;
- text-shadow: black -1px 0 1px, black 0 1px 1px, black 1px 0 1px, black 0 -1px 1px;
- }
-
- Title /* The title of a listing, including <ex>:pageinfo</ex>, <ex>:jumps</ex> */ \
- color: magenta; font-weight: bold;
- URL /* A URL */ \
- text-decoration: none; color: green; background: inherit;
- URL:hover text-decoration: underline; cursor: pointer;
- URLExtra /* Extra information about a URL */ \
- color: gray;
-
- FrameIndicator;;* {
- /* The styling applied to briefly indicate the active frame */
- background-color: red;
- opacity: 0.5;
- z-index: 999999;
- position: fixed;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- }
-
- Bell /* &dactyl.appName;’s visual bell */ \
- background-color: black !important;
-
- Hint;;* {
- /* A hint indicator. See <ex>:help hints</ex> */
- font: bold 10px "Droid Sans Mono", monospace !important;
- margin: -.2ex;
- padding: 0 0 0 1px;
- outline: 1px solid rgba(0, 0, 0, .5);
- background: rgba(255, 248, 231, .8);
- color: black;
- }
- Hint[active];;* background: rgba(255, 253, 208, .8);
- Hint::after;;* content: attr(text) !important;
- HintElem;;* /* The hintable element */ \
- background-color: yellow !important; color: black !important;
- HintActive;;* /* The hint element of link which will be followed by <k name="CR"/> */ \
- background-color: #88FF00 !important; color: black !important;
- HintImage;;* /* The indicator which floats above hinted images */ \
- opacity: .5 !important;
-
- Button /* A button widget */ \
- display: inline-block; font-weight: bold; cursor: pointer; color: black; text-decoration: none;
- Button:hover text-decoration: underline;
- Button[collapsed] visibility: collapse; width: 0;
- Button::before content: "["; color: gray; text-decoration: none !important;
- Button::after content: "]"; color: gray; text-decoration: none !important;
- Button:not([collapsed]) ~ Button:not([collapsed])::before content: "/[";
-
- Buttons /* A group of buttons */
-
- DownloadCell /* A table cell in the :downloads manager */ \
- display: table-cell; padding: 0 1ex;
-
- Downloads /* The :downloads manager */ \
- display: table; margin: 0; padding: 0;
- DownloadHead;;;CompTitle /* A heading in the :downloads manager */ \
- display: table-row;
- DownloadHead>*;;;DownloadCell
-
- Download /* A download in the :downloads manager */ \
- display: table-row;
- Download:not([active]) color: gray;
- Download:nth-child(2n+1) background: rgba(0, 0, 0, .04);
-
- Download>*;;;DownloadCell
- DownloadButtons /* A button group in the :downloads manager */
- DownloadPercent /* The percentage column for a download */
- DownloadProgress /* The progress column for a download */
- DownloadProgressHave /* The completed portion of the progress column */
- DownloadProgressTotal /* The remaining portion of the progress column */
- DownloadSource /* The download source column for a download */
- DownloadState /* The download state column for a download */
- DownloadTime /* The time remaining column for a download */
- DownloadTitle /* The title column for a download */
- DownloadTitle>Link>a max-width: 48ex; overflow: hidden; display: inline-block;
-
- AddonCell /* A cell in tell :addons manager */ \
- display: table-cell; padding: 0 1ex;
-
- Addons /* The :addons manager */ \
- display: table; margin: 0; padding: 0;
- AddonHead;;;CompTitle /* A heading in the :addons manager */ \
- display: table-row;
- AddonHead>*;;;AddonCell
-
- Addon /* An add-on in the :addons manager */ \
- display: table-row;
- Addon:nth-child(2n+1) background: rgba(0, 0, 0, .04);
-
- Addon>*;;;AddonCell
- AddonButtons
- AddonDescription
- AddonName max-width: 48ex; overflow: hidden;
- AddonStatus
- AddonVersion
-
- // </css>
- ]]></>, /&#x0d;/g, "\n")),
-
- helpCSS: UTF8(<><![CDATA[
- // <css>
- InlineHelpLink /* A help link shown in the command line or multi-line output area */ \
- font-size: inherit !important; font-family: inherit !important;
-
- Help;;;FontProportional /* A help page */ \
- line-height: 1.4em;
-
- HelpInclude /* A help page included in the consolidated help listing */ \
- margin: 2em 0;
-
- HelpArg;;;FontCode /* A required command argument indicator */ \
- color: #6A97D4;
- HelpOptionalArg;;;FontCode /* An optional command argument indicator */ \
- color: #6A97D4;
-
- HelpBody /* The body of a help page */ \
- display: block; margin: 1em auto; max-width: 100ex; padding-bottom: 1em; margin-bottom: 4em; border-bottom-width: 1px;
- HelpBorder;*;dactyl://help/* /* The styling of bordered elements */ \
- border-color: silver; border-width: 0px; border-style: solid;
- HelpCode;;;FontCode /* Code listings */ \
- display: block; white-space: pre; margin-left: 2em;
- HelpTT;html|tt;dactyl://help/*;FontCode /* Teletype text */
-
- HelpDefault;;;FontCode /* The default value of a help item */ \
- display: inline-block; margin: -1px 1ex 0 0; white-space: pre; vertical-align: text-top;
-
- HelpDescription /* The description of a help item */ \
- display: block; clear: right;
- HelpDescription[short] clear: none;
- HelpEm;html|em;dactyl://help/* /* Emphasized text */ \
- font-weight: bold; font-style: normal;
-
- HelpEx;;;FontCode /* An Ex command */ \
- display: inline-block; color: #527BBD;
-
- HelpExample /* An example */ \
- display: block; margin: 1em 0;
- HelpExample::before content: "__MSG_help.Example__: "; font-weight: bold;
-
- HelpInfo /* Arbitrary information about a help item */ \
- display: block; width: 20em; margin-left: auto;
- HelpInfoLabel /* The label for a HelpInfo item */ \
- display: inline-block; width: 6em; color: magenta; font-weight: bold; vertical-align: text-top;
- HelpInfoValue /* The details for a HelpInfo item */ \
- display: inline-block; width: 14em; text-decoration: none; vertical-align: text-top;
-
- HelpItem /* A help item */ \
- display: block; margin: 1em 1em 1em 10em; clear: both;
-
- HelpKey;;;FontCode /* A keyboard key specification */ \
- color: #102663;
- HelpKeyword /* A keyword */ \
- font-weight: bold; color: navy;
-
- HelpLink;html|a;dactyl://help/* /* A hyperlink */ \
- text-decoration: none !important;
- HelpLink[href]:hover text-decoration: underline !important;
- HelpLink[href^="mailto:"]::after content: "✉"; padding-left: .2em;
- HelpLink[rel=external] {
- /* A hyperlink to an external resource */
- /* Thanks, Wikipedia */
- background: transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAMAAAC67D+PAAAAFVBMVEVmmcwzmcyZzP8AZswAZv////////9E6giVAAAAB3RSTlP///////8AGksDRgAAADhJREFUGFcly0ESAEAEA0Ei6/9P3sEcVB8kmrwFyni0bOeyyDpy9JTLEaOhQq7Ongf5FeMhHS/4AVnsAZubxDVmAAAAAElFTkSuQmCC) no-repeat scroll right center;
- padding-right: 13px;
- }
-
- ErrorMsg HelpEx color: inherit; background: inherit; text-decoration: underline;
- ErrorMsg HelpKey color: inherit; background: inherit; text-decoration: underline;
- ErrorMsg HelpOption color: inherit; background: inherit; text-decoration: underline;
- ErrorMsg HelpTopic color: inherit; background: inherit; text-decoration: underline;
-
- HelpTOC /* The Table of Contents for a help page */
- HelpTOC>ol ol margin-left: -1em;
-
- HelpOrderedList;ol;dactyl://help/* /* Any ordered list */ \
- margin: 1em 0;
- HelpOrderedList1;ol[level="1"],ol;dactyl://help/* /* A first-level ordered list */ \
- list-style: outside decimal; display: block;
- HelpOrderedList2;ol[level="2"],ol ol;dactyl://help/* /* A second-level ordered list */ \
- list-style: outside upper-alpha;
- HelpOrderedList3;ol[level="3"],ol ol ol;dactyl://help/* /* A third-level ordered list */ \
- list-style: outside lower-roman;
- HelpOrderedList4;ol[level="4"],ol ol ol ol;dactyl://help/* /* A fourth-level ordered list */ \
- list-style: outside decimal;
-
- HelpList;html|ul;dactyl://help/* /* An unordered list */ \
- display: block; list-style-position: outside; margin: 1em 0;
- HelpListItem;html|li;dactyl://help/* /* A list item, ordered or unordered */ \
- display: list-item;
-
- HelpNote /* The indicator for a note */ \
- color: red; font-weight: bold;
-
- HelpOpt;;;FontCode /* An option name */ \
- color: #106326;
- HelpOptInfo;;;FontCode /* Information about the type and default values for an option entry */ \
- display: block; margin-bottom: 1ex; padding-left: 4em;
-
- HelpParagraph;html|p;dactyl://help/* /* An ordinary paragraph */ \
- display: block; margin: 1em 0em;
- HelpParagraph:first-child margin-top: 0;
- HelpParagraph:last-child margin-bottom: 0;
- HelpSpec;;;FontCode /* The specification for a help entry */ \
- display: block; margin-left: -10em; float: left; clear: left; color: #527BBD; margin-right: 1em;
-
- HelpString;;;FontCode /* A quoted string */ \
- color: green; font-weight: normal;
- HelpString::before content: '"';
- HelpString::after content: '"';
- HelpString[delim]::before content: attr(delim);
- HelpString[delim]::after content: attr(delim);
-
- HelpNews /* A news item */ position: relative;
- HelpNewsOld /* An old news item */ opacity: .7;
- HelpNewsNew /* A new news item */ font-style: italic;
- HelpNewsTag /* The version tag for a news item */ \
- font-style: normal; position: absolute; left: 100%; padding-left: 1em; color: #527BBD; opacity: .6; white-space: pre;
-
- HelpHead;html|h1,html|h2,html|h3,html|h4;dactyl://help/* {
- /* Any help heading */
- font-weight: bold;
- color: #527BBD;
- clear: both;
- }
- HelpHead1;html|h1;dactyl://help/* {
- /* A first-level help heading */
- margin: 2em 0 1em;
- padding-bottom: .2ex;
- border-bottom-width: 1px;
- font-size: 2em;
- }
- HelpHead2;html|h2;dactyl://help/* {
- /* A second-level help heading */
- margin: 2em 0 1em;
- padding-bottom: .2ex;
- border-bottom-width: 1px;
- font-size: 1.2em;
- }
- HelpHead3;html|h3;dactyl://help/* {
- /* A third-level help heading */
- margin: 1em 0;
- padding-bottom: .2ex;
- font-size: 1.1em;
- }
- HelpHead4;html|h4;dactyl://help/* {
- /* A fourth-level help heading */
- }
-
- HelpTab;html|dl;dactyl://help/* {
- /* A description table */
- display: table;
- width: 100%;
- margin: 1em 0;
- border-bottom-width: 1px;
- border-top-width: 1px;
- padding: .5ex 0;
- table-layout: fixed;
- }
- HelpTabColumn;html|column;dactyl://help/* display: table-column;
- HelpTabColumn:first-child width: 25%;
- HelpTabTitle;html|dt;dactyl://help/*;FontCode /* The title column of description tables */ \
- display: table-cell; padding: .1ex 1ex; font-weight: bold;
- HelpTabDescription;html|dd;dactyl://help/* /* The description column of description tables */ \
- display: table-cell; padding: .3ex 1em; text-indent: -1em; border-width: 0px;
- HelpTabDescription>*;;dactyl://help/* text-indent: 0;
- HelpTabRow;html|dl>html|tr;dactyl://help/* /* Entire rows in description tables */ \
- display: table-row;
-
- HelpTag;;;FontCode /* A help tag */ \
- display: inline-block; color: #527BBD; margin-left: 1ex; font-weight: normal;
- HelpTags /* A group of help tags */ \
- display: block; float: right; clear: right;
- HelpTopic;;;FontCode /* A link to a help topic */ \
- color: #102663;
- HelpType;;;FontCode /* An option type */ \
- color: #102663 !important; margin-right: 2ex;
-
- HelpWarning /* The indicator for a warning */ \
- color: red; font-weight: bold;
-
- HelpXML;;;FontCode /* Highlighted XML */ \
- color: #C5F779; background-color: #444444; font-family: Terminus, Fixed, monospace;
- HelpXMLBlock { white-space: pre; color: #C5F779; background-color: #444444;
- border: 1px dashed #aaaaaa;
- display: block;
- margin-left: 2em;
- font-family: Terminus, Fixed, monospace;
- }
- HelpXMLAttribute color: #C5F779;
- HelpXMLAttribute::after color: #E5E5E5; content: "=";
- HelpXMLComment color: #444444;
- HelpXMLComment::before content: "<!--";
- HelpXMLComment::after content: "-->";
- HelpXMLProcessing color: #C5F779;
- HelpXMLProcessing::before color: #444444; content: "<?";
- HelpXMLProcessing::after color: #444444; content: "?>";
- HelpXMLString color: #C5F779; white-space: pre;
- HelpXMLString::before content: '"';
- HelpXMLString::after content: '"';
- HelpXMLNamespace color: #FFF796;
- HelpXMLNamespace::after color: #777777; content: ":";
- HelpXMLTagStart color: #FFF796; white-space: normal; display: inline-block; text-indent: -1.5em; padding-left: 1.5em;
- HelpXMLTagEnd color: #71BEBE;
- HelpXMLText color: #E5E5E5;
- // </css>
- ]]></>)
+ CSS: Class.Memoize(function () File.readURL("resource://dactyl-skin/global-styles.css")),
+
+ helpCSS: Class.Memoize(function () File.readURL("resource://dactyl-skin/help-styles.css"))
}, {
});
-
JSMLoader.loadSubScript("resource://dactyl-local-content/config.js", this);
config.INIT = update(Object.create(config.INIT), config.INIT, {
init: function init(dactyl, modules, window) {
init.superapply(this, arguments);
let img = window.Image();
img.src = this.logo || "resource://dactyl-local-content/logo.png";
img.onload = util.wrapCallback(function () {
- const { highlight } = require("highlight");
highlight.loadCSS(<>{"!Logo {"}
display: inline-block;
background: url({img.src});
width: {img.width}px;
height: {img.height}px;
{"}"}</>);
img = null;
});
@@ -941,11 +608,11 @@ config.INIT = update(Object.create(confi
modules.yes_i_know_i_should_not_report_errors_in_these_branches_thanks.indexOf(this.branch) === -1)
dactyl.warn(_("warn.notDefaultBranch", config.appName, this.branch));
}, 1000);
}
});
endModule();
-} catch(e){ if (typeof e === "string") e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
+// catch(e){ if (typeof e === "string") e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
// vim: set fdm=marker sw=4 sts=4 et ft=javascript:
diff --git a/common/modules/contexts.jsm b/common/modules/contexts.jsm
--- a/common/modules/contexts.jsm
+++ b/common/modules/contexts.jsm
@@ -1,52 +1,69 @@
// Copyright (c) 2010-2011 by Kris Maglione <maglione.k@gmail.com>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
-
-try {
+/* use strict */
Components.utils.import("resource://dactyl/bootstrap.jsm");
defineModule("contexts", {
exports: ["Contexts", "Group", "contexts"],
- use: ["commands", "messages", "options", "services", "storage", "styles", "template", "util"]
+ require: ["services", "util"]
}, this);
+this.lazyRequire("overlay", ["overlay"]);
+
var Const = function Const(val) Class.Property({ enumerable: true, value: val });
var Group = Class("Group", {
init: function init(name, description, filter, persist) {
const self = this;
this.name = name;
this.description = description;
this.filter = filter || this.constructor.defaultFilter;
this.persist = persist || false;
this.hives = [];
- },
+ this.children = [];
+ },
+
+ get contexts() this.modules.contexts,
+
+ set lastDocument(val) { this._lastDocument = util.weakReference(val); },
+ get lastDocument() this._lastDocument && this._lastDocument.get(),
modifiable: true,
- cleanup: function cleanup() {
+ cleanup: function cleanup(reason) {
for (let hive in values(this.hives))
util.trapErrors("cleanup", hive);
this.hives = [];
for (let hive in keys(this.hiveMap))
delete this[hive];
- },
- destroy: function destroy() {
+
+ if (reason != "shutdown")
+ this.children.splice(0).forEach(this.contexts.closure.removeGroup);
+ },
+ destroy: function destroy(reason) {
for (let hive in values(this.hives))
util.trapErrors("destroy", hive);
- },
+
+ if (reason != "shutdown")
+ this.children.splice(0).forEach(this.contexts.closure.removeGroup);
+ },
argsExtra: function argsExtra() ({}),
+ makeArgs: function makeArgs(doc, context, args) {
+ let res = update({ doc: doc, context: context }, args);
+ return update(res, this.argsExtra(res), args);
+ },
+
get toStringParams() [this.name],
get builtin() this.modules.contexts.builtinGroups.indexOf(this) >= 0,
}, {
compileFilter: function (patterns, default_) {
if (arguments.length < 2)
default_ = false;
@@ -62,20 +79,31 @@ var Group = Class("Group", {
template.map(this.filters,
function (f) <span highlight={uri && f(uri) ? "Filter" : ""}>{f}</span>,
<>,</>),
filters: Option.parse.sitelist(patterns)
});
},
- defaultFilter: Class.memoize(function () this.compileFilter(["*"]))
-});
+ defaultFilter: Class.Memoize(function () this.compileFilter(["*"]))
+});
var Contexts = Module("contexts", {
+ init: function () {
+ this.pluginModules = {};
+ },
+
+ cleanup: function () {
+ for each (let module in this.pluginModules)
+ util.trapErrors("unload", module);
+
+ this.pluginModules = {};
+ },
+
Local: function Local(dactyl, modules, window) ({
init: function () {
const contexts = this;
this.modules = modules;
Object.defineProperty(modules.plugins, "contexts", Const({}));
this.groupList = [];
@@ -110,23 +138,23 @@ var Contexts = Module("contexts", {
completer: function (context) modules.completion.group(context)
});
memoize(modules, "userContext", function () contexts.Context(modules.io.getRCFile("~", true), contexts.user, [modules, true]));
memoize(modules, "_userContext", function () contexts.Context(modules.io.getRCFile("~", true), contexts.user, [modules.userContext]));
},
cleanup: function () {
- for (let hive in values(this.groupList))
- util.trapErrors("cleanup", hive);
- },
+ for each (let hive in this.groupList.slice())
+ util.trapErrors("cleanup", hive, "shutdown");
+ },
destroy: function () {
- for (let hive in values(this.groupList))
- util.trapErrors("destroy", hive);
+ for each (let hive in values(this.groupList.slice()))
+ util.trapErrors("destroy", hive, "shutdown");
for (let [name, plugin] in iter(this.modules.plugins.contexts))
if (plugin && "onUnload" in plugin && callable(plugin.onUnload))
util.trapErrors("onUnload", plugin);
},
signals: {
"browser.locationChange": function (webProgress, request, uri) {
@@ -174,39 +202,49 @@ var Contexts = Module("contexts", {
let isPlugin = array.nth(io.getRuntimeDirectories("plugins"),
function (dir) dir.contains(file, true),
0);
let isRuntime = array.nth(io.getRuntimeDirectories(""),
function (dir) dir.contains(file, true),
0);
+ let name = isPlugin ? file.getRelativeDescriptor(isPlugin).replace(File.PATH_SEP, "-")
+ : file.leafName;
+ let id = util.camelCase(name.replace(/\.[^.]*$/, ""));
+
let contextPath = file.path;
let self = Set.has(plugins, contextPath) && plugins.contexts[contextPath];
+ if (!self && isPlugin && false)
+ self = Set.has(plugins, id) && plugins[id];
+
if (self) {
if (Set.has(self, "onUnload"))
- self.onUnload();
+ util.trapErrors("onUnload", self);
}
else {
- let name = isPlugin ? file.getRelativeDescriptor(isPlugin).replace(File.PATH_SEP, "-")
- : file.leafName;
-
self = args && !isArray(args) ? args : newContext.apply(null, args || [userContext]);
update(self, {
- NAME: Const(name.replace(/\.[^.]*$/, "").replace(/-([a-z])/g, function (m, n1) n1.toUpperCase())),
+ NAME: Const(id),
PATH: Const(file.path),
CONTEXT: Const(self),
+ set isGlobalModule(val) {
+ // Hack.
+ if (val)
+ throw Contexts;
+ },
+
unload: Const(function unload() {
if (plugins[this.NAME] === this || plugins[this.PATH] === this)
if (this.onUnload)
- this.onUnload();
+ util.trapErrors("onUnload", this);
if (plugins[this.NAME] === this)
delete plugins[this.NAME];
if (plugins[this.PATH] === this)
delete plugins[this.PATH];
if (plugins.contexts[contextPath] === this)
@@ -247,16 +285,84 @@ var Contexts = Module("contexts", {
return plugins.contexts[contextPath] = self;
},
Script: function Script(file, group) {
return this.Context(file, group, [this.modules.userContext, true]);
},
+ Module: function Module(uri, isPlugin) {
+ const { io, plugins } = this.modules;
+
+ let canonical = uri.spec;
+ if (uri.scheme == "resource")
+ canonical = services["resource:"].resolveURI(uri);
+
+ if (uri instanceof Ci.nsIFileURL)
+ var file = File(uri.file);
+
+ let isPlugin = array.nth(io.getRuntimeDirectories("plugins"),
+ function (dir) dir.contains(file, true),
+ 0);
+
+ let name = isPlugin && file && file.getRelativeDescriptor(isPlugin)
+ .replace(File.PATH_SEP, "-");
+ let id = util.camelCase(name.replace(/\.[^.]*$/, ""));
+
+ let self = Set.has(this.pluginModules, canonical) && this.pluginModules[canonical];
+
+ if (!self) {
+ self = Object.create(jsmodules);
+
+ update(self, {
+ NAME: Const(id),
+
+ PATH: Const(file && file.path),
+
+ CONTEXT: Const(self),
+
+ get isGlobalModule() true,
+ set isGlobalModule(val) {
+ util.assert(val, "Loading non-global module as global",
+ false);
+ },
+
+ unload: Const(function unload() {
+ if (contexts.pluginModules[canonical] == this) {
+ if (this.onUnload)
+ util.trapErrors("onUnload", this);
+
+ delete contexts.pluginModules[canonical];
+ }
+
+ for each (let { plugins } in overlay.modules)
+ if (plugins[this.NAME] == this)
+ delete plugins[this.name];
+ })
+ });
+
+ JSMLoader.loadSubScript(uri.spec, self, File.defaultEncoding);
+ this.pluginModules[canonical] = self;
+ }
+
+ // This belongs elsewhere
+ if (isPlugin)
+ Object.defineProperty(plugins, self.NAME, {
+ configurable: true,
+ enumerable: true,
+ get: function () self,
+ set: function (val) {
+ util.dactyl(val).reportError(FailedAssertion(_("plugin.notReplacingContext", self.NAME), 3, false), true);
+ }
+ });
+
+ return self;
+ },
+
context: null,
/**
* Returns a frame object describing the currently executing
* command, if applicable, otherwise returns the passed frame.
*
* @param {nsIStackFrame} frame
*/
@@ -266,31 +372,40 @@ var Contexts = Module("contexts", {
__proto__: frame,
filename: this.context.file[0] == "[" ? this.context.file
: services.io.newFileURI(File(this.context.file)).spec,
lineNumber: this.context.line
};
return frame;
},
- groups: Class.memoize(function () this.matchingGroups(this.modules.buffer.uri)),
-
- allGroups: Class.memoize(function () Object.create(this.groupsProto, {
+ groups: Class.Memoize(function () this.matchingGroups()),
+
+ allGroups: Class.Memoize(function () Object.create(this.groupsProto, {
groups: { value: this.initializedGroups() }
})),
matchingGroups: function (uri) Object.create(this.groupsProto, {
groups: { value: this.activeGroups(uri) }
}),
- activeGroups: function (uri, doc) {
+ activeGroups: function (uri) {
+ if (uri instanceof Ci.nsIDOMDocument)
+ var [doc, uri] = [uri, uri.documentURIObject || util.newURI(uri.documentURI)];
+
if (!uri)
- ({ uri, doc }) = this.modules.buffer;
- return this.initializedGroups().filter(function (g) uri && g.filter(uri, doc));
- },
+ var { uri, doc } = this.modules.buffer;
+
+ return this.initializedGroups().filter(function (g) {
+ let res = uri && g.filter(uri, doc);
+ if (doc)
+ g.lastDocument = res && doc;
+ return res;
+ });
+ },
flush: function flush() {
delete this.groups;
delete this.allGroups;
},
initializedGroups: function (hive)
let (need = hive ? [hive] : Object.keys(this.hives))
@@ -305,28 +420,29 @@ var Contexts = Module("contexts", {
group = this.Group(name, description, filter, persist);
this.groupList.unshift(group);
this.groupMap[name] = group;
this.hiveProto.__defineGetter__(name, function () group[this._hive]);
}
if (replace) {
util.trapErrors("cleanup", group);
+
if (description)
group.description = description;
if (filter)
group.filter = filter;
group.persist = persist;
}
this.flush();
return group;
},
- removeGroup: function removeGroup(name, filter) {
+ removeGroup: function removeGroup(name) {
if (isObject(name)) {
if (this.groupList.indexOf(name) === -1)
return;
name = name.name;
}
let group = this.getGroup(name);
@@ -396,17 +512,17 @@ var Contexts = Module("contexts", {
let type = ["-builtin", "-ex", "-javascript", "-keys"].reduce(function (a, b) args[b] ? b : a, default_);
switch (type) {
case "-builtin":
let noremap = true;
/* fallthrough */
case "-keys":
let silent = args["-silent"];
- rhs = events.canonicalKeys(rhs, true);
+ rhs = DOM.Event.canonicalKeys(rhs, true);
var action = function action() {
events.feedkeys(action.macro(makeParams(this, arguments)),
noremap, silent);
};
action.macro = util.compileMacro(rhs, true);
break;
case "-ex":
@@ -445,31 +561,32 @@ var Contexts = Module("contexts", {
},
cleanup: function cleanup() {},
destroy: function destroy() {},
get modifiable() this.group.modifiable,
get argsExtra() this.group.argsExtra,
+ get makeArgs() this.group.makeArgs,
get builtin() this.group.builtin,
get name() this.group.name,
set name(val) this.group.name = val,
get description() this.group.description,
set description(val) this.group.description = val,
get filter() this.group.filter,
set filter(val) this.group.filter = val,
get persist() this.group.persist,
set persist(val) this.group.persist = val,
- prefix: Class.memoize(function () this.name === "builtin" ? "" : this.name + ":"),
+ prefix: Class.Memoize(function () this.name === "builtin" ? "" : this.name + ":"),
get toStringParams() [this.name]
})
}, {
commands: function initCommands(dactyl, modules, window) {
const { commands, contexts } = modules;
commands.add(["gr[oup]"],
@@ -502,18 +619,29 @@ var Contexts = Module("contexts", {
}
if (!group.builtin && args.has("-args")) {
group.argsExtra = contexts.bindMacro({ literalArg: "return " + args["-args"] },
"-javascript", util.identity);
group.args = args["-args"];
}
- if (args.context)
+ if (args.context) {
args.context.group = group;
+ if (args.context.context) {
+ args.context.context.group = group;
+
+ let parent = args.context.context.GROUP;
+ if (parent && parent != group) {
+ group.parent = parent;
+ if (!~parent.children.indexOf(group))
+ parent.children.push(group);
+ }
+ }
+ }
util.assert(!group.builtin ||
!["-description", "-locations", "-nopersist"]
.some(Set.has(args.explicitOpts)),
_("group.cantModifyBuiltin"));
},
{
argCount: "?",
@@ -675,11 +803,11 @@ var Contexts = Module("contexts", {
});
});
};
}
});
endModule();
-} catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
+// catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
// vim: set fdm=marker sw=4 ts=4 et ft=javascript:
diff --git a/common/modules/dom.jsm b/common/modules/dom.jsm
new file mode 100644
--- /dev/null
+++ b/common/modules/dom.jsm
@@ -0,0 +1,1589 @@
+// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
+// Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
+//
+// This work is licensed for reuse under an MIT license. Details are
+// given in the LICENSE.txt file included with this file.
+/* use strict */
+
+Components.utils.import("resource://dactyl/bootstrap.jsm");
+defineModule("dom", {
+ exports: ["$", "DOM", "NS", "XBL", "XHTML", "XUL"]
+}, this);
+
+var XBL = Namespace("xbl", "http://www.mozilla.org/xbl");
+var XHTML = Namespace("html", "http://www.w3.org/1999/xhtml");
+var XUL = Namespace("xul", "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+var NS = Namespace("dactyl", "http://vimperator.org/namespaces/liberator");
+default xml namespace = XHTML;
+
+function BooleanAttribute(attr) ({
+ get: function (elem) elem.getAttribute(attr) == "true",
+ set: function (elem, val) {
+ if (val === "false" || !val)
+ elem.removeAttribute(attr);
+ else
+ elem.setAttribute(attr, true);
+ }
+});
+
+/**
+ * @class
+ *
+ * A jQuery-inspired DOM utility framework.
+ *
+ * Please note that while this currently implements an Array-like
+ * interface, this is *not a defined interface* and is very likely to
+ * change in the near future.
+ */
+var DOM = Class("DOM", {
+ init: function init(val, context, nodes) {
+ let self;
+ let length = 0;
+
+ if (nodes)
+ this.nodes = nodes;
+
+ if (context instanceof Ci.nsIDOMDocument)
+ this.document = context;
+
+ if (typeof val == "string")
+ val = context.querySelectorAll(val);
+
+ if (val == null)
+ ;
+ else if (typeof val == "xml" && context instanceof Ci.nsIDOMDocument)
+ this[length++] = DOM.fromXML(val, context, this.nodes);
+ else if (val instanceof Ci.nsIDOMNode || val instanceof Ci.nsIDOMWindow)
+ this[length++] = val;
+ else if ("length" in val)
+ for (let i = 0; i < val.length; i++)
+ this[length++] = val[i];
+ else if ("__iterator__" in val || isinstance(val, ["Iterator", "Generator"]))
+ for (let elem in val)
+ this[length++] = elem;
+ else
+ this[length++] = val;
+
+ this.length = length;
+ return self || this;
+ },
+
+ __iterator__: function __iterator__() {
+ for (let i = 0; i < this.length; i++)
+ yield this[i];
+ },
+
+ Empty: function Empty() this.constructor(null, this.document),
+
+ nodes: Class.Memoize(function () ({})),
+
+ get items() {
+ for (let i = 0; i < this.length; i++)
+ yield this.eq(i);
+ },
+
+ get document() this._document || this[0] && (this[0].ownerDocument || this[0].document || this[0]),
+ set document(val) this._document = val,
+
+ attrHooks: array.toObject([
+ ["", {
+ href: { get: function (elem) elem.href || elem.getAttribute("href") },
+ src: { get: function (elem) elem.src || elem.getAttribute("src") },
+ checked: { get: function (elem) elem.hasAttribute("checked") ? elem.getAttribute("checked") == "true" : elem.checked,
+ set: function (elem, val) { elem.setAttribute("checked", !!val); elem.checked = val } },
+ collapsed: BooleanAttribute("collapsed"),
+ disabled: BooleanAttribute("disabled"),
+ hidden: BooleanAttribute("hidden"),
+ readonly: BooleanAttribute("readonly")
+ }]
+ ]),
+
+ matcher: function matcher(sel) function (elem) elem.mozMatchesSelector && elem.mozMatchesSelector(sel),
+
+ each: function each(fn, self) {
+ let obj = self || this.Empty();
+ for (let i = 0; i < this.length; i++)
+ fn.call(self || update(obj, [this[i]]), this[i], i);
+ return this;
+ },
+
+ eachDOM: function eachDOM(val, fn, self) {
+ XML.prettyPrinting = XML.ignoreWhitespace = false;
+ if (isString(val))
+ val = XML(val);
+
+ if (typeof val == "xml")
+ return this.each(function (elem, i) {
+ fn.call(this, DOM.fromXML(val, elem.ownerDocument), elem, i);
+ }, self || this);
+
+ let dom = this;
+ function munge(val, container, idx) {
+ if (val instanceof Ci.nsIDOMRange)
+ return val.extractContents();
+ if (val instanceof Ci.nsIDOMNode)
+ return val;
+
+ if (typeof val == "xml") {
+ val = dom.constructor(val, dom.document);
+ if (container)
+ container[idx] = val[0];
+ }
+
+ if (isObject(val) && "length" in val) {
+ let frag = dom.document.createDocumentFragment();
+ for (let i = 0; i < val.length; i++)
+ frag.appendChild(munge(val[i], val, i));
+ return frag;
+ }
+ return val;
+ }
+
+ if (callable(val))
+ return this.each(function (elem, i) {
+ util.withProperErrors(fn, this, munge(val.call(this, elem, i)), elem, i);
+ }, self || this);
+
+ if (this.length)
+ util.withProperErrors(fn, self || this, munge(val), this[0], 0);
+ return this;
+ },
+
+ eq: function eq(idx) {
+ return this.constructor(this[idx >= 0 ? idx : this.length + idx]);
+ },
+
+ find: function find(val) {
+ return this.map(function (elem) elem.querySelectorAll(val));
+ },
+
+ findAnon: function findAnon(attr, val) {
+ return this.map(function (elem) elem.ownerDocument.getAnonymousElementByAttribute(elem, attr, val));
+ },
+
+ filter: function filter(val, self) {
+ let res = this.Empty();
+
+ if (!callable(val))
+ val = this.matcher(val);
+
+ this.constructor(Array.filter(this, val, self || this));
+ let obj = self || this.Empty();
+ for (let i = 0; i < this.length; i++)
+ if (val.call(self || update(obj, [this[i]]), this[i], i))
+ res[res.length++] = this[i];
+
+ return res;
+ },
+
+ is: function is(val) {
+ return this.some(this.matcher(val));
+ },
+
+ reverse: function reverse() {
+ Array.reverse(this);
+ return this;
+ },
+
+ all: function all(fn, self) {
+ let res = this.Empty();
+
+ this.each(function (elem) {
+ while(true) {
+ elem = fn.call(this, elem)
+ if (elem instanceof Ci.nsIDOMElement)
+ res[res.length++] = elem;
+ else if (elem && "length" in elem)
+ for (let i = 0; i < tmp.length; i++)
+ res[res.length++] = tmp[j];
+ else
+ break;
+ }
+ }, self || this);
+ return res;
+ },
+
+ map: function map(fn, self) {
+ let res = this.Empty();
+ let obj = self || this.Empty();
+
+ for (let i = 0; i < this.length; i++) {
+ let tmp = fn.call(self || update(obj, [this[i]]), this[i], i);
+ if (isObject(tmp) && "length" in tmp)
+ for (let j = 0; j < tmp.length; j++)
+ res[res.length++] = tmp[j];
+ else if (tmp != null)
+ res[res.length++] = tmp;
+ }
+
+ return res;
+ },
+
+ slice: function eq(start, end) {
+ return this.constructor(Array.slice(this, start, end));
+ },
+
+ some: function some(fn, self) {
+ for (let i = 0; i < this.length; i++)
+ if (fn.call(self || this, this[i], i))
+ return true;
+ return false;
+ },
+
+ get parent() this.map(function (elem) elem.parentNode, this),
+
+ get offsetParent() this.map(function (elem) {
+ do {
+ var parent = elem.offsetParent;
+ if (parent instanceof Ci.nsIDOMElement && DOM(parent).position != "static")
+ return parent;
+ }
+ while (parent);
+ }, this),
+
+ get ancestors() this.all(function (elem) elem.parentNode),
+
+ get children() this.map(function (elem) Array.filter(elem.childNodes,
+ function (e) e instanceof Ci.nsIDOMElement),
+ this),
+
+ get contents() this.map(function (elem) elem.childNodes, this),
+
+ get siblings() this.map(function (elem) Array.filter(elem.parentNode.childNodes,
+ function (e) e != elem && e instanceof Ci.nsIDOMElement),
+ this),
+
+ get siblingsBefore() this.all(function (elem) elem.previousElementSibling),
+ get siblingsAfter() this.all(function (elem) elem.nextElementSibling),
+
+ get class() let (self = this) ({
+ toString: function () self[0].className,
+
+ get list() Array.slice(self[0].classList),
+ set list(val) self.attr("class", val.join(" ")),
+
+ each: function each(meth, arg) {
+ return self.each(function (elem) {
+ elem.classList[meth](arg);
+ })
+ },
+
+ add: function add(cls) this.each("add", cls),
+ remove: function remove(cls) this.each("remove", cls),
+ toggle: function toggle(cls, val, thisObj) {
+ if (callable(val))
+ return self.each(function (elem, i) {
+ this.class.toggle(cls, val.call(thisObj || this, elem, i));
+ });
+ return this.each(val == null ? "toggle" : val ? "add" : "remove", cls);
+ },
+
+ has: function has(cls) this[0].classList.has(cls)
+ }),
+
+ get highlight() let (self = this) ({
+ toString: function () self.attrNS(NS, "highlight") || "",
+
+ get list() let (s = this.toString().trim()) s ? s.split(/\s+/) : [],
+ set list(val) {
+ let str = array.uniq(val).join(" ").trim();
+ self.attrNS(NS, "highlight", str || null);
+ },
+
+ has: function has(hl) ~this.list.indexOf(hl),
+
+ add: function add(hl) self.each(function () {
+ highlight.loaded[hl] = true;
+ this.highlight.list = this.highlight.list.concat(hl);
+ }),
+
+ remove: function remove(hl) self.each(function () {
+ this.highlight.list = this.highlight.list.filter(function (h) h != hl);
+ }),
+
+ toggle: function toggle(hl, val, thisObj) self.each(function (elem, i) {
+ let { highlight } = this;
+ let v = callable(val) ? val.call(thisObj || this, elem, i) : val;
+
+ highlight[(v == null ? highlight.has(hl) : !v) ? "remove" : "add"](hl)
+ }),
+ }),
+
+ get rect() this[0] instanceof Ci.nsIDOMWindow ? { width: this[0].scrollMaxX + this[0].innerWidth,
+ height: this[0].scrollMaxY + this[0].innerHeight,
+ get right() this.width + this.left,
+ get bottom() this.height + this.top,
+ top: -this[0].scrollY,
+ left: -this[0].scrollX } :
+ this[0] ? this[0].getBoundingClientRect() : {},
+
+ get viewport() {
+ if (this[0] instanceof Ci.nsIDOMWindow)
+ return {
+ get width() this.right - this.left,
+ get height() this.bottom - this.top,
+ bottom: this[0].innerHeight,
+ right: this[0].innerWidth,
+ top: 0, left: 0
+ };
+
+ let r = this.rect;
+ return {
+ width: this[0].clientWidth,
+ height: this[0].clientHeight,
+ top: r.top + this[0].clientTop,
+ get bottom() this.top + this.height,
+ left: r.left + this[0].clientLeft,
+ get right() this.left + this.width
+ }
+ },
+
+ scrollPos: function scrollPos(left, top) {
+ if (arguments.length == 0) {
+ if (this[0] instanceof Ci.nsIDOMElement)
+ return { top: this[0].scrollTop, left: this[0].scrollLeft,
+ height: this[0].scrollHeight, width: this[0].scrollWidth,
+ innerHeight: this[0].clientHeight, innerWidth: this[0].innerWidth };
+
+ if (this[0] instanceof Ci.nsIDOMWindow)
+ return { top: this[0].scrollY, left: this[0].scrollX,
+ height: this[0].scrollMaxY + this[0].innerHeight,
+ width: this[0].scrollMaxX + this[0].innerWidth,
+ innerHeight: this[0].innerHeight, innerWidth: this[0].innerWidth };
+
+ return null;
+ }
+ let func = callable(left) && left;
+
+ return this.each(function (elem, i) {
+ if (func)
+ ({ left, top }) = func.call(this, elem, i);
+
+ if (elem instanceof Ci.nsIDOMWindow)
+ elem.scrollTo(left == null ? elem.scrollX : left,
+ top == null ? elem.scrollY : top);
+ else {
+ if (left != null)
+ elem.scrollLeft = left;
+ if (top != null)
+ elem.scrollTop = top;
+ }
+ });
+ },
+
+ /**
+ * Returns true if the given DOM node is currently visible.
+ * @returns {boolean}
+ */
+ get isVisible() {
+ let style = this[0] && this.style;
+ return style && style.visibility == "visible" && style.display != "none";
+ },
+
+ get editor() {
+ if (!this.length)
+ return;
+
+ this[0] instanceof Ci.nsIDOMNSEditableElement;
+ try {
+ if (this[0].editor instanceof Ci.nsIEditor)
+ var editor = this[0].editor;
+ }
+ catch (e) {
+ util.reportError(e);
+ }
+
+ try {
+ if (!editor)
+ editor = this[0].QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIEditingSession)
+ .getEditorForWindow(this[0]);
+ }
+ catch (e) {}
+
+ editor instanceof Ci.nsIPlaintextEditor;
+ editor instanceof Ci.nsIHTMLEditor;
+ return editor;
+ },
+
+ get isEditable() !!this.editor,
+
+ get isInput() isinstance(this[0], [Ci.nsIDOMHTMLInputElement,
+ Ci.nsIDOMHTMLTextAreaElement,
+ Ci.nsIDOMXULTextBoxElement])
+ && this.isEditable,
+
+ /**
+ * Returns an object representing a Node's computed CSS style.
+ * @returns {Object}
+ */
+ get style() {
+ let node = this[0];
+ if (node instanceof Ci.nsIDOMWindow)
+ node = node.document;
+ if (node instanceof Ci.nsIDOMDocument)
+ node = node.documentElement;
+ while (node && !(node instanceof Ci.nsIDOMElement) && node.parentNode)
+ node = node.parentNode;
+
+ try {
+ var res = node.ownerDocument.defaultView.getComputedStyle(node, null);
+ }
+ catch (e) {}
+
+ if (res == null) {
+ util.dumpStack(_("error.nullComputedStyle", node));
+ Cu.reportError(Error(_("error.nullComputedStyle", node)));
+ return {};
+ }
+ return res;
+ },
+
+ /**
+ * Parses the fields of a form and returns a URL/POST-data pair
+ * that is the equivalent of submitting the form.
+ *
+ * @returns {object} An object with the following elements:
+ * url: The URL the form points to.
+ * postData: A string containing URL-encoded post data, if this
+ * form is to be POSTed
+ * charset: The character set of the GET or POST data.
+ * elements: The key=value pairs used to generate query information.
+ */
+ // Nuances gleaned from browser.jar/content/browser/browser.js
+ get formData() {
+ function encode(name, value, param) {
+ param = param ? "%s" : "";
+ if (post)
+ return name + "=" + encodeComponent(value + param);
+ return encodeComponent(name) + "=" + encodeComponent(value) + param;
+ }
+
+ let field = this[0];
+ let form = field.form;
+ let doc = form.ownerDocument;
+
+ let charset = doc.characterSet;
+ let converter = services.CharsetConv(charset);
+ for each (let cs in form.acceptCharset.split(/\s*,\s*|\s+/)) {
+ let c = services.CharsetConv(cs);
+ if (c) {
+ converter = services.CharsetConv(cs);
+ charset = cs;
+ }
+ }
+
+ let uri = util.newURI(doc.baseURI.replace(/\?.*/, ""), charset);
+ let url = util.newURI(form.action, charset, uri).spec;
+
+ let post = form.method.toUpperCase() == "POST";
+
+ let encodeComponent = encodeURIComponent;
+ if (charset !== "UTF-8")
+ encodeComponent = function encodeComponent(str)
+ escape(converter.ConvertFromUnicode(str) + converter.Finish());
+
+ let elems = [];
+ if (field instanceof Ci.nsIDOMHTMLInputElement && field.type == "submit")
+ elems.push(encode(field.name, field.value));
+
+ for (let [, elem] in iter(form.elements))
+ if (elem.name && !elem.disabled) {
+ if (DOM(elem).isInput
+ || /^(?:hidden|textarea)$/.test(elem.type)
+ || elem.type == "submit" && elem == field
+ || elem.checked && /^(?:checkbox|radio)$/.test(elem.type))
+ elems.push(encode(elem.name, elem.value, elem === field));
+ else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
+ for (let [, opt] in Iterator(elem.options))
+ if (opt.selected)
+ elems.push(encode(elem.name, opt.value));
+ }
+ }
+
+ if (post)
+ return { url: url, postData: elems.join('&'), charset: charset, elements: elems };
+ return { url: url + "?" + elems.join('&'), postData: null, charset: charset, elements: elems };
+ },
+
+ /**
+ * Generates an XPath expression for the given element.
+ *
+ * @returns {string}
+ */
+ get xpath() {
+ function quote(val) "'" + val.replace(/[\\']/g, "\\$&") + "'";
+ if (!(this[0] instanceof Ci.nsIDOMElement))
+ return null;
+
+ let res = [];
+ let doc = this.document;
+ for (let elem = this[0];; elem = elem.parentNode) {
+ if (!(elem instanceof Ci.nsIDOMElement))
+ res.push("");
+ else if (elem.id)
+ res.push("id(" + quote(elem.id) + ")");
+ else {
+ let name = elem.localName;
+ if (elem.namespaceURI && (elem.namespaceURI != XHTML || doc.xmlVersion))
+ if (elem.namespaceURI in DOM.namespaceNames)
+ name = DOM.namespaceNames[elem.namespaceURI] + ":" + name;
+ else
+ name = "*[local-name()=" + quote(name) + " and namespace-uri()=" + quote(elem.namespaceURI) + "]";
+
+ res.push(name + "[" + (1 + iter(DOM.XPath("./" + name, elem.parentNode)).indexOf(elem)) + "]");
+ continue;
+ }
+ break;
+ }
+
+ return res.reverse().join("/");
+ },
+
+ /**
+ * Returns a string or XML representation of this node.
+ *
+ * @param {boolean} color If true, return a colored, XML
+ * representation of this node.
+ */
+ repr: function repr(color) {
+ XML.ignoreWhitespace = XML.prettyPrinting = false;
+
+ function namespaced(node) {
+ var ns = DOM.namespaceNames[node.namespaceURI] || /^(?:(.*?):)?/.exec(node.name)[0];
+ if (!ns)
+ return node.localName;
+ if (color)
+ return <><span highlight="HelpXMLNamespace">{ns}</span>{node.localName}</>
+ return ns + ":" + node.localName;
+ }
+
+ let res = [];
+ this.each(function (elem) {
+ try {
+ let hasChildren = elem.firstChild && (!/^\s*$/.test(elem.firstChild) || elem.firstChild.nextSibling)
+ if (color)
+ res.push(<span highlight="HelpXML"><span highlight="HelpXMLTagStart">&lt;{
+ namespaced(elem)} {
+ template.map(array.iterValues(elem.attributes),
+ function (attr)
+ <span highlight="HelpXMLAttribute">{namespaced(attr)}</span> +
+ <span highlight="HelpXMLString">{attr.value}</span>,
+ <> </>)
+ }{ !hasChildren ? "/>" : ">"
+ }</span>{ !hasChildren ? "" : <>...</> +
+ <span highlight="HtmlTagEnd">&lt;{namespaced(elem)}></span>
+ }</span>);
+ else {
+ let tag = "<" + [namespaced(elem)].concat(
+ [namespaced(a) + "=" + template.highlight(a.value, true)
+ for ([i, a] in array.iterItems(elem.attributes))]).join(" ");
+
+ res.push(tag + (!hasChildren ? "/>" : ">...</" + namespaced(elem) + ">"));
+ }
+ }
+ catch (e) {
+ res.push({}.toString.call(elem));
+ }
+ }, this);
+ return template.map(res, util.identity, <>,</>);
+ },
+
+ attr: function attr(key, val) {
+ return this.attrNS("", key, val);
+ },
+
+ attrNS: function attrNS(ns, key, val) {
+ if (val !== undefined)
+ key = array.toObject([[key, val]]);
+
+ let hooks = this.attrHooks[ns] || {};
+
+ if (isObject(key))
+ return this.each(function (elem, i) {
+ for (let [k, v] in Iterator(key)) {
+ if (callable(v))
+ v = v.call(this, elem, i);
+
+ if (Set.has(hooks, k) && hooks[k].set)
+ hooks[k].set.call(this, elem, v, k);
+ else if (v == null)
+ elem.removeAttributeNS(ns, k);
+ else
+ elem.setAttributeNS(ns, k, v);
+ }
+ });
+
+ if (!this.length)
+ return null;
+
+ if (Set.has(hooks, key) && hooks[key].get)
+ return hooks[key].get.call(this, this[0], key);
+
+ if (!this[0].hasAttributeNS(ns, key))
+ return null;
+
+ return this[0].getAttributeNS(ns, key);
+ },
+
+ css: update(function css(key, val) {
+ if (val !== undefined)
+ key = array.toObject([[key, val]]);
+
+ if (isObject(key))
+ return this.each(function (elem) {
+ for (let [k, v] in Iterator(key))
+ elem.style[css.property(k)] = v;
+ });
+
+ return this[0].style[css.property(key)];
+ }, {
+ name: function (property) property.replace(/[A-Z]/g, function (m0) "-" + m0.toLowerCase()),
+
+ property: function (name) name.replace(/-(.)/g, function (m0, m1) m1.toUpperCase())
+ }),
+
+ append: function append(val) {
+ return this.eachDOM(val, function (elem, target) {
+ target.appendChild(elem);
+ });
+ },
+
+ prepend: function prepend(val) {
+ return this.eachDOM(val, function (elem, target) {
+ target.insertBefore(elem, target.firstChild);
+ });
+ },
+
+ before: function before(val) {
+ return this.eachDOM(val, function (elem, target) {
+ target.parentNode.insertBefore(elem, target);
+ });
+ },
+
+ after: function after(val) {
+ return this.eachDOM(val, function (elem, target) {
+ target.parentNode.insertBefore(elem, target.nextSibling);
+ });
+ },
+
+ appendTo: function appendTo(elem) {
+ if (!(elem instanceof this.constructor))
+ elem = this.constructor(elem, this.document);
+ elem.append(this);
+ return this;
+ },
+
+ prependTo: function prependTo(elem) {
+ if (!(elem instanceof this.constructor))
+ elem = this.constructor(elem, this.document);
+ elem.prepend(this);
+ return this;
+ },
+
+ insertBefore: function insertBefore(elem) {
+ if (!(elem instanceof this.constructor))
+ elem = this.constructor(elem, this.document);
+ elem.before(this);
+ return this;
+ },
+
+ insertAfter: function insertAfter(elem) {
+ if (!(elem instanceof this.constructor))
+ elem = this.constructor(elem, this.document);
+ elem.after(this);
+ return this;
+ },
+
+ remove: function remove() {
+ return this.each(function (elem) {
+ if (elem.parentNode)
+ elem.parentNode.removeChild(elem);
+ }, this);
+ },
+
+ empty: function empty() {
+ return this.each(function (elem) {
+ while (elem.firstChild)
+ elem.removeChild(elem.firstChild);
+ }, this);
+ },
+
+ toggle: function toggle(val, self) {
+ if (callable(val))
+ return this.each(function (elem, i) {
+ this[val.call(self || this, elem, i) ? "show" : "hide"]();
+ });
+
+ if (arguments.length)
+ return this[val ? "show" : "hide"]();
+
+ let hidden = this.map(function (elem) elem.style.display == "none");
+ return this.each(function (elem, i) {
+ this[hidden[i] ? "show" : "hide"]();
+ });
+ },
+ hide: function hide() {
+ return this.each(function (elem) { elem.style.display = "none"; }, this);
+ },
+ show: function show() {
+ for (let i = 0; i < this.length; i++)
+ if (!this[i].dactylDefaultDisplay && this[i].style.display)
+ this[i].style.display = "";
+
+ this.each(function (elem) {
+ if (!elem.dactylDefaultDisplay)
+ elem.dactylDefaultDisplay = this.style.display;
+ });
+
+ return this.each(function (elem) {
+ elem.style.display = elem.dactylDefaultDisplay == "none" ? "block" : "";
+ }, this);
+ },
+
+ createContents: function createContents()
+ this.each(DOM.createContents, this),
+
+ isScrollable: function isScrollable(direction)
+ this.length && DOM.isScrollable(this[0], direction),
+
+ getSet: function getSet(args, get, set) {
+ if (!args.length)
+ return this[0] && get.call(this, this[0]);
+
+ let [fn, self] = args;
+ if (!callable(fn))
+ fn = function () args[0];
+
+ return this.each(function (elem, i) {
+ set.call(this, elem, fn.call(self || this, elem, i));
+ }, this);
+ },
+
+ html: function html(txt, self) {
+ return this.getSet(arguments,
+ function (elem) elem.innerHTML,
+ function (elem, val) { elem.innerHTML = val });
+ },
+
+ text: function text(txt, self) {
+ return this.getSet(arguments,
+ function (elem) elem.textContent,
+ function (elem, val) { elem.textContent = val });
+ },
+
+ val: function val(txt) {
+ return this.getSet(arguments,
+ function (elem) elem.value,
+ function (elem, val) { elem.value = val == null ? "" : val });
+ },
+
+ listen: function listen(event, listener, capture) {
+ if (isObject(event))
+ capture = listener;
+ else
+ event = array.toObject([[event, listener]]);
+
+ for (let [k, v] in Iterator(event))
+ event[k] = util.wrapCallback(v, true);
+
+ return this.each(function (elem) {
+ for (let [k, v] in Iterator(event))
+ elem.addEventListener(k, v, capture);
+ });
+ },
+ unlisten: function unlisten(event, listener, capture) {
+ if (isObject(event))
+ capture = listener;
+ else
+ event = array.toObject([[key, val]]);
+
+ return this.each(function (elem) {
+ for (let [k, v] in Iterator(event))
+ elem.removeEventListener(k, v.wrapper || v, capture);
+ });
+ },
+
+ dispatch: function dispatch(event, params, extraProps) {
+ this.canceled = false;
+ return this.each(function (elem) {
+ let evt = DOM.Event(this.document, event, params, elem);
+ if (!DOM.Event.dispatch(elem, evt, extraProps))
+ this.canceled = true;
+ }, this);
+ },
+
+ focus: function focus(arg, extra) {
+ if (callable(arg))
+ return this.listen("focus", arg, extra);
+
+ let elem = this[0];
+ let flags = arg || services.focus.FLAG_BYMOUSE;
+ try {
+ if (elem instanceof Ci.nsIDOMDocument)
+ elem = elem.defaultView;
+ if (elem instanceof Ci.nsIDOMElement)
+ services.focus.setFocus(elem, flags);
+ else if (elem instanceof Ci.nsIDOMWindow) {
+ services.focus.focusedWindow = elem;
+ if (services.focus.focusedWindow != elem)
+ services.focus.clearFocus(elem);
+ }
+ }
+ catch (e) {
+ util.dump(elem);
+ util.reportError(e);
+ }
+ return this;
+ },
+ blur: function blur(arg, extra) {
+ if (callable(arg))
+ return this.listen("blur", arg, extra);
+ return this.each(function (elem) { elem.blur(); }, this);
+ },
+
+ /**
+ * Scrolls an element into view if and only if it's not already
+ * fully visible.
+ */
+ scrollIntoView: function scrollIntoView(alignWithTop) {
+ return this.each(function (elem) {
+ function getAlignment(viewport) {
+ if (alignWithTop !== undefined)
+ return alignWithTop;
+ if (rect.bottom < viewport.top)
+ return true;
+ if (rect.top > viewport.bottom)
+ return false;
+ return Math.abs(rect.top) < Math.abs(viewport.bottom - rect.bottom)
+ }
+
+ let rect;
+ function fix(parent) {
+ if (!(parent[0] instanceof Ci.nsIDOMWindow)
+ && parent.style.overflow == "visible")
+ return;
+
+ ({ rect }) = DOM(elem);
+ let { viewport } = parent;
+ let isect = util.intersection(rect, viewport);
+
+ if (isect.height < Math.min(viewport.height, rect.height)) {
+ let { top } = parent.scrollPos();
+ if (getAlignment(viewport))
+ parent.scrollPos(null, top - (viewport.top - rect.top));
+ else
+ parent.scrollPos(null, top - (viewport.bottom - rect.bottom));
+
+ }
+ }
+
+ for (let parent in this.ancestors.items)
+ fix(parent);
+
+ fix(DOM(this.document.defaultView));
+ });
+ },
+}, {
+ /**
+ * Creates an actual event from a pseudo-event object.
+ *
+ * The pseudo-event object (such as may be retrieved from
+ * DOM.Event.parse) 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. @optional
+ */
+ Event: Class("Event", {
+ init: function Event(doc, type, opts, target) {
+ const DEFAULTS = {
+ HTML: {
+ type: type, bubbles: true, cancelable: false
+ },
+ 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,
+ get screenX() this.view.mozInnerScreenX
+ + Math.max(0, this.clientX + (DOM(target || opts.target).rect.left || 0)),
+ get screenY() this.view.mozInnerScreenY
+ + Math.max(0, this.clientY + (DOM(target || opts.target).rect.top || 0)),
+ clientX: 0,
+ clientY: 0,
+ ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
+ button: 0,
+ relatedTarget: null
+ }
+ };
+
+ opts = opts || {};
+ var t = this.constructor.types[type] || "";
+ var evt = doc.createEvent(t + "Events");
+
+ let params = DEFAULTS[t || "HTML"];
+ let args = Object.keys(params);
+ update(params, this.constructor.defaults[type],
+ iter.toObject([k, opts[k]] for (k in opts) if (k in params)));
+
+ evt["init" + t + "Event"].apply(evt, args.map(function (k) params[k]));
+ return evt;
+ }
+ }, {
+ init: function init() {
+ // 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 dactyl's source code for
+ // comparisons like if (key == "<Esc>") { ... }
+ this.keyTable = {
+ add: ["Plus", "Add"],
+ back_space: ["BS"],
+ count: ["count"],
+ delete: ["Del"],
+ escape: ["Esc", "Escape"],
+ insert: ["Insert", "Ins"],
+ leader: ["Leader"],
+ left_shift: ["LT", "<"],
+ nop: ["Nop"],
+ pass: ["Pass"],
+ return: ["Return", "CR", "Enter"],
+ right_shift: [">"],
+ slash: ["/"],
+ space: ["Space", " "],
+ subtract: ["Minus", "Subtract"]
+ };
+
+ this.key_key = {};
+ this.code_key = {};
+ this.key_code = {};
+ this.code_nativeKey = {};
+
+ for (let list in values(this.keyTable))
+ for (let v in values(list)) {
+ if (v.length == 1)
+ v = v.toLowerCase();
+ this.key_key[v.toLowerCase()] = v;
+ }
+
+ for (let [k, v] in Iterator(Ci.nsIDOMKeyEvent)) {
+ this.code_nativeKey[v] = k.substr(4);
+
+ k = k.substr(7).toLowerCase();
+ let names = [k.replace(/(^|_)(.)/g, function (m, n1, n2) n2.toUpperCase())
+ .replace(/^NUMPAD/, "k")];
+
+ if (names[0].length == 1)
+ names[0] = names[0].toLowerCase();
+
+ if (k in this.keyTable)
+ names = this.keyTable[k];
+
+ this.code_key[v] = names[0];
+ for (let [, name] in Iterator(names)) {
+ this.key_key[name.toLowerCase()] = name;
+ this.key_code[name.toLowerCase()] = v;
+ }
+ }
+
+ // 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";
+ }
+
+ return this;
+ },
+
+
+ code_key: Class.Memoize(function (prop) this.init()[prop]),
+ code_nativeKey: Class.Memoize(function (prop) this.init()[prop]),
+ keyTable: Class.Memoize(function (prop) this.init()[prop]),
+ key_code: Class.Memoize(function (prop) this.init()[prop]),
+ key_key: Class.Memoize(function (prop) this.init()[prop]),
+ pseudoKeys: Set(["count", "leader", "nop", "pass"]),
+
+ /**
+ * 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 untypeable key-combinations
+ * in macros.
+ *
+ * canonicalKeys(canonicalKeys(x)) == canonicalKeys(x) for all values
+ * of x.
+ *
+ * @param {string} keys Messy form.
+ * @param {boolean} unknownOk Whether unknown keys are passed
+ * through rather than being converted to <lt>keyname>.
+ * @default false
+ * @returns {string} Canonical form.
+ */
+ canonicalKeys: function canonicalKeys(keys, unknownOk) {
+ if (arguments.length === 1)
+ unknownOk = true;
+ return this.parse(keys, unknownOk).map(this.closure.stringify).join("");
+ },
+
+ iterKeys: function iterKeys(keys) iter(function () {
+ let match, re = /<.*?>?>|[^<]/g;
+ while (match = re.exec(keys))
+ yield match[0];
+ }()),
+
+ /**
+ * Converts an event string into an array of pseudo-event objects.
+ *
+ * These objects can be used as arguments to {@link #stringify} or
+ * {@link DOM.Event}, 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, .dactylString and
+ * .dactylShift these are set for characters that can never by
+ * typed, but may appear in mappings, for example <Nop> is passed as
+ * dactylString, and dactylShift is set when a user specifies
+ * <S-@> where @ is a non-case-changeable, non-space character.
+ *
+ * @param {string} keys The string to parse.
+ * @param {boolean} unknownOk Whether unknown keys are passed
+ * through rather than being converted to <lt>keyname>.
+ * @default false
+ * @returns {Array[Object]}
+ */
+ parse: function parse(input, unknownOk) {
+ if (isArray(input))
+ return array.flatten(input.map(function (k) this.parse(k, unknownOk), this));
+
+ if (arguments.length === 1)
+ unknownOk = true;
+
+ let out = [];
+ for (let match in util.regexp.iterate(/<.*?>?>|[^<]|<(?!.*>)/g, 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) {
+ evt_obj.charCode = evt_str.charCodeAt(0);
+ evt_obj._keyCode = this.key_code[evt_str[0].toLowerCase()];
+ evt_obj.shiftKey = evt_str !== evt_str.toLowerCase();
+ }
+ else {
+ let [match, modifier, keyname] = evt_str.match(/^<((?:[*12CASM⌘]-)*)(.+?)>$/i) || [false, '', ''];
+ modifier = Set(modifier.toUpperCase());
+ keyname = keyname.toLowerCase();
+ evt_obj.dactylKeyname = keyname;
+ if (/^u[0-9a-f]+$/.test(keyname))
+ keyname = String.fromCharCode(parseInt(keyname.substr(1), 16));
+
+ if (keyname && (unknownOk || keyname.length == 1 || /mouse$/.test(keyname) ||
+ this.key_code[keyname] || Set.has(this.pseudoKeys, keyname))) {
+ evt_obj.globKey ="*" in modifier;
+ evt_obj.ctrlKey ="C" in modifier;
+ evt_obj.altKey ="A" in modifier;
+ evt_obj.shiftKey ="S" in modifier;
+ evt_obj.metaKey ="M" in modifier || "⌘" in modifier;
+ evt_obj.dactylShift = evt_obj.shiftKey;
+
+ if (keyname.length == 1) { // normal characters
+ if (evt_obj.shiftKey)
+ keyname = keyname.toUpperCase();
+
+ evt_obj.dactylShift = evt_obj.shiftKey && keyname.toUpperCase() == keyname.toLowerCase();
+ evt_obj.charCode = keyname.charCodeAt(0);
+ evt_obj.keyCode = this.key_code[keyname.toLowerCase()];
+ }
+ else if (Set.has(this.pseudoKeys, keyname)) {
+ evt_obj.dactylString = "<" + this.key_key[keyname] + ">";
+ }
+ 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 { // an invalid sequence starting with <, treat as a literal
+ out = out.concat(this.parse("<lt>" + evt_str.substr(1)));
+ continue;
+ }
+ }
+
+ // 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>
+
+ evt_obj.modifiers = (evt_obj.ctrlKey && Ci.nsIDOMNSEvent.CONTROL_MASK)
+ | (evt_obj.altKey && Ci.nsIDOMNSEvent.ALT_MASK)
+ | (evt_obj.shiftKey && Ci.nsIDOMNSEvent.SHIFT_MASK)
+ | (evt_obj.metaKey && Ci.nsIDOMNSEvent.META_MASK);
+
+ out.push(evt_obj);
+ }
+ return out;
+ },
+
+ /**
+ * Converts the specified event to a string in dactyl key-code
+ * notation. Returns null for an unknown event.
+ *
+ * @param {Event} event
+ * @returns {string}
+ */
+ stringify: function stringify(event) {
+ if (isArray(event))
+ return event.map(function (e) this.stringify(e), this).join("");
+
+ if (event.dactylString)
+ return event.dactylString;
+
+ let key = null;
+ let modifier = "";
+
+ if (event.globKey)
+ modifier += "*-";
+ if (event.ctrlKey)
+ modifier += "C-";
+ if (event.altKey)
+ modifier += "A-";
+ if (event.metaKey)
+ modifier += "M-";
+
+ if (/^key/.test(event.type)) {
+ let charCode = event.type == "keyup" ? 0 : event.charCode; // Why? --Kris
+ if (charCode == 0) {
+ if (event.keyCode in this.code_key) {
+ key = this.code_key[event.keyCode];
+
+ if (event.shiftKey && (key.length > 1 || event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
+ modifier += "S-";
+ else if (!modifier && key.length === 1)
+ if (event.shiftKey)
+ key = key.toUpperCase();
+ else
+ key = key.toLowerCase();
+
+ if (!modifier && key.length == 1)
+ return key;
+ }
+ }
+ // [Ctrl-Bug] special handling of mysterious <C-[>, <C-\\>, <C-]>, <C-^>, <C-_> bugs (OS/X)
+ // (i.e., cntrl codes 27--31)
+ // ---
+ // For more information, see:
+ // [*] Referenced mailing list msg: http://www.mozdev.org/pipermail/pentadactyl/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?id=416227
+ // [*] Mozilla bug 432951: Ctrl+'foo' doesn't seem same charCode as Meta+'foo' on Cocoa
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=432951
+ // ---
+ //
+ // The following fixes are only activated if config.OS.isMacOSX.
+ // 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 config.OS.isMacOSX if desired.
+ //
+ else if (config.OS.isMacOSX && event.ctrlKey && charCode >= 27 && charCode <= 31) {
+ if (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(charCode + 64);
+ }
+ // a normal key like a, b, c, 0, etc.
+ else if (charCode) {
+ key = String.fromCharCode(charCode);
+
+ if (!/^[^<\s]$/i.test(key) && 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.dactylShift)
+ modifier += "S-";
+
+ key = this.code_key[this.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.dactylShift)
+ modifier += "S-";
+ if (/^\s$/.test(key))
+ key = let (s = charCode.toString(16)) "U" + "0000".substr(4 - s.length) + s;
+ else if (modifier.length == 0)
+ return key;
+ }
+ }
+ if (key == null) {
+ if (event.shiftKey)
+ modifier += "S-";
+ key = this.key_key[event.dactylKeyname] || event.dactylKeyname;
+ }
+ if (key == null)
+ return null;
+ }
+ 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;
+ }
+ }
+
+ if (key == null)
+ return null;
+
+ return "<" + modifier + key + ">";
+ },
+
+
+ defaults: {
+ load: { bubbles: false },
+ submit: { cancelable: true }
+ },
+
+ types: Class.Memoize(function () iter(
+ {
+ Mouse: "click mousedown mouseout mouseover mouseup dblclick " +
+ "hover " +
+ "popupshowing popupshown popuphiding popuphidden " +
+ "contextmenu",
+ Key: "keydown keypress keyup",
+ "": "change command dactyl-input input submit " +
+ "load unload pageshow pagehide DOMContentLoaded " +
+ "resize scroll"
+ }
+ ).map(function ([k, v]) v.split(" ").map(function (v) [v, k]))
+ .flatten()
+ .toObject()),
+
+ /**
+ * Dispatches an event to an element as if it were a native event.
+ *
+ * @param {Node} target The DOM node to which to dispatch the event.
+ * @param {Event} event The event to dispatch.
+ */
+ dispatch: Class.Memoize(function ()
+ config.haveGecko("2b")
+ ? function dispatch(target, event, extra) {
+ try {
+ this.feedingEvent = extra;
+
+ if (target instanceof Ci.nsIDOMElement)
+ // This causes a crash on Gecko<2.0, it seems.
+ return (target.ownerDocument || target.document || target).defaultView
+ .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
+ .dispatchDOMEventViaPresShell(target, event, true);
+ else {
+ target.dispatchEvent(event);
+ return !event.getPreventDefault();
+ }
+ }
+ catch (e) {
+ util.reportError(e);
+ }
+ finally {
+ this.feedingEvent = null;
+ }
+ }
+ : function dispatch(target, event, extra) {
+ try {
+ this.feedingEvent = extra;
+ target.dispatchEvent(update(event, extra));
+ }
+ finally {
+ this.feedingEvent = null;
+ }
+ })
+ }),
+
+ createContents: Class.Memoize(function () services.has("dactyl") && services.dactyl.createContents
+ || function (elem) {}),
+
+ isScrollable: Class.Memoize(function () services.has("dactyl") && services.dactyl.getScrollable
+ ? function (elem, dir) services.dactyl.getScrollable(elem) & (dir ? services.dactyl["DIRECTION_" + dir.toUpperCase()] : ~0)
+ : function (elem, dir) true),
+
+ /**
+ * The set of input element type attribute values that mark the element as
+ * an editable field.
+ */
+ editableInputs: Set(["date", "datetime", "datetime-local", "email", "file",
+ "month", "number", "password", "range", "search",
+ "tel", "text", "time", "url", "week"]),
+
+ /**
+ * Converts a given DOM Node, Range, or Selection to a string. If
+ * *html* is true, the output is HTML, otherwise it is presentation
+ * text.
+ *
+ * @param {nsIDOMNode | nsIDOMRange | nsISelection} node The node to
+ * stringify.
+ * @param {boolean} html Whether the output should be HTML rather
+ * than presentation text.
+ */
+ stringify: function stringify(node, html) {
+ if (node instanceof Ci.nsISelection && node.isCollapsed)
+ return "";
+
+ if (node instanceof Ci.nsIDOMNode) {
+ let range = node.ownerDocument.createRange();
+ range.selectNode(node);
+ node = range;
+ }
+ let doc = (node.getRangeAt ? node.getRangeAt(0) : node).startContainer;
+ doc = doc.ownerDocument || doc;
+
+ let encoder = services.HtmlEncoder();
+ encoder.init(doc, "text/unicode", encoder.OutputRaw|encoder.OutputPreformatted);
+ if (node instanceof Ci.nsISelection)
+ encoder.setSelection(node);
+ else if (node instanceof Ci.nsIDOMRange)
+ encoder.setRange(node);
+
+ let str = services.String(encoder.encodeToString());
+ if (html)
+ return str.data;
+
+ let [result, length] = [{}, {}];
+ services.HtmlConverter().convert("text/html", str, str.data.length*2, "text/unicode", result, length);
+ return result.value.QueryInterface(Ci.nsISupportsString).data;
+ },
+
+ /**
+ * Compiles a CSS spec and XPath pattern matcher based on the given
+ * list. List elements prefixed with "xpath:" are parsed as XPath
+ * patterns, while other elements are parsed as CSS specs. The
+ * returned function will, given a node, return an iterator of all
+ * descendants of that node which match the given specs.
+ *
+ * @param {[string]} list The list of patterns to match.
+ * @returns {function(Node)}
+ */
+ compileMatcher: function compileMatcher(list) {
+ let xpath = [], css = [];
+ for (let elem in values(list))
+ if (/^xpath:/.test(elem))
+ xpath.push(elem.substr(6));
+ else
+ css.push(elem);
+
+ return update(
+ function matcher(node) {
+ if (matcher.xpath)
+ for (let elem in DOM.XPath(matcher.xpath, node))
+ yield elem;
+
+ if (matcher.css)
+ for (let [, elem] in iter(util.withProperErrors("querySelectorAll", node, matcher.css)))
+ yield elem;
+ }, {
+ css: css.join(", "),
+ xpath: xpath.join(" | ")
+ });
+ },
+
+ /**
+ * Validates a list as input for {@link #compileMatcher}. Returns
+ * true if and only if every element of the list is a valid XPath or
+ * CSS selector.
+ *
+ * @param {[string]} list The list of patterns to test
+ * @returns {boolean} True when the patterns are all valid.
+ */
+ validateMatcher: function validateMatcher(list) {
+ return this.testValues(list, DOM.closure.testMatcher);
+ },
+
+ testMatcher: function testMatcher(value) {
+ let evaluator = services.XPathEvaluator();
+ let node = services.XMLDocument();
+ if (/^xpath:/.test(value))
+ util.withProperErrors("createExpression", evaluator, value.substr(6), DOM.XPath.resolver);
+ else
+ util.withProperErrors("querySelector", node, value);
+ return true;
+ },
+
+ /**
+ * Converts HTML special characters in *str* to the equivalent HTML
+ * entities.
+ *
+ * @param {string} str
+ * @returns {string}
+ */
+ escapeHTML: function escapeHTML(str) {
+ let map = { "'": "&apos;", '"': "&quot;", "%": "&#x25;", "&": "&amp;", "<": "&lt;", ">": "&gt;" };
+ return str.replace(/['"&<>]/g, function (m) map[m]);
+ },
+
+ /**
+ * Converts an E4X XML literal to a DOM node. Any attribute named
+ * highlight is present, it is transformed into dactyl:highlight,
+ * and the named highlight groups are guaranteed to be loaded.
+ *
+ * @param {Node} node
+ * @param {Document} doc
+ * @param {Object} nodes If present, nodes with the "key" attribute are
+ * stored here, keyed to the value thereof.
+ * @returns {Node}
+ */
+ fromXML: function fromXML(node, doc, nodes) {
+ XML.ignoreWhitespace = XML.prettyPrinting = false;
+ if (typeof node === "string") // Sandboxes can't currently pass us XML objects.
+ node = XML(node);
+
+ if (node.length() != 1) {
+ let domnode = doc.createDocumentFragment();
+ for each (let child in node)
+ domnode.appendChild(fromXML(child, doc, nodes));
+ return domnode;
+ }
+
+ switch (node.nodeKind()) {
+ case "text":
+ return doc.createTextNode(String(node));
+ case "element":
+ let domnode = doc.createElementNS(node.namespace(), node.localName());
+
+ for each (let attr in node.@*::*)
+ if (attr.name() != "highlight")
+ domnode.setAttributeNS(attr.namespace(), attr.localName(), String(attr));
+
+ for each (let child in node.*::*)
+ domnode.appendChild(fromXML(child, doc, nodes));
+ if (nodes && node.@key)
+ nodes[node.@key] = domnode;
+
+ if ("@highlight" in node)
+ highlight.highlightNode(domnode, String(node.@highlight), nodes || true);
+ return domnode;
+ default:
+ return null;
+ }
+ },
+
+ /**
+ * Evaluates an XPath expression in the current or provided
+ * document. It provides the xhtml, xhtml2 and dactyl XML
+ * namespaces. The result may be used as an iterator.
+ *
+ * @param {string} expression The XPath expression to evaluate.
+ * @param {Node} elem The context element.
+ * @param {boolean} asIterator Whether to return the results as an
+ * XPath iterator.
+ * @param {object} namespaces Additional namespaces to recognize.
+ * @optional
+ * @returns {Object} Iterable result of the evaluation.
+ */
+ XPath: update(
+ function XPath(expression, elem, asIterator, namespaces) {
+ try {
+ let doc = elem.ownerDocument || elem;
+
+ if (isArray(expression))
+ expression = DOM.makeXPath(expression);
+
+ let resolver = XPath.resolver;
+ if (namespaces) {
+ namespaces = update({}, DOM.namespaces, namespaces);
+ resolver = function (prefix) namespaces[prefix] || null;
+ }
+
+ let result = doc.evaluate(expression, elem,
+ resolver,
+ asIterator ? Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE : Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
+ null
+ );
+
+ return Object.create(result, {
+ __iterator__: {
+ value: asIterator ? function () { let elem; while ((elem = this.iterateNext())) yield elem; }
+ : function () { for (let i = 0; i < this.snapshotLength; i++) yield this.snapshotItem(i); }
+ }
+ });
+ }
+ catch (e) {
+ throw e.stack ? e : Error(e);
+ }
+ },
+ {
+ resolver: function lookupNamespaceURI(prefix) (DOM.namespaces[prefix] || null)
+ }),
+
+ /**
+ * Returns an XPath union expression constructed from the specified node
+ * tests. An expression is built with node tests for both the null and
+ * XHTML namespaces. See {@link DOM.XPath}.
+ *
+ * @param nodes {Array(string)}
+ * @returns {string}
+ */
+ makeXPath: function makeXPath(nodes) {
+ return array(nodes).map(util.debrace).flatten()
+ .map(function (node) /^[a-z]+:/.test(node) ? node : [node, "xhtml:" + node]).flatten()
+ .map(function (node) "//" + node).join(" | ");
+ },
+
+ namespaces: {
+ xul: XUL.uri,
+ xhtml: XHTML.uri,
+ html: XHTML.uri,
+ xhtml2: "http://www.w3.org/2002/06/xhtml2",
+ dactyl: NS.uri
+ },
+
+ namespaceNames: Class.Memoize(function ()
+ iter(this.namespaces).map(function ([k, v]) [v, k]).toObject()),
+});
+
+Object.keys(DOM.Event.types).forEach(function (event) {
+ let name = event.replace(/-(.)/g, function (m, m1) m1.toUpperCase());
+ if (!Set.has(DOM.prototype, name))
+ DOM.prototype[name] =
+ function _event(arg, extra) {
+ return this[callable(arg) ? "listen" : "dispatch"](event, arg, extra);
+ };
+});
+
+var $ = DOM;
+
+endModule();
+
+// catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
+
+// vim: set sw=4 ts=4 et ft=javascript:
diff --git a/common/modules/downloads.jsm b/common/modules/downloads.jsm
--- a/common/modules/downloads.jsm
+++ b/common/modules/downloads.jsm
@@ -1,33 +1,31 @@
// Copyright (c) 2011 by Kris Maglione <maglione.k@gmail.com>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
Components.utils.import("resource://dactyl/bootstrap.jsm");
defineModule("downloads", {
- exports: ["Download", "Downloads", "downloads"],
- use: ["io", "messages", "prefs", "services", "util"]
+ exports: ["Download", "Downloads", "downloads"]
}, this);
Cu.import("resource://gre/modules/DownloadUtils.jsm", this);
let prefix = "DOWNLOAD_";
var states = iter([v, k.slice(prefix.length).toLowerCase()]
for ([k, v] in Iterator(Ci.nsIDownloadManager))
if (k.indexOf(prefix) == 0))
.toObject();
var Download = Class("Download", {
init: function init(id, list) {
- let self = XPCSafeJSObjectWrapper(services.downloadManager.getDownload(id));
- self.__proto__ = this;
- this.instance = this;
+ let self = this;
+ this.download = services.downloadManager.getDownload(id);
this.list = list;
this.nodes = {
commandTarget: self
};
util.xmlToDom(
<tr highlight="Download" key="row" xmlns:dactyl={NS} xmlns={XHTML}>
<td highlight="DownloadTitle">
@@ -69,17 +67,17 @@ var Download = Class("Download", {
},
get status() states[this.state],
inState: function inState(states) states.indexOf(this.status) >= 0,
get alive() this.inState(["downloading", "notstarted", "paused", "queued", "scanning"]),
- allowedCommands: Class.memoize(function () let (self = this) ({
+ allowedCommands: Class.Memoize(function () let (self = this) ({
get cancel() self.cancelable && self.inState(["downloading", "paused", "starting"]),
get delete() !this.cancel && self.targetFile.exists(),
get launch() self.targetFile.exists() && self.inState(["finished"]),
get pause() self.inState(["downloading"]),
get remove() self.inState(["blocked_parental", "blocked_policy",
"canceled", "dirty", "failed", "finished"]),
get resume() self.resumable && self.inState(["paused"]),
get retry() self.inState(["canceled", "failed"])
@@ -167,17 +165,18 @@ var Download = Class("Download", {
[, self.timeRemaining] = DownloadUtils.getTimeLeft(seconds, this.timeRemaining);
if (this.timeRemaining)
this.nodes.time.textContent = util.formatSeconds(this.timeRemaining);
else
this.nodes.time.textContent = _("download.almostDone");
}
}
- let total = this.nodes.progressTotal.textContent = this.size ? util.formatBytes(this.size, 1, true) : _("download.unknown");
+ let total = this.nodes.progressTotal.textContent = this.size || !this.nActive ? util.formatBytes(this.size, 1, true)
+ : _("download.unknown");
let suffix = RegExp(/( [a-z]+)?$/i.exec(total)[0] + "$");
this.nodes.progressHave.textContent = util.formatBytes(this.amountTransferred, 1, true).replace(suffix, "");
this.nodes.percent.textContent = this.size ? Math.round(this.amountTransferred * 100 / this.size) + "%" : "";
},
updateStatus: function updateStatus() {
@@ -188,16 +187,24 @@ var Download = Class("Download", {
for (let node in values(this.nodes))
if (node.update)
node.update();
this.updateProgress();
}
});
+Object.keys(XPCOMShim([Ci.nsIDownload])).forEach(function (key) {
+ if (!(key in Download.prototype))
+ Object.defineProperty(Download.prototype, key, {
+ get: function get() this.download[key],
+ set: function set(val) this.download[key] = val,
+ configurable: true
+ });
+});
var DownloadList = Class("DownloadList",
XPCOM([Ci.nsIDownloadProgressListener,
Ci.nsIObserver,
Ci.nsISupportsWeakReference]), {
init: function init(modules, filter, sort) {
this.sortOrder = sort;
this.modules = modules;
@@ -208,17 +215,17 @@ var DownloadList = Class("DownloadList",
this.downloads = {};
},
cleanup: function cleanup() {
this.observe.unregister();
services.downloadManager.removeListener(this);
},
- message: Class.memoize(function () {
+ message: Class.Memoize(function () {
util.xmlToDom(<table highlight="Downloads" key="list" xmlns={XHTML}>
<tr highlight="DownloadHead">
<span>{_("title.Title")}</span>
<span>{_("title.Status")}</span>
<span/>
<span>{_("title.Progress")}</span>
<span/>
@@ -275,17 +282,17 @@ var DownloadList = Class("DownloadList",
}
},
leave: function leave(stack) {
if (stack.pop)
this.cleanup();
},
- allowedCommands: Class.memoize(function () let (self = this) ({
+ allowedCommands: Class.Memoize(function () let (self = this) ({
get clear() values(self.downloads).some(function (dl) dl.allowedCommands.remove)
})),
commands: {
clear: function () {
services.downloadManager.cleanUp();
}
},
@@ -311,26 +318,27 @@ var DownloadList = Class("DownloadList",
event.initEvent("dactyl-commandupdate", true, false);
this.document.dispatchEvent(event);
},
timeRemaining: Infinity,
updateProgress: function updateProgress() {
let downloads = values(this.downloads).toArray();
+ let active = downloads.filter(function (d) d.alive);
let self = Object.create(this);
for (let prop in values(["amountTransferred", "size", "speed", "timeRemaining"]))
- this[prop] = downloads.reduce(function (acc, dl) dl[prop] + acc, 0);
+ this[prop] = active.reduce(function (acc, dl) dl[prop] + acc, 0);
Download.prototype.updateProgress.call(self);
- let active = downloads.filter(function (dl) dl.alive).length;
- if (active)
- this.nodes.total.textContent = _("download.nActive", active);
+ this.nActive = active.length;
+ if (active.length)
+ this.nodes.total.textContent = _("download.nActive", active.length);
else for (let key in values(["total", "percent", "speed", "time"]))
this.nodes[key].textContent = "";
if (this.shouldSort("complete", "size", "speed", "time"))
this.sort();
},
observers: {
diff --git a/common/modules/finder.jsm b/common/modules/finder.jsm
--- a/common/modules/finder.jsm
+++ b/common/modules/finder.jsm
@@ -1,182 +1,221 @@
// Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
Components.utils.import("resource://dactyl/bootstrap.jsm");
defineModule("finder", {
exports: ["RangeFind", "RangeFinder", "rangefinder"],
- require: ["prefs"],
- use: ["messages", "services", "util"]
+ require: ["prefs"]
}, this);
+this.lazyRequire("buffer", ["Buffer"]);
+this.lazyRequire("overlay", ["overlay"]);
+
function equals(a, b) XPCNativeWrapper(a) == XPCNativeWrapper(b);
-try {
-
/** @instance rangefinder */
var RangeFinder = Module("rangefinder", {
Local: function (dactyl, modules, window) ({
init: function () {
this.dactyl = dactyl;
this.modules = modules;
this.window = window;
this.lastFindPattern = "";
},
+ get content() {
+ let { window } = this.modes.getStack(0).params;
+ return window || this.window.content;
+ },
+
get rangeFind() {
- let find = modules.buffer.localStore.rangeFind;
- if (find && find.stale || !isinstance(find, RangeFind))
+ let find = overlay.getData(this.content.document,
+ "range-find", null);
+
+ if (!isinstance(find, RangeFind) || find.stale)
return this.rangeFind = null;
return find;
},
- set rangeFind(val) modules.buffer.localStore.rangeFind = val
+ set rangeFind(val) overlay.setData(this.content.document,
+ "range-find", val)
}),
init: function init() {
prefs.safeSet("accessibility.typeaheadfind.autostart", false);
// The above should be sufficient, but: http://dactyl.sf.net/bmo/348187
prefs.safeSet("accessibility.typeaheadfind", false);
},
+ cleanup: function cleanup() {
+ for (let doc in util.iterDocuments()) {
+ let find = overlay.getData(doc, "range-find", null);
+ if (find)
+ find.highlight(true);
+
+ overlay.setData(doc, "range-find", null);
+ }
+ },
+
get commandline() this.modules.commandline,
get modes() this.modules.modes,
get options() this.modules.options,
- openPrompt: function (mode) {
+ openPrompt: function openPrompt(mode) {
+ this.modules.marks.push();
this.commandline;
- this.CommandMode(mode).open();
+ this.CommandMode(mode, this.content).open();
+
+ Buffer(this.content).resetCaret();
if (this.rangeFind && equals(this.rangeFind.window.get(), this.window))
this.rangeFind.reset();
this.find("", mode == this.modes.FIND_BACKWARD);
},
- bootstrap: function (str, backward) {
+ bootstrap: function bootstrap(str, backward) {
if (arguments.length < 2 && this.rangeFind)
backward = this.rangeFind.reverse;
let highlighted = this.rangeFind && this.rangeFind.highlighted;
let selections = this.rangeFind && this.rangeFind.selections;
let linksOnly = false;
let regexp = false;
let matchCase = this.options["findcase"] === "smart" ? /[A-Z]/.test(str) :
this.options["findcase"] === "ignore" ? false : true;
- str = str.replace(/\\(.|$)/g, function (m, n1) {
+ function replacer(m, n1) {
if (n1 == "c")
matchCase = false;
else if (n1 == "C")
matchCase = true;
else if (n1 == "l")
linksOnly = true;
else if (n1 == "L")
linksOnly = false;
else if (n1 == "r")
regexp = true;
else if (n1 == "R")
regexp = false;
else
return m;
return "";
- });
-
+ }
+
+ this.options["findflags"].forEach(function (f) replacer(f, f));
+
+ let pattern = str.replace(/\\(.|$)/g, replacer);
+
+ if (str)
+ this.lastFindPattern = str;
// It's possible, with :tabdetach for instance, for the rangeFind to
// actually move from one window to another, which breaks things.
if (!this.rangeFind
|| !equals(this.rangeFind.window.get(), this.window)
|| linksOnly != !!this.rangeFind.elementPath
|| regexp != this.rangeFind.regexp
|| matchCase != this.rangeFind.matchCase
|| !!backward != this.rangeFind.reverse) {
if (this.rangeFind)
this.rangeFind.cancel();
- this.rangeFind = RangeFind(this.window, matchCase, backward,
+ this.rangeFind = null;
+ this.rangeFind = RangeFind(this.window, this.content, matchCase, backward,
linksOnly && this.options.get("hinttags").matcher,
regexp);
this.rangeFind.highlighted = highlighted;
this.rangeFind.selections = selections;
}
- return this.lastFindPattern = str;
- },
-
- find: function (pattern, backwards) {
+ this.rangeFind.pattern = str;
+ return pattern;
+ },
+
+ find: function find(pattern, backwards) {
+ this.modules.marks.push();
let str = this.bootstrap(pattern, backwards);
+ this.backward = this.rangeFind.reverse;
+
if (!this.rangeFind.find(str))
this.dactyl.echoerr(_("finder.notFound", pattern),
this.commandline.FORCE_SINGLELINE);
return this.rangeFind.found;
},
- findAgain: function (reverse) {
+ findAgain: function findAgain(reverse) {
+ this.modules.marks.push();
if (!this.rangeFind)
this.find(this.lastFindPattern);
else if (!this.rangeFind.find(null, reverse))
this.dactyl.echoerr(_("finder.notFound", this.lastFindPattern),
this.commandline.FORCE_SINGLELINE);
else if (this.rangeFind.wrapped) {
let msg = this.rangeFind.backward ? _("finder.atTop")
: _("finder.atBottom");
this.commandline.echo(msg, "WarningMsg", this.commandline.APPEND_TO_MESSAGES
| this.commandline.FORCE_SINGLELINE);
}
else
- this.commandline.echo((this.rangeFind.backward ? "?" : "/") + this.lastFindPattern,
+ this.commandline.echo((this.rangeFind.backward ? "?" : "/") + this.rangeFind.pattern,
"Normal", this.commandline.FORCE_SINGLELINE);
if (this.options["hlfind"])
this.highlight();
this.rangeFind.focus();
},
- onCancel: function () {
+ onCancel: function onCancel() {
if (this.rangeFind)
this.rangeFind.cancel();
},
- onChange: function (command) {
+ onChange: function onChange(command) {
if (this.options["incfind"]) {
command = this.bootstrap(command);
this.rangeFind.find(command);
}
},
- onHistory: function () {
+ onHistory: function onHistory() {
this.rangeFind.found = false;
},
- onSubmit: function (command) {
+ onSubmit: function onSubmit(command) {
+ if (!command && this.lastFindPattern) {
+ this.find(this.lastFindPattern, this.backward);
+ this.findAgain();
+ return;
+ }
+
if (!this.options["incfind"] || !this.rangeFind || !this.rangeFind.found) {
this.clear();
- this.find(command || this.lastFindPattern, this.modes.extended & this.modes.FIND_BACKWARD);
- }
+ this.find(command || this.lastFindPattern, this.backward);
+ }
if (this.options["hlfind"])
this.highlight();
this.rangeFind.focus();
},
/**
* Highlights all occurrences of the last sought for string in the
* current buffer.
*/
- highlight: function () {
+ highlight: function highlight() {
if (this.rangeFind)
this.rangeFind.highlight();
},
/**
* Clears all find highlighting.
*/
- clear: function () {
+ clear: function clear() {
if (this.rangeFind)
this.rangeFind.highlight(true);
}
}, {
}, {
modes: function initModes(dactyl, modules, window) {
initModes.require("commandline");
@@ -200,18 +239,19 @@ var RangeFinder = Module("rangefinder",
commands.add(["noh[lfind]"],
"Remove the find highlighting",
function () { rangefinder.clear(); },
{ argCount: "0" });
},
commandline: function initCommandline(dactyl, modules, window) {
const { rangefinder } = modules;
rangefinder.CommandMode = Class("CommandFindMode", modules.CommandMode, {
- init: function init(mode) {
+ init: function init(mode, window) {
this.mode = mode;
+ this.window = window;
init.supercall(this);
},
historyKey: "find",
get prompt() this.mode === modules.modes.FIND_BACKWARD ? "?" : "/",
get onCancel() modules.rangefinder.closure.onCancel,
@@ -224,17 +264,17 @@ var RangeFinder = Module("rangefinder",
const { Buffer, buffer, config, mappings, modes, rangefinder } = modules;
var myModes = config.browserModes.concat([modes.CARET]);
mappings.add(myModes,
["/", "<find-forward>"], "Find a pattern starting at the current caret position",
function () { rangefinder.openPrompt(modes.FIND_FORWARD); });
mappings.add(myModes,
- ["?", "<find-backward>"], "Find a pattern backward of the current caret position",
+ ["?", "<find-backward>", "<S-Slash>"], "Find a pattern backward of the current caret position",
function () { rangefinder.openPrompt(modes.FIND_BACKWARD); });
mappings.add(myModes,
["n", "<find-next>"], "Find next",
function () { rangefinder.findAgain(false); });
mappings.add(myModes,
["N", "<find-previous>"], "Find previous",
@@ -252,17 +292,16 @@ var RangeFinder = Module("rangefinder",
function () {
rangefinder.find(Buffer.currentWord(buffer.focusedFrame, true), true);
rangefinder.findAgain();
});
},
options: function (dactyl, modules, window) {
const { options, rangefinder } = modules;
- const { prefs } = require("prefs");
options.add(["hlfind", "hlf"],
"Highlight all /find pattern matches on the current page after submission",
"boolean", false, {
setter: function (value) {
rangefinder[value ? "highlight" : "clear"]();
return value;
}
@@ -274,16 +313,30 @@ var RangeFinder = Module("rangefinder",
{
values: {
"smart": "Case is significant when capital letters are typed",
"match": "Case is always significant",
"ignore": "Case is never significant"
}
});
+ options.add(["findflags", "ff"],
+ "Default flags for find invocations",
+ "charlist", "",
+ {
+ values: {
+ "c": "Ignore case",
+ "C": "Match case",
+ "r": "Perform a regular expression search",
+ "R": "Perform a plain string search",
+ "l": "Search only in links",
+ "L": "Search all text"
+ }
+ });
+
options.add(["incfind", "if"],
"Find a pattern incrementally as it is typed rather than awaiting c_<Return>",
"boolean", true);
}
});
/**
* @class RangeFind
@@ -303,74 +356,69 @@ var RangeFinder = Module("rangefinder",
* of the builtin component, which always starts a find from the
* beginning of the first frame in the case of frameset documents,
* and cycles through all frames from beginning to end. This makes it
* impossible to choose the starting point of a find for such
* documents, and represents a major detriment to productivity where
* large amounts of data are concerned (e.g., for API documents).
*/
var RangeFind = Class("RangeFind", {
- init: function init(window, matchCase, backward, elementPath, regexp) {
- this.window = Cu.getWeakReference(window);
- this.content = window.content;
-
- this.baseDocument = Cu.getWeakReference(this.content.document);
+ init: function init(window, content, matchCase, backward, elementPath, regexp) {
+ this.window = util.weakReference(window);
+ this.content = content;
+
+ this.baseDocument = util.weakReference(this.content.document);
this.elementPath = elementPath || null;
this.reverse = Boolean(backward);
this.finder = services.Find();
this.matchCase = Boolean(matchCase);
this.regexp = Boolean(regexp);
this.reset();
this.highlighted = null;
this.selections = [];
this.lastString = "";
},
- get store() this.content.document.dactylStore = this.content.document.dactylStore || {},
+ get store() overlay.getData(this.content.document, "buffer", Object),
get backward() this.finder.findBackwards,
+ set backward(val) this.finder.findBackwards = val,
get matchCase() this.finder.caseSensitive,
set matchCase(val) this.finder.caseSensitive = Boolean(val),
- get regexp() this.finder.regularExpression || false,
- set regexp(val) {
- try {
- return this.finder.regularExpression = Boolean(val);
- }
- catch (e) {
- return false;
- }
- },
-
get findString() this.lastString,
+ get flags() this.matchCase ? "" : "i",
+
get selectedRange() {
let win = this.store.focusedFrame && this.store.focusedFrame.get() || this.content;
let selection = win.getSelection();
return (selection.rangeCount ? selection.getRangeAt(0) : this.ranges[0].range).cloneRange();
},
set selectedRange(range) {
this.range.selection.removeAllRanges();
this.range.selection.addRange(range);
this.range.selectionController.scrollSelectionIntoView(
this.range.selectionController.SELECTION_NORMAL, 0, false);
- this.store.focusedFrame = Cu.getWeakReference(range.startContainer.ownerDocument.defaultView);
- },
+ this.store.focusedFrame = util.weakReference(range.startContainer.ownerDocument.defaultView);
+ },
cancel: function cancel() {
this.purgeListeners();
+ if (this.range) {
this.range.deselect();
this.range.descroll();
- },
+ }
+ },
compareRanges: function compareRanges(r1, r2) {
try {
return this.backward ? r1.compareBoundaryPoints(r1.END_TO_START, r2)
: -r1.compareBoundaryPoints(r1.START_TO_END, r2);
}
catch (e) {
util.reportError(e);
@@ -395,17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment