Created
December 22, 2011 06:13
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, """), | |
- '"'); | |
- } | |
- 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"> ({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"> ({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} </li> | |
+ <li highlight="CompDesc ErrorMsg">{e} </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]>* 
 StatusCmdLine;[dactyl|highlight]>* | |
- CmdNormal;[dactyl|highlight] 
 StatusNormal;[dactyl|highlight] | |
- CmdErrorMsg;[dactyl|highlight] 
 StatusErrorMsg;[dactyl|highlight] | |
- CmdInfoMsg;[dactyl|highlight] 
 StatusInfoMsg;[dactyl|highlight] | |
- CmdModeMsg;[dactyl|highlight] 
 StatusModeMsg;[dactyl|highlight] | |
- CmdMoreMsg;[dactyl|highlight] 
 StatusMoreMsg;[dactyl|highlight] | |
- CmdQuestion;[dactyl|highlight] 
 StatusQuestion;[dactyl|highlight] | |
- CmdWarningMsg;[dactyl|highlight] 
 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> | |
- ]]></>, /
/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"><{ | |
+ 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"><{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 = { "'": "'", '"': """, "%": "%", "&": "&", "<": "<", ">": ">" }; | |
+ 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