]> git.donarmstrong.com Git - dactyl.git/commitdiff
Import 1.0rc1 supporting Firefox up to 11.* upstream/1.0rc1
authorMichael Schutte <michi@uiae.at>
Wed, 28 Dec 2011 19:43:04 +0000 (20:43 +0100)
committerMichael Schutte <michi@uiae.at>
Wed, 28 Dec 2011 19:43:04 +0000 (20:43 +0100)
123 files changed:
.hg_archival.txt [new file with mode: 0644]
.hgignore
.hgsub [new file with mode: 0644]
.hgsubstate [new file with mode: 0644]
BREAKING_CHANGES
LICENSE.txt
binary/chrome.manifest [new file with mode: 0644]
binary/config.mk [new file with mode: 0644]
binary/install.rdf [new file with mode: 0644]
binary/src/Makefile [new file with mode: 0644]
binary/src/config.h [new file with mode: 0644]
binary/src/dactylIUtils.idl [new file with mode: 0644]
binary/src/dactylModule.cpp [new file with mode: 0644]
binary/src/dactylUtils.cpp [new file with mode: 0644]
binary/src/dactylUtils.h [new file with mode: 0644]
binary/src/mozJSLoaderUtils.cpp [new file with mode: 0644]
binary/src/mozJSLoaderUtils.h [new file with mode: 0644]
binary/src/subscriptLoader.cpp [new file with mode: 0644]
common/Makefile
common/bootstrap.js
common/chrome.manifest
common/components/commandline-handler.js
common/components/protocols.js [deleted file]
common/config.json [new file with mode: 0644]
common/content/abbreviations.js
common/content/about.xul
common/content/autocommands.js
common/content/bindings.xml
common/content/bookmarks.js
common/content/browser.js
common/content/buffer.js [deleted file]
common/content/commandline.js
common/content/dactyl.js
common/content/disable-acr.jsm
common/content/editor.js
common/content/events.js
common/content/help.js
common/content/help.xsl
common/content/hints.js
common/content/history.js
common/content/key-processors.js [new file with mode: 0644]
common/content/mappings.js
common/content/marks.js
common/content/modes.js
common/content/mow.js
common/content/quickmarks.js
common/content/statusline.js
common/content/tabs.js
common/javascript.vim [deleted file]
common/locale/en-US/all.xml
common/locale/en-US/autocommands.xml
common/locale/en-US/browsing.xml
common/locale/en-US/buffer.xml
common/locale/en-US/cmdline.xml
common/locale/en-US/developer.xml
common/locale/en-US/faq.xml
common/locale/en-US/gui.xml
common/locale/en-US/hints.xml
common/locale/en-US/index.xml
common/locale/en-US/map.xml
common/locale/en-US/marks.xml
common/locale/en-US/messages.properties
common/locale/en-US/options.xml
common/locale/en-US/pattern.xml
common/locale/en-US/privacy.xml
common/locale/en-US/repeat.xml
common/locale/en-US/starting.xml
common/locale/en-US/styling.xml
common/locale/en-US/tabs.xml
common/locale/en-US/various.xml
common/modules/addons.jsm
common/modules/base.jsm
common/modules/bookmarkcache.jsm
common/modules/bootstrap.jsm
common/modules/buffer.jsm [new file with mode: 0644]
common/modules/cache.jsm [new file with mode: 0644]
common/modules/commands.jsm
common/modules/completion.jsm
common/modules/config.jsm
common/modules/contexts.jsm
common/modules/dom.jsm [new file with mode: 0644]
common/modules/downloads.jsm
common/modules/finder.jsm
common/modules/help.jsm [new file with mode: 0644]
common/modules/highlight.jsm
common/modules/io.jsm
common/modules/javascript.jsm
common/modules/main.jsm [new file with mode: 0644]
common/modules/messages.jsm
common/modules/options.jsm
common/modules/overlay.jsm
common/modules/prefs.jsm
common/modules/protocol.jsm [new file with mode: 0644]
common/modules/sanitizer.jsm
common/modules/services.jsm
common/modules/storage.jsm
common/modules/styles.jsm
common/modules/template.jsm
common/modules/util.jsm
common/process_manifest.awk
common/skin/dactyl.css
common/skin/global-styles.css [new file with mode: 0644]
common/skin/help-styles.css [new file with mode: 0644]
common/tests/functional/testDOMEvents.js [new file with mode: 0644]
common/tests/functional/testHelpCommands.js
common/tests/functional/testOptions.js
melodactyl/components/protocols.js [deleted symlink]
melodactyl/content/config.js
pentadactyl/NEWS
pentadactyl/TODO
pentadactyl/components/protocols.js [deleted symlink]
pentadactyl/config.json [new file with mode: 0644]
pentadactyl/content/config.js
pentadactyl/icon.png [new file with mode: 0644]
pentadactyl/icon16.png [new file with mode: 0644]
pentadactyl/icon64.png [new file with mode: 0644]
pentadactyl/install.rdf
pentadactyl/locale/en-US/tutorial.xml
teledactyl/components/protocols.js [deleted symlink]
teledactyl/config.json [new file with mode: 0644]
teledactyl/content/compose/dactyl.xul [deleted file]
teledactyl/content/config.js
teledactyl/content/mail.js

diff --git a/.hg_archival.txt b/.hg_archival.txt
new file mode 100644 (file)
index 0000000..50caf3e
--- /dev/null
@@ -0,0 +1,4 @@
+repo: 373f1649c80dea9be7b5bc9c57e8395f94f93ab1
+node: 91d4f03a135efe65e2e7de46c00ef09c78fdaae3
+branch: default
+tag: pentadactyl-1.0rc1
index 9cb94cb8c164b980c9d67d917194b378d4befa0f..fcc3ad8056db9ff6dfa783e8721821a02e401702 100644 (file)
--- a/.hgignore
+++ b/.hgignore
@@ -8,6 +8,11 @@ syntax: glob
 
 ## Generated by the build process
 *.xpi
+*.o
+*.so
+*.xpt
+*/.depend
+*/config.local.mk
 */locale/*/*.html
 */chrome
 */contrib/vim/*.vba
@@ -15,6 +20,10 @@ syntax: glob
 downloads/*
 .git/*
 
+binary/src/*/*.h
+
+common/tests/functional/log
+
 *.py[co]
 
 ## Editor backup and swap files
diff --git a/.hgsub b/.hgsub
new file mode 100644 (file)
index 0000000..2ee5d68
--- /dev/null
+++ b/.hgsub
@@ -0,0 +1 @@
+binary/components = https://code.google.com/p/dactyl.binary-modules/
diff --git a/.hgsubstate b/.hgsubstate
new file mode 100644 (file)
index 0000000..9d9a838
--- /dev/null
@@ -0,0 +1 @@
+9f8a25e7d9861892d8e6136764bb88139e0a7253 binary/components
index 8b137891791fe96927ad78e64b0aad7bded08bdc..6cd256442619631c1abf4906571857cc960116a5 100644 (file)
@@ -1 +1,2 @@
-
+1.0b8:
+ • The chrome-data: protocol has been removed.
index d8fcf8155a0a1d9f1552aa8507d72b534984907e..96ac8c734c6f4c8ad089015df726c16789469edd 100644 (file)
@@ -1,6 +1,6 @@
-Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
-              2007-2009 by Doug Kearns <dougkearns@gmail.com>
-              2008-2010 by Kris Maglione <maglione.k at Gmail>
+Copyright © 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
+Copyright © 2007-2011 by Doug Kearns <dougkearns@gmail.com>
+Copyright © 2008-2011 by Kris Maglione <maglione.k at Gmail>
 
 For a full list of authors, refer the AUTHORS file.
 
diff --git a/binary/chrome.manifest b/binary/chrome.manifest
new file mode 100644 (file)
index 0000000..ff98cb0
--- /dev/null
@@ -0,0 +1,8 @@
+
+manifest components/gecko-6.manifest appversion>=6.0
+manifest components/gecko-7.manifest appversion>=7.0
+manifest components/gecko-8.manifest platformversion>7.*
+manifest components/gecko-9.manifest platformversion>8.*
+manifest components/gecko-10.manifest platformversion>9.*
+
+# vim:se tw=0 ft=cfg:
diff --git a/binary/config.mk b/binary/config.mk
new file mode 100644 (file)
index 0000000..e7ae2e1
--- /dev/null
@@ -0,0 +1,39 @@
+
+GECKO_MAJOR ?= 10
+GECKO_MINOR ?= 0
+ABI_OS        := $(shell uname -s)
+ABI_ARCH      := $(shell uname -m)
+ABI_COMPILER  := gcc3
+ABI_PLATFORM  ?= $(ABI_OS)_$(ABI_ARCH)-$(ABI_COMPILER)
+ABI           ?= $(GECKO_MAJOR).$(GECKO_MINOR)-$(ABI_PLATFORM)
+DEFINES        = -DGECKO_MAJOR=$(GECKO_MAJOR) -DGECKO_MINOR=$(GECKO_MINOR)
+
+LIBEXT       ?= so
+
+SED := $(shell if [ "xoo" = x$$(echo foo | sed -E 's/f(o)/\1/' 2>/dev/null) ]; \
+              then echo sed -E; else echo sed -r;                              \
+              fi)
+
+
+PKGCONFIG      ?= pkg-config
+GECKO_SDK_PATH := $(shell $(PKGCONFIG) --libs libxul | $(SED) 's,([^-]|-[^L])*-L([^ ]+)/lib.*,\2,')
+
+CXX      ?= c++
+CPP       = $(CXX) -o
+LINK     ?= c++
+
+MKDEP    ?= $(CXX) -M
+
+PYTHON   ?= python2
+
+EXCPPFLAGS =    -fno-rtti              \
+                -fno-exceptions                \
+                -fshort-wchar          \
+               -fPIC                   \
+               -Os                     \
+               $(NULL)
+
+XPIDL   ?= $(PYTHON) $(GECKO_SDK_PATH)/sdk/bin
+IDL_H   ?= $(XPIDL)/header.py -o
+IDL_XPT ?= $(XPIDL)/typelib.py -o
+
diff --git a/binary/install.rdf b/binary/install.rdf
new file mode 100644 (file)
index 0000000..cf1f8c3
--- /dev/null
@@ -0,0 +1,35 @@
+<?xml version="1.0"?>
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+    <Description about="urn:mozilla:install-manifest"
+        em:id="binary@dactyl.googlecode.com"
+        em:type="2"
+        em:name="Dactyl Binary Utils"
+        em:version="0.1"
+        em:description="Binary utilities for dactyl add-ons"
+        em:unpack="true">
+
+        <em:targetApplication>
+            <Description
+                em:id="toolkit@mozilla.org"
+                em:minVersion="4.0"
+                em:maxVersion="9.*"/>
+        </em:targetApplication>
+
+        <em:targetApplication>
+            <Description
+                em:id="toolkit@mozilla.org"
+                em:minVersion="4.0"
+                em:maxVersion="9.*"/>
+        </em:targetApplication>
+
+        <em:targetApplication>
+            <Description
+                em:id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"
+                em:minVersion="4.0"
+                em:maxVersion="9.*"/>
+        </em:targetApplication>
+    </Description>
+</RDF>
+
+<!-- vim: set fdm=marker sw=4 ts=4 et: -->
diff --git a/binary/src/Makefile b/binary/src/Makefile
new file mode 100644 (file)
index 0000000..02adcc4
--- /dev/null
@@ -0,0 +1,111 @@
+ROOT = ..
+
+XPTDIR         = $(ROOT)/components/$(ABI)/
+SODIR          = $(ROOT)/components/$(ABI)/
+OBJDIR         = $(ABI)/
+
+MODULE         = $(SODIR)dactyl
+
+XPIDLSRCS      = \
+               dactylIUtils.idl \
+               $(NULL)
+
+CPPSRCS                = \
+               dactylModule.cpp \
+               dactylUtils.cpp \
+               mozJSLoaderUtils.cpp \
+               subscriptLoader.cpp \
+               $(NULL)
+
+HEADERS                = \
+                 config.h              \
+                 dactylUtils.h         \
+                 mozJSLoaderUtils.h    \
+                 $(XPIDLSRCS:%.idl=$(ABI)/%.h)
+
+GECKO_DEFINES  = -DMOZILLA_STRICT_API
+
+GECKO_INCLUDES = -I$(ABI)/                     \
+                -I$(GECKO_SDK_PATH)            \
+                -I$(GECKO_SDK_PATH)/idl        \
+                -I$(GECKO_SDK_PATH)/include
+
+GECKO_LDFLAGS =  -L$(GECKO_SDK_PATH)/bin \
+                -L$(GECKO_SDK_PATH)/lib \
+                -lxpcomglue_s  \
+                -lxpcom        \
+                -lnspr4        \
+                -shared        \
+                $(NULL)
+
+ifeq "$(shell uname -s)" "Darwin"
+       GECKO_LDFLAGS += -undefined dynamic_lookup
+endif
+
+include $(ROOT)/config.mk
+sinclude $(ROOT)/config.local.mk
+
+CPPFLAGS += $(EXCPPFLAGS)
+
+XPTS = $(XPIDLSRCS:%.idl=$(XPTDIR)%.xpt)
+OBJS = $(CPPSRCS:%.cpp=$(OBJDIR)%.o)
+MANIFEST = $(SODIR)/components.manifest
+
+all: build manifest
+
+dirs: $(XPTDIR) $(SODIR) $(OBJDIR)
+
+depend: .depend
+
+manifest: $(MANIFEST)
+
+module: dirs $(MODULE).so
+
+dll: dirs $(MODULE).dll
+
+xpts: $(XPTS)
+
+build: dirs module xpts
+
+clean:
+       rm $(MODULE).so
+
+
+$(OBJS): $(HEADERS)
+
+$(ABI)/%.h: %.idl
+       $(IDL_H) $@ $(GECKO_INCLUDES) $<
+
+$(XPTDIR)%.xpt: %.idl
+       $(IDL_XPT) $@ $(GECKO_INCLUDES) $<
+
+$(MANIFEST): Makefile
+       ( echo interfaces $(XPIDLSRCS:.idl=.xpt);       \
+         echo binary-component $(MODULE:$(SODIR)%=%).$(LIBEXT) )       \
+           >$@
+       
+       manifest=$(SODIR)/../gecko-$(GECKO_MAJOR).manifest;                     \
+       if [ $(GECKO_MAJOR) -lt 8 ]; then part=app; else part=platform; fi;     \
+       line="manifest $(ABI)/components.manifest abi=$(ABI_PLATFORM) $${part}version<$(GECKO_MAJOR).*";        \
+       grep >/dev/null 2>&1 "^$$line$$" $$manifest || echo $$line >>$$manifest
+
+_CPPFLAGS = $(CPPFLAGS) $(CXXFLAGS) $(GECKO_DEFINES) $(GECKO_INCLUDES) $(DEFINES)
+
+$(OBJDIR)%.o: %.cpp Makefile
+       $(CPP)$@ -c $(_CPPFLAGS) $<
+
+.depend: $(CPPSRCS) Makefile
+       $(MKDEP) $(_CPPFLAGS) $(CPPSRCS) | $(SED) 's;^[^ ];$(OBJDIR)&;' >.depend
+
+$(MODULE).so: $(OBJS)
+       $(LINK) -o $@ $(OBJS) $(LDFLAGS) $(GECKO_LDFLAGS)
+       chmod +x $@
+
+$(MODULE).dll: $(OBJS)
+       $(LINK)$@ $(GECKO_LDFLAGS) $(OBJS)
+
+$(sort $(XPTDIR) $(SODIR) $(OBJDIR)):
+       mkdir -p $@
+.PHONY: module xpts build clean all depend manifest
+
+sinclude .depend
diff --git a/binary/src/config.h b/binary/src/config.h
new file mode 100644 (file)
index 0000000..2d46cdb
--- /dev/null
@@ -0,0 +1,2 @@
+#pragma once
+#include "mozilla-config.h"
diff --git a/binary/src/dactylIUtils.idl b/binary/src/dactylIUtils.idl
new file mode 100644 (file)
index 0000000..36adf34
--- /dev/null
@@ -0,0 +1,38 @@
+/* Public Domain */
+
+#include "nsISupports.idl"
+#include "nsIDOMElement.idl"
+
+%{C++
+#include "jsapi.h"
+%}
+
+
+[scriptable, uuid(d8ef4492-8f4a-4f5d-8f19-1d71f7f895ed)]
+interface dactylIUtils : nsISupports
+{
+    const PRUint32 DIRECTION_HORIZONTAL = 1 << 0;
+    const PRUint32 DIRECTION_VERTICAL   = 1 << 1;
+
+    [implicit_jscontext]
+    jsval createGlobal();
+
+    [implicit_jscontext]
+    jsval evalInContext(in AString source,
+                        in jsval target,
+                        [optional] in ACString filename,
+                        [optional] in PRInt32 lineNumber);
+
+    void createContents(in nsIDOMElement element);
+
+    [implicit_jscontext]
+    jsval getGlobalForObject(in jsval object);
+
+    PRUint32 getScrollable(in nsIDOMElement element);
+
+    void loadSubScript (in wstring url
+                        /* [optional] in jsval context, */
+                        /* [optional] in wstring charset */);
+};
+
+/* vim:se sts=4 sw=4 et ft=idl: */
diff --git a/binary/src/dactylModule.cpp b/binary/src/dactylModule.cpp
new file mode 100644 (file)
index 0000000..3bc8f37
--- /dev/null
@@ -0,0 +1,42 @@
+#include "dactylUtils.h"
+
+#include "mozilla/ModuleUtils.h"
+
+#define NS_DACTYLUTILS_CID \
+{ 0x4d55a47c, 0x0627, 0x4339, \
+    { 0x97, 0x91, 0x52, 0xef, 0x5e, 0xd4, 0xc3, 0xd1 } }
+
+#define NS_DACTYLUTILS_CONTRACTID \
+    "@dactyl.googlecode.com/extra/utils"
+
+NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(dactylUtils, Init)
+
+NS_DEFINE_NAMED_CID(NS_DACTYLUTILS_CID);
+
+static const mozilla::Module::CIDEntry kDactylCIDs[] = {
+    { &kNS_DACTYLUTILS_CID, true, NULL, dactylUtilsConstructor },
+    { NULL }
+};
+
+static const mozilla::Module::ContractIDEntry kDactylContracts[] = {
+    { NS_DACTYLUTILS_CONTRACTID, &kNS_DACTYLUTILS_CID },
+    { NULL }
+};
+
+static const mozilla::Module::CategoryEntry kDactylCategories[] = {
+    { NULL }
+};
+
+static const mozilla::Module kDactylUtilsModule = {
+    mozilla::Module::kVersion,
+    kDactylCIDs,
+    kDactylContracts,
+    kDactylCategories,
+    NULL,
+    NULL,
+    NULL
+};
+
+NSMODULE_DEFN(dactylUtilsModule) = &kDactylUtilsModule;
+
+/* vim:se sts=4 sw=4 et cin ft=cpp: */
diff --git a/binary/src/dactylUtils.cpp b/binary/src/dactylUtils.cpp
new file mode 100644 (file)
index 0000000..23be0c1
--- /dev/null
@@ -0,0 +1,381 @@
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * Contributors, possibly:
+ *   Kris Maglione <maglione.k at Gmail>
+ *   Jeff Walden <jwalden@mit.edu>
+ *   Mike Shaver <shaver@zeroknowledge.com>
+ *   John Bandhauer <jband@netscape.com>
+ *   Robert Ginda <rginda@netscape.com>
+ *   Pierre Phaneuf <pp@ludusdesign.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either of the GNU General Public License Version 2 or later (the "GPL"),
+ * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK *****
+ */
+
+#include "dactylUtils.h"
+
+#include "jsdbgapi.h"
+// #include "jsobj.h"
+
+#include "nsStringAPI.h"
+
+#if GECKO_MAJOR < 10
+    static inline JSObject*
+    JS_GetGlobalForScopeChain(JSContext *cx) {
+        JSObject *callingScope = JS_GetScopeChain(cx);
+        if (!callingScope)
+            return nsnull;
+        return JS_GetGlobalForObject(cx, callingScope);
+    }
+#endif
+
+/*
+ * Evil. Evil, evil, evil.
+ */
+#define MOZILLA_INTERNAL_API
+#  define nsAString_h___
+#  define nsString_h___
+#  define nsStringFwd_h___
+#  define nsStringGlue_h__
+#  define nsContentUtils_h___
+class nsAFlatCString;
+typedef nsString nsSubstring;
+#  include "nsIScrollableFrame.h"
+#undef MOZILLA_INTERNAL_API
+#include "nsPresContext.h"
+#include "nsQueryFrame.h"
+
+#include "nsIContent.h"
+#include "nsIDOMXULElement.h"
+#include "nsIXULTemplateBuilder.h"
+#include "nsIObserverService.h"
+#include "nsIScriptSecurityManager.h"
+#include "nsIXPCScriptable.h"
+
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+
+
+class autoDropPrincipals {
+public:
+    autoDropPrincipals(JSContext *context, JSPrincipals *principals) : mContext(context), mJSPrincipals(principals) {}
+    ~autoDropPrincipals() {
+        JSPRINCIPALS_DROP(mContext, mJSPrincipals);
+    }
+
+private:
+    JSContext *mContext;
+    JSPrincipals *mJSPrincipals;
+};
+
+static JSBool
+Dump(JSContext *cx, uintN argc, jsval *vp)
+{
+    JSString *str;
+    if (!argc)
+        return JS_TRUE;
+
+    str = JS_ValueToString(cx, JS_ARGV(cx, vp)[0]);
+    if (!str)
+        return JS_FALSE;
+
+    size_t length;
+    const jschar *chars = JS_GetStringCharsAndLength(cx, str, &length);
+    if (!chars)
+        return JS_FALSE;
+
+    fputs(NS_ConvertUTF16toUTF8(reinterpret_cast<const PRUnichar*>(chars)).get(), stderr);
+    return JS_TRUE;
+}
+
+static JSFunctionSpec gGlobalFun[] = {
+    {"dump",    Dump,   1,0},
+    {nsnull,nsnull,0,0}
+};
+
+static dactylUtils* gService = nsnull;
+
+dactylUtils::dactylUtils()
+    : mRuntime(nsnull)
+{
+    NS_ASSERTION(gService == nsnull, "Service already exists");
+}
+
+dactylUtils::~dactylUtils()
+{
+    mRuntimeService = nsnull;
+    gService = nsnull;
+}
+
+nsresult
+dactylUtils::Init()
+{
+    nsresult rv;
+    NS_ENSURE_TRUE(!gService, NS_ERROR_UNEXPECTED);
+
+    mRuntimeService = do_GetService("@mozilla.org/js/xpc/RuntimeService;1", &rv);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    rv = mRuntimeService->GetRuntime(&mRuntime);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    nsCOMPtr<nsIScriptSecurityManager> secman =
+        do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID);
+    NS_ENSURE_TRUE(secman, NS_ERROR_FAILURE);
+
+    rv = secman->GetSystemPrincipal(getter_AddRefs(mSystemPrincipal));
+    NS_ENSURE_TRUE(rv, rv);
+    NS_ENSURE_TRUE(mSystemPrincipal, NS_ERROR_FAILURE);
+
+    return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS1(dactylUtils,
+                   dactylIUtils)
+
+NS_IMETHODIMP
+dactylUtils::CreateGlobal(JSContext *cx, jsval *out)
+{
+    nsresult rv;
+
+    // JS::AutoPreserveCompartment pc(cx);
+
+    nsCOMPtr<nsIXPCScriptable> backstagePass;
+    rv = mRuntimeService->GetBackstagePass(getter_AddRefs(backstagePass));
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    nsCOMPtr<nsIXPConnect> xpc =
+        do_GetService("@mozilla.org/js/xpc/XPConnect;1", &rv);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    // Make sure InitClassesWithNewWrappedGlobal() installs the
+    // backstage pass as the global in our compilation context.
+    JS_SetGlobalObject(cx, nsnull);
+
+    nsCOMPtr<nsIXPConnectJSObjectHolder> holder;
+    rv = xpc->InitClassesWithNewWrappedGlobal(cx, backstagePass,
+                                              NS_GET_IID(nsISupports),
+                                              mSystemPrincipal,
+                                              nsnull,
+                                              nsIXPConnect::
+                                                  FLAG_SYSTEM_GLOBAL_OBJECT,
+                                              getter_AddRefs(holder));
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    JSObject *global;
+    rv = holder->GetJSObject(&global);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    JSAutoEnterCompartment ac;
+    NS_ENSURE_TRUE(ac.enter(cx, global), NS_ERROR_FAILURE);
+
+    NS_ENSURE_TRUE(JS_DefineFunctions(cx, global, gGlobalFun),
+                   NS_ERROR_FAILURE);
+    NS_ENSURE_TRUE(JS_DefineProfilingFunctions(cx, global),
+                   NS_ERROR_FAILURE);
+
+    *out = OBJECT_TO_JSVAL(global);
+    return NS_OK;
+}
+
+NS_IMETHODIMP
+dactylUtils::EvalInContext(const nsAString &aSource,
+                           const jsval &aTarget,
+                           const nsACString &aFilename,
+                           PRInt32 aLineNumber,
+                           JSContext *cx,
+                           jsval *rval)
+{
+    nsresult rv;
+
+    nsCOMPtr<nsIXPConnect> xpc(do_GetService(nsIXPConnect::GetCID(), &rv));
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    nsCString filename;
+
+    if (!aFilename.IsEmpty())
+        filename.Assign(aFilename);
+    else {
+        nsCOMPtr<nsIStackFrame> frame;
+        xpc->GetCurrentJSStack(getter_AddRefs(frame));
+        NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE);
+        frame->GetFilename(getter_Copies(filename));
+        frame->GetLineNumber(&aLineNumber);
+    }
+
+    JSObject *target;
+    NS_ENSURE_FALSE(JSVAL_IS_PRIMITIVE(aTarget), NS_ERROR_UNEXPECTED);
+    target = JSVAL_TO_OBJECT(aTarget);
+
+
+    JSObject *result = target;
+    target = JS_FindCompilationScope(cx, target);
+    NS_ENSURE_TRUE(target, NS_ERROR_FAILURE);
+
+
+    nsCOMPtr<nsIScriptSecurityManager> secman =
+        do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID);
+    NS_ENSURE_TRUE(secman, NS_ERROR_FAILURE);
+
+    nsCOMPtr<nsIPrincipal> principal;
+    rv = secman->GetObjectPrincipal(cx, target, getter_AddRefs(principal));
+    NS_ENSURE_SUCCESS(rv, rv);
+
+
+    JSPrincipals *jsPrincipals;
+    rv = principal->GetJSPrincipals(cx, &jsPrincipals);
+    NS_ENSURE_SUCCESS(rv, rv);
+    autoDropPrincipals adp(cx, jsPrincipals);
+
+    JSObject *callingScope;
+    {
+        JSAutoRequest req(cx);
+
+        callingScope = JS_GetGlobalForScopeChain(cx);
+        NS_ENSURE_TRUE(callingScope, NS_ERROR_FAILURE);
+    }
+
+    {
+        JSAutoRequest req(cx);
+        JSAutoEnterCompartment ac;
+        jsval v;
+
+        NS_ENSURE_TRUE(ac.enter(cx, target), NS_ERROR_FAILURE);
+
+        JSBool ok =
+            JS_EvaluateUCScriptForPrincipals(cx, target,
+                                             jsPrincipals,
+                                             reinterpret_cast<const jschar*>
+                                                (PromiseFlatString(aSource).get()),
+                                             aSource.Length(),
+                                             filename.get(), aLineNumber, &v);
+
+        if (!ok) {
+            jsval exn;
+            if (!JS_GetPendingException(cx, &exn))
+                rv = NS_ERROR_FAILURE;
+            else {
+                JS_ClearPendingException(cx);
+
+                if (JS_WrapValue(cx, &exn))
+                    JS_SetPendingException(cx, exn);
+            }
+        }
+        else {
+            // Convert the result into something safe for our caller.
+            JSAutoRequest req(cx);
+            JSAutoEnterCompartment ac;
+
+            if (!ac.enter(cx, callingScope) || !JS_WrapValue(cx, &v))
+                rv = NS_ERROR_FAILURE;
+
+            if (NS_SUCCEEDED(rv))
+                *rval = v;
+        }
+    }
+
+    return rv;
+}
+
+NS_IMETHODIMP
+dactylUtils::CreateContents(nsIDOMElement *aElement)
+{
+    nsCOMPtr<nsIContent> content = do_QueryInterface(aElement);
+
+    for (nsIContent *element = content;
+         element;
+         element = element->GetParent()) {
+
+        nsCOMPtr<nsIDOMXULElement> xulelem = do_QueryInterface(element);
+        if (xulelem) {
+            nsCOMPtr<nsIXULTemplateBuilder> builder;
+            xulelem->GetBuilder(getter_AddRefs(builder));
+            if (builder) {
+                builder->CreateContents(content, PR_TRUE);
+                break;
+            }
+        }
+    }
+    return NS_OK;
+}
+
+namespace XPCWrapper {
+    extern JSObject *UnsafeUnwrapSecurityWrapper(JSObject *obj);
+};
+
+NS_IMETHODIMP
+dactylUtils::GetGlobalForObject(const jsval &aObject,
+                                JSContext *cx,
+                                jsval *rval)
+{
+    nsresult rv;
+
+    NS_ENSURE_FALSE(JSVAL_IS_PRIMITIVE(aObject),
+                    NS_ERROR_XPC_BAD_CONVERT_JS);
+
+    JSObject *obj = XPCWrapper::UnsafeUnwrapSecurityWrapper(JSVAL_TO_OBJECT(aObject));
+
+    JSObject *global = JS_GetGlobalForObject(cx, obj);
+    *rval = OBJECT_TO_JSVAL(global);
+
+    /*
+    // Outerize if necessary.
+    if (JSObjectOp outerize = global->getClass()->ext.outerObject)
+        *rval = OBJECT_TO_JSVAL(outerize(cx, global));
+    */
+
+    return NS_OK;
+}
+
+NS_IMETHODIMP
+dactylUtils::GetScrollable(nsIDOMElement *aElement, PRUint32 *rval)
+{
+    nsCOMPtr<nsIContent> content = do_QueryInterface(aElement);
+
+    nsIFrame *frame = content->GetPrimaryFrame();
+    nsIScrollableFrame *scrollFrame = do_QueryFrame(frame);
+
+    *rval = 0;
+    if (scrollFrame) {
+        nsPresContext::ScrollbarStyles ss = scrollFrame->GetScrollbarStyles();
+        PRUint32 scrollbarVisibility = scrollFrame->GetScrollbarVisibility();
+        nsRect scrollRange = scrollFrame->GetScrollRange();
+
+        if (ss.mHorizontal != NS_STYLE_OVERFLOW_HIDDEN &&
+             ((scrollbarVisibility & nsIScrollableFrame::HORIZONTAL) ||
+              scrollRange.width > 0))
+            *rval |= dactylIUtils::DIRECTION_HORIZONTAL;
+
+        if (ss.mVertical != NS_STYLE_OVERFLOW_HIDDEN &&
+             ((scrollbarVisibility & nsIScrollableFrame::VERTICAL) ||
+              scrollRange.height > 0))
+            *rval |= dactylIUtils::DIRECTION_VERTICAL;
+    }
+
+    return NS_OK;
+}
+
+/* vim:se sts=4 sw=4 et cin ft=cpp: */
diff --git a/binary/src/dactylUtils.h b/binary/src/dactylUtils.h
new file mode 100644 (file)
index 0000000..7454536
--- /dev/null
@@ -0,0 +1,36 @@
+
+#pragma once
+
+#include "config.h"
+#include "dactylIUtils.h"
+
+#include "nsISupports.h"
+#include "nsIPrincipal.h"
+#include "nsIXPConnect.h"
+
+#include "jsapi.h"
+#include "jsfriendapi.h"
+#include "nsIJSRuntimeService.h"
+#include "nsIJSContextStack.h"
+
+#include "nsCOMPtr.h"
+
+class dactylUtils : public dactylIUtils {
+public:
+    dactylUtils() NS_HIDDEN;
+    ~dactylUtils() NS_HIDDEN;
+
+    NS_DECL_ISUPPORTS
+    NS_DECL_DACTYLIUTILS
+
+    NS_HIDDEN_(nsresult) Init();
+
+private:
+
+    nsCOMPtr<nsIJSRuntimeService> mRuntimeService;
+    JSRuntime *mRuntime;
+
+    nsCOMPtr<nsIPrincipal> mSystemPrincipal;
+};
+
+/* vim:se sts=4 sw=4 et cin ft=cpp: */
diff --git a/binary/src/mozJSLoaderUtils.cpp b/binary/src/mozJSLoaderUtils.cpp
new file mode 100644 (file)
index 0000000..94b371b
--- /dev/null
@@ -0,0 +1,196 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010-2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Michael Wu <mwu@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either of the GNU General Public License Version 2 or later (the "GPL"),
+ * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#include "mozJSLoaderUtils.h"
+#include "nsAutoPtr.h"
+
+#include "jsapi.h"
+#include "jsxdrapi.h"
+
+#include "mozilla/scache/StartupCache.h"
+#include "mozilla/scache/StartupCacheUtils.h"
+
+#include "nsIChromeRegistry.h"
+#include "nsIIOService.h"
+#include "nsIResProtocolHandler.h"
+#include "nsNetUtil.h"
+
+using namespace mozilla::scache;
+
+static nsresult
+ReadScriptFromStream(JSContext *cx, nsIObjectInputStream *stream,
+                     JSScriptType **script)
+{
+    *script = nsnull;
+
+    PRUint32 size;
+    nsresult rv = stream->Read32(&size);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    char *data;
+    rv = stream->ReadBytes(size, &data);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    JSXDRState *xdr = JS_XDRNewMem(cx, JSXDR_DECODE);
+    NS_ENSURE_TRUE(xdr, NS_ERROR_OUT_OF_MEMORY);
+
+    xdr->userdata = stream;
+    JS_XDRMemSetData(xdr, data, size);
+
+    if (!JS_XDRScript(xdr, script)) {
+        rv = NS_ERROR_FAILURE;
+    }
+
+    // Update data in case ::JS_XDRScript called back into C++ code to
+    // read an XPCOM object.
+    //
+    // In that case, the serialization process must have flushed a run
+    // of counted bytes containing JS data at the point where the XPCOM
+    // object starts, after which an encoding C++ callback from the JS
+    // XDR code must have written the XPCOM object directly into the
+    // nsIObjectOutputStream.
+    //
+    // The deserialization process will XDR-decode counted bytes up to
+    // but not including the XPCOM object, then call back into C++ to
+    // read the object, then read more counted bytes and hand them off
+    // to the JSXDRState, so more JS data can be decoded.
+    //
+    // This interleaving of JS XDR data and XPCOM object data may occur
+    // several times beneath the call to ::JS_XDRScript, above.  At the
+    // end of the day, we need to free (via nsMemory) the data owned by
+    // the JSXDRState.  So we steal it back, nulling xdr's buffer so it
+    // doesn't get passed to ::JS_free by ::JS_XDRDestroy.
+
+    uint32 length;
+    data = static_cast<char*>(JS_XDRMemGetData(xdr, &length));
+    JS_XDRMemSetData(xdr, nsnull, 0);
+    JS_XDRDestroy(xdr);
+
+    // If data is null now, it must have been freed while deserializing an
+    // XPCOM object (e.g., a principal) beneath ::JS_XDRScript.
+    nsMemory::Free(data);
+
+    return rv;
+}
+
+static nsresult
+WriteScriptToStream(JSContext *cx, JSScriptType *script,
+                    nsIObjectOutputStream *stream)
+{
+    JSXDRState *xdr = JS_XDRNewMem(cx, JSXDR_ENCODE);
+    NS_ENSURE_TRUE(xdr, NS_ERROR_OUT_OF_MEMORY);
+
+    xdr->userdata = stream;
+    nsresult rv = NS_OK;
+
+    if (JS_XDRScript(xdr, &script)) {
+        // Get the encoded JSXDRState data and write it.  The JSXDRState owns
+        // this buffer memory and will free it beneath ::JS_XDRDestroy.
+        //
+        // If an XPCOM object needs to be written in the midst of the JS XDR
+        // encoding process, the C++ code called back from the JS engine (e.g.,
+        // nsEncodeJSPrincipals in caps/src/nsJSPrincipals.cpp) will flush data
+        // from the JSXDRState to aStream, then write the object, then return
+        // to JS XDR code with xdr reset so new JS data is encoded at the front
+        // of the xdr's data buffer.
+        //
+        // However many XPCOM objects are interleaved with JS XDR data in the
+        // stream, when control returns here from ::JS_XDRScript, we'll have
+        // one last buffer of data to write to aStream.
+
+        uint32 size;
+        const char* data = reinterpret_cast<const char*>
+                                           (JS_XDRMemGetData(xdr, &size));
+        NS_ASSERTION(data, "no decoded JSXDRState data!");
+
+        rv = stream->Write32(size);
+        if (NS_SUCCEEDED(rv)) {
+            rv = stream->WriteBytes(data, size);
+        }
+    } else {
+        rv = NS_ERROR_FAILURE; // likely to be a principals serialization error
+    }
+
+    JS_XDRDestroy(xdr);
+    return rv;
+}
+
+nsresult
+ReadCachedScript(nsIStartupCache* cache, nsACString &uri, JSContext *cx, JSScriptType **script)
+{
+    nsresult rv;
+
+    nsAutoArrayPtr<char> buf;
+    PRUint32 len;
+    rv = cache->GetBuffer(PromiseFlatCString(uri).get(), getter_Transfers(buf),
+                          &len);
+    if (NS_FAILED(rv)) {
+        return rv; // don't warn since NOT_AVAILABLE is an ok error
+    }
+
+    nsCOMPtr<nsIObjectInputStream> ois;
+    rv = NewObjectInputStreamFromBuffer(buf, len, getter_AddRefs(ois));
+    NS_ENSURE_SUCCESS(rv, rv);
+    buf.forget();
+
+    return ReadScriptFromStream(cx, ois, script);
+}
+
+nsresult
+WriteCachedScript(nsIStartupCache* cache, nsACString &uri, JSContext *cx, JSScriptType *script)
+{
+    nsresult rv;
+
+    nsCOMPtr<nsIObjectOutputStream> oos;
+    nsCOMPtr<nsIStorageStream> storageStream;
+    rv = NewObjectOutputWrappedStorageStream(getter_AddRefs(oos),
+                                             getter_AddRefs(storageStream),
+                                             true);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    rv = WriteScriptToStream(cx, script, oos);
+    oos->Close();
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    nsAutoArrayPtr<char> buf;
+    PRUint32 len;
+    rv = NewBufferFromStorageStream(storageStream, getter_Transfers(buf), &len);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    rv = cache->PutBuffer(PromiseFlatCString(uri).get(), buf, len);
+    return rv;
+}
diff --git a/binary/src/mozJSLoaderUtils.h b/binary/src/mozJSLoaderUtils.h
new file mode 100644 (file)
index 0000000..372fdb2
--- /dev/null
@@ -0,0 +1,88 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Michael Wu <mwu@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either of the GNU General Public License Version 2 or later (the "GPL"),
+ * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#ifndef mozJSLoaderUtils_h
+#define mozJSLoaderUtils_h
+
+#include "config.h"
+
+/*
+ * This is evil. Very evil.
+#define nsString_h___
+#include "nsStringGlue.h"
+ */
+
+#include "nsIStartupCache.h"
+#include "nsStringAPI.h"
+#include "jsapi.h"
+
+#if defined(GECKO_MAJOR) && GECKO_MAJOR < 9
+#include "jsapi.h"
+#   define JS_XDRScript JS_XDRScriptObject
+    typedef JSObject JSScriptType;
+
+#   if GECKO_MAJOR < 8
+#   define NewObjectInputStreamFromBuffer NS_NewObjectInputStreamFromBuffer
+#   define NewBufferFromStorageStream NS_NewBufferFromStorageStream
+#      if GECKO_MAJOR > 6
+#          define NewObjectOutputWrappedStorageStream NS_NewObjectOutputWrappedStorageStream
+#      else
+#          define NewObjectOutputWrappedStorageStream(a, b, c) NS_NewObjectOutputWrappedStorageStream((a), (b))
+#      endif
+#   endif
+#else
+    typedef JSScript JSScriptType;
+#endif
+
+class nsIURI;
+
+namespace mozilla {
+namespace scache {
+class StartupCache;
+}
+}
+
+nsresult
+ReadCachedScript(nsIStartupCache* cache, nsACString &uri,
+                 JSContext *cx, JSScriptType **scriptObj);
+
+nsresult
+WriteCachedScript(nsIStartupCache* cache, nsACString &uri,
+                  JSContext *cx, JSScriptType *scriptObj);
+#endif /* mozJSLoaderUtils_h */
diff --git a/binary/src/subscriptLoader.cpp b/binary/src/subscriptLoader.cpp
new file mode 100644 (file)
index 0000000..1c7b5cc
--- /dev/null
@@ -0,0 +1,583 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=4 sw=4 et tw=80:
+ *
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Communicator client code, released
+ * March 31, 1998.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1998
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Robert Ginda <rginda@netscape.com>
+ *   Kris Maglione <maglione.k@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either of the GNU General Public License Version 2 or later (the "GPL"),
+ * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * What follows is the ordinary mozIJSSubScriptLoader modified only to strip the
+ * useless, irritating, and troublesome "foo -> " prefixes from filenames.
+ */
+
+#include "dactylUtils.h"
+#include "mozJSLoaderUtils.h"
+
+#include "nsIServiceManager.h"
+#include "nsIXPConnect.h"
+
+#include "nsIURI.h"
+#include "nsIIOService.h"
+#include "nsIChannel.h"
+#include "nsIConsoleService.h"
+#include "nsIScriptError.h"
+#include "nsIInputStream.h"
+#include "nsNetCID.h"
+#include "nsAutoPtr.h"
+#include "nsNetUtil.h"
+#include "nsIProtocolHandler.h"
+#include "nsIScriptSecurityManager.h"
+#include "nsIFileURL.h"
+
+// ConvertToUTF16
+#include "nsICharsetConverterManager.h"
+#include "nsIUnicodeDecoder.h"
+template<class T>
+inline PRBool EnsureStringLength(T& aStr, PRUint32 aLen)
+{
+    aStr.SetLength(aLen);
+    return (aStr.Length() == aLen);
+}
+
+#include "jsapi.h"
+#include "jscntxt.h"
+#include "jsdbgapi.h"
+#include "jsfriendapi.h"
+
+#if GECKO_MAJOR < 10
+    static inline JSVersion
+    JS_GetVersion(JSContext *cx) {
+        return cx->findVersion();
+    }
+#endif
+
+#include "mozilla/FunctionTimer.h"
+#include "mozilla/scache/StartupCache.h"
+#include "mozilla/scache/StartupCacheUtils.h"
+
+using namespace mozilla::scache;
+class nsACString_internal : nsACString {};
+
+namespace mozilla {
+    namespace scache {
+        nsresult PathifyURI(nsIURI *in, nsACString_internal &out);
+    }
+}
+
+/* load() error msgs, XXX localize? */
+#define LOAD_ERROR_NOSERVICE "Error creating IO Service."
+#define LOAD_ERROR_NOURI "Error creating URI (invalid URL scheme?)"
+#define LOAD_ERROR_NOSCHEME "Failed to get URI scheme.  This is bad."
+#define LOAD_ERROR_URI_NOT_LOCAL "Trying to load a non-local URI."
+#define LOAD_ERROR_NOSTREAM  "Error opening input stream (invalid filename?)"
+#define LOAD_ERROR_NOCONTENT "ContentLength not available (not a local URL?)"
+#define LOAD_ERROR_BADCHARSET "Error converting to specified charset"
+#define LOAD_ERROR_BADREAD   "File Read Error."
+#define LOAD_ERROR_READUNDERFLOW "File Read Error (underflow.)"
+#define LOAD_ERROR_NOPRINCIPALS "Failed to get principals."
+#define LOAD_ERROR_NOSPEC "Failed to get URI spec.  This is bad."
+
+/* Internal linkage, bloody hell... */
+static nsresult
+ConvertToUTF16(nsIChannel* aChannel, const PRUint8* aData,
+               PRUint32 aLength, const nsString& aHintCharset,
+               nsString& aString)
+{
+  if (!aLength) {
+    aString.Truncate();
+    return NS_OK;
+  }
+
+  nsCAutoString characterSet;
+
+  nsresult rv = NS_OK;
+  if (aChannel) {
+    rv = aChannel->GetContentCharset(characterSet);
+  }
+
+  if (!aHintCharset.IsEmpty() && (NS_FAILED(rv) || characterSet.IsEmpty())) {
+    // charset name is always ASCII.
+    LossyCopyUTF16toASCII(aHintCharset, characterSet);
+  }
+
+  if (characterSet.IsEmpty()) {
+    // fall back to ISO-8859-1, see bug 118404
+    characterSet.AssignLiteral("ISO-8859-1");
+  }
+
+  nsCOMPtr<nsICharsetConverterManager> charsetConv =
+    do_GetService(NS_CHARSETCONVERTERMANAGER_CONTRACTID, &rv);
+
+  nsCOMPtr<nsIUnicodeDecoder> unicodeDecoder;
+
+  if (NS_SUCCEEDED(rv) && charsetConv) {
+    rv = charsetConv->GetUnicodeDecoder(characterSet.get(),
+                                        getter_AddRefs(unicodeDecoder));
+    if (NS_FAILED(rv)) {
+      // fall back to ISO-8859-1 if charset is not supported. (bug 230104)
+      rv = charsetConv->GetUnicodeDecoderRaw("ISO-8859-1",
+                                             getter_AddRefs(unicodeDecoder));
+    }
+  }
+
+  // converts from the charset to unicode
+  if (NS_SUCCEEDED(rv)) {
+    PRInt32 unicodeLength = 0;
+
+    rv = unicodeDecoder->GetMaxLength(reinterpret_cast<const char*>(aData),
+                                      aLength, &unicodeLength);
+    if (NS_SUCCEEDED(rv)) {
+      if (!EnsureStringLength(aString, unicodeLength))
+        return NS_ERROR_OUT_OF_MEMORY;
+
+      PRUnichar *ustr = aString.BeginWriting();
+
+      PRInt32 consumedLength = 0;
+      PRInt32 originalLength = aLength;
+      PRInt32 convertedLength = 0;
+      PRInt32 bufferLength = unicodeLength;
+      do {
+        rv = unicodeDecoder->Convert(reinterpret_cast<const char*>(aData),
+                                     (PRInt32 *) &aLength, ustr,
+                                     &unicodeLength);
+        if (NS_FAILED(rv)) {
+          // if we failed, we consume one byte, replace it with U+FFFD
+          // and try the conversion again.
+          ustr[unicodeLength++] = (PRUnichar)0xFFFD;
+          ustr += unicodeLength;
+
+          unicodeDecoder->Reset();
+        }
+        aData += ++aLength;
+        consumedLength += aLength;
+        aLength = originalLength - consumedLength;
+        convertedLength += unicodeLength;
+        unicodeLength = bufferLength - convertedLength;
+      } while (NS_FAILED(rv) && (originalLength > consumedLength) && (bufferLength > convertedLength));
+      aString.SetLength(convertedLength);
+    }
+  }
+  return rv;
+}
+
+// We just use the same reporter as the component loader
+static void
+mozJSLoaderErrorReporter(JSContext *cx, const char *message, JSErrorReport *rep)
+{
+    nsresult rv;
+
+    /* Use the console service to register the error. */
+    nsCOMPtr<nsIConsoleService> consoleService =
+        do_GetService(NS_CONSOLESERVICE_CONTRACTID);
+
+    /*
+     * Make an nsIScriptError, populate it with information from this
+     * error, then log it with the console service.  The UI can then
+     * poll the service to update the Error console.
+     */
+    nsCOMPtr<nsIScriptError> errorObject =
+        do_CreateInstance(NS_SCRIPTERROR_CONTRACTID);
+
+    if (consoleService && errorObject) {
+        /*
+         * Got an error object; prepare appropriate-width versions of
+         * various arguments to it.
+         */
+        nsAutoString fileUni;
+        fileUni.Assign(NS_ConvertUTF8toUTF16(rep->filename));
+
+        PRUint32 column = rep->uctokenptr - rep->uclinebuf;
+
+        rv = errorObject->Init(reinterpret_cast<const PRUnichar*>
+                                               (rep->ucmessage),
+                               fileUni.get(),
+                               reinterpret_cast<const PRUnichar*>
+                                               (rep->uclinebuf),
+                               rep->lineno, column, rep->flags,
+                               "component javascript");
+        if (NS_SUCCEEDED(rv)) {
+            rv = consoleService->LogMessage(errorObject);
+            if (NS_SUCCEEDED(rv)) {
+                // We're done!  Skip return to fall thru to stderr
+                // printout, for the benefit of those invoking the
+                // browser with -console
+                // return;
+            }
+        }
+    }
+
+    /*
+     * If any of the above fails for some reason, fall back to
+     * printing to stderr.
+     */
+}
+
+static JSBool
+Dump(JSContext *cx, uintN argc, jsval *vp)
+{
+    JSString *str;
+    if (!argc)
+        return JS_TRUE;
+
+    str = JS_ValueToString(cx, JS_ARGV(cx, vp)[0]);
+    if (!str)
+        return JS_FALSE;
+
+    size_t length;
+    const jschar *chars = JS_GetStringCharsAndLength(cx, str, &length);
+    if (!chars)
+        return JS_FALSE;
+
+    fputs(NS_ConvertUTF16toUTF8(reinterpret_cast<const PRUnichar*>(chars)).get(), stderr);
+    return JS_TRUE;
+}
+
+static nsresult
+ReportError(JSContext *cx, const char *msg)
+{
+    JS_SetPendingException(cx, STRING_TO_JSVAL(JS_NewStringCopyZ(cx, msg)));
+    return NS_OK;
+}
+
+static nsresult
+ReadScript(nsIURI *uri, JSContext *cx, JSObject *target_obj,
+           jschar *charset, const char *uriStr,
+           nsIIOService *serv, nsIPrincipal *principal,
+           JSScriptType **scriptObjp)
+{
+    nsCOMPtr<nsIChannel>     chan;
+    nsCOMPtr<nsIInputStream> instream;
+    JSPrincipals    *jsPrincipals;
+    JSErrorReporter  er;
+
+    nsresult rv;
+    // Instead of calling NS_OpenURI, we create the channel ourselves and call
+    // SetContentType, to avoid expensive MIME type lookups (bug 632490).
+    rv = NS_NewChannel(getter_AddRefs(chan), uri, serv,
+                       nsnull, nsnull, nsIRequest::LOAD_NORMAL);
+    if (NS_SUCCEEDED(rv)) {
+        chan->SetContentType(NS_LITERAL_CSTRING("application/javascript"));
+        rv = chan->Open(getter_AddRefs(instream));
+    }
+
+    if (NS_FAILED(rv)) {
+        return ReportError(cx, LOAD_ERROR_NOSTREAM);
+    }
+
+    PRInt32 len = -1;
+
+    rv = chan->GetContentLength(&len);
+    if (NS_FAILED(rv) || len == -1) {
+        return ReportError(cx, LOAD_ERROR_NOCONTENT);
+    }
+
+    nsCString buf;
+    rv = NS_ReadInputStreamToString(instream, buf, len);
+    if (NS_FAILED(rv))
+        return rv;
+
+    /* we can't hold onto jsPrincipals as a module var because the
+     * JSPRINCIPALS_DROP macro takes a JSContext, which we won't have in the
+     * destructor */
+    rv = principal->GetJSPrincipals(cx, &jsPrincipals);
+    if (NS_FAILED(rv) || !jsPrincipals) {
+        return ReportError(cx, LOAD_ERROR_NOPRINCIPALS);
+    }
+
+    /* set our own error reporter so we can report any bad things as catchable
+     * exceptions, including the source/line number */
+    er = JS_SetErrorReporter(cx, mozJSLoaderErrorReporter);
+
+    if (charset) {
+        nsString script;
+        rv = ConvertToUTF16(
+                nsnull, reinterpret_cast<const PRUint8*>(buf.get()), len,
+                nsDependentString(reinterpret_cast<PRUnichar*>(charset)), script);
+
+        if (NS_FAILED(rv)) {
+            JSPRINCIPALS_DROP(cx, jsPrincipals);
+            return ReportError(cx, LOAD_ERROR_BADCHARSET);
+        }
+
+        *scriptObjp =
+            JS_CompileUCScriptForPrincipals(cx, target_obj, jsPrincipals,
+                                            reinterpret_cast<const jschar*>(script.get()),
+                                            script.Length(), uriStr, 1);
+    } else {
+        *scriptObjp =
+            JS_CompileScriptForPrincipals(cx, target_obj, jsPrincipals, buf.get(),
+                                          len, uriStr, 1);
+    }
+
+    JSPRINCIPALS_DROP(cx, jsPrincipals);
+
+    /* repent for our evil deeds */
+    JS_SetErrorReporter(cx, er);
+
+    return NS_OK;
+}
+
+NS_IMETHODIMP /* args and return value are delt with using XPConnect and JSAPI */
+dactylUtils::LoadSubScript (const PRUnichar * aURL
+                            /* [, JSObject *target_obj] */)
+{
+    /*
+     * Loads a local url and evals it into the current cx
+     * Synchronous (an async version would be cool too.)
+     *   url: The url to load.  Must be local so that it can be loaded
+     *        synchronously.
+     *   target_obj: Optional object to eval the script onto (defaults to context
+     *               global)
+     *   returns: Whatever jsval the script pointed to by the url returns.
+     * Should ONLY (O N L Y !) be called from JavaScript code.
+     */
+
+    /* gotta define most of this stuff up here because of all the gotos,
+     * defined the rest up here to be consistent */
+    nsresult  rv;
+    JSBool    ok;
+
+#ifdef NS_FUNCTION_TIMER
+    NS_TIME_FUNCTION_FMT("%s (line %d) (url: %s)", MOZ_FUNCTION_NAME,
+                         __LINE__, NS_LossyConvertUTF16toASCII(aURL).get());
+#else
+    (void)aURL; // prevent compiler warning
+#endif
+
+    /* get JS things from the CallContext */
+    nsCOMPtr<nsIXPConnect> xpc = do_GetService(nsIXPConnect::GetCID());
+    if (!xpc) return NS_ERROR_FAILURE;
+
+    nsAXPCNativeCallContext *cc = nsnull;
+    rv = xpc->GetCurrentNativeCallContext(&cc);
+    if (NS_FAILED(rv)) return NS_ERROR_FAILURE;
+
+    JSContext *cx;
+    rv = cc->GetJSContext (&cx);
+    if (NS_FAILED(rv)) return NS_ERROR_FAILURE;
+
+    PRUint32 argc;
+    rv = cc->GetArgc (&argc);
+    if (NS_FAILED(rv)) return NS_ERROR_FAILURE;
+
+    jsval *argv;
+    rv = cc->GetArgvPtr (&argv);
+    if (NS_FAILED(rv)) return NS_ERROR_FAILURE;
+
+    jsval *rval;
+    rv = cc->GetRetValPtr (&rval);
+    if (NS_FAILED(rv)) return NS_ERROR_FAILURE;
+
+    /* set mJSPrincipals if it's not here already */
+    if (!mSystemPrincipal)
+    {
+        nsCOMPtr<nsIScriptSecurityManager> secman =
+            do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID);
+        if (!secman)
+            return rv;
+
+        rv = secman->GetSystemPrincipal(getter_AddRefs(mSystemPrincipal));
+        if (NS_FAILED(rv) || !mSystemPrincipal)
+            return rv;
+    }
+
+    JSAutoRequest ar(cx);
+
+    JSString *url;
+    JSObject *target_obj = nsnull;
+    jschar   *charset = nsnull;
+    ok = JS_ConvertArguments (cx, argc, argv, "S / o W", &url, &target_obj, &charset);
+    if (!ok)
+    {
+        /* let the exception raised by JS_ConvertArguments show through */
+        return NS_OK;
+    }
+
+    JSAutoByteString urlbytes(cx, url);
+    if (!urlbytes)
+    {
+        return NS_OK;
+    }
+
+    if (!target_obj)
+    {
+        /* if the user didn't provide an object to eval onto, find the global
+         * object by walking the parent chain of the calling object */
+
+        nsCOMPtr<nsIXPConnectWrappedNative> wn;
+        rv = cc->GetCalleeWrapper (getter_AddRefs(wn));
+        if (NS_FAILED(rv)) return NS_ERROR_FAILURE;
+
+        rv = wn->GetJSObject (&target_obj);
+        if (NS_FAILED(rv)) return NS_ERROR_FAILURE;
+
+        JSObject *maybe_glob = JS_GetParent (cx, target_obj);
+        while (maybe_glob != nsnull)
+        {
+            target_obj = maybe_glob;
+            maybe_glob = JS_GetParent (cx, maybe_glob);
+        }
+    }
+
+    // Remember an object out of the calling compartment so that we
+    // can properly wrap the result later.
+    nsCOMPtr<nsIPrincipal> principal = mSystemPrincipal;
+    JSObject *result_obj = target_obj;
+    target_obj = JS_FindCompilationScope(cx, target_obj);
+    if (!target_obj)
+        return NS_ERROR_FAILURE;
+
+    if (target_obj != result_obj)
+    {
+        nsCOMPtr<nsIScriptSecurityManager> secman =
+            do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID);
+        if (!secman)
+            return NS_ERROR_FAILURE;
+
+        rv = secman->GetObjectPrincipal(cx, target_obj, getter_AddRefs(principal));
+        NS_ENSURE_SUCCESS(rv, rv);
+    }
+
+    JSAutoEnterCompartment ac;
+    if (!ac.enter(cx, target_obj))
+        return NS_ERROR_UNEXPECTED;
+
+    /* load up the url.  From here on, failures are reflected as ``custom''
+     * js exceptions */
+    nsCOMPtr<nsIURI> uri;
+    nsCAutoString uriStr;
+    nsCAutoString scheme;
+
+    JSStackFrame* frame = nsnull;
+    JSScript* script = nsnull;
+
+    // Figure out who's calling us
+    do
+    {
+        frame = JS_FrameIterator(cx, &frame);
+
+        if (frame)
+            script = JS_GetFrameScript(cx, frame);
+    } while (frame && !script);
+
+    if (!script)
+    {
+        // No script means we don't know who's calling, bail.
+
+        return NS_ERROR_FAILURE;
+    }
+
+    nsCOMPtr<nsIIOService> serv = do_GetService(NS_IOSERVICE_CONTRACTID);
+    if (!serv) {
+        return ReportError(cx, LOAD_ERROR_NOSERVICE);
+    }
+
+    // Make sure to explicitly create the URI, since we'll need the
+    // canonicalized spec.
+    rv = NS_NewURI(getter_AddRefs(uri), urlbytes.ptr(), nsnull, serv);
+    if (NS_FAILED(rv)) {
+        return ReportError(cx, LOAD_ERROR_NOURI);
+    }
+
+    rv = uri->GetSpec(uriStr);
+    if (NS_FAILED(rv)) {
+        return ReportError(cx, LOAD_ERROR_NOSPEC);
+    }
+
+    rv = uri->GetScheme(scheme);
+    if (NS_FAILED(rv)) {
+        return ReportError(cx, LOAD_ERROR_NOSCHEME);
+    }
+
+    bool writeScript = false;
+    JSScriptType *scriptObj = nsnull;
+    JSVersion version = JS_GetVersion(cx);
+
+    nsCAutoString cachePath;
+    cachePath.Append("jssubloader/");
+    cachePath.AppendInt(version);
+    if (charset) {
+        cachePath.Append("/");
+        cachePath.Append(NS_ConvertUTF16toUTF8(
+                    nsDependentString(reinterpret_cast<PRUnichar*>(charset))));
+    }
+
+    if (false)
+        // This is evil. Very evil. Unfortunately, the PathifyURI symbol is
+        // exported, but with an argument type that we don't have access to.
+        PathifyURI(uri, *(nsACString_internal*)&cachePath);
+    else {
+        cachePath.Append("/");
+        cachePath.Append(uriStr);
+    }
+
+    // Suppress caching if we're compiling as content.
+    nsCOMPtr<nsIStartupCache> cache;
+    if (mSystemPrincipal) {
+        cache = do_GetService("@mozilla.org/startupcache/cache;1", &rv);
+        NS_ENSURE_SUCCESS(rv, rv);
+        rv = ReadCachedScript(cache, cachePath, cx, &scriptObj);
+    }
+
+    if (!scriptObj) {
+        rv = ReadScript(uri, cx, target_obj, charset, (char *)uriStr.get(), serv,
+                        principal, &scriptObj);
+        writeScript = true;
+    }
+
+    if (NS_FAILED(rv) || !scriptObj)
+        return rv;
+
+    ok = false;
+    if (scriptObj)
+        ok = JS_ExecuteScriptVersion(cx, target_obj, scriptObj, rval, version);
+
+    if (ok) {
+        JSAutoEnterCompartment rac;
+        if (!rac.enter(cx, result_obj) || !JS_WrapValue(cx, rval))
+            return NS_ERROR_UNEXPECTED;
+    }
+
+    if (cache && ok && writeScript) {
+        WriteCachedScript(cache, cachePath, cx, scriptObj);
+    }
+
+    cc->SetReturnValueWasSet (ok);
+    return NS_OK;
+}
+
index b9e8022805f97598f6781d79fe4696f62c3e1d92..ef118e804fc2be6aed3dd0ee4a5660c2d7e277bd 100644 (file)
@@ -32,15 +32,16 @@ MAKE_JAR      = sh $(BASE)/make_jar.sh
 
 # TODO: specify source files manually?
 JAR_BASES     = $(TOP) $(BASE)
+JAR_FILES     = config.json
 JAR_DIRS      = content skin locale modules
-JAR_TEXTS     = js jsm css dtd xml xul html xhtml xsl properties
+JAR_TEXTS     = js jsm css dtd xml xul html xhtml xsl properties json
 JAR_BINS      = png
 
 CHROME       = $(MANGLE)/
 JAR           = $(CHROME)$(NAME).jar
 
 XPI_BASES     = $(JAR_BASES) $(TOP)/..
-XPI_FILES     = bootstrap.js TODO AUTHORS Donors NEWS LICENSE.txt
+XPI_FILES     = icon.png icon64.png bootstrap.js TODO AUTHORS Donors NEWS LICENSE.txt
 XPI_DIRS      = components $(MANGLE) defaults
 XPI_TEXTS     = js jsm $(JAR_TEXTS)
 XPI_BINS      = $(JAR_BINS)
@@ -142,14 +143,20 @@ install:
                exit 1;                                                         \
        fi;                                                                     \
                                                                                \
-       ext="$$profile/extensions/$(UUID)";                                     \
-       mkdir -p "$$(dirname "$$ext")";                                         \
-       rm -rf "$$ext.xpi" "$$ext";                                             \
-       echo "Installing to $$ext";                                             \
-       if which cygpath >/dev/null 2>&1;                                       \
-       then cygpath -wa .;                                                     \
-       else pwd;                                                               \
-       fi >"$$ext"
+       install() {                                                             \
+               ext="$$profile/extensions/$$2";                                 \
+               mkdir -p "$$(dirname "$$ext")";                                 \
+               rm -rf "$$ext.xpi" "$$ext";                                     \
+                                                                               \
+               echo "Installing $$2 to $$ext";                                 \
+               if which cygpath >/dev/null 2>&1;                               \
+               then cygpath -wa $$1;                                           \
+               else (cd $$1; pwd);                                             \
+               fi >"$$ext";                                                    \
+       };                                                                      \
+       install . $(UUID);                                                      \
+       install ../binary binary@dactyl.googlecode.com;                         \
+
 installxpi: xpi
        $(HOSTAPP) $(XPI)
 
index ccf100761e2f56b331f9dfa664ec72b6098aa6ac..e029387cbea0e7a72404e9f3f9d59ec2793c471d 100755 (executable)
@@ -9,10 +9,7 @@
 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 = {};
@@ -29,24 +26,20 @@ const resourceProto = Services.io.getProtocolHandler("resource")
 const categoryManager = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
 const manager = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
 
-const BOOTSTRAP_JSM = "resource://dactyl/bootstrap.jsm";
-
+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);
@@ -65,6 +58,16 @@ 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);
@@ -77,6 +80,11 @@ function updateVersion() {
 
         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"];
     }
@@ -86,19 +94,24 @@ function updateVersion() {
 }
 
 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())
@@ -109,7 +122,8 @@ function startup(data, reason) {
             };
         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();
@@ -120,6 +134,14 @@ function startup(data, reason) {
     }
 }
 
+/**
+ * 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);
@@ -127,12 +149,12 @@ function FactoryProxy(url, 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);
@@ -145,7 +167,7 @@ FactoryProxy.prototype = {
 }
 
 function init() {
-    dump("dactyl: bootstrap: init\n");
+    debug("bootstrap: init");
 
     let manifestURI = getURI("chrome.manifest");
     let manifest = httpGet(manifestURI.spec)
@@ -185,50 +207,60 @@ function init() {
     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) {
+        // 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 != 5) // Temporary hack
+        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(BOOTSTRAP_JSM, global));
+        }
     }
 
-    if (!JSMLoader || JSMLoader.bump !== 5 || Cu.unload)
+    if (!JSMLoader || JSMLoader.bump !== 6 || Cu.unload)
         Cu.import(BOOTSTRAP_JSM, global);
 
+    JSMLoader.name = name;
     JSMLoader.bootstrap = this;
 
     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;
 
@@ -237,14 +269,19 @@ function init() {
 
     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);
@@ -264,6 +301,18 @@ function shutdown(data, reason) {
     }
 }
 
+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",
@@ -273,7 +322,6 @@ function reasonToString(reason) {
             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:
index 4995b0e8f0ecd1e021ee4b4e06553b03260b3a4d..3eae8e1ca00f20096f248bb9aa3d0d0caeb3dfee 100644 (file)
@@ -1,7 +1,9 @@
+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/
@@ -13,10 +15,3 @@ component {16dc34f7-6d22-4aa4-a67f-2921fb5dcb69} components/commandline-handler.
 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}
-
index 918d7bf300b8980c2d0d50e676f7c29224467b89..0fd118958e77d4b873c26ed3e8da0b65f6152f9e 100644 (file)
@@ -11,37 +11,72 @@ function 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: [{
-        category: "command-line-handler",
-        entry: "m-dactyl"
-    }],
+    _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) {
+        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)
+        };
 
-        // TODO: handle remote launches differently?
         try {
             this.optionValue = commandLine.handleFlagWithParam(config.name, false);
         }
diff --git a/common/components/protocols.js b/common/components/protocols.js
deleted file mode 100644 (file)
index 94eab5d..0000000
+++ /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/config.json b/common/config.json
new file mode 100644 (file)
index 0000000..f9aee18
--- /dev/null
@@ -0,0 +1,38 @@
+{
+    "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"
+    },
+
+    "option-defaults": {
+        "guioptions": "rb"
+    }
+}
index 71e6c93b969391810617c96bdfa3256eaf8d49ee..08294970d684e4e6541513620430f99c9129e4d7 100644 (file)
@@ -4,7 +4,7 @@
 //
 // 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 */
 
@@ -350,7 +350,9 @@ var Abbreviations = Module("abbreviations", {
                             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))
index 433402c89dd6285bc3e53566a312d79490db65ce..aba640b3e4c9b5fb8227be6f6d0b47ec5bdec1ff 100644 (file)
@@ -6,7 +6,7 @@
 <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"/>
index beb8fc52e2a199bd5fefab0ed1fa3f1e2d452679..31c3ad90826bcae04fddd387b0c39182311fb596 100644 (file)
@@ -4,16 +4,16 @@
 //
 // 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));
     }
 });
 
@@ -43,24 +43,26 @@ var AutoCmdHive = Class("AutoCmdHive", Contexts.Hive, {
     },
 
     /**
-     * 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));
+    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 *regexp*.
+     * Deletes all autocommands with a matching *event* and *filter*.
      *
      * @param {string} event The event name filter.
-     * @param {string} regexp The URL pattern filter.
+     * @param {string} filter The URL pattern filter.
      */
-    remove: function (event, regexp) {
-        this._store = this._store.filter(function (autoCmd) !autoCmd.match(event, regexp));
+    remove: function (event, filter) {
+        filter = filter && String(Group.compileFilter(filter));
+        this._store = this._store.filter(function (autoCmd) !autoCmd.match(event, filter));
     },
 });
 
@@ -69,12 +71,6 @@ var AutoCmdHive = Class("AutoCmdHive", Contexts.Hive, {
  */
 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),
@@ -107,6 +103,7 @@ var AutoCommands = Module("autocommands", {
             return cmds;
         }
 
+        XML.prettyPrinting = XML.ignoreWhitespace = false;
         commandline.commandOutput(
             <table>
                 <tr highlight="Title">
@@ -114,15 +111,16 @@ var AutoCommands = Module("autocommands", {
                 </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>) +
@@ -154,9 +152,7 @@ var AutoCommands = Module("autocommands", {
 
         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)) {
@@ -172,11 +168,19 @@ var AutoCommands = Module("autocommands", {
     }
 }, {
 }, {
+    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) {
@@ -191,9 +195,9 @@ var AutoCommands = Module("autocommands", {
 
                 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 == "*")
@@ -202,10 +206,10 @@ var AutoCommands = Module("autocommands", {
                     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,
@@ -282,7 +286,7 @@ var AutoCommands = Module("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"],
index 800230de0d51cb2c7bfcbe10088e0716a81e0c18..587c7a8727e53bed0c2fab41415c838575e4d613 100644 (file)
 
     <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"/>
 
     <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">
index 1ce6ee11a1bd2ff156ade600f6e8230e218dbcdf..ca1bdb001d9b3743cf1d2478b7588d20b60e407a 100644 (file)
@@ -4,9 +4,7 @@
 //
 // 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", {
@@ -38,7 +36,7 @@ var Bookmarks = Module("bookmarks", {
     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]
     }),
 
@@ -63,49 +61,54 @@ var Bookmarks = Module("bookmarks", {
      *      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 {
-            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;
+            var { id, unfiled, title, url, keyword, tags, post, charset, force } = unfiled;
+
+        let uri = util.createURI(url);
+        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);
-            }
-            if (bmark == undefined)
-                bmark = bookmarkcache.bookmarks[
-                    services.bookmarks.insertBookmark(
-                         services.bookmarks[unfiled ? "unfiledBookmarksFolder" : "bookmarksMenuFolder"],
-                         uri, -1, title || url)];
-            if (!bmark)
-                return false;
-
-            if (charset !== undefined)
-                bmark.charset = charset;
-            if (post !== undefined)
-                bmark.post = post;
-            if (keyword)
-                bmark.keyword = keyword;
+        if (tags) {
+            PlacesUtils.tagging.untagURI(uri, null);
+            PlacesUtils.tagging.tagURI(uri, tags);
         }
-        catch (e) {
-            util.reportError(e);
-            return false;
+
+        let updated = !!bmark;
+        if (bmark == undefined)
+            bmark = bookmarkcache.bookmarks[
+                services.bookmarks.insertBookmark(
+                     services.bookmarks[unfiled ? "unfiledBookmarksFolder" : "bookmarksMenuFolder"],
+                     uri, -1, title || url)];
+        else {
+            if (title)
+                bmark.title = title;
+            if (!uri.equals(bmark.uri))
+                bmark.uri = uri;
         }
 
-        return true;
+        util.assert(bmark);
+
+        if (charset !== undefined)
+            bmark.charset = charset;
+        if (post !== undefined)
+            bmark.post = post;
+        if (keyword)
+            bmark.keyword = keyword;
+
+        return updated;
     },
 
     /**
@@ -116,13 +119,13 @@ var Bookmarks = Module("bookmarks", {
      */
     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;
 
@@ -163,6 +166,7 @@ var Bookmarks = Module("bookmarks", {
             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) });
         }
@@ -247,29 +251,49 @@ var Bookmarks = Module("bookmarks", {
     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);
+                results = parser(req);
+            }
+            catch (e) {
+                util.reportError(e);
             }
-            catch (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
@@ -378,14 +402,6 @@ var Bookmarks = Module("bookmarks", {
 }, {
 }, {
     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"],
@@ -432,15 +448,19 @@ var Bookmarks = Module("bookmarks", {
                 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"],
@@ -449,13 +469,13 @@ var Bookmarks = Module("bookmarks", {
                     url: args.length === 0 ? buffer.uri.spec : args[0]
                 };
 
-                if (bookmarks.add(opts)) {
-                    let extra = (opts.title == opts.url) ? "" : " (" + opts.title + ")";
-                    dactyl.echomsg({ domains: [util.getHost(opts.url)], message: _("bookmark.added", opts.url + extra) },
-                                   1, commandline.FORCE_SINGLELINE);
-                }
-                else
-                    dactyl.echoerr(_("bookmark.cantAdd", opts.title.quote()));
+                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." + action, opts.url + extra) },
+                               1, commandline.FORCE_SINGLELINE);
             }, {
                 argCount: "?",
                 bang: true,
@@ -477,6 +497,11 @@ var Bookmarks = Module("bookmarks", {
                         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
                     }
                 ]
             });
@@ -553,6 +578,7 @@ var Bookmarks = Module("bookmarks", {
                 if (bmarks.length == 1) {
                     let bmark = bmarks[0];
 
+                    options["-id"] = bmark.id;
                     options["-title"] = bmark.title;
                     if (bmark.charset)
                         options["-charset"] = bmark.charset;
@@ -627,6 +653,7 @@ var Bookmarks = Module("bookmarks", {
                 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 () {
@@ -663,15 +690,19 @@ var Bookmarks = Module("bookmarks", {
             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;
@@ -689,9 +720,9 @@ var Bookmarks = Module("bookmarks", {
             });
         };
 
-        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);
     }
 });
 
index 5a6ba5ef15a599c84f4a2e44acbfa9fa81e3271e..cee3adbc67cc62cf42db6c15ff89f6929bd3143f 100644 (file)
@@ -4,7 +4,7 @@
 //
 // 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 */
 
@@ -13,8 +13,8 @@
  */
 var Browser = Module("browser", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
     init: function init() {
-        this.cleanupProgressListener = util.overlayObject(window.XULBrowserWindow,
-                                                          this.progressListener);
+        this.cleanupProgressListener = overlay.overlayObject(window.XULBrowserWindow,
+                                                             this.progressListener);
         util.addObserver(this);
     },
 
@@ -148,13 +148,16 @@ var Browser = Module("browser", XPCOM(Ci.nsISupportsWeakReference, ModuleBase),
 
             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
@@ -207,18 +210,39 @@ var Browser = Module("browser", XPCOM(Ci.nsISupportsWeakReference, ModuleBase),
             { 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",
@@ -226,15 +250,15 @@ var Browser = Module("browser", XPCOM(Ci.nsISupportsWeakReference, ModuleBase),
 
         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",
diff --git a/common/content/buffer.js b/common/content/buffer.js
deleted file mode 100644 (file)
index d0c732c..0000000
+++ /dev/null
@@ -1,2049 +0,0 @@
-// 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 */
-
-/**
- * A class to manage the primary web content buffer. The name comes
- * from Vim's term, 'buffer', which signifies instances of open
- * files.
- * @instance buffer
- */
-var Buffer = Module("buffer", {
-    init: function init() {
-        this.evaluateXPath = util.evaluateXPath;
-        this.pageInfo = {};
-
-        this.addPageInfoSection("e", "Search Engines", function (verbose) {
-
-            let n = 1;
-            let nEngines = 0;
-            for (let { document: doc } in values(buffer.allFrames())) {
-                let engines = util.evaluateXPath(["link[@href and @rel='search' and @type='application/opensearchdescription+xml']"], doc);
-                nEngines += engines.snapshotLength;
-
-                if (verbose)
-                    for (let link in engines)
-                        yield [link.title || /*L*/ "Engine " + n++,
-                               <a xmlns={XHTML} href={link.href} onclick="if (event.button == 0) { window.external.AddSearchProvider(this.href); return false; }" highlight="URL">{link.href}</a>];
-            }
-
-            if (!verbose && nEngines)
-                yield nEngines + /*L*/" engine" + (nEngines > 1 ? "s" : "");
-        });
-
-        this.addPageInfoSection("f", "Feeds", function (verbose) {
-            const feedTypes = {
-                "application/rss+xml": "RSS",
-                "application/atom+xml": "Atom",
-                "text/xml": "XML",
-                "application/xml": "XML",
-                "application/rdf+xml": "XML"
-            };
-
-            function isValidFeed(data, principal, isFeed) {
-                if (!data || !principal)
-                    return false;
-
-                if (!isFeed) {
-                    var type = data.type && data.type.toLowerCase();
-                    type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
-
-                    isFeed = ["application/rss+xml", "application/atom+xml"].indexOf(type) >= 0 ||
-                             // really slimy: general XML types with magic letters in the title
-                             type in feedTypes && /\brss\b/i.test(data.title);
-                }
-
-                if (isFeed) {
-                    try {
-                        window.urlSecurityCheck(data.href, principal,
-                                Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
-                    }
-                    catch (e) {
-                        isFeed = false;
-                    }
-                }
-
-                if (type)
-                    data.type = type;
-
-                return isFeed;
-            }
-
-            let nFeed = 0;
-            for (let [i, win] in Iterator(buffer.allFrames())) {
-                let doc = win.document;
-
-                for (let link in util.evaluateXPath(["link[@href and (@rel='feed' or (@rel='alternate' and @type))]"], doc)) {
-                    let rel = link.rel.toLowerCase();
-                    let feed = { title: link.title, href: link.href, type: link.type || "" };
-                    if (isValidFeed(feed, doc.nodePrincipal, rel == "feed")) {
-                        nFeed++;
-                        let type = feedTypes[feed.type] || "RSS";
-                        if (verbose)
-                            yield [feed.title, template.highlightURL(feed.href, true) + <span class="extra-info">&#xa0;({type})</span>];
-                    }
-                }
-
-            }
-
-            if (!verbose && nFeed)
-                yield nFeed + /*L*/" feed" + (nFeed > 1 ? "s" : "");
-        });
-
-        this.addPageInfoSection("g", "General Info", function (verbose) {
-            let doc = buffer.focusedFrame.document;
-
-            // get file size
-            const ACCESS_READ = Ci.nsICache.ACCESS_READ;
-            let cacheKey = doc.documentURI;
-
-            for (let proto in array.iterValues(["HTTP", "FTP"])) {
-                try {
-                    var cacheEntryDescriptor = services.cache.createSession(proto, 0, true)
-                                                       .openCacheEntry(cacheKey, ACCESS_READ, false);
-                    break;
-                }
-                catch (e) {}
-            }
-
-            let pageSize = []; // [0] bytes; [1] kbytes
-            if (cacheEntryDescriptor) {
-                pageSize[0] = util.formatBytes(cacheEntryDescriptor.dataSize, 0, false);
-                pageSize[1] = util.formatBytes(cacheEntryDescriptor.dataSize, 2, true);
-                if (pageSize[1] == pageSize[0])
-                    pageSize.length = 1; // don't output "xx Bytes" twice
-            }
-
-            let lastModVerbose = new Date(doc.lastModified).toLocaleString();
-            let lastMod = new Date(doc.lastModified).toLocaleFormat("%x %X");
-
-            if (lastModVerbose == "Invalid Date" || new Date(doc.lastModified).getFullYear() == 1970)
-                lastModVerbose = lastMod = null;
-
-            if (!verbose) {
-                if (pageSize[0])
-                    yield (pageSize[1] || pageSize[0]) + /*L*/" bytes";
-                yield lastMod;
-                return;
-            }
-
-            yield ["Title", doc.title];
-            yield ["URL", template.highlightURL(doc.location.href, true)];
-
-            let ref = "referrer" in doc && doc.referrer;
-            if (ref)
-                yield ["Referrer", template.highlightURL(ref, true)];
-
-            if (pageSize[0])
-                yield ["File Size", pageSize[1] ? pageSize[1] + " (" + pageSize[0] + ")"
-                                                : pageSize[0]];
-
-            yield ["Mime-Type", doc.contentType];
-            yield ["Encoding", doc.characterSet];
-            yield ["Compatibility", doc.compatMode == "BackCompat" ? "Quirks Mode" : "Full/Almost Standards Mode"];
-            if (lastModVerbose)
-                yield ["Last Modified", lastModVerbose];
-        });
-
-        this.addPageInfoSection("m", "Meta Tags", function (verbose) {
-            if (!verbose)
-                return [];
-
-            // get meta tag data, sort and put into pageMeta[]
-            let metaNodes = buffer.focusedFrame.document.getElementsByTagName("meta");
-
-            return Array.map(metaNodes, function (node) [(node.name || node.httpEquiv), template.highlightURL(node.content)])
-                        .sort(function (a, b) util.compareIgnoreCase(a[0], b[0]));
-        });
-
-        let identity = window.gIdentityHandler;
-        this.addPageInfoSection("s", "Security", function (verbose) {
-            if (!verbose || !identity)
-                return; // For now
-
-            // Modified from Firefox
-            function location(data) array.compact([
-                data.city, data.state, data.country
-            ]).join(", ");
-
-            switch (statusline.security) {
-            case "secure":
-            case "extended":
-                var data = identity.getIdentityData();
-
-                yield ["Host", identity.getEffectiveHost()];
-
-                if (statusline.security === "extended")
-                    yield ["Owner", data.subjectOrg];
-                else
-                    yield ["Owner", _("pageinfo.s.ownerUnverified", data.subjectOrg)];
-
-                if (location(data).length)
-                    yield ["Location", location(data)];
-
-                yield ["Verified by", data.caOrg];
-
-                if (identity._overrideService.hasMatchingOverride(identity._lastLocation.hostname,
-                                                              (identity._lastLocation.port || 443),
-                                                              data.cert, {}, {}))
-                    yield ["User exception", /*L*/"true"];
-                break;
-            }
-        });
-
-        dactyl.commands["buffer.viewSource"] = function (event) {
-            let elem = event.originalTarget;
-            let obj = { url: elem.getAttribute("href"), line: Number(elem.getAttribute("line")) };
-            if (elem.hasAttribute("column"))
-                obj.column = elem.getAttribute("column");
-
-            buffer.viewSource(obj);
-        };
-    },
-
-    // called when the active document is scrolled
-    _updateBufferPosition: function _updateBufferPosition() {
-        statusline.updateBufferPosition();
-        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);
-
-        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();
-        dactyl.assert(url instanceof Ci.nsIURL);
-
-        while (count-- && url.path != "/")
-            url.path = url.path.replace(/[^\/]+\/*$/, "");
-
-        dactyl.assert(!url.equals(buffer.documentURI));
-        dactyl.open(url.spec);
-    },
-
-    incrementURL: function incrementURL(count) {
-        let matches = buffer.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;
-    },
-
-    /**
-     * @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); },
-
-    /**
-     * @property {nsIURI} The current top-level document.
-     */
-    get doc() window.content.document,
-
-    /**
-     * @property {nsIURI} The current top-level document's URI.
-     */
-    get uri() util.newURI(content.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),
-
-    /**
-     * @property {string} The current top-level document's URL.
-     */
-    get URL() update(new String(content.location.href), util.newURI(content.location.href)),
-
-    /**
-     * @property {number} The buffer's height in pixels.
-     */
-    get pageHeight() content.innerHeight,
-
-    /**
-     * @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,
-    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,
-    set fullZoom(value) { this.setZoom(this.zoomLevel, value); },
-
-    /**
-     * @property {string} The current document's title.
-     */
-    get title() content.document.title,
-
-    /**
-     * @property {number} The buffer's horizontal scroll percentile.
-     */
-    get scrollXPercent() {
-        let elem = 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);
-        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);
-    },
-
-    /**
-     * 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)
-                frames.push(frame);
-            Array.forEach(frame.frames, rec);
-        })(win || content);
-        if (focusedFirst)
-            return frames.filter(function (f) f === buffer.focusedFrame).concat(
-                    frames.filter(function (f) f !== buffer.focusedFrame));
-        return frames;
-    },
-
-    /**
-     * @property {Window} Returns the currently focused frame.
-     */
-    get focusedFrame() {
-        let frame = this.localStore.focusedFrame;
-        return frame && frame.get() || content;
-    },
-    set focusedFrame(frame) {
-        this.localStore.focusedFrame = Cu.getWeakReference(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}
-     */
-    get currentWord() Buffer.currentWord(this.focusedFrame),
-    getCurrentWord: deprecated("buffer.currentWord", function getCurrentWord() Buffer.currentWord(this.focusedFrame, true)),
-
-    /**
-     * 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))
-            return true;
-
-        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;
-        case "moderate":
-            return doc.dactylFocusAllowed || elem.frameElement && elem.frameElement.ownerDocument.dactylFocusAllowed;
-        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]))
-            elem = elem.contentWindow;
-        if (elem.document)
-            elem.document.dactylFocusAllowed = true;
-
-        if (elem instanceof HTMLInputElement && elem.type == "file") {
-            Buffer.openUploadPrompt(elem);
-            this.lastInputField = elem;
-        }
-        else {
-            if (isinstance(elem, [HTMLInputElement, XULTextBoxElement]))
-                var flags = services.focus.FLAG_BYMOUSE;
-            else
-                flags = services.focus.FLAG_SHOWRING;
-            dactyl.focus(elem, flags);
-
-            if (elem instanceof Window) {
-                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);
-                let sel = (elem.ownerDocument || elem).defaultView.getSelection();
-                if (!sel.rangeCount || !RangeFind.intersects(range, sel.getRangeAt(0))) {
-                    range.collapse(true);
-                    sel.removeAllRanges();
-                    sel.addRange(range);
-                }
-            }
-
-            // for imagemap
-            if (elem instanceof HTMLAreaElement) {
-                try {
-                    let [x, y] = elem.getAttribute("coords").split(",").map(parseFloat);
-
-                    events.dispatch(elem, events.create(elem.ownerDocument, "mouseover", { screenX: x, screenY: y }));
-                }
-                catch (e) {}
-            }
-        }
-    },
-
-    /**
-     * Find the *count*th last link on a page matching one of the given
-     * regular expressions, or with a @rel or @rev attribute matching
-     * the given relation. Each frame is searched beginning with the
-     * last link and progressing to the first, once checking for
-     * matching @rel or @rev properties, and then once for each given
-     * regular expression. The first match is returned. All frames of
-     * the page are searched, beginning with the currently focused.
-     *
-     * 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 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];
-            }
-
-            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;
-                }
-            }
-        }
-
-        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();
-    },
-
-    /**
-     * 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 doc = elem.ownerDocument;
-        let win = doc.defaultView;
-        let { left: offsetX, top: offsetY } = elem.getBoundingClientRect();
-
-        if (isinstance(elem, [HTMLFrameElement, HTMLIFrameElement]))
-            return this.focusElement(elem);
-        if (isinstance(elem, HTMLLinkElement))
-            return dactyl.open(elem.href, where);
-
-        if (elem instanceof HTMLAreaElement) { // for imagemap
-            let coords = elem.getAttribute("coords").split(",");
-            offsetX = Number(coords[0]) + 1;
-            offsetY = Number(coords[1]) + 1;
-        }
-        else if (elem instanceof HTMLInputElement && elem.type == "file") {
-            Buffer.openUploadPrompt(elem);
-            return;
-        }
-
-        let ctrlKey = false, shiftKey = false;
-        switch (where) {
-        case dactyl.NEW_TAB:
-        case dactyl.NEW_BACKGROUND_TAB:
-            ctrlKey = true;
-            shiftKey = (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, {
-                    screenX: offsetX, screenY: offsetY,
-                    ctrlKey: ctrlKey, shiftKey: shiftKey, metaKey: ctrlKey
-                }));
-            });
-            let sel = util.selectionController(win);
-            sel.getSelection(sel.SELECTION_FOCUS_REGION).collapseToStart();
-        });
-    },
-
-    /**
-     * @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");
-    },
-
-    /**
-     * Saves a page link to disk.
-     *
-     * @param {HTMLAnchorElement} elem The page link to save.
-     */
-    saveLink: function saveLink(elem) {
-        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);
-
-            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);
-                },
-
-                completer: function (context) completion.savePage(context, elem)
-            }).open();
-        }
-        catch (e) {
-            dactyl.echoerr(e);
-        }
-    },
-
-    /**
-     * Saves the contents of a URI to disk.
-     *
-     * @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 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);
-
-                return onStateChange.superapply(this, arguments);
-            })
-        });
-
-        persist.saveURI(uri, null, null, null, null, file);
-    },
-
-    /**
-     * Scrolls the currently active element horizontally. See
-     * {@link Buffer.scrollHorizontal} for parameters.
-     */
-    scrollHorizontal: function scrollHorizontal(increment, number)
-        Buffer.scrollHorizontal(this.findScrollable(number, true), increment, number),
-
-    /**
-     * Scrolls the currently active element vertically. See
-     * {@link Buffer.scrollVertical} for parameters.
-     */
-    scrollVertical: function scrollVertical(increment, number)
-        Buffer.scrollVertical(this.findScrollable(number, false), increment, number),
-
-    /**
-     * 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),
-
-    _scrollByScrollSize: function _scrollByScrollSize(count, direction) {
-        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) {
-        direction = direction ? 1 : -1;
-        count = count || 1;
-
-        if (options["scroll"] > 0)
-            this.scrollVertical("lines", options["scroll"] * direction);
-        else
-            this.scrollVertical("pages", direction / 2);
-    },
-
-    /**
-     * Find the best candidate scrollable element for the given
-     * direction and orientation.
-     *
-     * @param {number} dir The direction in which the element must be
-     *   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)
-                elem = elem.parentNode;
-            for (; elem && elem.parentNode instanceof Element; 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;
-        }
-        catch (e) {}
-
-        try {
-            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)) {
-            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);
-    },
-
-    /**
-     * Find the best candidate scrollable frame in the current buffer.
-     */
-    findScrollableWindow: function findScrollableWindow() {
-        win = window.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;
-        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;
-    },
-
-    /**
-     * 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) {
-        const FUDGE = 10;
-
-        let path = options["jumptags"][arg];
-        dactyl.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])
-
-        let idx = Math.min((count || 1) - 1, elems.length);
-        dactyl.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));
-    },
-
-    // TODO: allow callback for filtering out unwanted frames? User defined?
-    /**
-     * Shifts the focus to another frame within the buffer. Each buffer
-     * contains at least one frame.
-     *
-     * @param {number} count The number of frames to skip through. A negative
-     *     count skips backwards.
-     */
-    shiftFrameFocus: function shiftFrameFocus(count) {
-        if (!(content.document instanceof HTMLDocument))
-            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))
-                       .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();
-        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);
-
-        // 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);
-
-        // 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);
-    },
-
-    /**
-     * 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) {
-        // 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, ", "),
-                ", ");
-
-            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));
-        }, <br/>);
-        dactyl.echo(list, commandline.FORCE_MULTILINE);
-    },
-
-    /**
-     * Stops loading and animations in the current content.
-     */
-    stop: function stop() {
-        if (config.stop)
-            config.stop();
-        else
-            config.browser.mCurrentBrowser.stop();
-    },
-
-    /**
-     * 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 win = document.commandDispatcher.focusedWindow;
-        if (win == window)
-            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");
-    },
-
-    /**
-     * Opens a viewer to inspect the source of the current buffer or the
-     * specified *url*. Either the default viewer or the configured external
-     * editor is used.
-     *
-     * @param {string|object|null} loc If a string, the URL of the source,
-     *      otherwise an object with some or all of the following properties:
-     *
-     *          url: The URL to view.
-     *          doc: The document to view.
-     *          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 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",
-                                  loc.url, null, null, loc.line);
-        }
-        else {
-            if (useExternalEditor)
-                this.viewSourceExternally(loc || doc);
-            else {
-                let url = loc || doc.location.href;
-                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);
-                else
-                    dactyl.open(url, { hide: true });
-            }
-        }
-    },
-
-    /**
-     * Launches an editor to view the source of the given document. The
-     * contents of the document are saved to a temporary local file and
-     * removed when the editor returns. This function returns
-     * immediately.
-     *
-     * @param {Document} doc The document to view.
-     * @param {function|object} callback If a function, the callback to be
-     *      called with two arguments: the nsIFile of the file, and temp, a
-     *      boolean which is true if the file is temporary. Otherwise, an object
-     *      with line and column properties used to determine where to open the
-     *      source.
-     *      @optional
-     */
-    viewSourceExternally: Class("viewSourceExternally",
-        XPCOM([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]), {
-        init: function init(doc, callback) {
-            this.callback = callable(callback) ? callback :
-                function (file, temp) {
-                    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);
-
-            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);
-
-            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;
-                persist.progressListener = this;
-                persist.saveURI(uri, null, null, null, null, this.file);
-            }
-            return null;
-        },
-
-        onStateChange: function onStateChange(progress, request, flags, status) {
-            if ((flags & this.STATE_STOP) && status == 0) {
-                try {
-                    var ok = this.callback(this.file, true);
-                }
-                finally {
-                    if (ok !== true)
-                        this.file.remove(false);
-                }
-            }
-            return 0;
-        }
-    }),
-
-    /**
-     * Increases the zoom level of the current buffer.
-     *
-     * @param {number} steps The number of zoom levels to jump.
-     * @param {boolean} fullZoom Whether to use full zoom or text zoom.
-     */
-    zoomIn: function zoomIn(steps, fullZoom) {
-        this.bumpZoomLevel(steps, fullZoom);
-    },
-
-    /**
-     * Decreases the zoom level of the current buffer.
-     *
-     * @param {number} steps The number of zoom levels to jump.
-     * @param {boolean} fullZoom Whether to use full zoom or text zoom.
-     */
-    zoomOut: function zoomOut(steps, fullZoom) {
-        this.bumpZoomLevel(-steps, fullZoom);
-    },
-
-    /**
-     * Adjusts the page zoom of the current buffer to the given absolute
-     * value.
-     *
-     * @param {number} value The new zoom value as a possibly fractional
-     *   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)
-            ZoomManager.useFullZoom = fullZoom;
-        try {
-            ZoomManager.zoom = value / 100;
-        }
-        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);
-    },
-
-    /**
-     * 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) {
-        if (fullZoom === undefined)
-            fullZoom = ZoomManager.useFullZoom;
-
-        let values = ZoomManager.zoomValues;
-        let cur = values.indexOf(ZoomManager.snap(ZoomManager.zoom));
-        let i = Math.constrain(cur + steps, 0, values.length - 1);
-
-        dactyl.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)
-}, {
-    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)),
-
-    /**
-     * 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 selection = win.getSelection();
-        if (selection.rangeCount == 0)
-            return "";
-
-        let range = selection.getRangeAt(0).cloneRange();
-        if (range.collapsed && range.startContainer instanceof Text) {
-            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);
-    },
-
-    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 type = node.contentType || node.QueryInterface(Ci.nsIImageLoadingContent)
-                                               .getRequest(0).mimeType;
-
-            if (type === "text/plain")
-                var 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"]);
-
-        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"]);
-
-        return names.filter(function ([leaf, title]) leaf)
-                    .map(function ([leaf, title]) [leaf.replace(util.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)),
-
-    isScrollable: function isScrollable(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 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("'");
-        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();
-    },
-
-    /**
-     * Scrolls the currently given element horizontally.
-     *
-     * @param {Element} elem The element to scroll.
-     * @param {string} increment 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")
-            increment = fontSize; // Good enough, I suppose.
-        else if (increment == "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.
-     *
-     * @param {Element} elem The element to scroll.
-     * @param {string} increment 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")
-            increment = fontSize;
-        else if (increment == "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);
-    },
-
-    /**
-     * 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,
-                        horizontal == null ? null
-                                           : (elem.scrollWidth - elem.clientWidth) * (horizontal / 100),
-                        vertical   == null ? null
-                                           : (elem.scrollHeight - elem.clientHeight) * (vertical / 100));
-    },
-
-    openUploadPrompt: function openUploadPrompt(elem) {
-        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", {}));
-            }
-        }).open(elem.value);
-    }
-}, {
-    commands: function initCommands(dactyl, modules, window) {
-        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,
-                              _("error.trailingCharacters"));
-
-                const PRINTER = "PostScript/default";
-                const BRANCH  = "print.printer_" + PRINTER + ".";
-
-                prefs.withContext(function () {
-                    if (arg) {
-                        prefs.set("print.print_printer", PRINTER);
-
-                        prefs.set(   "print.print_to_file", true);
-                        prefs.set(BRANCH + "print_to_file", true);
-
-                        prefs.set(   "print.print_to_filename", io.File(arg.substr(1)).path);
-                        prefs.set(BRANCH + "print_to_filename", io.File(arg.substr(1)).path);
-
-                        dactyl.echomsg(_("print.toFile", arg.substr(1)));
-                    }
-                    else
-                        dactyl.echomsg(_("print.sending"));
-
-                    prefs.set("print.always_print_silent", args.bang);
-                    prefs.set("print.show_print_progress", !args.bang);
-
-                    config.browser.contentWindow.print();
-                });
-
-                if (arg)
-                    dactyl.echomsg(_("print.printed", arg.substr(1)));
-                else
-                    dactyl.echomsg(_("print.sent"));
-            },
-            {
-                argCount: "?",
-                bang: true,
-                literal: 0
-            });
-
-        commands.add(["pa[geinfo]"],
-            "Show various page information",
-            function (args) {
-                let arg = args[0];
-                let opt = options.get("pageinfo");
-
-                dactyl.assert(!arg || opt.validator(opt.parse(arg)),
-                              _("error.invalidArgument", arg));
-                buffer.showPageInfo(true, arg);
-            },
-            {
-                argCount: "?",
-                completer: function (context) {
-                    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] || "";
-
-                let titles = buffer.alternateStyleSheets.map(function (stylesheet) stylesheet.title);
-
-                dactyl.assert(!arg || titles.indexOf(arg) >= 0,
-                              _("error.invalidArgument", arg));
-
-                if (options["usermode"])
-                    options["usermode"] = false;
-
-                window.stylesheetSwitchAll(buffer.focusedFrame, arg);
-            },
-            {
-                argCount: "?",
-                completer: function (context) completion.alternateStyleSheet(context),
-                literal: 0
-            });
-
-        commands.add(["re[load]"],
-            "Reload the current web page",
-            function (args) { 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 chosenData = null;
-                let filename = args[0];
-
-                let command = commandline.command;
-                if (filename) {
-                    if (filename[0] == "!")
-                        return buffer.viewSourceExternally(buffer.focusedFrame.document,
-                            function (file) {
-                                let output = io.system(filename.substr(1), file);
-                                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 + "*$"), ""));
-
-                    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)
-                                                    .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) {
-                    if (context.filter[0] == "!")
-                        return;
-                    if (/^>>/.test(context.filter))
-                        context.advance(/^>>\s*/.exec(context.filter)[0].length);
-
-                    completion.savePage(context, content.document);
-                    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")
-            });
-
-        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)) {
-                    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) {
-        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);
-            });
-        };
-    },
-    events: function initEvents(dactyl, modules, window) {
-        events.listen(config.browser, "scroll", buffer.closure._updateBufferPosition, false);
-    },
-    mappings: function initMappings(dactyl, modules, window) {
-        mappings.add([modes.NORMAL],
-            ["y", "<yank-location>"], "Yank current location to the clipboard",
-            function () { dactyl.clipboardWrite(buffer.uri.spec, 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",
-            function (args) { buffer.incrementURL(-Math.max(args.count, 1)); },
-            { count: true });
-
-        mappings.add([modes.NORMAL], ["gu", "<open-parent-path>"],
-            "Go to parent directory",
-            function (args) { buffer.climbUrlPath(Math.max(args.count, 1)); },
-            { count: true });
-
-        mappings.add([modes.NORMAL], ["gU", "<open-root-path>"],
-            "Go to the root of the website",
-            function () { buffer.climbUrlPath(-1); });
-
-        mappings.add([modes.COMMAND], [".", "<repeat-key>"],
-            "Repeat the last key event",
-            function (args) {
-                if (mappings.repeat) {
-                    for (let i in util.interruptibleRange(0, Math.max(args.count, 1), 100))
-                        mappings.repeat();
-                }
-            },
-            { count: true });
-
-        mappings.add([modes.NORMAL], ["i", "<Insert>"],
-            "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>"],
-            "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>"],
-            "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>"],
-            "Scroll document to the right",
-            function (args) { buffer.scrollHorizontal("columns", Math.max(args.count, 1)); },
-            { count: true });
-
-        mappings.add([modes.COMMAND], ["0", "^", "<scroll-begin>"],
-            "Scroll to the absolute left of the document",
-            function () { buffer.scrollToPercent(0, null); });
-
-        mappings.add([modes.COMMAND], ["$", "<scroll-end>"],
-            "Scroll to the absolute right of the document",
-            function () { buffer.scrollToPercent(100, null); });
-
-        mappings.add([modes.COMMAND], ["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>"],
-            "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>"],
-            "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>"],
-            "Scroll window downwards in the buffer",
-            function (args) { buffer._scrollByScrollSize(args.count, true); },
-            { count: true });
-
-        mappings.add([modes.COMMAND], ["<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>"],
-            "Scroll up a full page",
-            function (args) { buffer.scrollVertical("pages", -Math.max(args.count, 1)); },
-            { count: true });
-
-        mappings.add([modes.COMMAND], ["<Space>"],
-            "Scroll down a full page",
-            function (args) {
-                if (isinstance(content.document.activeElement, [HTMLInputElement, HTMLButtonElement]))
-                    return Events.PASS;
-                buffer.scrollVertical("pages", Math.max(args.count, 1));
-            },
-            { count: true });
-
-        mappings.add([modes.COMMAND], ["<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 });
-
-        mappings.add([modes.NORMAL], ["[f", "<next-frame>"],
-            "Focus previous frame",
-            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], ["]"],
-            "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); },
-            { count: true });
-
-        mappings.add([modes.NORMAL], ["}"],
-            "Jump to the next paragraph",
-            function (args) { buffer.findJump("p", args.count, false); },
-            { count: true });
-
-        mappings.add([modes.NORMAL], ["]]", "<next-page>"],
-            "Follow the link labeled 'next' or '>' if it exists",
-            function (args) {
-                buffer.findLink("next", options["nextpattern"], (args.count || 1) - 1, true);
-            },
-            { count: true });
-
-        mappings.add([modes.NORMAL], ["[[", "<previous-page>"],
-            "Follow the link labeled 'prev', 'previous' or '<' if it exists",
-            function (args) {
-                buffer.findLink("previous", options["previouspattern"], (args.count || 1) - 1, true);
-            },
-            { count: true });
-
-        mappings.add([modes.NORMAL], ["gf", "<view-source>"],
-            "Toggle between rendered and source view",
-            function () { buffer.viewSource(null, false); });
-
-        mappings.add([modes.NORMAL], ["gF", "<view-source-externally>"],
-            "View source with an external editor",
-            function () { buffer.viewSource(null, true); });
-
-        mappings.add([modes.NORMAL], ["gi", "<focus-input>"],
-            "Focus last used input field",
-            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))]))
-                                        .filter(function (elem) {
-                        if (isinstance(elem, [HTMLFrameElement, HTMLIFrameElement]))
-                            return Editor.getEditor(elem.contentWindow);
-
-                        if (elem.readOnly || elem instanceof HTMLInputElement && !Set.has(util.editableInputs, elem.type))
-                            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") &&
-                            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 });
-
-        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))
-                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 });
-            });
-
-        mappings.add([modes.NORMAL], ["p", "<MiddleMouse>", "<open-clipboard-url>"],
-            "Open (put) a URL based on the current clipboard contents in the current buffer",
-            function () {
-                dactyl.open(url());
-            });
-
-        mappings.add([modes.NORMAL], ["P", "<tab-open-clipboard-url>"],
-            "Open (put) a URL based on the current clipboard contents in a new buffer",
-            function () {
-                dactyl.open(url(), { from: "paste", where: dactyl.NEW_TAB });
-            });
-
-        // reloading
-        mappings.add([modes.NORMAL], ["r", "<reload>"],
-            "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>"],
-            "Copy selected text or current word",
-            function () {
-                let sel = buffer.currentWord;
-                dactyl.assert(sel);
-                dactyl.clipboardWrite(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>"],
-            "Enlarge text zoom of current web page by a larger amount",
-            function (args) { buffer.zoomIn(Math.max(args.count, 1) * 3, false); },
-            { count: true });
-
-        mappings.add([modes.NORMAL], ["zo", "-", "<text-zoom-out>"],
-            "Reduce text zoom of current web page",
-            function (args) { buffer.zoomOut(Math.max(args.count, 1), false); },
-            { count: true });
-
-        mappings.add([modes.NORMAL], ["zr", "<text-zoom-reduce>"],
-            "Reduce text zoom of current web page by a larger amount",
-            function (args) { buffer.zoomOut(Math.max(args.count, 1) * 3, false); },
-            { count: true });
-
-        mappings.add([modes.NORMAL], ["zz", "<text-zoom>"],
-            "Set text zoom value of current web page",
-            function (args) { buffer.setZoom(args.count > 1 ? args.count : 100, false); },
-            { count: true });
-
-        mappings.add([modes.NORMAL], ["ZI", "zI", "<full-zoom-in>"],
-            "Enlarge full zoom of current web page",
-            function (args) { buffer.zoomIn(Math.max(args.count, 1), true); },
-            { count: true });
-
-        mappings.add([modes.NORMAL], ["ZM", "zM", "<full-zoom-more>"],
-            "Enlarge full zoom of current web page by a larger amount",
-            function (args) { buffer.zoomIn(Math.max(args.count, 1) * 3, true); },
-            { count: true });
-
-        mappings.add([modes.NORMAL], ["ZO", "zO", "<full-zoom-out>"],
-            "Reduce full zoom of current web page",
-            function (args) { buffer.zoomOut(Math.max(args.count, 1), true); },
-            { count: true });
-
-        mappings.add([modes.NORMAL], ["ZR", "zR", "<full-zoom-reduce>"],
-            "Reduce full zoom of current web page by a larger amount",
-            function (args) { buffer.zoomOut(Math.max(args.count, 1) * 3, true); },
-            { count: true });
-
-        mappings.add([modes.NORMAL], ["zZ", "<full-zoom>"],
-            "Set full zoom value of current web page",
-            function (args) { buffer.setZoom(args.count > 1 ? args.count : 100, true); },
-            { count: true });
-
-        // page info
-        mappings.add([modes.NORMAL], ["<C-g>", "<page-info>"],
-            "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) {
-        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,
-                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);
-                    }
-                    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",
-            "string", '[^\\s.,!?:;/"\'^$%&?()[\\]{}<>#*+|=~_-]',
-            {
-                setter: function (value) {
-                    this.regexp = util.regexp(value);
-                    return value;
-                },
-                validator: function (value) RegExp(value)
-            });
-
-        options.add(["jumptags", "jt"],
-            "XPath or CSS selector strings of jumpable elements for extended hint modes",
-            "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)) });
-                    return vals;
-                },
-                validator: function (value) util.validateMatcher.call(this, value)
-                    && Object.keys(value).every(function (v) v.length == 1)
-            });
-
-        options.add(["nextpattern"],
-            "Patterns to use when guessing the next page in a document sequence",
-            "regexplist", UTF8("'\\bnext\\b',^>$,^(>>|»)$,^(>|»),(>|»)$,'\\bmore\\b'"),
-            { regexpFlags: "i" });
-
-        options.add(["previouspattern"],
-            "Patterns to use when guessing the previous page in a document sequence",
-            "regexplist", UTF8("'\\bprev|previous\\b',^<$,^(<<|«)$,^(<|«),(<|«)$"),
-            { regexpFlags: "i" });
-
-        options.add(["pageinfo", "pa"],
-            "Define which sections are shown by the :pageinfo command",
-            "charlist", "gesfm",
-            { 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",
-            "string", "status",
-            {
-                values: {
-                    "": "Don't show link destinations",
-                    "status": "Show link destinations in the status line",
-                    "command": "Show link destinations in the command line"
-                }
-            });
-
-        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:
index cae99423a67e83c50f1051ea9312bacb30305850..895ae4db847ac04ebc4fface42c38b8e6c44209d 100644 (file)
@@ -4,7 +4,7 @@
 //
 // 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 */
 
@@ -15,12 +15,12 @@ var CommandWidgets = Class("CommandWidgets", {
         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"
@@ -153,6 +153,8 @@ var CommandWidgets = Class("CommandWidgets", {
             }
         });
         this.updateVisibility();
+
+        this.initialized = true;
     },
     addElement: function addElement(obj) {
         const self = this;
@@ -243,19 +245,22 @@ var CommandWidgets = Class("CommandWidgets", {
         // 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);
+
+        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" })),
+    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") &&
@@ -272,27 +277,25 @@ var CommandWidgets = Class("CommandWidgets", {
         yield elem;
     },
 
-    completionContainer: Class.memoize(function () this.completionList.parentNode),
+    completionContainer: Class.Memoize(function () this.completionList.parentNode),
 
-    contextMenu: Class.memoize(function () {
+    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")),
+    multilineInput: Class.Memoize(function () document.getElementById("dactyl-multiline-input")),
 
-    mowContainer: Class.memoize(function () document.getElementById("dactyl-multiline-output-container"))
+    mowContainer: Class.Memoize(function () document.getElementById("dactyl-multiline-output-container"))
 }, {
     getEditor: function getEditor(elem) {
         elem.inputField.QueryInterface(Ci.nsIDOMNSEditableElement);
@@ -310,12 +313,18 @@ var CommandMode = Class("CommandMode", {
     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);
@@ -324,6 +333,8 @@ var CommandMode = Class("CommandMode", {
         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);
@@ -357,19 +368,23 @@ var CommandMode = Class("CommandMode", {
             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();
-                this[this.accepted ? "onSubmit" : "onCancel"](commandline.command);
+                if (!waiting)
+                    this[this.accepted ? "onSubmit" : "onCancel"](commandline.command);
                 if (commandline.messageCount === this.messageCount)
                     commandline.clearMessage();
             }, this);
@@ -386,7 +401,7 @@ var CommandMode = Class("CommandMode", {
             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();
         }
@@ -410,11 +425,8 @@ var CommandMode = Class("CommandMode", {
     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();
     },
@@ -429,7 +441,12 @@ var CommandExMode = Class("CommandExMode", CommandMode, {
     prompt: ["Normal", ":"],
 
     complete: function CEM_complete(context) {
-        context.fork("ex", 0, completion, "ex");
+        try {
+            context.fork("ex", 0, completion, "ex");
+        }
+        catch (e) {
+            context.message = _("error.error", e);
+        }
     },
 
     onSubmit: function CEM_onSubmit(command) {
@@ -577,7 +594,7 @@ var CommandLine = Module("commandline", {
         }, this);
     },
 
-    widgets: Class.memoize(function () CommandWidgets()),
+    widgets: Class.Memoize(function () CommandWidgets()),
 
     runSilently: function runSilently(func, self) {
         this.withSavedValues(["silent"], function () {
@@ -595,7 +612,9 @@ var CommandLine = Module("commandline", {
             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;
     },
@@ -892,7 +911,7 @@ var CommandLine = Module("commandline", {
         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");
     }
 }, {
@@ -928,13 +947,10 @@ var CommandLine = Module("commandline", {
             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
@@ -952,11 +968,15 @@ var CommandLine = Module("commandline", {
          * @param {string} val The new value.
          */
         replace: function replace(val) {
-            this.input.dactylKeyPress = undefined;
-            if (this.completions)
-                this.completions.previewClear();
-            this.input.value = val;
-            this.session.onHistory(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);
         },
 
         /**
@@ -1016,17 +1036,29 @@ var CommandLine = Module("commandline", {
      * @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);
 
@@ -1038,60 +1070,113 @@ var CommandLine = Module("commandline", {
                     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,
+        /**
+         * @private
+         */
+        onTab: function onTab(event) {
+            this.tabCount += event.shiftKey ? -1 : 1;
+            this.tabTimer.tell(event);
+        },
 
-        lastSubstring: "",
+        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);
+            let offset = this.selected ? this.selected[0].offset : this.start;
+            return commandline.command.slice(offset, this.caret);
         },
-        set completion(completion) {
-            this.previewClear();
 
-            // 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;
+        /**
+         * 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.
+                commandline.widgets.active.command.value = input;
+                this.editor.selection.focusNode.textContent = input;
 
-            // Reset the caret to one position after the completion.
-            this.caret = this.prefix.length + completion.length;
-            this._caret = this.caret;
+                this.caret = caret;
+                this._caret = this.caret;
 
-            this.input.dactylKeyPress = undefined;
+                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,
@@ -1102,31 +1187,194 @@ var CommandLine = Module("commandline", {
 
         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) {
@@ -1135,14 +1383,14 @@ var CommandLine = Module("commandline", {
                 }
                 // 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;
@@ -1150,21 +1398,26 @@ var CommandLine = Module("commandline", {
             let value = this.completion;
             if (util.compareIgnoreCase(value, substring.substr(0, value.length)))
                 return;
+
             substring = substring.substr(value.length);
             this.removeSubstring = substring;
 
-            let node = util.xmlToDom(<span highlight="Preview">{substring}</span>,
-                document);
-            let start = this.caret;
-            this.editor.insertNode(node, this.editor.rootElement, 1);
-            this.caret = start;
+            let node = DOM.fromXML(<span highlight="Preview">{substring}</span>,
+                                   document);
+
+            this.withSavedValues(["caret"], function () {
+                this.editor.insertNode(node, this.editor.rootElement, 1);
+            });
         },
 
+        /**
+         * 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 = "";
@@ -1179,103 +1432,92 @@ var CommandLine = Module("commandline", {
             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();
-        },
+        /**
+         * 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;
 
-        _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);
+            switch (idx) {
+            case this.UP:
+            case this.DOWN:
+                idx = this.nextItem(this.waiting || this.selected,
+                                    idx == this.UP ? -count : count,
+                                    true);
+                break;
 
-            this.itemList.reset();
-            this.itemList.selectItem(this.selected);
+            case this.CTXT_UP:
+            case this.CTXT_DOWN:
+                let groups = this.itemList.activeGroups;
+                let i = Math.max(0, groups.indexOf(this.itemList.selectedGroup));
 
-            this.preview();
-        },
+                i += idx == this.CTXT_DOWN ? 1 : -1;
+                i %= groups.length;
+                if (i < 0)
+                    i += groups.length;
 
-        select: function select(idx) {
-            switch (idx) {
-            case this.UP:
-                if (this.selected == null)
-                    idx = -2;
-                else
-                    idx = this.selected - 1;
+                var position = 0;
+                idx = [groups[i].context, 0];
                 break;
-            case this.DOWN:
-                if (this.selected == null)
-                    idx = 0;
-                else
-                    idx = this.selected + 1;
+
+            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;
+            if (!fromTab)
+                this.wildIndex = this.wildtypes.length - 1;
+
+            if (idx && idx[1] >= idx[0].items.length) {
+                this.waiting = idx;
+                statusline.progress = _("completion.waitingForResults");
+                return;
             }
-            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;
+            this.waiting = null;
 
-                        n += context.items.length;
-                    }
-                }
-                finally {
-                    this.waiting = false;
-                }
-
-                // See previous FIXME. This will break if new items in
-                // a previous context come in.
-                if (idx < 0)
-                    idx = this.items.length - 1;
-                if (this.items.length == 0)
-                    return;
+            this.itemList.select(idx, null, position);
+            this.selected = idx;
 
-                this.selected = idx;
-                this.completion = this.items[idx].result;
-            }
+            this.preview();
 
-            this.itemList.selectItem(idx);
+            if (this.selected == null)
+                statusline.progress = "";
+            else
+                statusline.progress = _("completion.matchIndex",
+                                        this.itemList.getOffset(idx),
+                                        this.itemList.itemCount);
         },
 
-        tabs: [],
-
-        tab: function tab(reverse, wildmode) {
+        /**
+         * 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;
 
@@ -1287,27 +1529,28 @@ var CommandLine = Module("commandline", {
             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;
                 }
 
@@ -1315,15 +1558,9 @@ var CommandLine = Module("commandline", {
                     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();
         }
     }),
@@ -1343,8 +1580,10 @@ var CommandLine = Module("commandline", {
         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;
     }
 }, {
@@ -1410,6 +1649,8 @@ var CommandLine = Module("commandline", {
             });
     },
     modes: function initModes() {
+        initModes.require("editor");
+
         modes.addMode("COMMAND_LINE", {
             char: "c",
             description: "Active when the command line is focused",
@@ -1458,6 +1699,14 @@ var CommandLine = Module("commandline", {
         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
@@ -1474,6 +1723,9 @@ var CommandLine = Module("commandline", {
 
         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;
@@ -1481,10 +1733,10 @@ var CommandLine = Module("commandline", {
              });
 
         [
-            [["<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 }) {
@@ -1493,16 +1745,50 @@ var CommandLine = Module("commandline", {
                  });
         });
 
-        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.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.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.tell(keypressEvents[0]);
+                 self.completions.tabTimer.flush();
+                 self.completions.select(self.completions.CTXT_UP);
              });
 
-        bind(["<A-S-Tab>", "<S-Tab>"], "Select the previous matching completion item",
+        bind(["<C-f>", "<PageDown>", "<compl-next-page>"],
+             "Select the next page of completions",
              function ({ keypressEvents, self }) {
                  dactyl.assert(self.completions);
-                 self.completions.tabTimer.tell(keypressEvents[0]);
+                 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",
@@ -1571,268 +1857,529 @@ var CommandLine = Module("commandline", {
 });
 
 /**
- * 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;
+    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);
     },
 
-    _dom: function _dom(xml, map) util.xmlToDom(xml instanceof XML ? xml : <>{xml}</>, this._doc, map),
+    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>
 
-    _autoSize: function _autoSize() {
-        if (!this._div)
-            return;
+            <div highlight="Completions">{
+            template.map(util.range(0, options["maxitems"] * 2), function (i)
+                <div highlight="CompItem NonText"><li>~</li></div>)
+            }</div>
+        </div>
+    </e4x>.elements(),
 
-        if (this._container.collapsed)
-            this._div.style.minWidth = document.getElementById("dactyl-commandline").scrollWidth + "px";
+    get itemCount() this.context.contextList.reduce(function (acc, ctxt) acc + ctxt.items.length, 0),
 
-        this._minHeight = Math.max(this._minHeight,
-            this._win.scrollY + this._divNodes.completions.getBoundingClientRect().bottom);
+    get visible() !this.container.collapsed,
+    set visible(val) this.container.collapsed = !val,
 
-        if (this._container.collapsed)
-            this._div.style.minWidth = "";
+    get activeGroups() this.context.contextList
+                           .filter(function (c) c.items.length || c.message || c.incomplete)
+                           .map(this.getGroup, this),
 
-        // FIXME: Belongs elsewhere.
-        mow.resize(false, Math.max(0, this._minHeight - this._container.height));
+    get selected() let (g = this.selectedGroup) g && g.selectedIdx != null
+        ? [g.context, g.selectedIdx] : null,
 
-        this._container.height = this._minHeight;
-        this._container.height -= mow.spaceNeeded;
-        mow.resize(false);
-        this.timeout(function () {
-            this._container.height -= mow.spaceNeeded;
-        });
-    },
+    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;
 
-    _getCompletion: function _getCompletion(index) this._completionElements.snapshotItem(index - this._startIndex),
+        group = this.getGroup(group);
 
-    _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"/>
-                <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);
+        start = (group.offsets.start + start + offset);
+        if (!noWrap)
+            start %= this.itemCount || 1;
+        if (start < 0 && (!noWrap || arguments[1] === null))
+            start += this.itemCount;
 
-        this._items.contextList.forEach(function init_eachContext(context) {
-            delete context.cache.nodes;
-            if (!context.items.length && !context.message && !context.incomplete)
-                return;
-            context.cache.nodes = [];
-            this._dom(<div key="root" highlight="CompGroup">
-                    <div highlight="Completions">
-                        { context.createRow(context.title || [], "CompTitle") }
-                    </div>
-                    <div highlight="CompTitleSep"/>
-                    <div key="message" highlight="CompMsg"/>
-                    <div key="up" highlight="CompLess"/>
-                    <div key="items" highlight="Completions"/>
-                    <div key="waiting" highlight="CompMsg">{ItemList.WAITING_MESSAGE}</div>
-                    <div key="down" highlight="CompMore"/>
-                </div>, context.cache.nodes);
-            this._divNodes.completions.appendChild(context.cache.nodes.root);
-        }, this);
+        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];
+    },
 
-        this.timeout(this._autoSize);
+    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;
     },
 
     /**
-     * Uses the entries in "items" to fill the listbox and does incremental
-     * filling to speed up things.
+     * Initializes the ItemList for use with a new root completion
+     * context.
      *
-     * @param {number} offset Start at this index and show options["maxitems"].
+     * @param {CompletionContext} context The new root context.
      */
-    _fill: function _fill(offset) {
-        XML.ignoreWhiteSpace = false;
-        let diff = offset - this._startIndex;
-        if (this._items == null || offset == null || diff == 0 || offset < 0)
-            return false;
+    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();
+    },
 
-        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];
+    /**
+     * 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;
         }
+    },
 
-        this._items.contextList.forEach(function fill_eachContext(context) {
-            let nodes = context.cache.nodes;
-            if (!nodes)
-                return;
-            haveCompletions = true;
+    /**
+     * 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);
+        }
 
-            let root = nodes.root;
-            let items = nodes.items;
-            let [start, end, waiting] = getRows(context);
+        this.updateOffsets();
 
-            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);
+        DOM(this.nodes.noCompletions).toggle(!groups.length);
+
+        this.startPos = null;
+        this.select(groups[0] && groups[0].context, null);
 
-        this._divNodes.noCompletions.style.display = haveCompletions ? "none" : "block";
+        this._resize.tell();
+    },
 
-        this._completionElements = util.evaluateXPath("//xhtml:div[@dactyl:highlight='CompItem']", this._doc);
+    /**
+     * 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();
 
-        return true;
+        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);
     },
 
-    clear: function clear() { this.setItems(); this._doc.body.innerHTML = ""; },
-    get visible() !this._container.collapsed,
-    set visible(val) this._container.collapsed = !val,
+    /**
+     * 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);
 
-    reset: function reset(brief) {
-        this._startIndex = this._endIndex = this._selIndex = -1;
-        this._div = null;
-        if (!brief)
-            this.selectItem(-1);
+        if (!this.selected)
+            this.win.scrollTo(0, 0);
+
+        this._resize.tell(ItemList.RESIZE_BRIEF);
     },
 
-    // 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;
-        }
+    onResize: function onResize() {
+        if (this.selectedGroup)
+            this.selectedGroup.rescrollFunc();
     },
 
-    // select index, refill list if necessary
-    selectItem: function selectItem(index) {
-        //let now = Date.now();
+    minHeight: 0,
 
-        if (this._div == null)
-            this._init();
+    /**
+     * Resizes the list after an update.
+     * @private
+     */
+    resize: function resize(flags) {
+        let { completions, root } = this.nodes;
 
-        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 (!this.visible)
+            root.style.minWidth = document.getElementById("dactyl-commandline").scrollWidth + "px";
 
-        if (index == -1 || index == null || index == len) { // wrapped around
-            if (this._selIndex < 0)
-                newOffset = 0;
-            this._selIndex = -1;
-            index = -1;
-        }
-        else {
-            if (index <= this._startIndex + contextLines)
-                newOffset = index - contextLines;
-            if (index >= this._endIndex - contextLines)
-                newOffset = index + contextLines - maxItems + 1;
+        let { minHeight } = this;
+        if (mow.visible && this.isAboveMow) // Kludge.
+            minHeight -= mow.wantedHeight;
 
-            newOffset = Math.min(newOffset, len - maxItems);
-            newOffset = Math.max(newOffset, 0);
+        let needed = this.win.scrollY + DOM(completions).rect.bottom;
+        this.minHeight = Math.max(minHeight, needed);
 
-            this._selIndex = index;
-        }
+        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));
 
-        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));
+            this.container.height = this.minHeight - mow.spaceNeeded;
+            mow.resize(false);
+            this.timeout(function () {
+                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;
 
-        //if (index == 0)
-        //    this.start = now;
-        //if (index == Math.min(len - 1, 100))
-        //    util.dump({ time: Date.now() - this.start });
+        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();
     },
 
-    onKeyPress: function onKeyPress(event) false
+    /**
+     * 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])
 }, {
-    WAITING_MESSAGE: _("completion.generating")
+    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">
+                    { this.context.createRow(this.context.title || [], "CompTitle") }
+                </div>
+                <div highlight="CompTitleSep"/>
+                <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>
+            </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;
+
+            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 {
+                    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:
index 1a244ff641412240d89ec6c7c4726bdcd993529e..dc9df88be5fd42c2adfc36014e01c939a3d56bb8 100644 (file)
@@ -4,7 +4,7 @@
 //
 // 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 */
 
@@ -29,10 +29,6 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         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();
         };
@@ -40,7 +36,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         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":
@@ -60,10 +56,16 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         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");
@@ -97,9 +99,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
                     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 {
@@ -110,20 +110,14 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         }
     },
 
-    /** @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.
@@ -134,29 +128,27 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         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");
-                    items.push(item);
+                    if (!targetPath || targetPath.indexOf(item.dactylPath) == 0)
+                        items.push(item);
                 }
                 else {
                     let path = parent;
                     if (item.localName == "menu")
                         path += item.getAttribute("label") + ".";
-                    addChildren(item, path);
+                    if (!targetPath || targetPath.indexOf(path) == 0)
+                        addChildren(item, path);
                 }
             }
         }
@@ -166,14 +158,24 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         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 }),
 
@@ -202,7 +204,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
     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) {
@@ -215,7 +217,6 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
             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) {
@@ -249,7 +250,8 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
                 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)));
 
@@ -261,10 +263,11 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
                 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;
@@ -278,10 +281,10 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
                 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;
@@ -303,7 +306,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
             };
             XML.ignoreWhitespace = true;
             if (!elems.bell)
-                util.overlayWindow(window, {
+                overlay.overlayWindow(window, {
                     objects: elems,
                     prepend: <>
                         <window id={document.documentElement.id} xmlns={XUL}>
@@ -335,16 +338,20 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
      * 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) {
+    clipboardRead: function clipboardRead(which) {
         try {
-            const clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
-            const transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
+            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);
 
@@ -363,12 +370,19 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
      * 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
+     * @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) {
-        const clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
-        clipboardHelper.copyString(str);
+    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) };
@@ -406,8 +420,10 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
     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)
@@ -465,43 +481,50 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
 
     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;
-
-                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];
+
+        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;
                 }
-                return context[EVAL_RESULT];
-            }
-            finally {
-                delete context[EVAL_ERROR];
-                delete context[EVAL_RESULT];
-                delete context[EVAL_STRING];
+                catch (e) {}
+                throw context[EVAL_ERROR];
             }
+            return context[EVAL_RESULT];
+        }
+        finally {
+            delete context[EVAL_ERROR];
+            delete context[EVAL_RESULT];
+            delete context[EVAL_STRING];
+        }
     },
 
     /**
@@ -543,19 +566,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
     },
 
     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);
     },
 
     /**
@@ -621,33 +632,6 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
      */
     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
      */
@@ -663,243 +647,18 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         }
     },
 
+    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);
-                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);
-                }
+    initHelp: function initHelp() {
+        if ("noscriptOverlay" in window)
+            noscriptOverlay.safeAllow("dactyl:", true, false);
 
-            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) {
@@ -908,120 +667,6 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         return UTF8(xml.toXMLString());
     },
 
-    exportHelp: JavaScript.setCompleter(function (path) {
-        const FILE = io.File(path);
-        const PATH = FILE.leafName.replace(/\..*/, "") + "/";
-        const TIME = Date.now();
-
-        if (!FILE.exists() && (/\/$/.test(path) && !/\./.test(FILE.leafName)))
-            FILE.create(FILE.DIRECTORY_TYPE, octal(755));
-
-        dactyl.initHelp();
-        if (FILE.isDirectory()) {
-            var addDataEntry = function addDataEntry(file, data) FILE.child(file).write(data);
-            var addURIEntry  = function addURIEntry(file, uri) addDataEntry(file, util.httpGet(uri).responseText);
-        }
-        else {
-            var zip = services.ZipWriter();
-            zip.open(FILE, File.MODE_CREATE | File.MODE_WRONLY | File.MODE_TRUNCATE);
-
-            addURIEntry = function addURIEntry(file, uri)
-                zip.addEntryChannel(PATH + file, TIME, 9,
-                    services.io.newChannel(uri, null, null), false);
-            addDataEntry = function addDataEntry(file, data) // Unideal to an extreme.
-                addURIEntry(file, "data:text/plain;charset=UTF-8," + encodeURI(data));
-        }
-
-        let empty = Set("area base basefont br col frame hr img input isindex link meta param"
-                            .split(" "));
-        function fix(node) {
-            switch(node.nodeType) {
-            case Node.ELEMENT_NODE:
-                if (isinstance(node, [HTMLBaseElement]))
-                    return;
-
-                data.push("<"); data.push(node.localName);
-                if (node instanceof HTMLHtmlElement)
-                    data.push(" xmlns=" + XHTML.uri.quote(),
-                              " xmlns:dactyl=" + NS.uri.quote());
-
-                for (let { name, value } in array.iterValues(node.attributes)) {
-                    if (name == "dactyl:highlight") {
-                        Set.add(styles, value);
-                        name = "class";
-                        value = "hl-" + value;
-                    }
-                    if (name == "href") {
-                        value = node.href || value;
-                        if (value.indexOf("dactyl://help-tag/") == 0) {
-                            let uri = services.io.newChannel(value, null, null).originalURI;
-                            value = uri.spec == value ? "javascript:;" : uri.path.substr(1);
-                        }
-                        if (!/^#|[\/](#|$)|^[a-z]+:/.test(value))
-                            value = value.replace(/(#|$)/, ".xhtml$1");
-                    }
-                    if (name == "src" && value.indexOf(":") > 0) {
-                        chromeFiles[value] = value.replace(/.*\//, "");
-                        value = value.replace(/.*\//, "");
-                    }
-
-                    data.push(" ", name, '="',
-                              <>{value}</>.toXMLString().replace(/"/g, "&quot;"),
-                              '"');
-                }
-                if (node.localName in empty)
-                    data.push(" />");
-                else {
-                    data.push(">");
-                    if (node instanceof HTMLHeadElement)
-                        data.push(<link rel="stylesheet" type="text/css" href="help.css"/>.toXMLString());
-                    Array.map(node.childNodes, fix);
-                    data.push("</", node.localName, ">");
-                }
-                break;
-            case Node.TEXT_NODE:
-                data.push(<>{node.textContent}</>.toXMLString());
-            }
-        }
-
-        let chromeFiles = {};
-        let styles = {};
-        for (let [file, ] in Iterator(services["dactyl:"].FILE_MAP)) {
-            let url = "dactyl://help/" + file;
-            dactyl.open(url);
-            util.waitFor(function () content.location.href == url && buffer.loaded
-                            && content.document.documentElement instanceof HTMLHtmlElement,
-                         15000);
-            events.waitForPageLoad();
-            var data = [
-                '<?xml version="1.0" encoding="UTF-8"?>\n',
-                '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"\n',
-                '          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n'
-            ];
-            fix(content.document.documentElement);
-            addDataEntry(file + ".xhtml", data.join(""));
-        }
-
-        let data = [h for (h in highlight) if (Set.has(styles, h.class) || /^Help/.test(h.class))]
-            .map(function (h) h.selector
-                               .replace(/^\[.*?=(.*?)\]/, ".hl-$1")
-                               .replace(/html\|/g, "") + "\t" + "{" + h.cssText + "}")
-            .join("\n");
-        addDataEntry("help.css", data.replace(/chrome:[^ ")]+\//g, ""));
-
-        addDataEntry("tag-map.json", JSON.stringify(services["dactyl:"].HELP_TAGS));
-
-        let m, re = /(chrome:[^ ");]+\/)([^ ");]+)/g;
-        while ((m = re.exec(data)))
-            chromeFiles[m[0]] = m[2];
-
-        for (let [uri, leaf] in Iterator(chromeFiles))
-            addURIEntry(leaf, uri);
-
-        if (zip)
-            zip.close();
-    }, [function (context, args) completion.file(context)]),
-
     /**
      * Generates a help entry and returns it as a string.
      *
@@ -1040,6 +685,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         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> : <></>
                 }{
@@ -1050,6 +696,9 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         }
         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>;
@@ -1061,7 +710,8 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
             };
         }
         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: [] };
         }
@@ -1075,7 +725,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
                     </>;
 
         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)
@@ -1084,10 +734,11 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         res.* += <>
             <item>
                 <tags>{template.map(obj.names.slice().reverse(), tag, " ")}</tags>
-                <spec>{
-                    spec(template.highlightRegexp((obj.specs || obj.names)[0],
-                                                  /\[(.*?)\]/g,
-                                                  function (m, n0) <oa>{n0}</oa>))
+                <spec>{let (name = (obj.specs || obj.names)[0])
+                          spec(template.highlightRegexp(tag(name),
+                               /\[(.*?)\]/g,
+                               function (m, n0) <oa>{n0}</oa>),
+                               name)
                 }</spec>{
                 !obj.type ? "" : <>
                 <type>{obj.type}</type>
@@ -1128,30 +779,6 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
                   .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.
      *
@@ -1173,7 +800,9 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
                 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);
@@ -1218,7 +847,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
      * @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, _))
@@ -1228,35 +857,41 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         }
     },
 
-    onClick: function onClick(event) {
-        if (event.originalTarget instanceof Element) {
-            let command = event.originalTarget.getAttributeNS(NS, "command");
-            if (command && event.button == 0) {
-                event.preventDefault();
+    events: {
+        click: function onClick(event) {
+            let elem = event.originalTarget;
 
-                if (dactyl.commands[command])
-                    dactyl.withSavedValues(["forceNewTab"], function () {
-                        dactyl.forceNewTab = event.ctrlKey || event.shiftKey || event.button == 1;
-                        dactyl.commands[command](event);
-                    });
+            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(["forceTarget"], function () {
+                            if (event.ctrlKey || event.shiftKey || event.button == 1)
+                                dactyl.forceTarget = dactyl.NEW_TAB;
+                            dactyl.commands[command](event);
+                        });
+                }
             }
-        }
-    },
+        },
 
-    onExecute: function onExecute(event) {
-        let cmd = event.originalTarget.getAttribute("dactyl-execute");
-        commands.execute(cmd, null, false, null,
-                         { file: /*L*/"[Command Line]", line: 1 });
+        "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:
      *
@@ -1297,8 +932,9 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
             flags |= params[opt] && Ci.nsIWebNavigation["LOAD_FLAGS_" + flag];
 
         let where = params.where || dactyl.CURRENT_TAB;
-        let background = ("background" in params) ? params.background
-                                                  : params.where == dactyl.NEW_BACKGROUND_TAB;
+        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))
@@ -1310,21 +946,23 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
             return;
 
         let browser = config.tabbrowser;
-        function open(urls, where) {
+        function open(loc, where) {
             try {
-                let url = Array.concat(urls)[0];
-                let postdata = Array.concat(urls)[1];
+                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:
@@ -1333,7 +971,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
                     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;
                 }
             }
@@ -1343,10 +981,8 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
             // 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;
 
@@ -1378,7 +1014,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         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);
@@ -1390,7 +1026,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
 
             // 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.
@@ -1408,7 +1044,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         }, this);
     },
     stringToURLArray: deprecated("dactyl.parseURLs", "parseURLs"),
-    urlish: Class.memoize(function () util.regexp(<![CDATA[
+    urlish: Class.Memoize(function () util.regexp(<![CDATA[
             ^ (
                 <domain>+ (:\d+)? (/ .*) |
                 <domain>+ (:\d+) |
@@ -1470,10 +1106,12 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
     /**
      * 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);
     },
 
@@ -1492,7 +1130,12 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
             return func.apply(self || this, Array.slice(arguments, 2));
         }
         catch (e) {
-            dactyl.reportError(e, true);
+            try {
+                dactyl.reportError(e, true);
+            }
+            catch (e) {
+                util.reportError(e);
+            }
             return e;
         }
     },
@@ -1507,7 +1150,8 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         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)
@@ -1519,8 +1163,10 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
                 util.reportError(error);
             return;
         }
+
         if (error.result == Cr.NS_BINDING_ABORTED)
             return;
+
         if (echo)
             dactyl.echoerr(error, commandline.FORCE_SINGLELINE);
         else
@@ -1545,10 +1191,9 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
             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;
@@ -1573,9 +1218,90 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
 }, {
     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 () {
@@ -1671,12 +1397,12 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
 
         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))
-                           if (!Dactyl.toolbarHidden(document.getElementById(v[1][0])))].join(""),
+                    "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(),
 
@@ -1700,12 +1426,15 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
 
         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) {
-                        document.title = document.title.replace(RegExp("(.*)" + util.regexp.escape(old)), "$1" + 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")) {
@@ -1744,21 +1473,13 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
             {
                 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",
@@ -1797,7 +1518,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
             "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));
@@ -1829,30 +1550,6 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
                 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) {
@@ -1903,47 +1600,66 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
                 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: [
-                    {
-                        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
-                    }
-                ]
+                options: startupOptions
             });
 
         commands.add(["res[tart]"],
-            "Force " + config.appName + " to restart",
-            function () { dactyl.restart(); },
-            { argCount: "0" });
+            "Force " + config.host + " to restart",
+            function (args) {
+                if (args["+purgecaches"])
+                    cache.flush();
 
-        function findToolbar(name) util.evaluateXPath(
+                dactyl.restart(args.string);
+            },
+            {
+                argCount: "0",
+                options: startupOptions
+            });
+
+        function findToolbar(name) DOM.XPath(
             "//*[@toolbarname=" + util.escapeString(name, "'") + " or " +
                 "@toolbarname=" + util.escapeString(name.trim(), "'") + "]",
             document).snapshotItem(0);
@@ -2092,10 +1808,14 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
             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
@@ -2110,15 +1830,6 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
             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;
@@ -2134,7 +1845,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
         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) {
@@ -2148,9 +1859,17 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
 
         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);
 
@@ -2168,17 +1887,14 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), {
 
             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();
index 3481cda8bc70755849b15c955ec24a107492caa7..5f5b4feac44bd6f47c21e97aa1ed412ef8182f1f 100644 (file)
@@ -8,8 +8,7 @@ const OVERLAY_URLS = [
     "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");
@@ -64,7 +63,10 @@ function chromeDocuments() {
             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) {}
         }
     }
 }
index d2fe3224b9f063b010fe42eae926f1418a41df07..262391ece5c0696b42388e4cc65c8082146a2764 100644 (file)
@@ -3,7 +3,7 @@
 //
 // 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 */
 
 // 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"]();
+    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;
         }
-        catch (e) {}
+        return this.selection.getRangeAt(0);
+    },
+    set selectedRange(range) {
+        this.selection.removeAllRanges();
+        if (range != null)
+            this.selection.addRange(range);
     },
 
-    selectedText: function () String(Editor.getEditor(null).selection),
+    get selectedText() String(this.selection),
 
-    pasteClipboard: function (clipboard, toStart) {
-        let elem = dactyl.focusedElement;
-        if (elem.inputField)
-            elem = elem.inputField;
+    get preserveSelection() this.editor && !this.editor.shouldTxnSetSelection,
+    set preserveSelection(val) {
+        if (this.editor)
+            this.editor.setShouldTxnSetSelection(!val);
+    },
 
-        if (elem.setSelectionRange) {
-            let text = dactyl.clipboardRead(clipboard);
-            if (!text)
-                return;
-            if (isinstance(elem, [HTMLInputElement, XULTextBoxElement]))
-                text = text.replace(/\n+/g, "");
+    copy: function copy(range, name) {
+        range = range || this.selection;
 
-            // 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;
+        if (!range.collapsed)
+            this.setRegister(name, range);
+    },
 
-            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;
+    cut: function cut(range, name) {
+        if (range)
+            this.selectedRange = range;
 
-            if (/^(search|text)$/.test(elem.type))
-                Editor.getEditor(elem).rootElement.firstChild.textContent = value;
+        if (!this.selection.isCollapsed)
+            this.setRegister(name, this.selection);
 
-            elem.selectionStart = Math.min(start + (toStart ? 0 : text.length), elem.value.length);
-            elem.selectionEnd = elem.selectionStart;
+        this.editor.deleteSelection(0);
+    },
 
-            elem.scrollTop = top;
-            elem.scrollLeft = left;
+    paste: function paste(name) {
+        let text = this.getRegister(name);
+        dactyl.assert(text && this.editor instanceof Ci.nsIPlaintextEditor);
 
-            events.dispatch(elem, events.create(elem.ownerDocument, "input"));
-        }
+        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 &&
-                          controller.supportsCommand(cmd) &&
-                          controller.isCommandEnabled(cmd));
+    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;
+            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) {
@@ -92,133 +218,135 @@ var Editor = Module("editor", {
         }
     },
 
-    // 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;
+    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);
+    },
 
-        if (cmd == motion) {
-            motion = "j";
-            count--;
-        }
+    mungeRange: function mungeRange(range, munger, selectEnd) {
+        let { editor } = this;
+        editor.beginPlaceHolderTransaction(null);
 
-        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;
-        }
-    },
+        let [container, offset] = ["startContainer", "startOffset"];
+        if (selectEnd)
+            [container, offset] = ["endContainer", "endOffset"];
 
-    // 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;
-        }
+        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 (forward) {
-            if (pos <= Editor.getEditor().selectionEnd || pos > Editor.getEditor().value.length)
-                return;
+                if (text == node.textContent)
+                    continue;
 
-            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;
+                if (selectEnd)
+                    delta = text.length - node.textContent.length;
 
-            do { // TODO: test code for endless loops
-                this.executeCommand("cmd_selectCharPrevious", 1);
+                if (editor instanceof Ci.nsIPlaintextEditor) {
+                    this.selectedRange = RangeFind.nodeContents(node);
+                    editor.insertText(text);
+                }
+                else
+                    node.textContent = text;
             }
-            while (Editor.getEditor().selectionStart != pos);
+            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 (key, count, backward) {
+    findChar: function findNumber(key, count, backward, offset) {
+        count  = count || 1; // XXX ?
+        offset = (offset || 0) - !!backward;
 
-        let editor = Editor.getEditor();
-        if (!editor)
-            return -1;
+        // 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);
 
-        // XXX
-        if (count == null)
-            count = 1;
+        let range = this.selectedRange.cloneRange();
+        let collapse = DOM(this.element).whiteSpace == "normal";
 
-        let code = events.fromString(key)[0].charCode;
-        util.assert(code);
-        let char = String.fromCharCode(code);
+        // 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)
 
-        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;
+        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();
 
-            while (caret < end && caret >= 0 && count--)
-                caret = text.indexOf(char, caret + 1);
+        // 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);
         }
 
-        if (count > 0)
-            caret = -1;
-        if (caret == -1)
-            dactyl.beep();
-        return caret;
+        // 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");
     },
 
     /**
@@ -249,7 +377,11 @@ var Editor = Module("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") + " ",
@@ -262,18 +394,32 @@ var Editor = Module("editor", {
 
         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("");
+            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(/(?:<\/[^>"]+>)+$/, "");
+            }
         }
 
-        let origGroup = textBox && textBox.getAttributeNS(NS, "highlight") || "";
+        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();
@@ -290,7 +436,9 @@ var Editor = Module("editor", {
                 tmpfile.remove(false);
 
             if (textBox) {
-                dactyl.focus(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;
@@ -307,10 +455,12 @@ var Editor = Module("editor", {
             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)
@@ -325,8 +475,9 @@ var Editor = Module("editor", {
                 throw Error(_("io.cantCreateTempFile"));
 
             if (textBox) {
-                highlight.highlightNode(textBox, origGroup + " EditorEditing");
-                textBox.blur();
+                if (!keepFocus)
+                    textBox.blur();
+                DOM(textBox).highlight.add("EditorEditing");
             }
 
             if (!tmpfile.write(text))
@@ -349,38 +500,167 @@ var Editor = Module("editor", {
      * @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);
+            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);
+            }
         }
 
-        let nodeRange = range.cloneRange();
-        nodeRange.selectNodeContents(range.startContainer);
-        let text = String(nodeRange);
+        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);
@@ -405,93 +685,181 @@ var Editor = Module("editor", {
             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;
-        }
+        return DOM(elem).editor;
+    }
+}, {
+    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 };
 
-    getController: function () {
-        let ed = dactyl.focusedElement;
-        if (!ed || !ed.controllers)
-            return null;
+            context.match = function (r) !this.filter || ~this.filter.indexOf(r);
 
-        return ed.controllers.getControllerForCommand("cmd_beginLine");
-    }
-}, {
-    mappings: function () {
+            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() {
 
-        // 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));
+        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();
+            }
+        };
 
-                let controller = buffer.selectionController;
+        // 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 = {
+                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();
+                    buffer.resetCaret();
 
-                try {
-                    controller[caretModeMethod](caretModeArg, arg);
-                }
-                catch (e) {
-                    dactyl.assert(again && e.result === Cr.NS_ERROR_FAILURE);
-                    fixSelection();
-                    caretExecute(arg, false);
+                if (caretModeMethod == "pageMove") { // Grr.
+                    buffer.scrollVertical("pages", caretModeArg ? 1 : -1);
+                    buffer.resetCaret();
                 }
+                else
+                    controller[caretModeMethod](caretModeArg, arg);
             }
 
-            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;
+                    count = count || 1;
 
-                    let editor_ = Editor.getEditor(null);
+                    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;
 
-                    editor.executeCommand(textEditCommand, count);
+                    if (editor.editor)
+                        editor.executeCommand(textEditCommand, count);
+                    else {
+                        while (count--)
+                            caretExecute(false);
+                    }
                 },
                 extraInfo);
         }
@@ -500,42 +868,62 @@ var Editor = Module("editor", {
         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);
+            editor.selectionController.repaintSelection(editor.selectionController.SELECTION_NORMAL);
         }
-        function move(forward, re)
+
+        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
@@ -548,9 +936,9 @@ var Editor = Module("editor", {
         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",
@@ -582,77 +970,158 @@ var Editor = Module("editor", {
         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);
+                    dactyl.assert(caretOk || editor.isTextEdit);
+                    if (editor.isTextEdit)
+                        doTxn(editor.selectedRange, editor);
+                    else
+                        cmd(editor, buffer.selection.getRangeAt(0));
                 },
-                { count: true, motion: true });
+                { count: true, type: "motion" });
         }
 
-        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); });
+        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);
 
-        mappings.add([modes.INPUT],
-            ["<C-u>"], "Delete until beginning of current line",
-            function () {
-                // Deletes the whole line. What the hell.
-                // editor.executeCommand("cmd_deleteToBeginningOfLine", 1);
+        addMotionMap(["gu"], "Lowercase text", false,
+             function (editor, range) {
+                 editor.mungeRange(range, String.toLocaleLowerCase);
+             });
 
-                editor.executeCommand("cmd_selectBeginLine", 1);
-                if (Editor.getController().isCommandEnabled("cmd_delete"))
-                    editor.executeCommand("cmd_delete", 1);
+        addMotionMap(["gU"], "Uppercase text", false,
+            function (editor, range) {
+                editor.mungeRange(range, String.toLocaleUpperCase);
             });
 
-        mappings.add([modes.INPUT],
-            ["<C-k>"], "Delete until end of current line",
-            function () { editor.executeCommand("cmd_deleteToEndOfLine", 1); });
+        mappings.add([modes.OPERATOR],
+            ["c", "d", "y"], "Select the entire line",
+            function ({ command, count }) {
+                dactyl.assert(command == modes.getStack(0).params.forCommand);
 
-        mappings.add([modes.INPUT],
-            ["<C-a>"], "Move cursor to beginning of current line",
-            function () { editor.executeCommand("cmd_beginLine", 1); });
+                let sel = editor.selection;
+                sel.modify("move", "backward", "lineboundary");
+                sel.modify("extend", "forward", "lineboundary");
 
-        mappings.add([modes.INPUT],
-            ["<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",
-            function () { events.feedkeys("<BS>", true); });
-
-        mappings.add([modes.INPUT],
-            ["<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",
-            function () { editor.editFieldExternally(); });
-
-        mappings.add([modes.INPUT],
-            ["<C-t>"], "Edit text field in Vi mode",
-            function () {
-                dactyl.assert(dactyl.focusedElement);
-                dactyl.assert(!editor.isTextEdit);
-                modes.push(modes.TEXT_EDIT);
-            });
+                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.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);
+             });
+
+        bind(["<C-k>"], "Delete until end of current line",
+             function () { editor.executeCommand("cmd_deleteToEndOfLine", 1); });
+
+        bind(["<C-a>"], "Move cursor to beginning of current line",
+             function () { editor.executeCommand("cmd_beginLine", 1); });
+
+        bind(["<C-e>"], "Move cursor to end of current line",
+             function () { editor.executeCommand("cmd_endLine", 1); });
+
+        bind(["<C-h>"], "Delete character to the left",
+             function () { events.feedkeys("<BS>", true); });
+
+        bind(["<C-d>"], "Delete character to the right",
+             function () { editor.executeCommand("cmd_deleteCharForward", 1); });
+
+        bind(["<S-Insert>"], "Insert clipboard/selection",
+             function () { editor.paste(); });
+
+        bind(["<C-i>"], "Edit text field with an external editor",
+             function () { editor.editFieldExternally(); });
+
+        bind(["<C-t>"], "Edit text field in Text Edit mode",
+             function () {
+                 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],
@@ -673,52 +1142,58 @@ var Editor = Module("editor", {
             ["<C-]>", "<C-5>"], "Expand Insert mode abbreviation",
             function () { editor.expandAbbreviation(modes.INSERT); });
 
-        // text edit mode
-        mappings.add([modes.TEXT_EDIT],
-            ["u"], "Undo changes",
-            function (args) {
-                editor.executeCommand("cmd_undo", Math.max(args.count, 1));
-                editor.unselectText();
-            },
-            { count: true });
-
-        mappings.add([modes.TEXT_EDIT],
-            ["<C-r>"], "Redo undone changes",
-            function (args) {
-                editor.executeCommand("cmd_redo", Math.max(args.count, 1));
-                editor.unselectText();
-            },
-            { count: true });
+        let bind = function bind(names, description, action, params)
+            mappings.add([modes.TEXT_EDIT], names, description,
+                         action, update({ type: "editor" }, params));
 
-        mappings.add([modes.TEXT_EDIT],
-            ["D"], "Delete the characters under the cursor until the end of the line",
-            function () { editor.executeCommand("cmd_deleteToEndOfLine"); });
 
-        mappings.add([modes.TEXT_EDIT],
-            ["o"], "Open line below current",
-            function () {
-                editor.executeCommand("cmd_endLine", 1);
-                modes.push(modes.INSERT);
-                events.feedkeys("<Return>");
-            });
+        bind(["<C-a>"], "Increment the next number",
+             function ({ count }) { editor.modifyNumber(count || 1) },
+             { count: true });
 
-        mappings.add([modes.TEXT_EDIT],
-            ["O"], "Open line above current",
-            function () {
-                editor.executeCommand("cmd_beginLine", 1);
-                modes.push(modes.INSERT);
-                events.feedkeys("<Return>");
-                editor.executeCommand("cmd_linePrevious", 1);
-            });
+        bind(["<C-x>"], "Decrement the next number",
+             function ({ count }) { editor.modifyNumber(-(count || 1)) },
+             { count: true });
 
-        mappings.add([modes.TEXT_EDIT],
-            ["X"], "Delete character to the left",
-            function (args) { editor.executeCommand("cmd_deleteCharBackward", Math.max(args.count, 1)); },
+        // text edit mode
+        bind(["u"], "Undo changes",
+             function (args) {
+                 editor.executeCommand("cmd_undo", Math.max(args.count, 1));
+                 editor.deselect();
+             },
+             { count: true });
+
+        bind(["<C-r>"], "Redo undone changes",
+             function (args) {
+                 editor.executeCommand("cmd_redo", Math.max(args.count, 1));
+                 editor.deselect();
+             },
+             { count: true });
+
+        bind(["D"], "Delete characters from the cursor to the end of the line",
+             function () { editor.executeCommand("cmd_deleteToEndOfLine"); });
+
+        bind(["o"], "Open line below current",
+             function () {
+                 editor.executeCommand("cmd_endLine", 1);
+                 modes.push(modes.INSERT);
+                 events.feedkeys("<Return>");
+             });
+
+        bind(["O"], "Open line above current",
+             function () {
+                 editor.executeCommand("cmd_beginLine", 1);
+                 modes.push(modes.INSERT);
+                 events.feedkeys("<Return>");
+                 editor.executeCommand("cmd_linePrevious", 1);
+             });
+
+        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",
-            function (args) { editor.executeCommand("cmd_deleteCharForward", Math.max(args.count, 1)); },
+        bind(["x"], "Delete character to the right",
+             function (args) { editor.executeCommand("cmd_deleteCharForward", Math.max(args.count, 1)); },
             { count: true });
 
         // visual mode
@@ -730,16 +1205,15 @@ var Editor = Module("editor", {
             ["v", "V"], "End Visual mode",
             function () { modes.pop(); });
 
-        mappings.add([modes.TEXT_EDIT],
-            ["V"], "Start Visual Line mode",
-            function () {
-                modes.push(modes.VISUAL, modes.LINE);
-                editor.executeCommand("cmd_beginLine", 1);
-                editor.executeCommand("cmd_selectLineNext", 1);
-            });
+        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");
@@ -747,95 +1221,110 @@ var Editor = Module("editor", {
             });
 
         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);
+                    selection = buffer.focusedFrame.getSelection();
+
+                util.assert(selection.focusNode);
+                let { focusOffset, anchorOffset, focusNode, anchorNode } = selection;
+                selection.collapse(focusNode, focusOffset);
+                selection.extend(anchorNode, anchorOffset);
             });
 
-        mappings.add([modes.VISUAL, modes.TEXT_EDIT],
-            ["p"], "Paste clipboard contents",
-            function ({ count }) {
+        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 });
 
-        // finding characters
-        mappings.add([modes.TEXT_EDIT, modes.VISUAL],
-            ["f"], "Move to a character on the current line after the cursor",
-            function ({ arg, count }) {
-                let pos = editor.findChar(arg, Math.max(count, 1));
-                if (pos >= 0)
-                    editor.moveToPosition(pos, true, modes.main == modes.VISUAL);
+        mappings.add([modes.COMMAND],
+            ['"'], "Bind a register to the next command",
+            function ({ arg }) {
+                editor.pushRegister(arg);
             },
-            { arg: true, count: true });
+            { arg: true });
 
-        mappings.add([modes.TEXT_EDIT, modes.VISUAL],
-            ["F"], "Move to a character on the current line before the cursor",
-            function ({ arg, count }) {
-                let pos = editor.findChar(arg, Math.max(count, 1), true);
-                if (pos >= 0)
-                    editor.moveToPosition(pos, false, modes.main == modes.VISUAL);
+        mappings.add([modes.INPUT],
+            ["<C-'>", '<C-">'], "Bind a register to the next command",
+            function ({ arg }) {
+                editor.pushRegister(arg);
             },
-            { arg: true, count: true });
+            { arg: true });
 
-        mappings.add([modes.TEXT_EDIT, modes.VISUAL],
-            ["t"], "Move before a character on the current line",
-            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 });
+        let bind = function bind(names, description, action, params)
+            mappings.add([modes.TEXT_EDIT, modes.OPERATOR, modes.VISUAL],
+                         names, description,
+                         action, update({ type: "editor" }, params));
 
-        mappings.add([modes.TEXT_EDIT, modes.VISUAL],
-            ["T"], "Move before 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 + 1, false, modes.main == modes.VISUAL);
-            },
-            { arg: true, count: true });
+        // finding characters
+        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 }) {
+                 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 }) {
+                 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 }) {
+                 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 }) {
+                 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,
-                                           [[modes.AUTOCOMPLETE]].concat(Array.slice(arguments)))
+        let bind = function bind() mappings.add.apply(mappings,
+                                                      [[modes.AUTOCOMPLETE]].concat(Array.slice(arguments)))
 
         bind(["<Esc>"], "Return to Insert mode",
              function () Events.PASS_THROUGH);
@@ -855,8 +1344,7 @@ var Editor = Module("editor", {
         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>', {
@@ -878,6 +1366,42 @@ var Editor = Module("editor", {
         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);
+                }
+            }
+        });
     }
 });
 
index 117d64b5891fcd9daf7969bd8d7f6bf238b2c92a..4b1841d916036233bf06ffe07537fbcfc4bf8573 100644 (file)
 //
 // 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.
@@ -337,6 +22,18 @@ var EventHive = Class("EventHive", Contexts.Hive, {
         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.
@@ -350,24 +47,17 @@ var EventHive = Class("EventHive", Contexts.Hive, {
      *      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);
         }
     },
@@ -382,14 +72,25 @@ var EventHive = Class("EventHive", Contexts.Hive, {
      *      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
 });
 
 /**
@@ -401,16 +102,8 @@ var Events = Module("events", {
     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 -->
@@ -427,74 +120,84 @@ var Events = Module("events", {
         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._pseudoKeys = Set(["count", "leader", "nop", "pass"]);
+        this.popups = {
+            active: [],
 
-        this._key_key = {};
-        this._code_key = {};
-        this._key_code = {};
-        this._code_nativeKey = {};
+            activeMenubar: null,
 
-        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;
-            }
+            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);
+            },
 
-        for (let [k, v] in Iterator(KeyEvent)) {
-            this._code_nativeKey[v] = k.substr(4);
+            events: {
+                DOMMenuBarActive: function onDOMMenuBarActive(event) {
+                    this.activeMenubar = event.target;
+                    if (modes.main != modes.MENU)
+                        modes.push(modes.MENU);
+                },
 
-            k = k.substr(7).toLowerCase();
-            let names = [k.replace(/(^|_)(.)/g, function (m, n1, n2) n2.toUpperCase())
-                          .replace(/^NUMPAD/, "k")];
+                DOMMenuBarInactive: function onDOMMenuBarInactive(event) {
+                    this.activeMenubar = null;
+                    modes.remove(modes.MENU, true);
+                },
 
-            if (names[0].length == 1)
-                names[0] = names[0].toLowerCase();
+                popupshowing: function onPopupShowing(event) {
+                    this.update(event.originalTarget);
+                },
+
+                popupshown: function onPopupShown(event) {
+                    let elem = event.originalTarget;
+                    this.update(elem);
 
-            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;
+                    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);
+                }
             }
-        }
+        };
 
-        // 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.listen(window, this, "events", true);
+        this.listen(window, this.popups, "events", true);
+    },
 
-        this._activeMenubar = false;
-        this.listen(window, this, "events");
+    cleanup: function cleanup() {
+        let elem = dactyl.focusedElement;
+        if (DOM(elem).isEditable)
+            util.trapErrors("removeEditActionListener",
+                            DOM(elem).editor, editor);
     },
 
     signals: {
@@ -514,7 +217,8 @@ var Events = Module("events", {
      */
     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);
@@ -549,21 +253,18 @@ var Events = Module("events", {
         dactyl.assert(macro == null || /[a-zA-Z0-9]/.test(macro),
                       _("macro.invalid", macro));
 
-        modes.recording = !!macro;
+        modes.recording = macro;
 
-        if (/[A-Z]/.test(macro)) { // uppercase (append)
+        if (/[A-Z]/.test(macro)) { // Append.
             macro = macro.toLowerCase();
-            this._macroKeys = events.fromString((this._macros.get(macro) || { keys: "" }).keys, true)
-                                    .map(events.closure.toString);
+            this._macroKeys = DOM.Event.iterKeys(editor.getRegister(macro))
+                                 .toArray();
         }
-        else if (macro) {
+        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));
@@ -578,27 +279,24 @@ var Events = Module("events", {
      * @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
-            // TODO: ignore this like Vim?
-            dactyl.echoerr(_("macro.noSuch", this._lastMacro));
-        return res;
+        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 false;
     },
 
     /**
@@ -609,7 +307,7 @@ var Events = Module("events", {
      */
     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)));
     },
 
     /**
@@ -620,9 +318,9 @@ var Events = Module("events", {
      */
     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);
         }
     },
 
@@ -641,8 +339,8 @@ var Events = Module("events", {
             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);
@@ -676,9 +374,9 @@ var Events = Module("events", {
 
             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)
@@ -691,10 +389,10 @@ var Events = Module("events", {
                     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;
@@ -703,8 +401,9 @@ var Events = Module("events", {
                         ["<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);
                 }
@@ -717,7 +416,7 @@ var Events = Module("events", {
             util.reportError(e);
         }
         finally {
-            this.feedingEvent = null;
+            DOM.Event.feedingEvent = null;
             this.feedingKeys = wasFeeding;
             if (quiet)
                 commandline.quiet = wasQuiet;
@@ -726,360 +425,22 @@ var Events = Module("events", {
         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"];
+    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 }),
 
-        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()),
+    toString: function toString() {
+        if (!arguments.length)
+            return toString.supercall(this);
 
-    /**
-     * 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("");
+        deprecated.warn(toString, "toString", "DOM.Event.stringify");
+        return DOM.Event.stringify.apply(DOM.Event, arguments);
     },
 
-    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;
-                }
-            }),
-
     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.
@@ -1106,7 +467,7 @@ var Events = Module("events", {
                          ["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"))
@@ -1212,19 +573,12 @@ var Events = Module("events", {
     },
 
     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
@@ -1245,14 +599,21 @@ var Events = Module("events", {
         // 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])) {
@@ -1305,13 +666,13 @@ var Events = Module("events", {
             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;
+                DOM.Event.feedingEvent = null;
 
-                let key = events.toString(event);
+                let key = DOM.Event.stringify(event);
 
                 // Hack to deal with <BS> and so forth not dispatching input
                 // events
@@ -1320,7 +681,7 @@ var Events = Module("events", {
                     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;
                     });
                 }
@@ -1409,7 +770,7 @@ var Events = Module("events", {
                 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);
@@ -1424,7 +785,7 @@ var Events = Module("events", {
                 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
@@ -1433,16 +794,20 @@ var Events = Module("events", {
                     !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.
@@ -1461,30 +826,9 @@ var Events = Module("events", {
 
             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);
+                    overlay.setData(elem, "focus-allowed", true);
+                overlay.setData(win.document, "focus-allowed", true);
             }
-            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);
         },
 
         resize: function onResize(event) {
@@ -1494,13 +838,14 @@ var Events = Module("events", {
                 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;
@@ -1519,34 +864,30 @@ var Events = Module("events", {
                 return;
 
             if (isinstance(elem, [HTMLEmbedElement, HTMLEmbedElement])) {
-                modes.push(modes.EMBED);
+                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 (options["insertmode"])
-                        modes.push(modes.INSERT);
-                    else {
-                        modes.push(modes.TEXT_EDIT);
-                        if (elem.selectionEnd - elem.selectionStart > 0)
-                            modes.push(modes.VISUAL);
-                    }
+                    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;
+                    buffer.lastInputField = elem || win;
                 return;
             }
 
-            if (Events.isInputElement(elem)) {
+            if (elem && Events.isInputElement(elem)) {
                 if (!haveInput)
                     modes.push(modes.INSERT);
 
@@ -1564,7 +905,9 @@ var Events = Module("events", {
             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 });
         }
@@ -1574,18 +917,18 @@ var Events = Module("events", {
             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);
@@ -1594,7 +937,7 @@ var Events = Module("events", {
 
     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,
@@ -1603,11 +946,11 @@ var Events = Module("events", {
     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)
@@ -1621,12 +964,9 @@ var Events = Module("events", {
     },
 
     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) {
@@ -1634,6 +974,14 @@ var Events = Module("events", {
         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",
@@ -1677,7 +1025,10 @@ var Events = Module("events", {
 
         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",
@@ -1710,6 +1061,7 @@ var Events = Module("events", {
         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;
             },
@@ -1775,18 +1127,23 @@ var Events = Module("events", {
 
                 get pass() (this.flush(), this.pass),
 
-                keepQuotes: true,
-
-                setter: function (values) {
-                    values.forEach(function (filter) {
+                parse: function parse() {
+                    let value = parse.superapply(this, arguments);
+                    value.forEach(function (filter) {
                         let vals = Option.splitList(filter.result);
-                        filter.keys = events.fromString(vals[0]).map(events.closure.toString);
+                        filter.keys = DOM.Event.parse(vals[0]).map(DOM.Event.closure.stringify);
 
-                        filter.commandKeys = vals.slice(1).map(events.closure.canonicalKeys);
+                        filter.commandKeys = vals.slice(1).map(DOM.Event.closure.canonicalKeys);
                         filter.inputKeys = filter.commandKeys.filter(bind("test", /^<[ACM]-/));
                     });
+                    return value;
+                },
+
+                keepQuotes: true,
+
+                setter: function (value) {
                     this.flush();
-                    return values;
+                    return value;
                 }
             });
 
@@ -1808,18 +1165,6 @@ var Events = Module("events", {
         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);
-            }
-        });
     }
 });
 
index 9faf7a2ae17c40ac997ec63c61c02b242be68e18..0bde1ee4c4c5c6fe6887f2fddb3be04f8395141b 100644 (file)
@@ -2,7 +2,7 @@
 //
 // 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;
index a93f9a7a8f6fe96c6a299e3466f3a56acc4e6799..f1dfce9eb684486e2879cbd3c22d37dbc29b56f6 100644 (file)
@@ -95,6 +95,7 @@
     <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: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'">
     <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: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: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: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"/>
+        <div dactyl:highlight="HelpXMLBlock">
+            <xsl:apply-templates mode="xml-highlight"/>
+        </div>
     </xsl:template>
 
+
     <!-- Plugins {{{1 -->
 
     <xsl:template name="info">
 
     <!-- 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: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)">
index a30ef295057f33655020e429bf58aacb61b918f0..004f90716208469a8c42eaa10e952bb680ed1f91 100644 (file)
@@ -4,7 +4,7 @@
 //
 // 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 */
@@ -17,9 +17,8 @@ var HintSession = Class("HintSession", CommandMode, {
 
         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);
@@ -27,7 +26,7 @@ var HintSession = Class("HintSession", CommandMode, {
         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 = [];
@@ -35,6 +34,7 @@ var HintSession = Class("HintSession", CommandMode, {
         this.usedTabKey = false;
         this.validHints = []; // store the indices of the "hints" array with valid elements
 
+        mappings.pushCommand();
         this.open();
 
         this.top = opts.window || content;
@@ -98,6 +98,8 @@ var HintSession = Class("HintSession", CommandMode, {
         leave.superapply(this, arguments);
 
         if (!stack.push) {
+            mappings.popCommand();
+
             if (hints.hintSession == this)
                 hints.hintSession = null;
             if (this.top) {
@@ -255,7 +257,7 @@ var HintSession = Class("HintSession", CommandMode, {
     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];
@@ -298,7 +300,7 @@ var HintSession = Class("HintSession", CommandMode, {
                 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);
@@ -309,12 +311,11 @@ var HintSession = Class("HintSession", CommandMode, {
 
         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 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 baseNodeAbsolute = util.xmlToDom(<span highlight="Hint" style="display: none;"/>, doc);
+            let baseNode = DOM(<span highlight="Hint" style="display: none;"/>, doc)[0];
 
             let mode = this.hintMode;
             let res = mode.matcher(doc);
@@ -342,7 +343,7 @@ var HintSession = Class("HintSession", CommandMode, {
                 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);
@@ -402,7 +403,7 @@ var HintSession = Class("HintSession", CommandMode, {
      */
     onKeyPress: function onKeyPress(eventList) {
         const KILL = false, PASS = true;
-        let key = events.toString(eventList[0]);
+        let key = DOM.Event.stringify(eventList[0]);
 
         this.clearTimeout();
 
@@ -500,6 +501,7 @@ var HintSession = Class("HintSession", CommandMode, {
                 this.timeout(next, 50);
         }).call(this);
 
+        mappings.pushCommand();
         if (!this.continue) {
             modes.pop();
             if (timeout)
@@ -509,6 +511,7 @@ var HintSession = Class("HintSession", CommandMode, {
         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)
@@ -532,7 +535,7 @@ var HintSession = Class("HintSession", CommandMode, {
             // 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;
@@ -754,9 +757,9 @@ var Hints = Module("hints", {
         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));
 
@@ -824,7 +827,7 @@ var Hints = Module("hints", {
 
         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"])) {
@@ -1031,15 +1034,21 @@ var Hints = Module("hints", {
 
     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))
@@ -1077,7 +1086,24 @@ var Hints = Module("hints", {
         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"]],
@@ -1191,53 +1217,58 @@ var Hints = Module("hints", {
         });
     },
     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"]
@@ -1249,23 +1280,23 @@ var Hints = Module("hints", {
                         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"],
@@ -1277,7 +1308,7 @@ var Hints = Module("hints", {
                     "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"));
                 }
index a4bb2838a2102317a65682a4a562f8df7404f146..94985d2648024d9d7afb2b7906b009c3a58aaa78 100644 (file)
 //
 // 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));
+        let res = /^([+-])(.+)/.exec(sort);
+        dactyl.assert(res, _("error.invalidSort", sort));
 
-        options.sortingMode = options[order];
+        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) {
-            let node = root.getChild(i);
-            return {
-                url: node.uri,
-                title: node.title,
-                icon: node.icon ? node.icon : DEFAULT_FAVICON
-            };
-        }).toArray();
-        root.containerOpen = false; // close a container after using it!
+        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 : BookmarkCache.DEFAULT_FAVICON
+                };
+            }).toArray();
+        }
+        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;
+    /**
+     * 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 {
+            sh.index = Math.constrain(sh.index + steps, 0, sh.length - 1);
+        }
+        catch (e if e.result == Cr.NS_ERROR_FILE_NOT_FOUND) {}
+    },
 
-        if (current == start && steps < 0 || current == end && steps > 0)
-            dactyl.beep();
-        else {
-            let index = Math.constrain(current + steps, start, end);
-            try {
-                window.getWebNavigation().gotoIndex(index);
+    /**
+     * 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;
             }
-            catch (e) {} // We get NS_ERROR_FILE_NOT_FOUND if files in history don't exist
-        }
+
+        dactyl.assert(idx != null);
+        sh.index = idx;
     },
 
     goToStart: function goToStart() {
@@ -238,6 +293,33 @@ var History = Module("history", {
                 ],
                 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) {
@@ -267,30 +349,34 @@ var History = Module("history", {
             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)); },
-            { count: true });
+        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 });
     }
 });
 
diff --git a/common/content/key-processors.js b/common/content/key-processors.js
new file mode 100644 (file)
index 0000000..e1ba321
--- /dev/null
@@ -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);
+    }
+});
+
index 155849e46fa73a6fb848beef87b9be44f2172129..149f3fb86c170e264d2b03435e6e57e03b59b1f3 100644 (file)
@@ -4,7 +4,7 @@
 //
 // 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 */
 
@@ -17,7 +17,7 @@
  *     *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}
@@ -29,7 +29,7 @@
  * @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;
@@ -38,14 +38,17 @@ var Map = Class("Map", {
 
         Object.freeze(this.modes);
 
-        if (extraInfo)
-            this.update(extraInfo);
+        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]),
+    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)],
 
@@ -69,12 +72,24 @@ var Map = Class("Map", {
      *     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
@@ -104,9 +119,9 @@ var Map = Class("Map", {
                 .map(function ([i, prop]) [prop, this[i]], arguments)
                 .toObject();
 
-        args = update({ context: contexts.context },
-                      this.hive.argsExtra(args),
-                      args);
+        args = this.hive.makeArgs(this.hive.group.lastDocument,
+                                  contexts.context,
+                                  args);
 
         let self = this;
         function repeat() self.action(args)
@@ -114,10 +129,14 @@ var Map = Class("Map", {
             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();
         }
@@ -127,12 +146,17 @@ var Map = Class("Map", {
         }
         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, {
@@ -272,7 +296,7 @@ var MapHive = Class("MapHive", Contexts.Hive, {
             delete this.states;
         },
 
-        states: Class.memoize(function () {
+        states: Class.Memoize(function () {
             var states = {
                 candidates: {},
                 mappings: {}
@@ -282,7 +306,7 @@ var MapHive = Class("MapHive", Contexts.Hive, {
                 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;
@@ -298,6 +322,31 @@ var MapHive = Class("MapHive", Contexts.Hive, {
  */
 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(),
@@ -308,7 +357,7 @@ var Mappings = Module("mappings", {
 
     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)
@@ -317,14 +366,19 @@ var Mappings = Module("mappings", {
 
     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) {
-            if (/^<\*-/.test(key))
-                return ["<", this.prefixes, key.slice(3)];
-            return key;
-        }, this).flatten().array).map(function (k) events.canonicalKeys(k));
+        if (!/<\*-/.test(keys))
+            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) DOM.Event.canonicalKeys(k));
+
+        if (keys != arguments[0])
+            return [arguments[0]].concat(keys);
+        return keys;
     },
 
     iterate: function (mode) {
@@ -394,7 +448,7 @@ var Mappings = Module("mappings", {
     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.
@@ -403,7 +457,7 @@ var Mappings = Module("mappings", {
      */
     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
@@ -571,14 +625,13 @@ var Mappings = Module("mappings", {
                             .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
@@ -749,10 +802,10 @@ var Mappings = Module("mappings", {
                     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))),
+                                     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,
@@ -769,7 +822,7 @@ var Mappings = Module("mappings", {
         };
     },
     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]))]
index 8e5632838a6270d2fbb675090b6e2c186f454424..e32d0c6f9c61be4de44cff94a9738131eef0ecbc 100644 (file)
@@ -4,7 +4,7 @@
 //
 // 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
@@ -32,7 +32,24 @@ var Marks = Module("marks", {
                    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.
@@ -41,29 +58,89 @@ var Marks = Module("marks", {
      * 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;
+    add: function (name, silent) {
+        let mark = this.Mark();
 
-        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 });
-            if (!silent)
-                dactyl.log(_("mark.addURL", Marks.markToString(mark, res)), 5);
+        if (Marks.isURLMark(name)) {
+            mark.tab = util.weakReference(tabs.getTab());
+            this._urlMarks.set(name, mark);
+            var message = "mark.addURL";
         }
-        else if (Marks.isLocalMark(mark)) {
-            let marks = this._localMarks.get(doc.documentURI, {});
-            marks[mark] = { location: doc.documentURI, position: position, timestamp: Date.now()*1000 };
+        else if (Marks.isLocalMark(name)) {
+            this._localMarks.get(mark.location, {})[name] = mark;
             this._localMarks.changed();
-            if (!silent)
-                dactyl.log(_("mark.addLocal", Marks.markToString(mark, marks[mark])), 5);
+            message = "mark.addLocal";
+        }
+
+        if (!silent)
+            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.
@@ -106,7 +183,7 @@ var Marks = Module("marks", {
             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;
                 }
@@ -114,9 +191,9 @@ var Marks = Module("marks", {
             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);
@@ -147,13 +224,30 @@ var Marks = Module("marks", {
             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*.
      *
@@ -174,18 +268,20 @@ var Marks = Module("marks", {
             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;
             }
@@ -194,15 +290,25 @@ var Marks = Module("marks", {
 }, {
     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) : "");
+        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: function isLocalMark(mark) /^[a-z`']$/.test(mark),
+    isLocalMark: bind("test", /^[a-z`']$/),
 
-    isURLMark: function isURLMark(mark) /^[A-Z]$/.test(mark)
+    isURLMark: bind("test", /^[A-Z]$/)
 }, {
     events: function () {
         let appContent = document.getElementById("appcontent");
@@ -271,7 +377,9 @@ var Marks = Module("marks", {
             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;
         };
     },
index c498830903670defc855de8d4e1723fe4acc806b..50b2ee5b16f4570025d76636cb7bf1ebc74f70ca 100644 (file)
@@ -4,7 +4,7 @@
 //
 // 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 */
 
@@ -57,24 +57,8 @@ var Modes = Module("modes", {
             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]
         }, {
@@ -87,6 +71,8 @@ var Modes = Module("modes", {
                     modes.pop();
                 else if (!stack.pop && !this.pref)
                     this.pref = true;
+                if (!stack.pop)
+                    buffer.resetCaret();
             },
 
             leave: function (stack) {
@@ -94,27 +80,6 @@ var Modes = Module("modes", {
                     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",
@@ -122,20 +87,10 @@ var Modes = Module("modes", {
             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
@@ -207,69 +162,36 @@ var Modes = Module("modes", {
 
             }
         });
-
-        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;
-        return 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,
@@ -302,6 +224,18 @@ var Modes = Module("modes", {
         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))
@@ -327,9 +261,7 @@ var Modes = Module("modes", {
         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;
@@ -354,12 +286,11 @@ var Modes = Module("modes", {
         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;
 
@@ -440,6 +371,9 @@ var Modes = Module("modes", {
     },
 
     push: function push(mainMode, extendedMode, params) {
+        if (this.main == this.IGNORE)
+            this.pop();
+
         this.set(mainMode, extendedMode, params, { push: this.topOfStack });
     },
 
@@ -447,8 +381,7 @@ var Modes = Module("modes", {
         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;
@@ -489,7 +422,7 @@ var Modes = Module("modes", {
         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++,
@@ -501,12 +434,12 @@ var Modes = Module("modes", {
 
         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)) {
@@ -520,7 +453,7 @@ var Modes = Module("modes", {
 
         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,
 
@@ -528,15 +461,15 @@ var Modes = Module("modes", {
 
         hidden: false,
 
-        input: Class.memoize(function input() this.insert || this.bases.length && this.bases.some(function (b) b.input)),
+        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)),
+        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)),
+        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,
 
@@ -584,24 +517,67 @@ var Modes = Module("modes", {
         }, 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(); });
+            function () { modes.pop(null, { fromEscape: true }); });
 
-        mappings.add([modes.MENU], ["<C-c>"],
-            "Leave Menu mode",
+        mappings.add([modes.AUTOCOMPLETE, modes.MENU], ["<C-c>"],
+            "Leave Autocomplete or 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",
@@ -642,16 +618,17 @@ var Modes = Module("modes", {
 
         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) });
     }
 });
 
index 5c7a28c698e9c1a09cc859f194261cde1382f06d..770929dd5fb580df96979c5d50810aa2b7bfb297 100644 (file)
@@ -4,7 +4,7 @@
 //
 // 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() {
@@ -21,11 +21,9 @@ var MOW = Module("mow", {
             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;
 
@@ -37,14 +35,14 @@ var MOW = Module("mow", {
         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
             },
@@ -67,7 +65,7 @@ var MOW = Module("mow", {
                         </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"
@@ -81,9 +79,9 @@ var MOW = Module("mow", {
     __noSuchMethod__: function (meth, args) Buffer[meth].apply(Buffer, [this.body].concat(args)),
 
     get widget() this.widgets.multilineOutput,
-    widgets: Class.memoize(function widgets() commandline.widgets),
+    widgets: Class.Memoize(function widgets() commandline.widgets),
 
-    body: Class.memoize(function body() this.widget.contentDocument.documentElement),
+    body: Class.Memoize(function body() this.widget.contentDocument.documentElement),
     get document() this.widget.contentDocument,
     get window() this.widget.contentWindow,
 
@@ -94,7 +92,7 @@ var MOW = Module("mow", {
      * @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)
@@ -103,12 +101,15 @@ var MOW = Module("mow", {
         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 = [];
         }
@@ -119,14 +120,16 @@ var MOW = Module("mow", {
         // 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}/>,
-                                       this.document);
+            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);
@@ -135,24 +138,24 @@ var MOW = Module("mow", {
             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>;
+            let style = isString(data) ? "pre-wrap" : "nowrap";
+            this.lastOutput = <div style={"white-space: " + style} highlight={highlightGroup}>{data}</div>;
 
-            var output = util.xmlToDom(this.lastOutput, this.document);
+            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.empty();
         }
 
-        body.appendChild(output);
+        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)
@@ -170,7 +173,7 @@ var MOW = Module("mow", {
             };
 
             if (event.target instanceof HTMLAnchorElement)
-                switch (events.toString(event)) {
+                switch (DOM.Event.stringify(event)) {
                 case "<LeftMouse>":
                     openLink(dactyl.CURRENT_TAB);
                     break;
@@ -219,7 +222,7 @@ var MOW = Module("mow", {
     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();
@@ -241,16 +244,21 @@ var MOW = Module("mow", {
 
         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 = "";
 
@@ -258,9 +266,10 @@ var MOW = Module("mow", {
     },
 
     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);
     },
 
     /**
@@ -279,7 +288,7 @@ var MOW = Module("mow", {
 
         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")];
@@ -294,7 +303,7 @@ var MOW = Module("mow", {
             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;
@@ -303,6 +312,12 @@ var MOW = Module("mow", {
     })
 }, {
 }, {
+    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;
@@ -315,6 +330,11 @@ var MOW = Module("mow", {
                 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,
@@ -341,36 +361,36 @@ var MOW = Module("mow", {
 
         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); });
index 74a34c7286f5c942a06f4a19e194cfcb80e4f8db..d2a5627cd98e95ed7209d155272c94b8e6fd8e11 100644 (file)
@@ -4,7 +4,7 @@
 //
 // 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 */
 
@@ -99,9 +99,9 @@ var QuickMarks = Module("quickmarks", {
      */
     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);
 
index b5dbee70d417e97426443a35d5017ac4f563a8cb..8eefc33ecd7f2878d0e2684a7dabd5181a297844 100644 (file)
@@ -4,7 +4,7 @@
 //
 // 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 */
 
@@ -12,7 +12,6 @@ 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") {
@@ -23,7 +22,7 @@ var StatusLine = Module("statusline", {
                 #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 {
@@ -42,7 +41,7 @@ var StatusLine = Module("statusline", {
                     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[
@@ -83,7 +82,7 @@ var StatusLine = Module("statusline", {
         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()
         });
@@ -141,12 +140,16 @@ var StatusLine = Module("statusline", {
                 webProgress.DOMWindow.document.dactylSecurity = this.security;
         },
         "browser.stateChange": function onStateChange(webProgress, request, flags, status) {
-            if (flags & Ci.nsIWebProgressListener.STATE_START)
-                this.progress = 0;
-            if (flags & Ci.nsIWebProgressListener.STATE_STOP) {
-                this.progress = "";
+            const L = Ci.nsIWebProgressListener;
+
+            if (flags & (L.STATE_IS_DOCUMENT | L.STATE_IS_WINDOW))
+                if (flags & L.STATE_START)
+                    this.progress = 0;
+                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 () {
index c6210807247f555928f78738202d1896178639cb..9b906607ecd12af76702e686cbd9c99fb37b6ac8 100644 (file)
@@ -4,7 +4,7 @@
 //
 // 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 */
 
@@ -23,7 +23,7 @@ var Tabs = Module("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,
@@ -32,12 +32,12 @@ var Tabs = Module("tabs", {
                                           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 () {
@@ -57,7 +57,7 @@ var Tabs = Module("tabs", {
         }
     },
 
-    _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)) {
@@ -65,6 +65,9 @@ var Tabs = Module("tabs", {
             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");
         }
     },
 
@@ -75,18 +78,21 @@ var Tabs = Module("tabs", {
                 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);
+                        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.setAttribute("dactylOrdinal", i + 1);
             tab.dactylOrdinal = i + 1;
         }
         statusline.updateTabCount(true);
@@ -127,12 +133,7 @@ var Tabs = Module("tabs", {
     /**
      * @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),
 
@@ -151,8 +152,8 @@ var Tabs = Module("tabs", {
     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;
     },
 
     /**
@@ -161,6 +162,11 @@ var Tabs = Module("tabs", {
      */
     get localStore() this.getLocalStore(),
 
+    localStorePrototype: memoize({
+        instance: {},
+        get options() ({})
+    }),
+
     /**
      * @property {[Object]} The array of closed tabs for the current
      *     session.
@@ -219,16 +225,21 @@ var Tabs = Module("tabs", {
      *
      * @returns {Window}
      */
-    getGroups: function getGroups() {
-        if ("_groups" in this)
+    getGroups: function getGroups(func) {
+        let iframe = document.getElementById("tab-view");
+        this._groups = iframe ? iframe.contentWindow : null;
+
+        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();
+            TabView._initFrame(func);
 
-        let iframe = document.getElementById("tab-view");
         this._groups = iframe ? iframe.contentWindow : null;
-        if (this._groups)
+        if (this._groups && !func)
             util.waitFor(function () this._groups.TabItems, this);
         return this._groups;
     },
@@ -320,6 +331,60 @@ var Tabs = Module("tabs", {
         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.
      *
@@ -531,75 +596,49 @@ var Tabs = Module("tabs", {
         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",
-            function (args) {
-                let removed = 0;
-                for (let tab in matchTabs(args, args.bang, true)) {
-                    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));
-            }, {
-                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: 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 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", args[0]));
+                }, {
+                    argCount: "?",
+                    bang: true,
+                    count: true,
+                    completer: function (context) completion.buffer(context),
+                    literal: 0,
+                    privateData: true
+                });
+        });
 
         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);
             },
             {
@@ -616,7 +655,7 @@ var Tabs = Module("tabs", {
         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);
             },
             {
@@ -647,8 +686,22 @@ var Tabs = Module("tabs", {
         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(["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);
                 });
             }, {
@@ -735,7 +788,7 @@ var Tabs = Module("tabs", {
             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); }, {
@@ -778,10 +831,10 @@ var Tabs = Module("tabs", {
                     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);
                 }, {
@@ -799,7 +852,8 @@ var Tabs = Module("tabs", {
             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),
@@ -840,50 +894,71 @@ var Tabs = Module("tabs", {
                                   _("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;
+                        tabIndex = Array.indexOf(tabs.allTabs, target) - 1;
                     }
 
-                    let dummy = browser.addTab("about:blank");
+                    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:
-                            context.filters.push(function ({ item }) item != window);
-                            completion.window(context);
+                            if (args["-group"])
+                                completion.tabGroup(context);
+                            else {
+                                context.filters.push(function ({ item }) item != window);
+                                completion.window(context);
+                            }
                             break;
                         case 1:
-                            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);
+                            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
+                        }
+                    ]
                 });
         }
 
@@ -940,7 +1015,88 @@ var Tabs = Module("tabs", {
                 { 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(); });
@@ -949,7 +1105,18 @@ var Tabs = Module("tabs", {
             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); });
@@ -978,7 +1145,7 @@ var Tabs = Module("tabs", {
             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 }) {
@@ -1004,12 +1171,12 @@ var Tabs = Module("tabs", {
                 { 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 });
 
@@ -1032,17 +1199,17 @@ var Tabs = Module("tabs", {
                 { 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;
                     }
 
@@ -1063,7 +1230,7 @@ var Tabs = Module("tabs", {
                 }
             });
 
-        if (config.hasTabbrowser) {
+        if (config.has("tabbrowser")) {
             let activateGroups = [
                 ["all", "Activate everything"],
                 ["addons", ":addo[ns] command"],
@@ -1090,7 +1257,7 @@ var Tabs = Module("tabs", {
                             if (group[2])
                                 prefs.safeSet("browser.tabs." + group[2],
                                               !(valueSet["all"] ^ valueSet[group[0]]),
-                                              _("option.activate.safeSet"));
+                                              _("option.safeSet", "activate"));
                         return newValues;
                     }
                 });
@@ -1125,9 +1292,9 @@ var Tabs = Module("tabs", {
                         }
 
                         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: {
diff --git a/common/javascript.vim b/common/javascript.vim
deleted file mode 100644 (file)
index c1bfe25..0000000
+++ /dev/null
@@ -1,343 +0,0 @@
-" Vim syntax file
-" Language:     JavaScript
-" Maintainer:   Yi Zhao (ZHAOYI) <zzlinux AT hotmail DOT com>
-" Last Change:  May 17, 2007
-" Version:      0.7.5
-" Changes:      1, Get the vimdiff problem fixed finally.
-"                Matthew Gallant reported the problem and test the fix. ;)
-"               2, Follow the suggestioin from Ingo Karkat.
-"                The 'foldtext' and 'foldlevel' settings should only be
-"                changed if the file being edited is pure JavaScript,
-"                not if JavaScript syntax is embedded inside other syntaxes.
-"               3, Remove function FT_JavaScriptDoc().
-"                Since VIM do the better than me.
-"
-" TODO:
-"  - Add the HTML syntax inside the JSDoc
-
-if !exists("main_syntax")
-  if version < 600
-    syntax clear
-  elseif exists("b:current_syntax")
-    finish
-  endif
-  let main_syntax = 'javascript'
-endif
-
-"" Drop fold if it set but VIM doesn't support it.
-let b:javascript_fold='true'
-if version < 600    " Don't support the old version
-  unlet! b:javascript_fold
-endif
-
-syn include @xmlTop syntax/xml.vim
-unlet b:current_syntax
-
-syn include @cssTop syntax/css.vim
-unlet b:current_syntax
-
-"" dollar sigh is permittd anywhere in an identifier
-setlocal iskeyword+=$
-
-syntax sync fromstart
-syntax sync maxlines=200
-
-"" JavaScript comments
-syntax keyword javaScriptCommentTodo    TODO FIXME XXX TBD contained
-syntax region  javaScriptLineComment    start=+\/\/+ end="\v$|(\</?(css|e4x)\>)@=" keepend contains=javaScriptCommentTodo,@Spell
-syntax region  javaScriptLineComment    start=+^\s*\/\/+ skip=+\n\s*\/\/+ end="\v$|(\</?(css|e4x)\>)@=" keepend contains=javaScriptCommentTodo,@Spell fold
-syntax region  javaScriptCvsTag         start="\$\cid:" end="\$" oneline contained
-syntax region  javaScriptComment        start="/\*"  end="\v\*/|(\</?(css|e4x)\>)@=" contains=javaScriptCommentTodo,javaScriptCvsTag,@Spell fold
-
-"" JSDoc support start
-if !exists("javascript_ignore_javaScriptdoc")
-  syntax case ignore
-
-  "" syntax coloring for javadoc comments (HTML)
-  "syntax include @javaHtml <sfile>:p:h/html.vim
-  "unlet b:current_syntax
-
-  syntax region javaScriptDocComment    matchgroup=javaScriptComment start="/\*\*\s*$"  end="\*/" contains=javaScriptDocTags,javaScriptCommentTodo,javaScriptCvsTag,@javaScriptHtml,@Spell fold
-  syntax match  javaScriptDocTags       contained "@\(param\|argument\|requires\|exception\|throws\|type\|class\|extends\|see\|link\|member\|module\|method\|title\|namespace\|optional\|default\|base\|file\)\>" nextgroup=javaScriptDocParam,javaScriptDocSeeTag skipwhite
-  syntax match  javaScriptDocTags       contained "@\(beta\|deprecated\|description\|fileoverview\|author\|license\|version\|returns\=\|constructor\|private\|protected\|final\|ignore\|addon\|exec\)\>"
-  syntax match  javaScriptDocParam      contained "\%(#\|\w\|\.\|:\|\/\)\+"
-  syntax region javaScriptDocSeeTag     contained matchgroup=javaScriptDocSeeTag start="{" end="}" contains=javaScriptDocTags
-
-  syntax case match
-endif   "" JSDoc end
-
-syntax case match
-
-"" Syntax in the JavaScript code
-syntax match   javaScriptSpecial        "\\\d\d\d\|\\x\x\{2\}\|\\u\x\{4\}\|\\."
-syntax region  javaScriptStringD        start=+"+  skip=+\\\\\|\\$"+  end=+"+  contains=javaScriptSpecial,@htmlPreproc
-syntax region  javaScriptStringS        start=+'+  skip=+\\\\\|\\$'+  end=+'+  contains=javaScriptSpecial,@htmlPreproc
-syntax region  javaScriptRegexpString   start=+/\(\*\|/\)\@!+ skip=+\\\\\|\\/+ end=+/[gim]\{-,3}+ contains=javaScriptSpecial,@htmlPreproc oneline
-syntax match   javaScriptNumber         /\<-\=\d\+L\=\>\|\<0[xX]\x\+\>/
-syntax match   javaScriptFloat          /\<-\=\%(\d\+\.\d\+\|\d\+\.\|\.\d\+\)\%([eE][+-]\=\d\+\)\=\>/
-syntax match   javaScriptLabel          /\(?\s*\)\@<!\<\w\+\(\s*:\)\@=/
-
-"" JavaScript Prototype
-syntax keyword javaScriptPrototype      prototype
-
-"" Programm Keywords
-syntax keyword javaScriptSource         import export
-syntax keyword javaScriptType           const this var void yield arguments
-syntax keyword javaScriptOperator       delete new in instanceof let typeof
-syntax keyword javaScriptBoolean        true false
-syntax keyword javaScriptNull           null
-
-"" Statement Keywords
-syntax keyword javaScriptConditional    if else
-syntax keyword javaScriptRepeat         do while for
-syntax keyword javaScriptBranch         break continue switch case default return
-syntax keyword javaScriptStatement      try catch throw with finally
-
-syntax keyword javaScriptGlobalObjects  Array Boolean Date Function Infinity JavaArray JavaClass JavaObject JavaPackage Math Number NaN Object Packages RegExp String Undefined java netscape sun
-
-syntax keyword javaScriptExceptions     Error EvalError RangeError ReferenceError SyntaxError TypeError URIError
-
-syntax keyword javaScriptFutureKeys     abstract enum int short boolean export interface static byte extends long super char final native synchronized class float package throws goto private transient debugger implements protected volatile double import public
-
-"" DOM/HTML/CSS specified things
-
-  " DOM2 Objects
-  syntax keyword javaScriptGlobalObjects  DOMImplementation DocumentFragment Document Node NodeList NamedNodeMap CharacterData Attr Element Text Comment CDATASection DocumentType Notation Entity EntityReference ProcessingInstruction
-  syntax keyword javaScriptExceptions     DOMException
-
-  " DOM2 CONSTANT
-  syntax keyword javaScriptDomErrNo       INDEX_SIZE_ERR DOMSTRING_SIZE_ERR HIERARCHY_REQUEST_ERR WRONG_DOCUMENT_ERR INVALID_CHARACTER_ERR NO_DATA_ALLOWED_ERR NO_MODIFICATION_ALLOWED_ERR NOT_FOUND_ERR NOT_SUPPORTED_ERR INUSE_ATTRIBUTE_ERR INVALID_STATE_ERR SYNTAX_ERR INVALID_MODIFICATION_ERR NAMESPACE_ERR INVALID_ACCESS_ERR
-  syntax keyword javaScriptDomNodeConsts  ELEMENT_NODE ATTRIBUTE_NODE TEXT_NODE CDATA_SECTION_NODE ENTITY_REFERENCE_NODE ENTITY_NODE PROCESSING_INSTRUCTION_NODE COMMENT_NODE DOCUMENT_NODE DOCUMENT_TYPE_NODE DOCUMENT_FRAGMENT_NODE NOTATION_NODE
-
-  " HTML events and internal variables
-  syntax case ignore
-  syntax keyword javaScriptHtmlEvents     onblur onclick oncontextmenu ondblclick onfocus onkeydown onkeypress onkeyup onmousedown onmousemove onmouseout onmouseover onmouseup onresize
-  syntax case match
-
-" Follow stuff should be highligh within a special context
-" While it can't be handled with context depended with Regex based highlight
-" So, turn it off by default
-if exists("javascript_enable_domhtmlcss")
-
-    " DOM2 things
-    syntax match javaScriptDomElemAttrs     contained /\%(nodeName\|nodeValue\|nodeType\|parentNode\|childNodes\|firstChild\|lastChild\|previousSibling\|nextSibling\|attributes\|ownerDocument\|namespaceURI\|prefix\|localName\|tagName\)\>/
-    syntax match javaScriptDomElemFuncs     contained /\%(insertBefore\|replaceChild\|removeChild\|appendChild\|hasChildNodes\|cloneNode\|normalize\|isSupported\|hasAttributes\|getAttribute\|setAttribute\|removeAttribute\|getAttributeNode\|setAttributeNode\|removeAttributeNode\|getElementsByTagName\|getAttributeNS\|setAttributeNS\|removeAttributeNS\|getAttributeNodeNS\|setAttributeNodeNS\|getElementsByTagNameNS\|hasAttribute\|hasAttributeNS\)\>/ nextgroup=javaScriptParen skipwhite
-    " HTML things
-    syntax match javaScriptHtmlElemAttrs    contained /\%(className\|clientHeight\|clientLeft\|clientTop\|clientWidth\|dir\|id\|innerHTML\|lang\|length\|offsetHeight\|offsetLeft\|offsetParent\|offsetTop\|offsetWidth\|scrollHeight\|scrollLeft\|scrollTop\|scrollWidth\|style\|tabIndex\|title\)\>/
-    syntax match javaScriptHtmlElemFuncs    contained /\%(blur\|click\|focus\|scrollIntoView\|addEventListener\|dispatchEvent\|removeEventListener\|item\)\>/ nextgroup=javaScriptParen skipwhite
-
-    " CSS Styles in JavaScript
-    syntax keyword javaScriptCssStyles      contained color font fontFamily fontSize fontSizeAdjust fontStretch fontStyle fontVariant fontWeight letterSpacing lineBreak lineHeight quotes rubyAlign rubyOverhang rubyPosition
-    syntax keyword javaScriptCssStyles      contained textAlign textAlignLast textAutospace textDecoration textIndent textJustify textJustifyTrim textKashidaSpace textOverflowW6 textShadow textTransform textUnderlinePosition
-    syntax keyword javaScriptCssStyles      contained unicodeBidi whiteSpace wordBreak wordSpacing wordWrap writingMode
-    syntax keyword javaScriptCssStyles      contained bottom height left position right top width zIndex
-    syntax keyword javaScriptCssStyles      contained border borderBottom borderLeft borderRight borderTop borderBottomColor borderLeftColor borderTopColor borderBottomStyle borderLeftStyle borderRightStyle borderTopStyle borderBottomWidth borderLeftWidth borderRightWidth borderTopWidth borderColor borderStyle borderWidth borderCollapse borderSpacing captionSide emptyCells tableLayout
-    syntax keyword javaScriptCssStyles      contained margin marginBottom marginLeft marginRight marginTop outline outlineColor outlineStyle outlineWidth padding paddingBottom paddingLeft paddingRight paddingTop
-    syntax keyword javaScriptCssStyles      contained listStyle listStyleImage listStylePosition listStyleType
-    syntax keyword javaScriptCssStyles      contained background backgroundAttachment backgroundColor backgroundImage gackgroundPosition backgroundPositionX backgroundPositionY backgroundRepeat
-    syntax keyword javaScriptCssStyles      contained clear clip clipBottom clipLeft clipRight clipTop content counterIncrement counterReset cssFloat cursor direction display filter layoutGrid layoutGridChar layoutGridLine layoutGridMode layoutGridType
-    syntax keyword javaScriptCssStyles      contained marks maxHeight maxWidth minHeight minWidth opacity MozOpacity overflow overflowX overflowY verticalAlign visibility zoom cssText
-    syntax keyword javaScriptCssStyles      contained scrollbar3dLightColor scrollbarArrowColor scrollbarBaseColor scrollbarDarkShadowColor scrollbarFaceColor scrollbarHighlightColor scrollbarShadowColor scrollbarTrackColor
-
-    " Highlight ways
-    syntax match javaScriptDotNotation      "\." nextgroup=javaScriptPrototype,javaScriptDomElemAttrs,javaScriptDomElemFuncs,javaScriptHtmlElemAttrs,javaScriptHtmlElemFuncs
-    syntax match javaScriptDotNotation      "\.style\." nextgroup=javaScriptCssStyles
-
-endif "DOM/HTML/CSS
-
-"" end DOM/HTML/CSS specified things
-
-
-"" Code blocks
-syntax cluster javaScriptAll       contains=javaScriptComment,javaScriptLineComment,javaScriptDocComment,javaScriptStringD,javaScriptStringS,javaScriptRegexpString,javaScriptNumber,javaScriptFloat,javaScriptLabel,javaScriptSource,javaScriptType,javaScriptOperator,javaScriptBoolean,javaScriptNull,javaScriptFunction,javaScriptConditional,javaScriptRepeat,javaScriptBranch,javaScriptStatement,javaScriptGlobalObjects,javaScriptExceptions,javaScriptFutureKeys,javaScriptDomErrNo,javaScriptDomNodeConsts,javaScriptHtmlEvents,javaScriptDotNotation,javascriptE4X,javascriptCSS,javascriptCDATA
-syntax region  javaScriptBracket   matchgroup=javaScriptBracket transparent start="\[" end="\]" contains=@javaScriptAll,javaScriptParensErrB,javaScriptParensErrC,javaScriptBracket,javaScriptParen,javaScriptBlock,@htmlPreproc
-syntax region  javaScriptParen     matchgroup=javaScriptParen   transparent start="("  end=")"  contains=@javaScriptAll,javaScriptParensErrA,javaScriptParensErrC,javaScriptParen,javaScriptBracket,javaScriptBlock,@htmlPreproc
-syntax region  javaScriptBlock     matchgroup=javaScriptBlock   transparent start="{"  end="}"  contains=@javaScriptAll,javaScriptParensErrA,javaScriptParensErrB,javaScriptParen,javaScriptBracket,javaScriptBlock,@htmlPreproc
-
-syntax region  javascriptCDATA matchgroup=javascriptCDATA start="<\!\[CDATA\[" end="\]\]>" keepend contains=javascriptCSS
-syntax region  javascriptCSS   matchgroup=javascriptCSSDelimiter start="<css>" end="</css>" contains=@cssTop
-syntax region  javascriptE4X   matchgroup=javascriptE4XDelimiter start="<e4x>" end="</e4x>" contains=@xmlTop
-syntax region  javascriptE4X   matchgroup=javascriptE4XDelimiter start="<>" end="</>" contains=@xmlTop oneline
-
-"" catch errors caused by wrong parenthesis
-syntax match   javaScriptParensError    ")\|}\|\]"
-syntax match   javaScriptParensErrA     contained "\]"
-syntax match   javaScriptParensErrB     contained ")"
-syntax match   javaScriptParensErrC     contained "}"
-
-if main_syntax == "javascript"
-  syntax sync ccomment javaScriptComment
-endif
-
-"" Fold control
-if exists("b:javascript_fold")
-    syntax match   javaScriptFunction       /\<function\>/ nextgroup=javaScriptFuncName skipwhite
-    syntax match   javaScriptOpAssign       /=\@<!=/ nextgroup=javaScriptFuncBlock skipwhite skipempty
-    syntax region  javaScriptFuncName       contained matchgroup=javaScriptFuncName start=/\%(\$\|\w\)*\s*(/ end=/)/ contains=javaScriptLineComment,javaScriptComment nextgroup=javaScriptFuncBlock skipwhite skipempty
-    syntax region  javaScriptFuncBlock      contained matchgroup=javaScriptFuncBlock start="{" end="}" contains=@javaScriptAll,javaScriptParensErrA,javaScriptParensErrB,javaScriptParen,javaScriptBracket,javaScriptBlock fold
-
-    if &l:filetype=='javascript' && !&diff
-      " Fold setting
-      " Redefine the foldtext (to show a JS function outline) and foldlevel
-      " only if the entire buffer is JavaScript, but not if JavaScript syntax
-      " is embedded in another syntax (e.g. HTML).
-      setlocal foldmethod=syntax
-      setlocal foldlevel=4
-    endif
-else
-    syntax keyword javaScriptFunction       function
-    setlocal foldmethod<
-    setlocal foldlevel<
-endif
-
-" Define the default highlighting.
-" For version 5.7 and earlier: only when not done already
-" For version 5.8 and later: only when an item doesn't have highlighting yet
-if version >= 508 || !exists("did_javascript_syn_inits")
-  if version < 508
-    let did_javascript_syn_inits = 1
-    command -nargs=+ HiLink hi link <args>
-  else
-    command -nargs=+ HiLink hi def link <args>
-  endif
-  HiLink javascriptCDATA                String
-  HiLink javaScriptComment              Comment
-  HiLink javaScriptLineComment          Comment
-  HiLink javaScriptDocComment           Comment
-  HiLink javaScriptCommentTodo          Todo
-  HiLink javaScriptCvsTag               Function
-  HiLink javaScriptDocTags              Special
-  HiLink javaScriptDocSeeTag            Function
-  HiLink javaScriptDocParam             Function
-  HiLink javaScriptStringS              String
-  HiLink javaScriptStringD              String
-  HiLink javaScriptRegexpString         String
-  HiLink javaScriptCharacter            Character
-  HiLink javaScriptPrototype            Type
-  HiLink javaScriptConditional          Conditional
-  HiLink javaScriptBranch               Conditional
-  HiLink javaScriptRepeat               Repeat
-  HiLink javaScriptStatement            Statement
-  HiLink javaScriptFunction             Function
-  HiLink javaScriptError                Error
-  HiLink javaScriptParensError          Error
-  HiLink javaScriptParensErrA           Error
-  HiLink javaScriptParensErrB           Error
-  HiLink javaScriptParensErrC           Error
-  HiLink javaScriptOperator             Operator
-  HiLink javaScriptType                 Type
-  HiLink javaScriptNull                 Type
-  HiLink javaScriptNumber               Number
-  HiLink javaScriptFloat                Number
-  HiLink javaScriptBoolean              Boolean
-  HiLink javaScriptLabel                Label
-  HiLink javaScriptSpecial              Special
-  HiLink javaScriptSource               Special
-  HiLink javaScriptGlobalObjects        Special
-  HiLink javaScriptExceptions           Special
-
-  HiLink javaScriptDomErrNo             Constant
-  HiLink javaScriptDomNodeConsts        Constant
-  HiLink javaScriptDomElemAttrs         Label
-  HiLink javaScriptDomElemFuncs         PreProc
-
-  HiLink javaScriptHtmlEvents           Special
-  HiLink javaScriptHtmlElemAttrs        Label
-  HiLink javaScriptHtmlElemFuncs        PreProc
-
-  HiLink javaScriptCssStyles            Label
-
-  delcommand HiLink
-endif
-
-" Define the htmlJavaScript for HTML syntax html.vim
-"syntax clear htmlJavaScript
-"syntax clear javaScriptExpression
-syntax cluster  htmlJavaScript contains=@javaScriptAll,javaScriptBracket,javaScriptParen,javaScriptBlock,javaScriptParenError
-syntax cluster  javaScriptExpression contains=@javaScriptAll,javaScriptBracket,javaScriptParen,javaScriptBlock,javaScriptParenError,@htmlPreproc
-
-let b:current_syntax = "javascript"
-if main_syntax == 'javascript'
-  unlet main_syntax
-endif
-
-
-if exists('b:did_indent')
-  finish
-endif
-let b:did_indent = 1
-
-setlocal indentexpr=GetJsIndent()
-setlocal indentkeys=0{,0},0),:,!^F,O,e,=*/
-" Clean CR when the file is in Unix format
-if &fileformat == "unix"
-    silent! %s/\r$//g
-endif
-" Only define the functions once per Vim session.
-"if exists("*GetJsIndent")
-"    finish
-"endif
-"function! GetJsIndent()
-"    let pnum = prevnonblank(v:lnum - 1)
-"    if pnum == 0
-"       return 0
-"    endif
-"    let line = getline(v:lnum)
-"    let pline = getline(pnum)
-"    let ind = indent(pnum)
-"
-"    if pline =~ '{\s*$\|[\s*$\|(\s*$'
-"      let ind = ind + &sw
-"    endif
-"
-"    if pline =~ ';\s*$' && line =~ '^\s*}'
-"        let ind = ind - &sw
-"    endif
-"
-"    if pline =~ '\s*]\s*$' && line =~ '^\s*),\s*$'
-"      let ind = ind - &sw
-"    endif
-"
-"    if pline =~ '\s*]\s*$' && line =~ '^\s*}\s*$'
-"      let ind = ind - &sw
-"    endif
-"
-"    if line =~ '^\s*});\s*$\|^\s*);\s*$' && pline !~ ';\s*$'
-"      let ind = ind - &sw
-"    endif
-"
-"    if line =~ '^\s*})' && pline =~ '\s*,\s*$'
-"      let ind = ind - &sw
-"    endif
-"
-"    if line =~ '^\s*}();\s*$' && pline =~ '^\s*}\s*$'
-"      let ind = ind - &sw
-"    endif
-"
-"    if line =~ '^\s*}),\s*$'
-"      let ind = ind - &sw
-"    endif
-"
-"    if pline =~ '^\s*}\s*$' && line =~ '),\s*$'
-"       let ind = ind - &sw
-"    endif
-"
-"    if pline =~ '^\s*for\s*' && line =~ ')\s*$'
-"       let ind = ind + &sw
-"    endif
-"
-"    if line =~ '^\s*}\s*$\|^\s*]\s*$\|\s*},\|\s*]);\s*\|\s*}]\s*$\|\s*};\s*$\|\s*})$\|\s*}).el$' && pline !~ '\s*;\s*$\|\s*]\s*$' && line !~ '^\s*{' && line !~ '\s*{\s*}\s*'
-"          let ind = ind - &sw
-"    endif
-"
-"    if pline =~ '^\s*/\*'
-"      let ind = ind + 1
-"    endif
-"
-"    if pline =~ '\*/$'
-"      let ind = ind - 1
-"    endif
-"    return ind
-"endfunction
-
-" vim: ts=4
index 59c18308e46e1ff2b9c558415efdf6f26347c8d8..7f6281f543b1be193d361a79c5a3f4a2b92d6399 100644 (file)
@@ -35,6 +35,7 @@
 <include href="developer" tag="developer.xml"/>
 <include href="various" tag="various.xml"/>
 <include href="plugins" tag="plugins.xml"/>
+<include href="versions" tag="versions.xml"/>
 <include href="faq" tag="faq.xml"/>
 <include href="index" tag="index.xml"/>
 
index b2719c05452d795559fb925fbc40d9d7de4994f2..6cb779be3be6019be4e173dbd439c2da1d3175c5 100644 (file)
 
 <p>Enable <em>Pass Through</em> mode on <em>some</em> Google sites:</p>
 
-<code><ex>:autocmd LocationChange</ex> <str delim="'">^https?://(www|mail)\.google\.com/</str> <ex>:normal!</ex> <k name="C-z"/></code>
+<code><ex>:autocmd LocationChange</ex> <str delim="">www.google.com</str>,<str delim="">mail.google.com</str> <ex>:normal!</ex> <k name="C-z"/></code>
 
 <p>or</p>
 
-<code><ex>:autocmd LocationChange</ex> <str delim="">www.google.com</str>,<str delim="">mail.google.com</str> <ex>:normal!</ex> <k name="C-z"/></code>
+<code><ex>:autocmd LocationChange</ex> <str delim="'">^https?://(www|mail)\.google\.com/</str> <ex>:normal!</ex> <k name="C-z"/></code>
 
 <p>Set the filetype to mail when editing email at Gmail:</p>
 
index 0e0c4075e05244a8ea4e32a137cc3504273cd61a..de536683fba57122c0c4e969c042e0baad84181d 100644 (file)
 <h1 tag="surfing browsing">Browsing</h1>
 <toc start="2"/>
 
-<h2 tag="bypassing-&dactyl.name;">Bypassing &dactyl.appName;</h2>
-
-&dactyl.appName; overrides nearly all &dactyl.host; keys in order to
-make browsing more pleasant for Vim users. On the occasions when you
-want to bypass &dactyl.appName;'s key handling and pass keys directly to
-&dactyl.host; or to a web page, you have several options:
-
-<item>
-    <tags><![CDATA[<pass-next-key-builtin> <A-b>]]></tags>
-    <spec><![CDATA[<A-b>]]></spec>
-    <description>
-        <p>
-            Process the next key as a builtin mapping, ignoring any user defined
-            mappings and <o>passkeys</o> settings.
-        </p>
-    </description>
-</item>
-
-<item>
-    <tags><![CDATA[send-key <pass-next-key> <C-v> CTRL-V]]></tags>
-    <spec><![CDATA[<C-v>]]></spec>
-    <description>
-        <p>
-            Pass the next key press directly to &dactyl.host;.
-        </p>
-    </description>
-</item>
-
-<item>
-    <tags><![CDATA[pass-through <pass-all-keys> <C-z> CTRL-Z]]></tags>
-    <spec><![CDATA[<C-z>]]></spec>
-    <description>
-        <p>
-            Pass all keys except for <k name="Esc"/> directly to
-            &dactyl.host;. When <k name="Esc"/> is pressed,
-            resume normal key handling. This is especially useful
-            for web sites which make heavy use of key bindings.
-        </p>
-    </description>
-</item>
-
-<p>
-    See also <o>passkeys</o> and <o>passunknown</o> for ways to permanently pass
-    all or particular keys under certain conditions.
-</p>
-
 <h2 tag="opening">Opening web pages</h2>
 
 <item>
@@ -132,6 +86,16 @@ want to bypass &dactyl.appName;'s key handling and pass keys directly to
     </description>
 </item>
 
+<item>
+    <tags>O</tags>
+    <spec>O</spec>
+    <description short="true">
+        <p>
+            Open an <ex>:open</ex> prompt followed by the current URL.
+        </p>
+    </description>
+</item>
+
 <item>
     <tags>T</tags>
     <spec>T</spec>
@@ -142,6 +106,22 @@ want to bypass &dactyl.appName;'s key handling and pass keys directly to
     </description>
 </item>
 
+<item>
+    <tags>s</tags>
+    <spec>s</spec>
+    <description short="true">
+        <p>Open a search prompt.</p>
+    </description>
+</item>
+
+<item>
+    <tags>S</tags>
+    <spec>S</spec>
+    <description short="true">
+        <p>Open a search prompt for a new tab.</p>
+    </description>
+</item>
+
 <item>
     <tags>:tabdu :tabduplicate</tags>
     <spec>:<oa>count</oa>tabdu<oa>plicate</oa><oa>!</oa></spec>
@@ -154,16 +134,6 @@ want to bypass &dactyl.appName;'s key handling and pass keys directly to
     </description>
 </item>
 
-<item>
-    <tags>O</tags>
-    <spec>O</spec>
-    <description short="true">
-        <p>
-            Open an <ex>:open</ex> prompt followed by the current URL.
-        </p>
-    </description>
-</item>
-
 <item>
     <tags>w :winopen :wopen</tags>
     <spec>:wino<oa>pen</oa><oa>!</oa> <oa>args</oa></spec>
@@ -253,57 +223,159 @@ want to bypass &dactyl.appName;'s key handling and pass keys directly to
     </description>
 </item>
 
+<h2 tag="history">History</h2>
+
 <item>
-    <tags><![CDATA[<open-home-directory> ~]]></tags>
-    <spec>~</spec>
-    <description short="true">
-        <p>Open home directory. Equivalent to <ex>:open ~/</ex></p>
+    <tags><![CDATA[<C-o>]]></tags>
+    <strut/>
+    <spec><oa>count</oa><![CDATA[<C-o>]]></spec>
+    <description>
+        <p>
+            Go to an older position in the jump list.
+            If <oa>count</oa> is specified, jump <oa>count</oa> positions backward.
+        </p>
     </description>
 </item>
 
-<h2 tag="navigating">Navigating</h2>
+<item>
+    <tags><![CDATA[<C-i>]]></tags>
+    <strut/>
+    <spec><oa>count</oa><![CDATA[<C-i>]]></spec>
+    <description>
+        <p>
+            Go to a newer position in the jump list.
+            If <oa>count</oa> is specified, jump <oa>count</oa> positions forward.
+        </p>
+    </description>
+</item>
 
 <item>
-    <tags><![CDATA[H <C-o> CTRL-O :ba :back]]></tags>
+    <tags>:ju :jumps</tags>
+    <strut/>
+    <spec>:ju<oa>mps</oa></spec>
+    <description>
+        <p>
+            Display the jump list.
+            The jump numbers shown are suitable as arguments to <k name="C-o"/>
+            or <k name="C-i"/>.
+        </p>
+    </description>
+</item>
+
+<item>
+    <tags><![CDATA[<M-Left> <A-Left> H]]></tags>
+    <strut/>
+    <spec>[count]H</spec>
+    <description>
+        <p>
+            Go back in the browser history. If <oa>count</oa> is specified, go
+            back <oa>count</oa> pages.
+        </p>
+    </description>
+</item>
+
+<item>
+    <tags><![CDATA[<M-Right> <A-Right> L]]></tags>
+    <strut/>
+    <spec><oa>count</oa>L</spec>
+    <description>
+        <p>
+            Go forward in the browser history. If <oa>count</oa> is specified,
+            go forward <oa>count</oa> pages.
+        </p>
+    </description>
+</item>
+
+<item>
+    <tags>:ba :back</tags>
     <spec>:<oa>count</oa>ba<oa>ck</oa> <oa>url</oa></spec>
-    <spec>:ba<oa>ck</oa>!</spec>
-    <spec><oa>count</oa>&lt;C-o></spec>
+    <spec>:<oa>count</oa>ba<oa>ck</oa>!</spec>
     <description>
         <p>
-            Go <oa>count</oa> pages back in the browser history. If
-            <oa>url</oa> is specified go back to the first matching
-            URL. The special version <ex>:back!</ex> goes to the
-            beginning of the browser history.
+            Go back in the browser history. If <oa>count</oa> is specified, go
+            back <oa>count</oa> pages.
+        </p>
+        <p>
+            The special version <ex>:back!</ex> goes to the beginning of the browser history.
         </p>
     </description>
 </item>
 
 <item>
-    <tags><![CDATA[L <C-i> CTRL-I :fo :fw :forward]]></tags>
+    <tags>:fw :fo :forward</tags>
     <spec>:<oa>count</oa>fo<oa>rward</oa> <oa>url</oa></spec>
-    <spec>:fo<oa>rward</oa>!</spec>
-    <spec><oa>count</oa>&lt;C-i></spec>
+    <spec>:<oa>count</oa>fo<oa>rward</oa>!</spec>
     <description>
         <p>
-            Go <oa>count</oa> pages forward in the browser history.
-            If <oa>url</oa> is specified go forward to the first
-            matching URL. The special version <ex>:forward!</ex>
-            goes to the end of the browser history.
+            Go forward in the browser history. If <oa>count</oa> is specified,
+            go forward <oa>count</oa> pages.
+        </p>
+        <p>
+            The special version <ex>:forward!</ex> goes to the end of the browser history.
         </p>
     </description>
 </item>
 
 <item>
-    <tags>:ju :jumps</tags>
-    <spec>:ju<oa>mps</oa></spec>
+    <tags><![CDATA[[d]]></tags>
+    <spec><oa>count</oa>[d</spec>
+    <description>
+        <p>
+            Go to the <oa>count</oa>th previous domain in the history stack.
+        </p>
+    </description>
+</item>
+
+<item>
+    <tags><![CDATA[]d]]></tags>
+    <spec><oa>count</oa>]d</spec>
+    <description>
+        <p>
+            Go to the <oa>count</oa>th next domain in the history stack.
+        </p>
+    </description>
+</item>
+
+<item>
+    <tags>:hs :hist :history</tags>
+    <spec>:hist<oa>ory</oa><oa>!</oa> <oa>filter</oa></spec>
     <description>
-        <p>List all jumps, i.e., the current tab's session history.</p>
+        <p>
+            Show recently visited URLs. Opens the message window at the bottom of the screen
+            with all history items whose page titles or URLs match
+            <oa>filter</oa>.
+        </p>
 
         <p>
-            Current history position is marked with <em>></em>.
-            Jump numbers may be used as counts for
-            <ex>:back</ex> or <ex>:forward</ex>.
+            The special version <ex>:history!</ex> works the same as
+            <ex>:history</ex> except that it opens all matching
+            pages in new tabs rather than listing them.
         </p>
+
+        <p>The pages may also be filtered via the following options,</p>
+
+        <dl dt="width: 8em;">
+            <dt>-max</dt>
+            <dd>
+                The maximum number of items to list or open
+                (short name <em>-m</em>).
+            </dd>
+            <dt>-sort</dt>
+            <dd>
+                The sort order of the results
+                (short name <em>-s</em>).
+            </dd>
+        </dl>
+    </description>
+</item>
+
+<h2 tag="navigating">Navigating</h2>
+
+<item>
+    <tags><![CDATA[<open-home-directory> ~]]></tags>
+    <spec>~</spec>
+    <description short="true">
+        <p>Open home directory. Equivalent to <ex>:open ~/</ex></p>
     </description>
 </item>
 
index 7f514a91f46d58d8dc2fa02f932347bce07e8f1c..42462c72db782fbbbabc12016bc0688457708555 100644 (file)
     <description>
         <p>
             Go to the end of the document. With <oa>count</oa>,
-            behaves exactly the same as <oa>gg</oa>.
+            go to the <oa>count</oa>th line as determined by <o>linenumbers</o>,
+            or by the line height of the document body otherwise.
         </p>
     </description>
 </item>
 
 <item>
-    <tags>&lt;scroll-percent> N%</tags>
+    <tags>&lt;scroll-percent> N% %</tags>
     <spec><a>count</a>%</spec>
     <description short="true">
         <p>Scroll to <a>count</a> percent of the document.</p>
     <description>
         <p>
             Scroll window downwards by the amount specified in the
-            <o>scroll</o> option. With <oa>count</oa>, scroll as if
-            <o>scroll</o> were set to <oa>count</oa>.
+            <o>scroll</o> option. With <oa>count</oa>, set the <o>scroll</o>
+            option to <oa>count</oa> before executing the command.
         </p>
     </description>
 </item>
     <spec><oa>count</oa>&lt;C-u></spec>
     <description>
         <p>
-            Scroll window upwards by the amount specified in the
-            <o>scroll</o> option. With <oa>count</oa>, scroll as if
-            <o>scroll</o> were set to <oa>count</oa>.
+            Scroll window upwards by the amount specified in the <o>scroll</o>
+            option. With <oa>count</oa>, set the <o>scroll</o> option to
+            <oa>count</oa> before executing the command.
         </p>
     </description>
 </item>
     </description>
 </item>
 
+<item>
+    <tags>g]</tags>
+    <spec><oa>count</oa>g]<a>arg</a></spec>
+    <description short="true">
+        <p>Jump to the next off-screen element as defined by <o>jumptags</o>.</p>
+    </description>
+</item>
+
+
 <item>
     <tags>{</tags>
     <spec><oa>count</oa>{</spec>
 </item>
 
 <item>
-    <tags>&lt;yank-word> Y</tags>
+    <tags>&lt;yank-selection> Y</tags>
     <spec>Y</spec>
     <description short="true">
         <p>Copy currently selected text to the system clipboard.</p>
index b44c8bb3c904a79685c43a2893ba53f397212a6f..4b53f7bcc5b4906be1c49af9af1c5df86223780b 100644 (file)
@@ -73,7 +73,7 @@
 </item>
 
 <item>
-    <tags><![CDATA[c_<Up>]]></tags>
+    <tags><![CDATA[c_<A-p> c_<Up> c_<cmd-prev-match>]]></tags>
     <strut/>
     <spec>&lt;Up></spec>
     <description>
@@ -85,7 +85,7 @@
 </item>
 
 <item>
-    <tags><![CDATA[c_<Down>]]></tags>
+    <tags><![CDATA[c_<A-n> c_<Down> c_<cmd-next-match>]]></tags>
     <strut/>
     <spec>&lt;Down></spec>
     <description>
@@ -97,7 +97,7 @@
 </item>
 
 <item>
-    <tags><![CDATA[c_<C-p> c_<S-Up> c_<PageUp>]]></tags>
+    <tags><![CDATA[c_<C-p> c_<S-Up> c_<cmd-prev>]]></tags>
     <spec>&lt;S-Up></spec>
     <strut/>
     <spec>&lt;PageUp></spec>
 </item>
 
 <item>
-    <tags><![CDATA[c_<C-n> c_<S-Down> c_<PageDown>]]></tags>
+    <tags><![CDATA[c_<C-n> c_<S-Down> c_<cmd-next>]]></tags>
     <spec>&lt;S-Down></spec>
     <spec>&lt;PageDown></spec>
     <description>
 <h2 tag="cmdline-completion">Command-line completion</h2>
 
 <item>
-    <tags><![CDATA[c_<Tab>]]></tags>
+    <tags><![CDATA[c_<Tab> c_<compl-next>]]></tags>
     <strut/>
     <spec>&lt;Tab></spec>
     <description>
 </item>
 
 <item>
-    <tags><![CDATA[c_<S-Tab>]]></tags>
+    <tags><![CDATA[c_<S-Tab> c_<compl-prev>]]></tags>
     <strut/>
     <spec>&lt;S-Tab></spec>
     <description>
 </item>
 
 <item>
-    <tags><![CDATA[c_<A-Tab>]]></tags>
+    <tags><![CDATA[c_<A-Tab> c_<A-compl-next>]]></tags>
     <strut/>
     <spec>&lt;A-Tab></spec>
     <description>
 </item>
 
 <item>
-    <tags><![CDATA[c_<A-S-Tab>]]></tags>
+    <tags><![CDATA[c_<A-S-Tab> c_<A-compl-prev>]]></tags>
     <strut/>
     <spec>&lt;A-S-Tab></spec>
     <description>
     </description>
 </item>
 
+<item>
+    <tags><![CDATA[c_<A-f> c_<C-Tab> c_<compl-next-group>]]></tags>
+    <spec>&lt;C-Tab></spec>
+    <description>
+        <p>Select the next matching completion group.</p>
+    </description>
+</item>
+
+<item>
+    <tags><![CDATA[c_<A-S-F> c_<C-S-Tab> c_<compl-prev-group>]]></tags>
+    <spec>&lt;C-S-Tab></spec>
+    <description>
+        <p>Select the previous matching completion group.</p>
+    </description>
+</item>
+
+
 <h2 tag="cmdline-lines">Ex command lines</h2>
 
 <item>
index 43fb99ed1417962f5e9be7f97d8d5578869e3d76..869d4c431075dfe15050a7eb4e81c3942ce0705d 100644 (file)
 <xml-block><escape><hl key="HelpXMLString">use strict</hl>;
 XML.ignoreWhitespace = <hl key="Boolean">false</hl>;
 XML.prettyPrinting   = <hl key="Boolean">false</hl>;
-<hl key="HelpXML">var</hl> INFO = <!-- Cursed manual XML highlighting! -->
+<hl key="HelpXMLBase">var</hl> INFO = <!-- Cursed manual XML highlighting! -->
 <hl key="HelpXMLTagStart">&lt;plugin
     <hl key="HelpXMLAttribute">name</hl><hl key="HelpXMLString">flashblock</hl>
     <hl key="HelpXMLAttribute">version</hl><hl key="HelpXMLString">1.0</hl>
index 2c78e9c6b68711468bd73e1c830f404b60a4fe4a..40aeb2f7bdb08aefaf69397c9dc32406d3a81ebc 100644 (file)
             <h3 tag="faq-lasttab">How can I prevent <k>d</k> on the last tab from closing the window?</h3>
             <p><ex>:set!</ex> <hl key="HelpOpt">browser.tabs.closeWindowWithLastTab</hl>=<hl key="Boolean">false</hl></p>
 
-            <h3 tag="faq-regexpsearch">How can I use regular expressions in the page search?</h3>
-            <p>
-                Regular expression search is possible with the <tt>/Find Bar/</tt>
-                extension installed, in which case it can be toggled with the
-                <em>\r</em> and <em>\R</em> search flags. See also
-                <ex>:help</ex> <t>pattern</t>.
-            </p>
-
-            <h3 tag="faq-autocomplete"><strut/>How can I prevent the command line completion list showing until I press <k name="Tab"/>?</h3>
+            <h3 tag="faq-autocomplete"><strut/>How can I prevent the command line completion list showing until I press <k name="Tab" link="c_&lt;Tab>"/>?</h3>
             <p>
                 You can disable it entirely with <se opt="autocomplete"/> or for
                 specific types of command completion by choosing more
index 4c3d200142e4e5c512540e82fdd3fa14902dcbba..9c127daad3402ed6a300b4e6b41d59884d24ab41 100644 (file)
 </item>
 
 <item>
-    <tags>:extde :extdelete</tags>
+    <tags>:extrm :extde :extdelete</tags>
     <spec>:extde<oa>lete</oa> <a>extension</a></spec>
     <spec>:extde<oa>lete</oa>!</spec>
     <strut/>
index e44a12a99c3349ff1517b884d4364613ab4dbb71..769f7da727af9e460addccbebbc0930cb6f218fa 100644 (file)
@@ -84,8 +84,8 @@
             <t>quick-hints</t>, except that each sub-mode highlights a
             more specialized set of elements, and performs a unique action on
             the selected link. Because of the panoply of extended hint modes
-            available, after pressing <k>;</k>, pressing <k name="Tab"/> brings
-            up the completion list with each hints mode and its description.
+            available, after pressing <k>;</k>, pressing <k name="Tab" link="false"/>
+            brings up the completion list with each hints mode and its description.
         </p>
 
         <p><a>mode</a> may be one of:</p>
index 99b3c79a2a0bf2db9ca01f3b91f3a02533e48faf..d6a9981bb1160f970d922dbd32ffec7df72c3785 100644 (file)
@@ -26,6 +26,8 @@ This file contains a list of all available commands, mappings and options.
 
 <h2 tag="n-map-index">Normal mode</h2>
 
+<h2 tag="caret-map-index">Caret mode</h2>
+
 <h2 tag="v-map-index">Visual mode</h2>
 
 <h2 tag="c-map-index">Command-line editing</h2>
index 8921e811dd9f41db269f98b0bb378e2748f45438..9ff5c88bbd0dff244a4c5de3b983aaad69f55898 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?xml-stylesheet type="text/xsl" href="dactyl://content/help.xsl"?>
 
-<!DOCTYPE document SYSTEM "dactyl://content/modes.dtd">
+<!DOCTYPE document SYSTEM "dactyl://cache/modes.dtd">
 
 <document
     name="map"
     For instance,
 </p>
 
-<code><ex>:map <k name="F2" link="false"/></ex> <ex>:echo Date()<k name="CR"/></ex></code>
+<code><ex>:map <k name="F2" link="false"/></ex> <ex>:styletoggle</ex> <em>-name</em> <k name="A-Tab" link="c_&lt;Tab>"/></code>
 
 <p>
-    causes “<ex>:echo Date()<k name="CR"/></ex>” to be typed out
-    whenever <k name="F2" link="false"/> is pressed, thus echoing the full date
-    to the command line.
+    causes “<tt><ex>:styletoggle</ex> <em>-name</em> <k name="A-Tab" link="c_&lt;Tab"/></tt>” to be typed out
+    whenever <k name="F2" link="false"/> is pressed, providing a way to toggle
+    a tab-completed named user style.
+</p>
+
+<p>
+    You can also map keys to <link topic="ex-scripts">Ex</link> or
+    <link topic=":js">JavaScript</link> commands, see the
+    <link topic="map-examples">examples</link>.
 </p>
 
 <p tag=":map-modes">
@@ -56,6 +62,7 @@
     <dt>i</dt> <dd>Insert mode: When interacting with text fields on a website</dd>
     <dt>t</dt> <dd>Text Edit mode: When editing text fields in Vim-like Normal mode</dd>
     <dt>c</dt> <dd>Command Line mode: When typing into the &dactyl.appName; command line</dd>
+    <dt>o</dt> <dd>Operator mode: When moving the cursor</dd>
 </dl>
 
 <p>
@@ -65,7 +72,7 @@
     prefixed with one of the above letters. For instance,
     <ex>:imap</ex> creates a new key mapping in Insert mode, while
     <ex>:cunmap</ex> removes a key mapping from Command Line mode.
-    Other modes can be specified using the -modes option described below.
+    Other modes can be specified using the <em>-modes</em> option described below.
 </p>
 
 <warning>
     <dt></dt> <dd></dd>
 
     <dt>-arg</dt> <dd>Accept an argument after the requisite key press. Sets the <tt>arg</tt> parameter to the result. (short name <em>-a</em>)</dd>
-    <dt>-builtin</dt> <dd>Execute this mapping as if there were no user-defined mappings (short name <em>-b</em>)</dd>
+    <dt>-builtin</dt> <dd>Execute this mapping as if there were no user-defined mappings. (short name <em>-b</em>)</dd>
     <dt>-count</dt> <dd>Accept a count before the requisite key press. Sets the <tt>count</tt> parameter to the result. (short name <em>-c</em>)</dd>
-    <dt>-description</dt> <dd>A description of this mapping (short name <em>-d</em>)</dd>
-    <dt>-ex</dt> <dd>Execute <a>rhs</a> as an Ex command rather than keys (short name <em>-e</em>)</dd>
+    <dt>-description</dt> <dd>A description of this mapping. (short name <em>-d</em>)</dd>
+    <dt>-ex</dt> <dd>Execute <a>rhs</a> as an Ex command rather than keys. (short name <em>-e</em>)</dd>
     <dt>-group=<a>group</a></dt> <dd>Add this command to the given <t>group</t> (short name <em>-g</em>). When listing commands this limits the output to the specified group.</dd>
-    <dt>-javascript</dt> <dd>Execute <a>rhs</a> as JavaScript rather than keys (short names <em>-js</em>, <em>-j</em>)</dd>
+    <dt>-javascript</dt> <dd>Execute <a>rhs</a> as JavaScript rather than keys. (short names <em>-js</em>, <em>-j</em>)</dd>
     <dt>-literal=<a>n</a></dt> <dd>Parse the <a>n</a>th argument without specially processing any quote or meta characters. (short name <em>-l</em>)</dd>
-    <dt>-modes=<a>modes</a></dt> <dd>Create this mapping in the given modes (short names <em>-mode</em>, <em>-m</em>)</dd>
-    <dt>-nopersist</dt> <dd>Do not save this mapping to an auto-generated rc file (short name <em>-n</em>)</dd>
-    <dt>-silent</dt> <dd>Do not echo any generated keys to the command line (short name <em>-s</em>, also <em>&lt;silent></em> for Vim compatibility)</dd>
+    <dt>-modes=<a>modes</a></dt> <dd>Create this mapping in the given modes. (short names <em>-mode</em>, <em>-m</em>)</dd>
+    <dt>-nopersist</dt> <dd>Do not save this mapping to an auto-generated rc file. (short name <em>-n</em>)</dd>
+    <dt>-silent</dt> <dd>Do not echo any generated keys to the command line. (short name <em>-s</em>, also <em>&lt;silent></em> for Vim compatibility)</dd>
 </dl>
 
 <item>
     </description>
 </item>
 
+<h3 tag="map-examples">Mapping examples</h3>
+
+<p>Make <k name="A-n" link="false"/> do the same as <k name="Down" link="false"/> in input <t>modes</t>:</p>
+
+<code><ex>:map</ex> <em>-b</em> <em>-m</em> input <k name="A-n" link="false"/> <k name="Down" link="false"/></code>
+
+<p>Toggle the tab line with <k name="A-t" link="false"/>:</p>
+
+<code><ex>:map</ex> <em>-ex</em> <k name="A-t" link="false"/> <se opt="showtabline" op="!="><str delim="">always</str>,<str delim="">never</str></se></code>
+
+<p>Make <k name="A-i" link="false"/> toggle image display:</p>
+
+<code><ex>:map</ex> <k name="A-i" link="false"/> <em>-js</em> &lt;&lt;<em>EOF</em>
+let (pref = <str>permissions.default.image</str>)
+    prefs.set(pref, prefs.get(pref) == 1 ? 2 : 1);
+tabs.reload(config.browser.mCurrentTab);
+<em>EOF</em></code>
+
+
+<h2 tag="bypassing-&dactyl.name;">Bypassing &dactyl.appName;</h2>
+
+&dactyl.appName; overrides nearly all &dactyl.host; keys in order to
+make browsing more pleasant for Vim users. On the occasions when you
+want to bypass &dactyl.appName;'s key handling and pass keys directly to
+&dactyl.host; or to a web page, you have several options:
+
+<item>
+    <tags><![CDATA[<pass-next-key-builtin> <A-b>]]></tags>
+    <spec><![CDATA[<A-b>]]></spec>
+    <description>
+        <p>
+            Process the next key as a builtin mapping, ignoring any user defined
+            mappings and <o>passkeys</o> settings.
+        </p>
+    </description>
+</item>
+
+<item>
+    <tags><![CDATA[send-key <pass-next-key> <C-v>]]></tags>
+    <spec><![CDATA[<C-v>]]></spec>
+    <description>
+        <p>
+            Pass the next key press directly to &dactyl.host;.
+        </p>
+    </description>
+</item>
+
+<item>
+    <tags><![CDATA[pass-through <pass-all-keys> <C-z>]]></tags>
+    <spec><![CDATA[<C-z>]]></spec>
+    <description>
+        <p>
+            Pass all keys except for <k name="Esc"/> directly to
+            &dactyl.host;. When <k name="Esc"/> is pressed,
+            resume normal key handling. This is especially useful
+            for web sites which make heavy use of key bindings.
+        </p>
+    </description>
+</item>
+
+<p>
+    See also <o>passkeys</o> and <o>passunknown</o> for ways to permanently pass
+    all or particular keys under certain conditions.
+</p>
+
 <h2 tag="abbreviations">Abbreviations</h2>
 
 <p>
 
         <p>
             By default, user commands accept no arguments. This can be changed by specifying
-            the <tt>-nargs</tt> option.
+            the <em>-nargs</em> option.
         </p>
 
         <p>The valid values are:</p>
         <p>
             Completion for arguments to user-defined commands is not available by default.
             Completion can be enabled by specifying one of the following arguments to the
-            -complete option when defining the command.
+            <em>-complete</em> option when defining the command.
         </p>
 
         <dl tag=":command-complete-arg-list"/>
 
         <p>
             Custom completion can be provided by specifying the
-            <str>custom,<a>thing</a></str> argument to <tt>-complete</tt>. If
+            <str>custom,<a>thing</a></str> argument to <em>-complete</em>. If
             <a>thing</a> evaluates to a function (i.e., it is a variable holding
             a function value, or a string containing the definition itself), it
             is called with two arguments: a completion context, and an object
         <h3 tag="E177 E178 :command-count">Count handling</h3>
 
         <p>
-            By default, user commands do not accept a count. Use the -count option if
+            By default, user commands do not accept a count. Use the <em>-count</em> option if
             you'd like to have a count passed to your user command. This will then be
             available for expansion as &lt;count> in the replacement.
         </p>
 
         <p>
             By default, a user command does not have a special version, i.e. a version
-            executed with the ! modifier. Providing the -bang option will enable this
+            executed with the ! modifier. Providing the <em>-bang</em> option will enable this
             and &lt;bang> will be available in the replacement.
         </p>
 
         <h3 tag=":command-description">Command description</h3>
 
         <p>
-            The command's description text can be set with -description. Otherwise it will
+            The command's description text can be set with <em>-description</em>. Otherwise it will
             default to "User-defined command".
         </p>
 
     </description>
 </item>
 
-<h2 tag="command-examples">Examples</h2>
+<h3 tag="command-examples">Command examples</h3>
+
+<p>A command to search via DuckDuckGo:</p>
+
+<code><ex>:command</ex> <em>-nargs</em>=* <str delim="">ddg</str> open ddg &lt;args></code>
 
-<p>Add a :Google command to search via google:</p>
-<code><ex>:command -nargs=* Google open google &lt;args></ex></code>
+<p>
+    A command to search for contents of the current selection using a
+    tab-completed search engine in the current or a new tab (depending on how
+    much you bang on the keyboard):
+</p>
 
-<!-- TODO: add decent examples -->
+<code><ex>:com!</ex> <str delim="">search-selection</str>,<str delim="">ss</str> <em>-bang</em> <em>-nargs</em>=? <em>-complete</em> search
+        \ <em>-js</em> commands.execute((bang ? <str>open </str> : <str>tabopen </str>)
+        \ + args + <str> </str> + buffer.currentWord)</code>
 
 </document>
 
index b85e93618825b12191d6eddb1e36bd5683dbfa90..75aaf29a5cc2bed61f3be30bf9c6b783dcae6499 100644 (file)
@@ -14,7 +14,7 @@
 
 <p>
     &dactyl.appName; supports a number of different methods of
-    marking your place, in order to easily return later,
+    marking your place, in order to easily return later:
 </p>
 
 <ul>
@@ -22,7 +22,6 @@
     <li><em>QuickMarks</em> allow you to quickly save and return to as many as 62 (a-zA-Z0-9) different web sites with a quick keyboard shortcut.</li>
     <li><em>Local marks</em> allow you to store and return to a position within the current web page.</li>
     <li><em>URL marks</em> allow you to store and return to the position and URL of the current web page.</li>
-    <li><em>History</em> marks every opened page with data on when and how often it has been visited.</li>
 </ul>
 
 <h2 tag="bookmarks">Bookmarks</h2>
                 sites that require query parameters in encodings other than
                 UTF-8 (short name <em>-c</em>).
             </dd>
+            <dt>-id</dt>
+            <dd>
+                The numerical ID of a bookmark to update.
+            </dd>
             <dt>-keyword</dt>
             <dd>
                 A keyword which may be used to open the bookmark via
@@ -89,7 +92,7 @@
         <p>
             If <oa>!</oa> is present, a new bookmark is always
             added. Otherwise, the first bookmark matching
-            <oa>url</oa> is updated.
+            <oa>id</oa>, <oa>keyword</oa>, or <oa>url</oa> is updated.
         </p>
     </description>
 </item>
     </description>
 </item>
 
-<h2 tag="history">History</h2>
-
-<p>
-    Though not traditionally considered a mark, history behaves very
-    similarly to bookmarks both in &dactyl.host; and
-    &dactyl.appName;. Every visited page is marked and weighted by
-    when and how often it is visited, and can be retrieved both in
-    history list and location completions. Indeed, the ‘frecency’
-    algorithm used to determine the results of location completions
-    (see the <o>complete</o> option) means that history is often a
-    more effective type of mark than bookmarks themselves.
-</p>
-
-<item>
-    <tags><![CDATA[<C-o>]]></tags>
-    <strut/>
-    <spec><![CDATA[[count]<C-o>]]></spec>
-    <description>
-        <p>
-            Go to an older position in the jump list. This currently
-            entails moving backward in page history, but in the
-            future will take into account page positions as well.
-            If <oa>count</oa> is specified go back <oa>count</oa> pages.
-        </p>
-    </description>
-</item>
-
-<item>
-    <tags><![CDATA[<C-i>]]></tags>
-    <strut/>
-    <spec><![CDATA[[count]<C-i>]]></spec>
-    <description>
-        <p>
-            Go to an newer position in the jump list. This currently
-            entails moving forward in page history, but in the
-            future will take into account page positions as well.
-            If <oa>count</oa> is specified go forward <oa>count</oa> pages.
-        </p>
-    </description>
-</item>
-
-<item>
-    <tags><![CDATA[<M-Left> <A-Left> H]]></tags>
-    <strut/>
-    <spec>[count]H</spec>
-    <description>
-        <p>Go back in the browser history. If <oa>count</oa> is specified go back <oa>count</oa> pages.</p>
-    </description>
-</item>
-
-<item>
-    <tags><![CDATA[<M-Right> <A-Right> L]]></tags>
-    <strut/>
-    <spec><oa>count</oa>L</spec>
-    <description>
-        <p>
-            Go forward in the browser history. If <oa>count</oa> is specified go forward <oa>count</oa>
-            pages.
-        </p>
-    </description>
-</item>
-
-<item>
-    <tags>:ba :back</tags>
-    <spec>:<oa>count</oa>ba<oa>ck</oa> <oa>url</oa></spec>
-    <spec>:<oa>count</oa>ba<oa>ck</oa>!</spec>
-    <description>
-        <p>
-            Go back in the browser history. If <oa>count</oa> is specified go back <oa>count</oa> pages.
-        </p>
-        <p>
-            The special version <ex>:back!</ex> goes to the beginning of the browser history.
-        </p>
-    </description>
-</item>
-
-<item>
-    <tags>:fw :fo :forward</tags>
-    <spec>:<oa>count</oa>fo<oa>rward</oa> <oa>url</oa></spec>
-    <spec>:<oa>count</oa>fo<oa>rward</oa>!</spec>
-    <description>
-        <p>
-            Go forward in the browser history. If <oa>count</oa> is specified go forward <oa>count</oa>
-            pages.
-        </p>
-        <p>
-            The special version <ex>:forward!</ex> goes to the end of the browser history.
-        </p>
-    </description>
-</item>
-
-<item>
-    <tags>:hs :hist :history</tags>
-    <spec>:hist<oa>ory</oa><oa>!</oa> <oa>filter</oa></spec>
-    <description>
-        <p>
-            Show recently visited URLs. Opens the message window at the bottom of the screen
-            with all history items whose page titles or URLs match
-            <oa>filter</oa>.
-        </p>
-
-        <p>
-            The special version <ex>:history!</ex> works the same as
-            <ex>:history</ex> except that it opens all matching
-            pages in new tabs rather than listing them.
-        </p>
-
-        <p>The pages may also be filtered via the following options,</p>
-
-        <dl dt="width: 8em;">
-            <dt>-max</dt>
-            <dd>
-                The maximum number of items to list or open
-                (short name <em>-m</em>).
-            </dd>
-            <dt>-sort</dt>
-            <dd>
-                The sort order of the results
-                (short name <em>-s</em>).
-            </dd>
-        </dl>
-    </description>
-</item>
 
 <h2 tag="quickmarks">QuickMarks</h2>
 
index e9c29911b5c5fcabd98100440d48fd403b076bda..2a7ce15bf290712678a62fb520fbc3f99b38f6e7 100644 (file)
@@ -1,5 +1,13 @@
 # TODO: normalize this debacle of Vim legacy messages
 
+error.damnYouJägermonkey = + \
+       Hallo. It looks like you've run into a problem resulting from       \
+       overzealous optimizations by the JavaScript engine. We've disabled  \
+       future use of these optimizations, but you'll need to restart your  \
+       browser in order to correct the problems they've caused. You can    \
+       re-enable them by resetting the javascript.options.methodjit.chrome \
+       preference.
+
 abbreviation.noSuch = No such abbreviation
 abbreviation.none = No abbreviations found
 
@@ -29,14 +37,20 @@ autocmd.noMatching = No matching autocommands
 autocmd.noGroup-1 = No such group or event: %S
 autocmd.cantExecuteAll = Can't execute autocommands for ALL events
 
+autocomplete.description-1 = Native '%S' autocompletions
+autocomplete.noSuchProvider-1 = No such autocomplete provider: '%S'
+autocomplete.title-1 = '%S'
+
 bookmark.noMatching-2 = No bookmarks matching tags %S and string %S
 bookmark.noMatchingTags-1 = No bookmarks matching tags %S
 bookmark.noMatchingString-1 = No bookmarks matching string %S
 bookmark.none = No bookmarks set
+bookmark.bangOrID = Only one of ! or -id may be given
 bookmark.cantAdd-1 = Could not add bookmark %S
 bookmark.allDeleted = All bookmarks deleted
 bookmark.removed-1 = Removed bookmark: %S
 bookmark.added-1 = Added bookmark: %S
+bookmark.updated-1 = Updated bookmark: %S
 bookmark.deleted-1 = %S bookmark(s) deleted
 bookmark.prompt.deleteAll = This will delete all bookmarks. Would you like to continue? (yes/[no]):
 
@@ -50,6 +64,12 @@ buffer.noClosed = No matching closed tab
 buffer.noAlternate = No alternate page
 buffer.backgroundLoaded = Background tab loaded: %S
 
+buffer.save.altText   = Alternate Text
+buffer.save.filename  = File Name
+buffer.save.linkText  = Link Text
+buffer.save.pageName  = Page Name
+buffer.save.suggested = Server-suggested Name
+
 # TODO: categorise these
 buffer.noTitle = [No Title]
 buffer.noName = [No Name]
@@ -106,10 +126,11 @@ completion.additional = (additional)
 completion.generating = Generating results...
 completion.noCompletions = No Completions
 completion.waitingFor-1 = Waiting for %S
+completion.waitingForResults = Waiting for results
 completion.waitingForKeyPress = Waiting for key press
 completion.matchIndex-2 = match %S of %S
 
-dactyl.created-1 = "(created %S)"
+dactyl.created-1 = created %S
 dactyl.parsingCommandLine-1 = Parsing command line options: %S
 dactyl.notCommand-2 = E492: Not a %S command: %S
 dactyl.sourcingPlugins-1 = Sourcing plugin directory: %S...
@@ -122,6 +143,9 @@ dactyl.sourced-1 = Sourced: %S
 dactyl.prompt.openMany-1 = This will open %S new tabs. Would you like to continue? (yes/[no]):
 dactyl.yank-1 = Yank %S
 
+dactyl.cheerUp = Cheer up
+dactyl.somberDown = Somber down
+
 deprecated.for.theOptionsSystem = the options system
 
 dialog.notAvailable-1 = Dialog %S not available
@@ -236,6 +260,7 @@ mow.contextMenu.selectAll = Select All
 
 option.noSuch = No such option
 option.noSuch-1 = No such option: %S
+option.noSuchType-1 = No such option type: %S
 option.replaceExisting-1 = Warning: %S already exists: replacing existing option
 option.intRequired = Integer value required
 option.operatorNotSupported-2 = Operator %S not supported for option type %S
@@ -246,14 +271,12 @@ option.defaultValue = Default Value
 
 option.bufferLocal = buffer local
 
-option.activate.safeSet = See the 'activate' option.
+option.safeSet = See the '%S' option.
 option.guioptions.safeSet = See 'guioptions' scrollbar flags.
+
 option.hintkeys.duplicate = Duplicate keys not allowed
 # The string 'passkeys' must appear exactly, including U0027 apostrophes
 option.passkeys.passedBy = passed by 'passkeys'
-option.popups.safeSet = See the 'activate' option.
-option.showtabline.safeSet = See 'showtabline' option.
-option.visualbell.safeSet = See 'visualbell' option.
 
 pageinfo.s.ownerUnverified = %S (unverified)
 
@@ -319,12 +342,15 @@ title.Totals = Totals
 title.URI = URI
 title.Version = Version
 
+title.HPos = HPos
+title.VPos = VPos
+
 variable.none = No variables found
 
 window.cantAttachSame = Can't reattach to the same window
 window.noIndex-1 = Window %S does not exist
 
-zoom.outOfRange-2 = Zoom value out of range (%S - %S%%)
+zoom.outOfRange-2 = Zoom value out of range (%S - %S%%)
 zoom.illegal = Illegal zoom value
 
 error.clipboardEmpty = No clipboard data
index 3aa6f5a94d17df1ecf748bf13e08a0e1592d3412..6de3231b5065d8c795fa4698ad07499ad8e14672 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?xml-stylesheet type="text/xsl" href="dactyl://content/help.xsl"?>
 
-<!DOCTYPE document SYSTEM "dactyl://content/options.dtd">
+<!DOCTYPE document SYSTEM "dactyl://cache/options.dtd">
 
 <document
     name="options"
@@ -14,7 +14,7 @@
 
 <p>
     &dactyl.appName; has a number of internal variables and switches which can be set to
-    achieve special effects. These options come in 8 forms:
+    achieve special effects. These options come in the following forms:
 </p>
 
 <dl dt="width: 10em;">
@@ -53,7 +53,7 @@
     <dt/><dd tag="regexpmap"/>
     <dt>regexpmap</dt>
     <dd>
-        A combination of a <em>stringmap</em> and a <em>regexplist</em>. Each key
+        A combination of a <t>stringmap</t> and a <t>regexplist</t>. Each key
         in the <a>key</a>:<a>value</a> pair is a regexp. If the regexp begins with a
         <tt>!</tt>, the sense of the match is negated, such that a non-matching
         expression will be considered a match and <html:i>vice versa</html:i>.
 
 <item>
     <tags>'cl' 'cookielifetime'</tags>
-    <spec>'cookielifetime'</spec>
+    <spec>'cookielifetime' 'cl'</spec>
     <type>&option.cookielifetime.type;</type>
     <default>&option.cookielifetime.default;</default>
     <description>
     <description>
         <p>Items which are completed at the <ex>:open</ex> prompts. Available items:</p>
 
-        <dl dt="width: 6em;">
-            <dt>s</dt> <dd>Search engines and keyword URLs</dd>
-            <dt>f</dt> <dd>Local files</dd>
-            <dt>l</dt> <dd>&dactyl.host; location bar entries (bookmarks and history sorted in an intelligent way)</dd>
-            <dt>b</dt> <dd>Bookmarks</dd>
-            <dt>h</dt> <dd>History</dd>
-            <dt>S</dt> <dd>Search engine suggestions</dd>
+        <dl dt="width: 6.5em;">
+            <dt>search</dt> <dd>Search engines and keyword URLs</dd>
+            <dt>file</dt> <dd>Local files</dd>
+            <dt>location</dt> <dd>&dactyl.host; location bar entries (bookmarks and history sorted in an intelligent way)</dd>
+            <dt>bookmark</dt> <dd>Bookmarks</dd>
+            <dt>history</dt> <dd>History</dd>
+            <dt>suggestion</dt> <dd>Search engine suggestions</dd>
         </dl>
 
+        <p>
+            Additionally, native search providers can be added by prefixing
+            their names with the string <str delim="'">native:</str>. These
+            providers are often added by other add-ons and are occasionally
+            useful.
+        </p>
+
         <p>
             The order is important, such that <se opt="complete"><str delim="">bsf</str></se> will
             list bookmarks followed by matching quick searches and then
             matching files.
         </p>
 
+        <note>
+            For backward compatibility, this option currently accepts a single
+            entry containing single-letter names for completers. This usage
+            is deprecated and will be removed in the future.
+        </note>
+
         <warning>
-            Using <em>b</em> and <em>h</em> can make completion very slow if
+            Using <em>bookmark</em> and <em>history</em> can make completion very slow if
             there are many items.
         </warning>
     </description>
 </item>
 
+<item>
+    <tags>'ds' 'defsearch'</tags>
+    <spec>'defsearch' 'ds'</spec>
+    <type>&option.defsearch.type;</type>
+    <default>&option.defsearch.default;</default>
+    <description>
+        <p>
+            Sets the default search engine. The default search engine is
+            used by <ex>:open</ex> and related commands for arguments which
+            include no search or bookmark keywords and can't otherwise be
+            converted into URLs or existing file names.
+        </p>
+
+        <p>
+            This means that with <o>defsearch</o> set to <str>youtube</str>,
+            <ex>:open Tim Minchin</ex> behaves exactly as
+            <ex>:open youtube Tim Minchin</ex>, so long as you don't have a
+            search or bookmark keyword called ‘Tim’.
+        </p>
+    </description>
+</item>
+
 <item>
     <tags>'dls' 'dlsort' 'downloadsort'</tags>
-    <spec>'downloadsort'</spec>
-    <type>stringlist</type>
-    <default>-active,+filename</default>
+    <spec>'downloadsort' 'dlsort' 'dls'</spec>
+    <strut/>
+    <type>&option.downloadsort.type;</type>
+    <default>&option.downloadsort.default;</default>
     <description>
         <p>
             <ex>:downloads</ex> sort order, in order of precedence.
     </description>
 </item>
 
-
-<item>
-    <tags>'ds' 'defsearch'</tags>
-    <spec>'defsearch' 'ds'</spec>
-    <type>&option.defsearch.type;</type>
-    <default>&option.defsearch.default;</default>
-    <description>
-        <p>
-            Sets the default search engine. The default search engine is
-            used by <ex>:open</ex> and related commands for arguments which
-            include no search or bookmark keywords and can't otherwise be
-            converted into URLs or existing file names.
-        </p>
-
-        <p>
-            This means that with <o>defsearch</o> set to <str>youtube</str>,
-            <ex>:open Tim Minchin</ex> behaves exactly as
-            <ex>:open youtube Tim Minchin</ex>, so long as you don't have a
-            search or bookmark keyword called ‘Tim’.
-        </p>
-    </description>
-</item>
-
 <item>
     <tags>'editor'</tags>
     <spec>'editor'</spec>
     <spec>'extendedhinttags' 'eht'</spec>
     <strut/>
     <type>&option.extendedhinttags.type;</type>
-    <default>[asOTvVWy]:a[href],area[href],img[src],iframe[src],
+    <default>[asOTvVWy]:':-moz-any-link',area[href],img[src],iframe[src],
           [f]:body,
           [F]:body,code,div,html,p,pre,span,
           [iI]:img,
     </description>
 </item>
 
+<item>
+    <tags>'ff' 'findflags'</tags>
+    <spec>'findflags' 'ff'</spec>
+    <type>&option.findflags.type;</type>
+    <default>&option.findflags.default;</default>
+    <description>
+        <p>Default flags for find invocations.</p>
+
+        <dl>
+            <dt>C</dt> <dd>Match case</dd>
+            <dt>L</dt> <dd>Search all text</dd>
+            <dt>R</dt> <dd>Perform a plain string search</dd>
+            <dt>c</dt> <dd>Ignore case</dd>
+            <dt>l</dt> <dd>Search only in links</dd>
+            <dt>r</dt> <dd>Perform a regular expression search</dd>
+        </dl>
+    </description>
+</item>
+
 <item>
     <tags>'fh' 'followhints'</tags>
     <spec>'followhints' 'fh'</spec>
 
         <dl dt="width: 6em;">
             <dt>0</dt>      <dd>Follow the first hint as soon as typed text uniquely identifies it.</dd>
-            <dt>1</dt>      <dd>Follow the selected hint on <k name="CR"/>.</dd>
+            <dt>1</dt>      <dd>Follow the selected hint on <k name="CR" link="false"/>.</dd>
         </dl>
     </description>
 </item>
     <strut/>
     <spec>'hinttags' 'ht'</spec>
     <type>&option.hinttags.type;</type>
-    <default>a,area,button,iframe,input:not([type=hidden]),select,textarea,
+    <default>:-moz-any-link,area,button,iframe,input:not([type=hidden]),select,textarea,
           [onclick],[onmouseover],[onmousedown],[onmouseup],[oncommand],
-          [tabindex],[role=link],[role=button]</default>
+          [tabindex],[role=link],[role=button],[contenteditable=true]</default>
     <description>
         <p>
             A list of CSS selectors or XPath expressions used to select elements
             hint. The timeout is measured since the last time a key listed in
             <o>hintkeys</o> was pressed. It has no effect when narrowing hints
             by typing part of their text. Set to 0 (the default) to only follow
-            hints after pressing <k name="CR"/> or when the hint is unique.
+            hints after pressing <k name="CR" link="false"/> or when the hint is unique.
         </p>
     </description>
 </item>
     </description>
 </item>
 
+<item>
+    <tags>'isk' 'iskeyword'</tags>
+    <spec>'iskeyword' 'isk'</spec>
+    <type>&option.iskeyword.type;</type>
+    <default>&option.iskeyword.default;</default>
+    <description>
+        <p>Regular expression defining which characters constitute words.</p>
+    </description>
+</item>
+
 <item>
     <tags>'nojsd' 'nojsdebugger'</tags>
     <tags>'jsd' 'jsdebugger'</tags>
 
 <item>
     <tags>'jt' 'jumptags'</tags>
-    <spec>'jumptags'</spec>
+    <spec>'jumptags' 'jt'</spec>
     <type>&option.jumptags.type;</type>
     <default>&option.jumptags.default;</default>
     <description>
     </description>
 </item>
 
+<item>
+    <tags>'ln' 'linenumbers'</tags>
+    <spec>'linenumbers' 'ln'</spec>
+    <type>&option.linenumbers.type;</type>
+    <default>&option.linenumbers.default;</default>
+    <description>
+        <p>
+            Patterns used to determine line numbers used by <k>G</k>. May be
+            either a selector expression as accepted by <o>hinttags</o>, in
+            which case the first matching element whose text content is equal to
+            the desired line number is used or the <oa>count</oa>th element
+            failing that, or the string <str delim="'">func:</str> followed by a
+            function which, given arguments for the document and desired line
+            number, must return the target element.
+        </p>
+    </description>
+</item>
+
+
 <item>
     <tags>'lpl' 'loadplugins'</tags>
     <spec>'loadplugins' 'lpl'</spec>
     <description>
         <p>
             The default list of private items to sanitize. See
-            <ex>:sanitize</ex> for a list and explanation of possible values.
+            <ex>:sanitize</ex> for a list and explanation of possible values. A
+            value of <str>all</str> will cause all items to be sanitized. Items
+            may be excluded by prefixing them with a <tt>!</tt>. The first
+            matching item takes precedence.
         </p>
     </description>
 </item>
             Number of lines to scroll with <k name="C-u"/> and <k name="C-d"/>
             commands. The number of lines scrolled defaults to half the window
             size. When a <oa>count</oa> is specified to the <k name="C-u"/> or
-            <k name="C-d"/> commands, that value is used instead. When the
-            value is <em>0</em>, it defaults to half the window height.
+            <k name="C-d"/> commands, set this option to <oa>count</oa> before
+            executing the command. Setting this to <em>0</em> restores the
+            default behaviour.
+        </p>
+    </description>
+</item>
+
+<item>
+    <tags>'scs' 'scrollsteps'</tags>
+    <spec>'scrollsteps' 'scs'</spec>
+    <type>&option.scrollsteps.type;</type>
+    <default>&option.scrollsteps.default;</default>
+    <description>
+        <p>
+            The number of steps in which to smooth scroll to a new position. If
+            set to 1, smooth scrolling is not used.
         </p>
     </description>
 </item>
 
+<item>
+    <tags>'sct' 'scrolltime'</tags>
+    <spec>'scrolltime' 'sct'</spec>
+    <type>&option.scrolltime.type;</type>
+    <default>&option.scrolltime.default;</default>
+    <description>
+        <p>The time, in milliseconds, in which to smooth scroll to a new position.</p>
+    </description>
+    </item>
+
 <item>
     <tags>'sh' 'shell'</tags>
     <spec>'shell' 'sh'</spec>
     </description>
 </item>
 
+<item>
+    <tags>'spl' 'spelllang'</tags>
+    <spec>'spelllang' 'spl'</spec>
+    <type>&option.spelllang.type;</type>
+    <default>&option.spelllang.default;</default>
+    <description short="true">
+        <p>The language used by the spell checker.</p>
+    </description>
+</item>
+
 <item>
     <tags>'sf' 'strictfocus'</tags>
     <spec>'strictfocus' 'sf'</spec>
     </description>
 </item>
 
+<item>
+    <tags>'ys' 'yankshort'</tags>
+    <spec>'yankshort' 'ys'</spec>
+    <type>&option.yankshort.type;</type>
+    <default>&option.yankshort.default;</default>
+    <description>
+        <p>Yank the canonical short URL of a web page where provided.</p>
+    </description>
+</item>
+
+
 </document>
 
 <!-- vim:se sts=4 sw=4 et: -->
index 6ecac3a04a31d9ea0ed37fcff5e710d5e50119dc..78088274e30d004efe378dfe0820105c6ee6def9 100644 (file)
     <li>
         Escape sequences to toggle link-only and case-sensitive find.
     </li>
+    <li>
+        Crude regular expression searches are supported.
+    </li>
 </ul>
 
-<p>
-    Regular expression find, however, is not currently available unless the
-    /Find Bar/ service is installed, in which case it may be toggled on with
-    a find flag.
-</p>
-
 <item>
     <tags><![CDATA[<find-forward> /]]></tags>
-    <spec>/<a>pattern</a><k name="CR"/></spec>
+    <spec>/<a>pattern</a><k name="CR" link="false"/></spec>
     <description>
         <p>Find <a>pattern</a> starting at the current caret position.</p>
 
         <p>
-            The following escape sequences can be used to modify the
-            behavior of the find. When flags conflict, the last to
-            appear is the one that takes effect.
+            The following escape sequences can be used anywhere in
+            <a>pattern</a> to modify the behavior of the search. When flags
+            conflict, the last to appear is the one that takes effect.
         </p>
 
         <dl dt="width: 6em;">
             <dt>\C</dt> <dd>Perform case sensitive find (default if <o>findcase</o>=<str>match</str>).</dd>
             <dt>\l</dt> <dd>Search only in links, as defined by <o>hinttags</o>.</dd>
             <dt>\L</dt> <dd>Search the entire page.</dd>
-        </dl>
-
-        <p>
-            Additionally, if the /Find Bar/ extension is installed, the
-            following flags may be used,
-        </p>
-        <dl dt="width: 6em;">
             <dt>\r</dt> <dd>Process the entire pattern as a regular expression.</dd>
             <dt>\R</dt> <dd>Process the entire pattern as an ordinary string.</dd>
         </dl>
 </item>
 
 <item>
-    <tags><![CDATA[<find-forward> ?]]></tags>
-    <spec>?<a>pattern</a><k name="CR"/></spec>
+    <tags><![CDATA[<find-backward> <S-Slash> ?]]></tags>
+    <spec>?<a>pattern</a><k name="CR" link="false"/></spec>
     <description>
         <p>
             Find a pattern backward of the current caret position in exactly the
-            same manner as <k>/</k>
+            same manner as <k>/</k>.
         </p>
     </description>
 </item>
 </item>
 
 <item>
-    <tags><![CDATA[<find-word-next> *]]></tags>
+    <tags><![CDATA[<find-word-forward> *]]></tags>
     <spec>*</spec>
     <description short="true">
         <p>Search forward for the next occurrence of the word under cursor.</p>
 </item>
 
 <item>
-    <tags><![CDATA[<find-word-previous> #]]></tags>
+    <tags><![CDATA[<find-word-backward> #]]></tags>
     <spec>#</spec>
     <description short="true">
         <p>Search backward for the previous occurrence of the word under cursor.</p>
index 7782ff99cc0cabef9afed00b5a9fcf1b2718f8c7..db936e490ed05130e0cd6ae0bf39fdf81dd46dfa 100644 (file)
             Items may be any of:
         </p>
 
-        <dl>
-            <dt>all         </dt>       <dd>All items</dd>
-            <dt>cache       </dt>       <dd>Cache</dd>
-            <dt>commandline </dt>       <dd>Command-line history</dd>
-            <dt>cookies     </dt>       <dd>Cookies</dd>
-            <dt>downloads   </dt>       <dd>Download history</dd>
-            <dt>formdata    </dt>       <dd>Saved form and search history</dd>
-            <dt>history     </dt>       <dd>Browsing history</dd>
-            <dt>host        </dt>       <dd>All data from the given host</dd>
-            <dt>marks       </dt>       <dd>Local and URL marks</dd>
-            <dt>macros      </dt>       <dd>Saved macros</dd>
-            <dt>messages    </dt>       <dd>Saved <ex>:messages</ex></dd>
-            <dt>offlineapps </dt>       <dd>Offline website data</dd>
-            <dt>options     </dt>       <dd>Options containing hostname data</dd>
-            <dt>passwords   </dt>       <dd>Saved passwords</dd>
-            <dt>sessions    </dt>       <dd>Authenticated sessions</dd>
-            <dt>sitesettings</dt>       <dd>Site preferences</dd>
-        </dl>
+        <dl tag="sanitize-items"/>
+
+        <p>
+            Items may be excluded by prefixing them with a <tt>!</tt>. The first
+            matching item takes precedence. Therefore, the value
+            <tt>!commandline all</tt> will sanitize all items but the command
+            line. A value of <tt>all !commandline</tt> will sanitize all items.
+        </p>
 
         <p>
             When <em>history</em> items are sanitized, all command-line
             If no <oa>action</oa> is given, the value of <o>cookies</o> is used.
         </p>
 
-        <example><ex>:map -b</ex> <k link="false">c</k> <ex>:cookies</ex> <k name="A-Tab" link="false"/></example>
+        <example><ex>:map -b</ex> <k link="false">c</k> <ex>:cookies</ex> <k name="A-Tab" link="c_&lt;Tab>"/></example>
     </description>
 </item>
 
index c8696839a72da79e1fe9cb1a59f52dc2dac3641c..e4e7d4dadbf9ee61d7037f1c8688d9ea0d3266d8 100644 (file)
@@ -27,8 +27,7 @@
     <description>
         <p>
             Repeat the last keyboard mapping <oa>count</oa> times. Note that,
-            unlike in Vim, this does not apply solely to editing commands,
-            mainly because &dactyl.appName; doesn't have them.
+            unlike in Vim, this also applies to other than editing commands.
         </p>
     </description>
 </item>
 
         <dl>
             <dt>builtin</dt> <dd>The default group for builtin items. Can not be modified in any way by scripts.</dd>
-            <dt>default</dt> <dd>The default group for this script.</dd>
+            <dt>default</dt> <dd>The default group for the script containing this <tt>:group</tt> command.</dd>
             <dt>user</dt> <dd>The default group for the command line and <t>&dactyl.name;rc</t>.</dd>
         </dl>
 
index 389355045afa80f5ed170b4fa01ec75090d79d2e..0a9ce2b3af5b83a7b7abb8b5dfdf0c0073a68103 100644 (file)
 <h2 tag="startup-options">Command-line options</h2>
 
 <p>
-    Command-line options can be passed to &dactyl.appName; via the -&dactyl.name; &dactyl.host;
+    Command-line options can be passed to &dactyl.appName; via the <em>-&dactyl.name;</em> &dactyl.host;
     option. These are passed as single string argument.
     E.g., <tt>&dactyl.hostbin; -&dactyl.name; <str><t>++cmd</t> 'set exrc' <t>+u</t> 'tempRcFile' <t>++noplugin</t></str></tt>
 </p>
 
+<p>
+    The <em>-&dactyl.name;-remote</em> command-line option can be used to
+    execute a single Ex command in an already running Pentadactyl instance.
+</p>
+
 <item>
     <tags>+c</tags>
     <strut/>
     </description>
 </item>
 
+<item>
+    <tags>+purgecaches</tags>
+    <strut/>
+    <spec>+purgecaches</spec>
+    <description>
+        <p>
+            Purges &dactyl.appName; caches at startup. May occasionally be
+            necessary after making local changes to the source tree.
+        </p>
+    </description>
+</item>
+
 <h2 tag="initialization startup">Initialization</h2>
 
 <p>At startup, &dactyl.appName; completes the following tasks in order. </p>
             repository, this is a good way to update to the latest version or
             to test your changes.
         </p>
+
         <p>
             Any arguments supplied are parsed as command-line arguments as
             specified in <t>startup-options</t>.
         </p>
+
         <warning>
             Not all plugins are designed to cleanly un-apply during a rehash.
             While official plugins are safe, beware of possible instability
 
 <item>
     <tags>:res :restart</tags>
-    <spec>:res<oa>tart</oa></spec>
+    <spec>:res<oa>tart</oa> <oa>arg</oa> …</spec>
     <description short="true">
         <p>Force &dactyl.host; to restart. Useful when installing extensions.</p>
+
+        <p>
+            Any arguments supplied are parsed as command-line arguments as
+            specified in <t>startup-options</t>.
+        </p>
     </description>
 </item>
 
index 0ff26336e4ce6b98cdd07747a6647f6bfc568b40..d93cfb0d8e786fedee8988e78acc2036c525c407 100644 (file)
             <dt>HelpXMLTagStart</dt>            <dd></dd>
             <dt>HelpXMLText</dt>                <dd></dd>
             <dt>Hint</dt>                       <dd></dd>
-            <dt>HintActive</dt>                 <dd>The hint element of link which will be followed by <k name="CR"/></dd>
+            <dt>HintActive</dt>                 <dd>The hint element of link which will be followed by <k name="CR" link="false"/></dd>
             <dt>HintElem</dt>                   <dd>The hintable element</dd>
             <dt>HintImage</dt>                  <dd>The indicator which floats above hinted images</dd>
             <dt>Hint[active]</dt>               <dd></dd>
index d275bfe301e7c88e5759837ca0bbc04a68c089c7..f4379266061982ab031b558c5a2bd2f83443031f 100644 (file)
 
 <h2 tag="opening-tabs">Opening tabs</h2>
 
+<item>
+    <tags><![CDATA[<new-tab-next> <C-t>]]></tags>
+    <spec><oa>count</oa><![CDATA[<C-t>]]></spec>
+    <description>
+        <p>Execute the next <oa>count</oa> mappings in a new tab.</p>
+    </description>
+</item>
+
+<item>
+    <tags>:bg :background</tags>
+    <spec>:background</spec>
+    <description>
+        <p>Execute a command opening any new tabs in the background.</p>
+    </description>
+</item>
+
+
 <item>
     <tags>:tab</tags>
     <strut/>
     <spec><oa>count</oa>gb</spec>
     <description>
         <p>
-            Repeat last <ex>:buffer<oa>!</oa></ex> command. This is useful to quickly jump between
-            buffers which have a similar URL or title.
+            Repeat last <ex>:buffer</ex> command. This is useful to quickly
+            jump between buffers which have a similar URL or title.
         </p>
     </description>
 </item>
     <spec><oa>count</oa>gB</spec>
     <description>
         <p>
-            Repeat last <ex>:buffer<oa>!</oa></ex> command in the reverse direction.
+            Repeat last <ex>:buffer</ex> command in the reverse direction.
         </p>
     </description>
 </item>
             buffer list. If this is the last buffer in a window, the window
             will be closed.
         </p>
+
+        <p>The following options are available:</p>
+
+        <dl dt="width: 8em;">
+            <dt>-group</dt>
+            <dd>
+                Attach to a tab group in the current window rather than to a
+                separate window.
+                (short name <em>-g</em>).
+            </dd>
+        </dl>
     </description>
 </item>
 
 <item>
     <tags>d</tags>
     <tags>:tabc :tabclose</tags>
-    <tags>:bun :bunload :bw :bwipeout :bd :bdelete</tags>
-    <spec>:<oa>count</oa>bd<oa>elete</oa><oa>!</oa> <oa>arg</oa></spec>
-    <spec>:<oa>count</oa>bun<oa>load</oa><oa>!</oa> <oa>arg</oa></spec>
-    <spec>:<oa>count</oa>bw<oa>ipeout</oa><oa>!</oa> <oa>arg</oa></spec>
     <spec>:<oa>count</oa>tabc<oa>lose</oa><oa>!</oa> <oa>arg</oa></spec>
     <spec><oa>count</oa>d</spec>
     <description>
         </p>
 
         <p>
-            When used with <oa>arg</oa>, remove all tabs which contain <oa>arg</oa> in the
-            currently opened hostname. With <oa>!</oa>, remove all tabs for which
-            the currently opened page's URL or title contains <oa>arg</oa>.
+            When used with <oa>arg</oa>, remove all visible tabs which match the
+            <t>site-filter</t> <oa>arg</oa>. With <oa>!</oa>, remove all tabs
+            for which the currently opened page's URL or title matches the
+            regular expression <oa>arg</oa>.
+        </p>
+    </description>
+</item>
+
+<item>
+    <tags>:bd :bdelete</tags>
+    <spec>:<oa>count</oa>bd<oa>elete</oa><oa>!</oa> <oa>arg</oa></spec>
+    <description>
+        <p>
+            Like <ex>:tabclose</ex> but include hidden tabs.
         </p>
     </description>
 </item>
index 39d80dc018ac0f802cd880e840f6babd3404eaa9..e54bec3fc99a63039ad88eec97671ddc6e38667f 100644 (file)
@@ -87,7 +87,7 @@
 </item>
 
 <item>
-    <tags><![CDATA[<redraw-screen> <C-l> CTRL-L :redr :redraw]]></tags>
+    <tags><![CDATA[<redraw-screen> <C-l> :redr :redraw]]></tags>
     <strut/>
     <spec>:redr<oa>aw</oa></spec>
     <description>
 </item>
 
 <item>
-    <tags><![CDATA[<Insert> i]]></tags>
+    <tags><![CDATA[<Insert>]]> i caret-mode visual-mode</tags>
     <strut/>
     <spec>i</spec>
     <description>
         <p>
-            Start Caret mode. This mode resembles the Vim's Normal mode where
-            the text cursor is visible on the web page. The <k link="false">v</k> key
-            enters Visual mode, where text is selected as the cursor moves.
+            Start Caret mode. This mode resembles Vim's Normal mode where the
+            text cursor is visible on the web page. The <k mode="caret">v</k>
+            key enters Visual mode, where text is selected as the cursor moves.
+        </p>
+        <p>
+            You can see all mappings effective in Caret or Visual mode using
+            the <ex>:listkeys</ex> command.
         </p>
     </description>
 </item>
index 6f43950c46b44649bcb836869c4ecddb1ce5335b..c1e4e335514f8627d69e40dd70fdcc58b78ada50 100644 (file)
@@ -3,15 +3,14 @@
 //
 // 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) {
@@ -59,7 +58,6 @@ var updateAddons = Class("UpgradeListener", AddonListener, {
 
     },
     onUpdateAvailable: function (addon, install) {
-        util.dump("onUpdateAvailable");
         this.upgrade.push(addon);
         install.addListener(this);
         install.install();
@@ -76,7 +74,7 @@ var updateAddons = Class("UpgradeListener", AddonListener, {
 
 var actions = {
     delete: {
-        name: "extde[lete]",
+        name: ["extde[lete]", "extrm"],
         description: "Uninstall an extension",
         action: callResult("uninstall"),
         perm: "uninstall"
@@ -111,15 +109,16 @@ var actions = {
         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"
     },
@@ -150,7 +149,6 @@ var Addon = Class("Addon", {
             <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>
@@ -158,6 +156,7 @@ var Addon = Class("Addon", {
                     <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);
@@ -225,6 +224,7 @@ var Addon = Class("Addon", {
         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)
@@ -278,15 +278,15 @@ var AddonList = Class("AddonList", {
         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>{_("title.Status")}</td>
                             <td/>
+                            <td>{_("title.Status")}</td>
                             <td>{_("title.Description")}</td>
                         </tr>
                       </table>, this.document, this.nodes);
@@ -348,7 +348,7 @@ var AddonList = Class("AddonList", {
 });
 
 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)])
@@ -425,11 +425,10 @@ var Addons = Module("addons", {
 
                     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);
@@ -440,7 +439,7 @@ var Addons = Module("addons", {
                     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);
@@ -478,10 +477,14 @@ var Addons = Module("addons", {
             };
         };
 
-        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) {
@@ -539,7 +542,7 @@ else
                     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);
index d07ba1230732b78e0f69eb944b84e8017c91f3fc..bc767f68887cf45147ac8e9787c6a21b5c2e33d0 100644 (file)
@@ -2,22 +2,21 @@
 //
 // 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 Cc = Components.classes;
-var Ci = Components.interfaces;
-var Cr = Components.results;
-var Cu = Components.utils;
+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;
@@ -47,15 +46,15 @@ if (!Object.defineProperty)
                     }
                     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);
@@ -123,6 +122,14 @@ if (!Object.keys)
 
 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;
@@ -133,17 +140,17 @@ function defineModule(name, params, module) {
 
     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);
+        require(module, mod, null, name);
+    module.__proto__ = jsmodules;
 
-    for (let [, mod] in Iterator(params.use || []))
-        if (loaded.hasOwnProperty(mod))
-            require(module, mod, "use");
-        else {
-            use[mod] = use[mod] || [];
-            use[mod].push(module);
-        }
+    module._lastModule = currentModule;
     currentModule = module;
     module.startTime = Date.now();
 }
@@ -151,20 +158,21 @@ function defineModule(name, params, module) {
 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) {
@@ -184,15 +192,15 @@ defineModule.time = function time(major, minor, func, self) {
 }
 
 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;
+    require(jsmodules, currentModule.NAME);
+    currentModule = currentModule._lastModule;
 }
 
-function require(obj, name, from) {
+function require(obj, name, from, targetName) {
     try {
         if (arguments.length === 1)
             [obj, name] = [{}, obj];
@@ -200,9 +208,14 @@ function require(obj, name, from) {
         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) {
@@ -215,25 +228,20 @@ function require(obj, name, from) {
 }
 
 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
@@ -331,7 +339,7 @@ deprecated.warn = function warn(func, name, alternative, frame) {
     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));
 }
 
 /**
@@ -346,6 +354,7 @@ function keys(obj) iter(function keys() {
         if (hasOwnProperty.call(obj, k))
             yield k;
 }());
+
 /**
  * Iterates over all of the top-level, iterable property values of an
  * object.
@@ -594,7 +603,7 @@ 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));
@@ -611,13 +620,13 @@ function call(fn) {
  */
 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);
+                memoize(res, prop, get);
         }
-        return obj;
+        return res;
     }
 
     try {
@@ -625,9 +634,15 @@ function memoize(obj, key, getter) {
             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)
@@ -680,18 +695,18 @@ function update(target) {
             if (desc.value instanceof Class.Property)
                 desc = desc.value.init(k, target) || desc.value;
 
-            if (typeof desc.value === "function" && target.__proto__) {
-                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 {
+                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));
+                    }
+                }
                 Object.defineProperty(target, k, desc);
             }
             catch (e) {}
@@ -732,22 +747,26 @@ function 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;
             })]]>,
@@ -769,10 +788,12 @@ function Class() {
     }
 
     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);
@@ -839,7 +860,7 @@ Class.extend = function extend(subclass, superclass, overrides) {
  *      property's value.
  * @returns {Class.Property}
  */
-Class.memoize = function memoize(getter, wait)
+Class.Memoize = function Memoize(getter, wait)
     Class.Property({
         configurable: true,
         enumerable: true,
@@ -847,6 +868,7 @@ Class.memoize = function memoize(getter, wait)
             let done = false;
 
             if (wait)
+                // Crazy, yeah, I know. -- Kris
                 this.get = function replace() {
                     let obj = this.instance || this;
                     Object.defineProperty(obj, key,  {
@@ -871,13 +893,34 @@ Class.memoize = function memoize(getter, wait)
                     return this[key];
                 };
             else
-                this.get = function replace() {
+                this.get = function g_Memoize() {
                     let obj = this.instance || this;
-                    Class.replaceProperty(obj, key, null);
-                    return Class.replaceProperty(obj, key, getter.call(this, key));
+                    try {
+                        Class.replaceProperty(obj, key, null);
+                        return Class.replaceProperty(obj, key, getter.call(this, key));
+                    }
+                    catch (e) {
+                        util.reportError(e);
+                    }
                 };
 
-            this.set = function replace(val) Class.replaceProperty(this.instance || this, val);
+            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);
         }
     });
 
@@ -893,6 +936,9 @@ Class.prototype = {
      */
     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 {
@@ -926,10 +972,14 @@ Class.prototype = {
             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.
@@ -968,10 +1018,18 @@ Class.prototype = {
                 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) {
@@ -1015,16 +1073,35 @@ memoize(Class.prototype, "closure", Class.makeClosure);
 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 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) }));
 
-    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))),
-        { 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.
@@ -1059,17 +1136,32 @@ var ErrorBase = Class("ErrorBase", Error, {
  * @returns {Class}
  */
 function Module(name, prototype) {
-    let init = callable(prototype) ? 4 : 3;
-    const module = Class.apply(Class, Array.slice(arguments, 0, init));
-    let instance = module();
-    module.className = name.toLowerCase();
+    try {
+        let init = callable(prototype) ? 4 : 3;
+        let proto = arguments[callable(prototype) ? 2 : 1];
 
-    instance.INIT = update(Object.create(Module.INIT),
-                           arguments[init] || {});
+        proto._metaInit_ = function () {
+            delete module.prototype._metaInit_;
+            currentModule[name.toLowerCase()] = this;
+        };
 
-    currentModule[module.className] = instance;
-    defineModule.modules.push(instance);
-    return module;
+        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) {
@@ -1133,13 +1225,15 @@ function Struct() {
     });
     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")),
@@ -1181,7 +1275,7 @@ let StructBase = Class("StructBase", Array, {
 
     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;
     }
 });
@@ -1211,7 +1305,7 @@ var Timer = Class("Timer", {
         }
         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);
         }
@@ -1297,9 +1391,13 @@ function octal(decimal) parseInt(decimal, 8);
  * 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);
 
index e85a7c6c978b4c415bd847f788eaf6365692b37f..55328a1fe3aa8dfe4c8db978fd437aac7bfcad08 100644 (file)
@@ -2,14 +2,25 @@
 //
 // 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));
@@ -19,7 +30,13 @@ update(Bookmark.prototype, {
         ["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")
@@ -28,15 +45,9 @@ update(Bookmark.prototype, {
         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); });
@@ -63,9 +74,9 @@ var BookmarkCache = Module("BookmarkCache", XPCOM(Ci.nsINavBookmarkObserver), {
 
     __iterator__: function () (val for ([, val] in Iterator(bookmarkcache.bookmarks))),
 
-    get bookmarks() Class.replaceProperty(this, "bookmarks", this.load()),
+    bookmarks: Class.Memoize(function () this.load()),
 
-    keywords: Class.memoize(function () array.toObject([[b.keyword, b] for (b in this) if (b.keyword)])),
+    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]),
@@ -79,9 +90,13 @@ var BookmarkCache = Module("BookmarkCache", XPCOM(Ci.nsINavBookmarkObserver), {
     _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);
@@ -96,7 +111,7 @@ var BookmarkCache = Module("BookmarkCache", XPCOM(Ci.nsINavBookmarkObserver), {
     },
 
     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];
@@ -129,7 +144,7 @@ var BookmarkCache = Module("BookmarkCache", XPCOM(Ci.nsINavBookmarkObserver), {
      */
     isBookmarked: function isBookmarked(uri) {
         if (isString(uri))
-            uri = util.newURI(uri);
+            uri = newURI(uri);
 
         try {
             return services.bookmarks
@@ -154,27 +169,23 @@ var BookmarkCache = Module("BookmarkCache", XPCOM(Ci.nsINavBookmarkObserver), {
     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;
@@ -218,12 +229,15 @@ var BookmarkCache = Module("BookmarkCache", XPCOM(Ci.nsINavBookmarkObserver), {
         }
     }
 }, {
+    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 "";
index d51f8696b1f7192c1c6a47577f01a8c8a7e312dc..4343ec215330129868fc4d6a859502f930e8f994 100644 (file)
@@ -2,7 +2,7 @@
 //
 // 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 {
 
@@ -14,16 +14,11 @@ 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 === 5)
+if (JSMLoader && JSMLoader.bump === 6)
     JSMLoader.global = this;
 else
     JSMLoader = {
-        bump: 5,
+        bump: 6,
 
         builtin: Cu.Sandbox(this),
 
@@ -31,6 +26,8 @@ else
 
         factories: [],
 
+        name: "dactyl",
+
         global: this,
 
         globals: JSMLoader ? JSMLoader.globals : {},
@@ -133,6 +130,8 @@ else
         purge: function purge() {
             dump("dactyl: JSMLoader: purge\n");
 
+            this.bootstrap = null;
+
             if (Cu.unload) {
                 Object.keys(this.modules).reverse().forEach(function (url) {
                     try {
@@ -167,6 +166,24 @@ else
             }
         },
 
+        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),
diff --git a/common/modules/buffer.jsm b/common/modules/buffer.jsm
new file mode 100644 (file)
index 0000000..94b1692
--- /dev/null
@@ -0,0 +1,2533 @@
+// 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.
+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", {
+    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() {
+        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 = 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 { 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(this.documentURI));
+        dactyl.open(url.spec);
+    },
+
+    incrementURL: function incrementURL(count) {
+        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 {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() {
+        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 = util.weakReference(value); },
+
+    /**
+     * @property {nsIURI} The current top-level 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(this.win.location.href),
+
+    /**
+     * @property {nsIURI} The current top-level document's URI, sans any
+     *     fragment identifier.
+     */
+    get documentURI() this.doc.documentURIObject || util.newURI(this.doc.documentURI),
+
+    /**
+     * @property {string} The current top-level document's URL.
+     */
+    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() 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() {
+        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() 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() this.doc.title,
+
+    /**
+     * @property {number} The buffer's horizontal scroll percentile.
+     */
+    get scrollXPercent() {
+        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 = Buffer.Scrollable(this.findScrollable(0, false));
+        if (elem.scrollHeight - elem.clientHeight === 0)
+            return 0;
+        return elem.scrollTop * 100 / (elem.scrollHeight - elem.clientHeight);
+    },
+
+    /**
+     * @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 Ci.nsIDOMHTMLBodyElement)
+                frames.push(frame);
+            Array.forEach(frame.frames, rec);
+        })(win || this.win);
+
+        if (focusedFirst)
+            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() || this.win;
+    },
+    set focusedFrame(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}
+     */
+    get currentWord() Buffer.currentWord(this.focusedFrame),
+    getCurrentWord: deprecated("buffer.currentWord", function getCurrentWord() Buffer.currentWord(this.focusedFrame, true)),
+
+    /**
+     * 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 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 overlay.getData(elem)["focus-allowed"]
+                    || elem.frameElement && overlay.getData(elem.frameElement)["focus-allowed"];
+        case "moderate":
+            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;
+        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)
+            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, [Ci.nsIDOMHTMLInputElement,
+                                  Ci.nsIDOMXULTextBoxElement]))
+                var flags = services.focus.FLAG_BYMOUSE;
+            else
+                flags = services.focus.FLAG_SHOWRING;
+
+            // 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);
+                let sel = (elem.ownerDocument || elem).defaultView.getSelection();
+                if (!sel.rangeCount || !RangeFind.intersects(range, sel.getRangeAt(0))) {
+                    range.collapse(true);
+                    sel.removeAllRanges();
+                    sel.addRange(range);
+                }
+            }
+
+            // for imagemap
+            if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
+                try {
+                    let [x, y] = elem.getAttribute("coords").split(",").map(parseFloat);
+
+                    DOM(elem).mouseover({ screenX: x, screenY: y });
+                }
+                catch (e) {}
+            }
+        }
+    },
+
+    /**
+     * Find the *count*th last link on a page matching one of the given
+     * regular expressions, or with a @rel or @rev attribute matching
+     * the given relation. Each frame is searched beginning with the
+     * last link and progressing to the first, once checking for
+     * matching @rel or @rev properties, and then once for each given
+     * regular expression. The first match is returned. All frames of
+     * the page are searched, beginning with the currently focused.
+     *
+     * 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
+     */
+    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];
+            }
+
+            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;
+
+            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, [Ci.nsIDOMHTMLFrameElement,
+                              Ci.nsIDOMHTMLIFrameElement]))
+            return this.focusElement(elem);
+
+        if (isinstance(elem, Ci.nsIDOMHTMLLinkElement))
+            return dactyl.open(elem.href, where);
+
+        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 Ci.nsIDOMHTMLInputElement && elem.type == "file") {
+            Buffer.openUploadPrompt(elem);
+            return;
+        }
+
+        let { dactyl } = this.modules;
+
+        let ctrlKey = false, shiftKey = false;
+        switch (dactyl.forceTarget || where) {
+        case dactyl.NEW_TAB:
+        case dactyl.NEW_BACKGROUND_TAB:
+            ctrlKey = true;
+            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);
+            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: 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 {
+            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));
+                    }
+
+                    self.saveURI(uri, file);
+                },
+
+                completer: function (context) completion.savePage(context, elem)
+            }).open();
+        }
+        catch (e) {
+            dactyl.echoerr(e);
+        }
+    },
+
+    /**
+     * Saves the contents of a URI to disk.
+     *
+     * @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)
+                    util.trapErrors(callback, self, uri, file, progress, request, flags, status);
+
+                return onStateChange.superapply(this, arguments);
+            })
+        });
+
+        persist.saveURI(uri, null, null, null, null, file);
+    },
+
+    /**
+     * Scrolls the currently active element horizontally. See
+     * {@link Buffer.scrollHorizontal} for parameters.
+     */
+    scrollHorizontal: function scrollHorizontal(increment, number)
+        Buffer.scrollHorizontal(this.findScrollable(number, true), increment, number),
+
+    /**
+     * Scrolls the currently active element vertically. See
+     * {@link Buffer.scrollVertical} for parameters.
+     */
+    scrollVertical: function scrollVertical(increment, number)
+        Buffer.scrollVertical(this.findScrollable(number, false), increment, number),
+
+    /**
+     * 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);
+    },
+
+    /**
+     * Find the best candidate scrollable element for the given
+     * direction and orientation.
+     *
+     * @param {number} dir The direction in which the element must be
+     *   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 Ci.nsIDOMElement) && elem.parentNode)
+                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;
+        }
+        catch (e) {}
+
+        try {
+            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 Ci.nsIDOMElement)) {
+            let doc = this.findScrollableWindow().document;
+            elem = find(doc.body || doc.getElementsByTagName("body")[0] ||
+                        doc.documentElement);
+        }
+        let doc = this.focusedFrame.document;
+        return util.assert(elem || doc.body || doc.documentElement);
+    },
+
+    /**
+     * Find the best candidate scrollable frame in the current buffer.
+     */
+    findScrollableWindow: function findScrollableWindow() {
+        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 = 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;
+    },
+
+    /**
+     * 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
+     * @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];
+        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);
+        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));
+    },
+
+    // TODO: allow callback for filtering out unwanted frames? User defined?
+    /**
+     * Shifts the focus to another frame within the buffer. Each buffer
+     * contains at least one frame.
+     *
+     * @param {number} count The number of frames to skip through. A negative
+     *     count skips backwards.
+     */
+    shiftFrameFocus: function shiftFrameFocus(count) {
+        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 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)
+            util.dactyl.beep();
+        next = Math.constrain(next, 0, frames.length - 1);
+
+        // focus next frame and scroll into view
+        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 = 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) {
+        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 = 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.call(self, true));
+        }, <br/>);
+
+        commandline.commandOutput(list);
+    },
+
+    /**
+     * Stops loading and animations in the current content.
+     */
+    stop: function stop() {
+        let { config } = this.modules;
+
+        if (config.stop)
+            config.stop();
+        else
+            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 == 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");
+    },
+
+    /**
+     * Opens a viewer to inspect the source of the current buffer or the
+     * specified *url*. Either the default viewer or the configured external
+     * editor is used.
+     *
+     * @param {string|object|null} loc If a string, the URL of the source,
+     *      otherwise an object with some or all of the following properties:
+     *
+     *          url: The URL to view.
+     *          doc: The document to view.
+     *          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",
+                                  loc.url, null, null, loc.line);
+        }
+        else {
+            if (useExternalEditor)
+                this.viewSourceExternally(loc || doc);
+            else {
+                let url = loc || doc.location.href;
+                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)
+                    this.docShell.gotoIndex(sh.index);
+                else
+                    dactyl.open(url, { hide: true });
+            }
+        }
+    },
+
+    /**
+     * Launches an editor to view the source of the given document. The
+     * contents of the document are saved to a temporary local file and
+     * removed when the editor returns. This function returns
+     * immediately.
+     *
+     * @param {Document} doc The document to view.
+     * @param {function|object} callback If a function, the callback to be
+     *      called with two arguments: the nsIFile of the file, and temp, a
+     *      boolean which is true if the file is temporary. Otherwise, an object
+     *      with line and column properties used to determine where to open the
+     *      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, 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;
+                persist.progressListener = this;
+                persist.saveURI(uri, null, null, null, null, this.file);
+            }
+            return null;
+        },
+
+        onStateChange: function onStateChange(progress, request, flags, status) {
+            if ((flags & this.STATE_STOP) && status == 0) {
+                try {
+                    var ok = this.callback(this.file, true);
+                }
+                finally {
+                    if (ok !== true)
+                        this.file.remove(false);
+                }
+            }
+            return 0;
+        }
+    }),
+
+    /**
+     * Increases the zoom level of the current buffer.
+     *
+     * @param {number} steps The number of zoom levels to jump.
+     * @param {boolean} fullZoom Whether to use full zoom or text zoom.
+     */
+    zoomIn: function zoomIn(steps, fullZoom) {
+        this.bumpZoomLevel(steps, fullZoom);
+    },
+
+    /**
+     * Decreases the zoom level of the current buffer.
+     *
+     * @param {number} steps The number of zoom levels to jump.
+     * @param {boolean} fullZoom Whether to use full zoom or text zoom.
+     */
+    zoomOut: function zoomOut(steps, fullZoom) {
+        this.bumpZoomLevel(-steps, fullZoom);
+    },
+
+    /**
+     * Adjusts the page zoom of the current buffer to the given absolute
+     * value.
+     *
+     * @param {number} value The new zoom value as a possibly fractional
+     *   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) {
+        let { dactyl, statusline } = this.modules;
+        let { ZoomManager } = this;
+
+        if (fullZoom === undefined)
+            fullZoom = ZoomManager.useFullZoom;
+        else
+            ZoomManager.useFullZoom = fullZoom;
+
+        value /= 100;
+        try {
+            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 (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(this.zoomLevel / 100));
+        let i = Math.constrain(cur + steps, 0, values.length - 1);
+
+        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() 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"),
+
+    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) {
+            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 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();
+
+        let ext = "";
+        if (isinstance(node, [Ci.nsIDOMDocument,
+                              Ci.nsIDOMHTMLImageElement])) {
+            let type = node.contentType || node.QueryInterface(Ci.nsIImageLoadingContent)
+                                               .getRequest(0).mimeType;
+
+            if (type === "text/plain")
+                ext = "." + (currExt || "txt");
+            else
+                ext = "." + services.mime.getPrimaryExtension(type, currExt);
+        }
+        else if (currExt)
+            ext = "." + currExt;
+
+        let re = ext ? RegExp("(\\." + currExt + ")?$", "i") : /$/;
+
+        var names = [];
+        if (node.title)
+            names.push([node.title,
+                       _("buffer.save.pageName")]);
+
+        if (node.alt)
+            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(config.OS.illegalCharacters, encodeURIComponent)
+                                                       .replace(re, ext), title]);
+    },
+
+    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 = 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.
+     * @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;
+    },
+
+    /**
+     * 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} 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(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 (unit == "pages")
+            increment = elem.clientWidth - fontSize;
+        else
+            throw Error();
+
+        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} 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(node, unit, number) {
+        let fontSize = parseInt(DOM(node).style.lineHeight);
+
+        let elem = Buffer.Scrollable(node);
+        let increment;
+        if (unit == "lines")
+            increment = fontSize;
+        else if (unit == "pages")
+            increment = elem.clientHeight - fontSize;
+        else
+            throw Error();
+
+        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(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);
+                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] == ">" && !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);
+
+                        prefs.set(   "print.print_to_file", true);
+                        prefs.set(BRANCH + "print_to_file", true);
+
+                        prefs.set(   "print.print_to_filename", io.File(arg.substr(1)).path);
+                        prefs.set(BRANCH + "print_to_filename", io.File(arg.substr(1)).path);
+
+                        dactyl.echomsg(_("print.toFile", arg.substr(1)));
+                    }
+                    else
+                        dactyl.echomsg(_("print.sending"));
+
+                    prefs.set("print.always_print_silent", args.bang);
+                    prefs.set("print.show_print_progress", !args.bang);
+
+                    config.browser.contentWindow.print();
+                });
+
+                if (arg)
+                    dactyl.echomsg(_("print.printed", arg.substr(1)));
+                else
+                    dactyl.echomsg(_("print.sent"));
+            },
+            {
+                argCount: "?",
+                bang: true,
+                literal: 0
+            });
+
+        commands.add(["pa[geinfo]"],
+            "Show various page information",
+            function (args) {
+                let arg = args[0];
+                let opt = options.get("pageinfo");
+
+                dactyl.assert(!arg || opt.validator(opt.parse(arg)),
+                              _("error.invalidArgument", arg));
+                buffer.showPageInfo(true, arg);
+            },
+            {
+                argCount: "?",
+                completer: function (context) {
+                    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] || "";
+
+                let titles = buffer.alternateStyleSheets.map(function (stylesheet) stylesheet.title);
+
+                dactyl.assert(!arg || titles.indexOf(arg) >= 0,
+                              _("error.invalidArgument", arg));
+
+                if (options["usermode"])
+                    options["usermode"] = false;
+
+                window.stylesheetSwitchAll(buffer.focusedFrame, arg);
+            },
+            {
+                argCount: "?",
+                completer: function (context) modules.completion.alternateStyleSheet(context),
+                literal: 0
+            });
+
+        commands.add(["re[load]"],
+            "Reload the current web page",
+            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 { 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) {
+                                let output = io.system(filename.substr(1), file);
+                                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);
+
+                    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 = 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, 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) 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))
+                    level = Math.round(buffer.zoomLevel + parseInt(arg, 10));
+                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.savePage = function savePage(context, node) {
+            context.fork("generated", context.filter.replace(/[^/]*$/, "").length,
+                         this, function (context) {
+                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 () {
+                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",
+            function (args) { buffer.incrementURL(-Math.max(args.count, 1)); },
+            { count: true });
+
+        mappings.add([modes.NORMAL], ["gu", "<open-parent-path>"],
+            "Go to parent directory",
+            function (args) { buffer.climbUrlPath(Math.max(args.count, 1)); },
+            { count: true });
+
+        mappings.add([modes.NORMAL], ["gU", "<open-root-path>"],
+            "Go to the root of the website",
+            function () { buffer.climbUrlPath(-1); });
+
+        mappings.add([modes.COMMAND], [".", "<repeat-key>"],
+            "Repeat the last key event",
+            function (args) {
+                if (mappings.repeat) {
+                    for (let i in util.interruptibleRange(0, Math.max(args.count, 1), 100))
+                        mappings.repeat();
+                }
+            },
+            { count: true });
+
+        mappings.add([modes.NORMAL], ["i", "<Insert>"],
+            "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.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.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.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.NORMAL], ["0", "^", "<scroll-begin>"],
+            "Scroll to the absolute left of the document",
+            function () { buffer.scrollToPercent(0, null); });
+
+        mappings.add([modes.NORMAL], ["$", "<scroll-end>"],
+            "Scroll to the absolute right of the document",
+            function () { buffer.scrollToPercent(100, null); });
+
+        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.NORMAL], ["G", "<End>", "<scroll-bottom>"],
+            "Go to the end of the document",
+            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.NORMAL], ["<C-d>", "<scroll-down>"],
+            "Scroll window downwards in the buffer",
+            function (args) { buffer._scrollByScrollSize(args.count, true); },
+            { count: true });
+
+        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.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.NORMAL], ["<Space>"],
+            "Scroll down a full page",
+            function (args) {
+                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.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 });
+
+        mappings.add([modes.NORMAL], ["[f", "<next-frame>"],
+            "Focus previous frame",
+            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); },
+            { count: true });
+
+        mappings.add([modes.NORMAL], ["}"],
+            "Jump to the next paragraph",
+            function (args) { buffer.findJump("p", args.count, false); },
+            { count: true });
+
+        mappings.add([modes.NORMAL], ["]]", "<next-page>"],
+            "Follow the link labeled 'next' or '>' if it exists",
+            function (args) {
+                buffer.findLink("next", options["nextpattern"], (args.count || 1) - 1, true);
+            },
+            { count: true });
+
+        mappings.add([modes.NORMAL], ["[[", "<previous-page>"],
+            "Follow the link labeled 'prev', 'previous' or '<' if it exists",
+            function (args) {
+                buffer.findLink("previous", options["previouspattern"], (args.count || 1) - 1, true);
+            },
+            { count: true });
+
+        mappings.add([modes.NORMAL], ["gf", "<view-source>"],
+            "Toggle between rendered and source view",
+            function () { buffer.viewSource(null, false); });
+
+        mappings.add([modes.NORMAL], ["gF", "<view-source-externally>"],
+            "View source with an external editor",
+            function () { buffer.viewSource(null, true); });
+
+        mappings.add([modes.NORMAL], ["gi", "<focus-input>"],
+            "Focus last used input field",
+            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 DOM.XPath(xpath, win.document))]))
+                                        .filter(function (elem) {
+                        if (isinstance(elem, [Ci.nsIDOMHTMLFrameElement,
+                                              Ci.nsIDOMHTMLIFrameElement]))
+                            return Editor.getEditor(elem.contentWindow);
+
+                        elem = DOM(elem);
+
+                        if (elem[0].readOnly || !DOM(elem).isEditable)
+                            return false;
+
+                        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);
+                DOM(elem).scrollIntoView();
+            },
+            { count: true });
+
+        function url() {
+            let url = dactyl.clipboardRead();
+            dactyl.assert(url, _("error.clipboardEmpty"));
+
+            let proto = /^([-\w]+):/.exec(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 });
+            });
+
+        mappings.add([modes.NORMAL], ["p", "<MiddleMouse>", "<open-clipboard-url>"],
+            "Open (put) a URL based on the current clipboard contents in the current buffer",
+            function () {
+                dactyl.open(url());
+            });
+
+        mappings.add([modes.NORMAL], ["P", "<tab-open-clipboard-url>"],
+            "Open (put) a URL based on the current clipboard contents in a new buffer",
+            function () {
+                dactyl.open(url(), { from: "paste", where: dactyl.NEW_TAB });
+            });
+
+        // reloading
+        mappings.add([modes.NORMAL], ["r", "<reload>"],
+            "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.NORMAL], ["Y", "<yank-selection>"],
+            "Copy selected text or current word",
+            function () {
+                let sel = buffer.currentWord;
+                dactyl.assert(sel);
+                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>"],
+            "Enlarge text zoom of current web page by a larger amount",
+            function (args) { buffer.zoomIn(Math.max(args.count, 1) * 3, false); },
+            { count: true });
+
+        mappings.add([modes.NORMAL], ["zo", "-", "<text-zoom-out>"],
+            "Reduce text zoom of current web page",
+            function (args) { buffer.zoomOut(Math.max(args.count, 1), false); },
+            { count: true });
+
+        mappings.add([modes.NORMAL], ["zr", "<text-zoom-reduce>"],
+            "Reduce text zoom of current web page by a larger amount",
+            function (args) { buffer.zoomOut(Math.max(args.count, 1) * 3, false); },
+            { count: true });
+
+        mappings.add([modes.NORMAL], ["zz", "<text-zoom>"],
+            "Set text zoom value of current web page",
+            function (args) { buffer.setZoom(args.count > 1 ? args.count : 100, false); },
+            { count: true });
+
+        mappings.add([modes.NORMAL], ["ZI", "zI", "<full-zoom-in>"],
+            "Enlarge full zoom of current web page",
+            function (args) { buffer.zoomIn(Math.max(args.count, 1), true); },
+            { count: true });
+
+        mappings.add([modes.NORMAL], ["ZM", "zM", "<full-zoom-more>"],
+            "Enlarge full zoom of current web page by a larger amount",
+            function (args) { buffer.zoomIn(Math.max(args.count, 1) * 3, true); },
+            { count: true });
+
+        mappings.add([modes.NORMAL], ["ZO", "zO", "<full-zoom-out>"],
+            "Reduce full zoom of current web page",
+            function (args) { buffer.zoomOut(Math.max(args.count, 1), true); },
+            { count: true });
+
+        mappings.add([modes.NORMAL], ["ZR", "zR", "<full-zoom-reduce>"],
+            "Reduce full zoom of current web page by a larger amount",
+            function (args) { buffer.zoomOut(Math.max(args.count, 1) * 3, true); },
+            { count: true });
+
+        mappings.add([modes.NORMAL], ["zZ", "<full-zoom>"],
+            "Set full zoom value of current web page",
+            function (args) { buffer.setZoom(args.count > 1 ? args.count : 100, true); },
+            { count: true });
+
+        // page info
+        mappings.add([modes.NORMAL], ["<C-g>", "<page-info>"],
+            "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 () 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 {
+                        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 words",
+            "string", '[^\\s.,!?:;/"\'^$%&?()[\\]{}<>#*+|=~_-]',
+            {
+                setter: function (value) {
+                    this.regexp = util.regexp(value);
+                    return value;
+                },
+                validator: function (value) RegExp(value)
+            });
+
+        options.add(["jumptags", "jt"],
+            "XPath or CSS selector strings of jumpable elements for extended hint modes",
+            "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: DOM.compileMatcher(Option.splitList(v)) });
+                    return vals;
+                },
+                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'/.source),
+            { regexpFlags: "i" });
+
+        options.add(["previouspattern"],
+            "Patterns to use when guessing the previous page in a document sequence",
+            "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() });
+
+        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",
+            "string", "status",
+            {
+                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) buffer.contentViewer.authorStyleDisabled = value,
+                getter: function () buffer.contentViewer.authorStyleDisabled
+            });
+
+        options.add(["yankshort", "ys"],
+            "Yank the canonical short URL of a web page where provided",
+            "sitelist", ["youtube.com", "bugzilla.mozilla.org"]);
+    }
+});
+
+Buffer.addPageInfoSection("e", "Search Engines", function (verbose) {
+    let n = 1;
+    let nEngines = 0;
+
+    for (let { document: doc } in values(this.allFrames())) {
+        let engines = DOM("link[href][rel=search][type='application/opensearchdescription+xml']", doc);
+        nEngines += engines.length;
+
+        if (verbose)
+            for (let link in engines)
+                yield [link.title || /*L*/ "Engine " + n++,
+                       <a xmlns={XHTML} href={link.href}
+                          onclick="if (event.button == 0) { window.external.AddSearchProvider(this.href); return false; }"
+                          highlight="URL">{link.href}</a>];
+    }
+
+    if (!verbose && nEngines)
+        yield nEngines + /*L*/" engine" + (nEngines > 1 ? "s" : "");
+});
+
+Buffer.addPageInfoSection("f", "Feeds", function (verbose) {
+    const feedTypes = {
+        "application/rss+xml": "RSS",
+        "application/atom+xml": "Atom",
+        "text/xml": "XML",
+        "application/xml": "XML",
+        "application/rdf+xml": "XML"
+    };
+
+    function isValidFeed(data, principal, isFeed) {
+        if (!data || !principal)
+            return false;
+
+        if (!isFeed) {
+            var type = data.type && data.type.toLowerCase();
+            type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
+
+            isFeed = ["application/rss+xml", "application/atom+xml"].indexOf(type) >= 0 ||
+                     // really slimy: general XML types with magic letters in the title
+                     type in feedTypes && /\brss\b/i.test(data.title);
+        }
+
+        if (isFeed) {
+            try {
+                services.security.checkLoadURIStrWithPrincipal(principal, data.href,
+                        services.security.DISALLOW_INHERIT_PRINCIPAL);
+            }
+            catch (e) {
+                isFeed = false;
+            }
+        }
+
+        if (type)
+            data.type = type;
+
+        return isFeed;
+    }
+
+    let nFeed = 0;
+    for (let [i, win] in Iterator(this.allFrames())) {
+        let doc = win.document;
+
+        for (let link in DOM("link[href][rel=feed], link[href][rel=alternate][type]", doc)) {
+            let rel = link.rel.toLowerCase();
+            let feed = { title: link.title, href: link.href, type: link.type || "" };
+            if (isValidFeed(feed, doc.nodePrincipal, rel == "feed")) {
+                nFeed++;
+                let type = feedTypes[feed.type] || "RSS";
+                if (verbose)
+                    yield [feed.title, template.highlightURL(feed.href, true) + <span class="extra-info">&#xa0;({type})</span>];
+            }
+        }
+
+    }
+
+    if (!verbose && nFeed)
+        yield nFeed + /*L*/" feed" + (nFeed > 1 ? "s" : "");
+});
+
+Buffer.addPageInfoSection("g", "General Info", function (verbose) {
+    let doc = this.focusedFrame.document;
+
+    // get file size
+    const ACCESS_READ = Ci.nsICache.ACCESS_READ;
+    let cacheKey = doc.documentURI;
+
+    for (let proto in array.iterValues(["HTTP", "FTP"])) {
+        try {
+            var cacheEntryDescriptor = services.cache.createSession(proto, 0, true)
+                                               .openCacheEntry(cacheKey, ACCESS_READ, false);
+            break;
+        }
+        catch (e) {}
+    }
+
+    let pageSize = []; // [0] bytes; [1] kbytes
+    if (cacheEntryDescriptor) {
+        pageSize[0] = util.formatBytes(cacheEntryDescriptor.dataSize, 0, false);
+        pageSize[1] = util.formatBytes(cacheEntryDescriptor.dataSize, 2, true);
+        if (pageSize[1] == pageSize[0])
+            pageSize.length = 1; // don't output "xx Bytes" twice
+    }
+
+    let lastModVerbose = new Date(doc.lastModified).toLocaleString();
+    let lastMod = new Date(doc.lastModified).toLocaleFormat("%x %X");
+
+    if (lastModVerbose == "Invalid Date" || new Date(doc.lastModified).getFullYear() == 1970)
+        lastModVerbose = lastMod = null;
+
+    if (!verbose) {
+        if (pageSize[0])
+            yield (pageSize[1] || pageSize[0]) + /*L*/" bytes";
+        yield lastMod;
+        return;
+    }
+
+    yield ["Title", doc.title];
+    yield ["URL", template.highlightURL(doc.location.href, true)];
+
+    let ref = "referrer" in doc && doc.referrer;
+    if (ref)
+        yield ["Referrer", template.highlightURL(ref, true)];
+
+    if (pageSize[0])
+        yield ["File Size", pageSize[1] ? pageSize[1] + " (" + pageSize[0] + ")"
+                                        : pageSize[0]];
+
+    yield ["Mime-Type", doc.contentType];
+    yield ["Encoding", doc.characterSet];
+    yield ["Compatibility", doc.compatMode == "BackCompat" ? "Quirks Mode" : "Full/Almost Standards Mode"];
+    if (lastModVerbose)
+        yield ["Last Modified", lastModVerbose];
+});
+
+Buffer.addPageInfoSection("m", "Meta Tags", function (verbose) {
+    if (!verbose)
+        return [];
+
+    // get meta tag data, sort and put into pageMeta[]
+    let metaNodes = this.focusedFrame.document.getElementsByTagName("meta");
+
+    return Array.map(metaNodes, function (node) [(node.name || node.httpEquiv), template.highlightURL(node.content)])
+                .sort(function (a, b) util.compareIgnoreCase(a[0], b[0]));
+});
+
+Buffer.addPageInfoSection("s", "Security", function (verbose) {
+    let { statusline } = this.modules
+
+    let identity = this.topWindow.gIdentityHandler;
+
+    if (!verbose || !identity)
+        return; // For now
+
+    // Modified from Firefox
+    function location(data) array.compact([
+        data.city, data.state, data.country
+    ]).join(", ");
+
+    switch (statusline.security) {
+    case "secure":
+    case "extended":
+        var data = identity.getIdentityData();
+
+        yield ["Host", identity.getEffectiveHost()];
+
+        if (statusline.security === "extended")
+            yield ["Owner", data.subjectOrg];
+        else
+            yield ["Owner", _("pageinfo.s.ownerUnverified", data.subjectOrg)];
+
+        if (location(data).length)
+            yield ["Location", location(data)];
+
+        yield ["Verified by", data.caOrg];
+
+        if (identity._overrideService.hasMatchingOverride(identity._lastLocation.hostname,
+                                                      (identity._lastLocation.port || 443),
+                                                      data.cert, {}, {}))
+            yield ["User exception", /*L*/"true"];
+        break;
+    }
+});
+
+} catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
+
+endModule();
+
+// vim: set fdm=marker sw=4 ts=4 et ft=javascript:
diff --git a/common/modules/cache.jsm b/common/modules/cache.jsm
new file mode 100644 (file)
index 0000000..c240846
--- /dev/null
@@ -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:
index 7d7093e99a41befb806ccd157b0a3dd2b3cb6c86..eca375bfd65d502493183bbd545ff34a871f07ce 100644 (file)
@@ -4,15 +4,14 @@
 //
 // 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);
 
 /**
@@ -26,6 +25,7 @@ defineModule("commands", {
  * @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),
@@ -72,6 +72,11 @@ update(CommandOption, {
      * @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
@@ -118,22 +123,17 @@ update(CommandOption, {
 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],
@@ -165,8 +165,11 @@ var Command = Class("Command", {
         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));
@@ -185,8 +188,7 @@ var Command = Class("Command", {
      * @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.
@@ -206,23 +208,27 @@ var Command = Class("Command", {
         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(""),
@@ -275,9 +281,15 @@ var Command = Class("Command", {
      * @property {Array} The options this command takes.
      * @see Commands@parseArguments
      */
-    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)
+    optionMap: Class.Memoize(function () array(this.options)
                 .map(function (opt) opt.names.map(function (name) [name, opt]))
                 .flatten().toObject()),
 
@@ -288,19 +300,19 @@ var Command = Class("Command", {
         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) {
@@ -316,10 +328,14 @@ var Command = Class("Command", {
         });
 
         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;
@@ -373,6 +389,10 @@ var Command = Class("Command", {
             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
@@ -450,12 +470,55 @@ var Ex = Module("Ex", {
 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,
@@ -478,6 +541,8 @@ var CommandHive = Class("CommandHive", Contexts.Hive, {
         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);
@@ -485,15 +550,17 @@ var CommandHive = Class("CommandHive", Contexts.Hive, {
         let names = array.flatten(extra.parsedSpecs);
         let name = names[0];
 
-        util.assert(!names.some(function (name) name in commands.builtin._map),
-                    _("command.cantReplace", name));
+        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));
+            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);
         }
 
@@ -501,7 +568,8 @@ var CommandHive = Class("CommandHive", Contexts.Hive, {
         let closure = function () self._map[name];
 
         memoize(this._map, name, function () commands.Command(specs, description, action, extra));
-        memoize(this._list, this._list.length, closure);
+        if (!extra.hidden)
+            memoize(this._list, this._list.length, closure);
         for (let alias in values(names.slice(1)))
             memoize(this._map, alias, closure);
 
@@ -535,9 +603,22 @@ var CommandHive = Class("CommandHive", Contexts.Hive, {
      *     its names matches *name* exactly.
      * @returns {Command}
      */
-    get: function get(name, full) this._map[name]
-            || !full && array.nth(this._list, function (cmd) cmd.hasName(name), 0)
-            || null,
+    get: function get(name, full) {
+        let cmd = this._map[name]
+               || !full && array.nth(this._list, function (cmd) cmd.hasName(name), 0)
+               || 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*.
@@ -560,6 +641,7 @@ var CommandHive = Class("CommandHive", Contexts.Hive, {
  */
 var Commands = Module("commands", {
     lazyInit: true,
+    lazyDepends: true,
 
     Local: function Local(dactyl, modules, window) let ({ Group, contexts } = modules) ({
         init: function init() {
@@ -571,6 +653,13 @@ var Commands = Module("commands", {
             });
         },
 
+        reallyInit: function reallyInit() {
+            if (false)
+                this.builtin.cache();
+            else
+                this.modules.moduleManager.initDependencies("commands");
+        },
+
         get context() contexts.context,
 
         get readHeredoc() modules.io.readHeredoc,
@@ -747,8 +836,11 @@ var Commands = Module("commands", {
                                           .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);
@@ -756,13 +848,14 @@ var Commands = Module("commands", {
                 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(" ");
     },
@@ -1166,16 +1259,16 @@ var Commands = Module("commands", {
         ]]>, /U/g, "\\u"), "x")
     }),
 
-    validName: Class.memoize(function validName() util.regexp("^" + this.nameRegexp.source + "$")),
+    validName: Class.Memoize(function validName() util.regexp("^" + this.nameRegexp.source + "$")),
 
-    commandRegexp: Class.memoize(function commandRegexp() util.regexp(<![CDATA[
+    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<group> <name>) : )?
+                    (?P<cmd>      (?:-? [()] | <name> | !)? ))
                 (?P<bang>     !?)
                 (?P<space>    \s*)
             )
@@ -1240,10 +1333,8 @@ var Commands = Module("commands", {
                 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 });
@@ -1368,7 +1459,7 @@ var Commands = Module("commands", {
                 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) {
@@ -1385,6 +1476,40 @@ var Commands = Module("commands", {
             }
         };
 
+        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" };
@@ -1395,6 +1520,13 @@ var Commands = Module("commands", {
     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) {
@@ -1410,7 +1542,7 @@ var Commands = Module("commands", {
                                 _("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)) {
@@ -1439,7 +1571,7 @@ var Commands = Module("commands", {
                                         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
@@ -1505,7 +1637,7 @@ var Commands = Module("commands", {
                                     ["+", "One or more arguments are allowed"]],
                         default: "0",
                         type: CommandOption.STRING,
-                        validator: function (arg) /^[01*?+]$/.test(arg)
+                        validator: bind("test", /^[01*?+]$/)
                     },
                     {
                         names: ["-nopersist", "-n"],
@@ -1576,7 +1708,7 @@ var Commands = Module("commands", {
                     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"],
@@ -1606,9 +1738,9 @@ var Commands = Module("commands", {
     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) {
index 9671e95aad543f8eadf3a5f6acaaf8498bef26c0..ff9c0917dade8a5403118350df32baa0b1485682 100644 (file)
@@ -4,14 +4,11 @@
 //
 // 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);
 
 /**
@@ -212,6 +209,18 @@ var CompletionContext = Class("CompletionContext", {
         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}
@@ -222,28 +231,34 @@ var CompletionContext = Class("CompletionContext", {
      * @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 [];
-                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 = 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) {
@@ -253,7 +268,7 @@ var CompletionContext = Class("CompletionContext", {
     },
     // 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);
@@ -295,10 +310,12 @@ var CompletionContext = Class("CompletionContext", {
             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
@@ -338,11 +355,16 @@ var CompletionContext = Class("CompletionContext", {
      * 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))
@@ -350,7 +372,7 @@ var CompletionContext = Class("CompletionContext", {
                 // 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));
@@ -405,7 +427,7 @@ var CompletionContext = Class("CompletionContext", {
         this.noUpdate = false;
     },
 
-    ignoreCase: Class.memoize(function () {
+    ignoreCase: Class.Memoize(function () {
         let mode = this.wildcase;
         if (mode == "match")
             return false;
@@ -469,7 +491,7 @@ var CompletionContext = Class("CompletionContext", {
         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
@@ -542,10 +564,11 @@ var CompletionContext = Class("CompletionContext", {
                 // 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)
@@ -589,7 +612,7 @@ var CompletionContext = Class("CompletionContext", {
         }
         this.offset += count;
         if (this._filter)
-            this._filter = this._filter.substr(advance);
+            this._filter = this._filter.substr(arguments[0] || 0);
     },
 
     /**
@@ -624,15 +647,38 @@ var CompletionContext = Class("CompletionContext", {
         return iter.map(util.range(start, end, step), function (i) items[i]);
     },
 
+    getRow: function getRow(idx, doc) {
+        let cache = this.cache.rows;
+        if (cache) {
+            if (idx in this.items && !(idx in this.cache.rows))
+                try {
+                    cache[idx] = util.xmlToDom(this.createRow(this.items[idx]),
+                                               doc || this.doc);
+                }
+                catch (e) {
+                    util.reportError(e);
+                    cache[idx] = util.xmlToDom(
+                        <div highlight="CompItem" style="white-space: nowrap">
+                            <li highlight="CompResult">{this.text}&#xa0;</li>
+                            <li highlight="CompDesc ErrorMsg">{e}&#xa0;</li>
+                        </div>, doc || this.doc);
+                }
+            return cache[idx];
+        }
+    },
+
     getRows: function getRows(start, end, doc) {
         let self = this;
         let items = this.items;
         let cache = this.cache.rows;
         let step = start > end ? -1 : 1;
+
         start = Math.max(0, start || 0);
         end = Math.min(items.length, end != null ? end : items.length);
+
+        this.doc = doc;
         for (let i in util.range(start, end, step))
-            yield [i, cache[i] = cache[i] || util.xmlToDom(self.createRow(items[i]), doc)];
+            yield [i, this.getRow(i)];
     },
 
     /**
@@ -823,7 +869,7 @@ var CompletionContext = Class("CompletionContext", {
 
     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]);
@@ -850,6 +896,7 @@ var Completion = Module("completion", {
     Local: function (dactyl, modules, window) ({
         urlCompleters: {},
 
+        get modules() modules,
         get options() modules.options,
 
         // FIXME
@@ -878,7 +925,7 @@ var Completion = Module("completion", {
             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)
@@ -920,9 +967,11 @@ var Completion = Module("completion", {
 
         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)];
                 };
             });
 
@@ -931,7 +980,7 @@ var Completion = Module("completion", {
 
         // 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);
     },
@@ -942,6 +991,64 @@ var Completion = Module("completion", {
         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;
@@ -963,11 +1070,13 @@ var Completion = Module("completion", {
             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(
@@ -1054,8 +1163,32 @@ var Completion = Module("completion", {
 
         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",
@@ -1085,6 +1218,6 @@ var Completion = Module("completion", {
 
 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:
index 4da5a88d6f50249a4fdba6cfaa07d3945cf0186c..c9c1420fba6396ed0edbd3597b141da15a483ecf 100644 (file)
 //
 // 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)
         });
     },
 
-    loadStyles: function loadStyles(force) {
-        const { highlight } = require("highlight");
-        const { _ } = require("messages");
+    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) {
         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 {
+                !TabIconNumber  {
                     font-weight: bold;
                     color: white;
                     text-align: center;
@@ -60,48 +174,40 @@ var ConfigBase = Class("ConfigBase", {
 
             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];
 
@@ -119,18 +225,10 @@ var ConfigBase = Class("ConfigBase", {
                         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));
     }),
 
@@ -142,18 +240,105 @@ var ConfigBase = Class("ConfigBase", {
      * @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 &&
@@ -169,26 +354,47 @@ var ConfigBase = Class("ConfigBase", {
      *     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)))
@@ -203,10 +409,10 @@ var ConfigBase = Class("ConfigBase", {
         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),
+        "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
+        "hg.latest": Class.Memoize(function () this.code + "source/browse/"), // XXX
         "irc": "irc://irc.oftc.net/#pentadactyl",
     }),
 
@@ -233,7 +439,6 @@ var ConfigBase = Class("ConfigBase", {
     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;
@@ -241,8 +446,9 @@ var ConfigBase = Class("ConfigBase", {
         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"/>
@@ -261,15 +467,23 @@ var ConfigBase = Class("ConfigBase", {
             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
@@ -277,7 +491,7 @@ var ConfigBase = Class("ConfigBase", {
          */
         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),
     }),
 
     /**
@@ -291,45 +505,15 @@ var ConfigBase = Class("ConfigBase", {
      * @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: {},
 
     /**
@@ -360,21 +544,12 @@ var ConfigBase = Class("ConfigBase", {
 
     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.
@@ -394,525 +569,18 @@ var ConfigBase = Class("ConfigBase", {
      * @property {string} The leaf name of any temp files created by
      *     {@link io.createTempFile}.
      */
-    get tempFile() this.name + ".tmp",
+    get tempFile() this.name + ".txt",
 
     /**
      * @constant
      * @property {string} The default highlighting rules.
      * See {@link Highlights#loadCSS} for details.
      */
-    CSS: UTF8(String.replace(<><![CDATA[
-        // <css>
-        Boolean      /* JavaScript booleans */       color: red;
-        Function     /* JavaScript functions */      color: navy;
-        Null         /* JavaScript null values */    color: blue;
-        Number       /* JavaScript numbers */        color: blue;
-        Object       /* JavaScript objects */        color: maroon;
-        String       /* String values */             color: green; white-space: pre;
-        Comment      /* JavaScriptor CSS comments */ color: gray;
-
-        Key          /* Keywords */                  font-weight: bold;
-
-        Enabled      /* Enabled item indicator text */  color: blue;
-        Disabled     /* Disabled item indicator text */ color: red;
-
-        FontFixed           /* The font used for fixed-width text */ \
-                                             font-family: monospace !important;
-        FontCode            /* The font used for code listings */ \
-                            font-size: 9pt;  font-family: monospace !important;
-        FontProportional    /* The font used for proportionally spaced text */ \
-                            font-size: 10pt; font-family: "Droid Sans", "Helvetica LT Std", Helvetica, "DejaVu Sans", Verdana, sans-serif !important;
-
-        // Hack to give these groups slightly higher precedence
-        // than their unadorned variants.
-        CmdCmdLine;[dactyl|highlight]>*  &#x0d; StatusCmdLine;[dactyl|highlight]>*
-        CmdNormal;[dactyl|highlight]     &#x0d; StatusNormal;[dactyl|highlight]
-        CmdErrorMsg;[dactyl|highlight]   &#x0d; StatusErrorMsg;[dactyl|highlight]
-        CmdInfoMsg;[dactyl|highlight]    &#x0d; StatusInfoMsg;[dactyl|highlight]
-        CmdModeMsg;[dactyl|highlight]    &#x0d; StatusModeMsg;[dactyl|highlight]
-        CmdMoreMsg;[dactyl|highlight]    &#x0d; StatusMoreMsg;[dactyl|highlight]
-        CmdQuestion;[dactyl|highlight]   &#x0d; StatusQuestion;[dactyl|highlight]
-        CmdWarningMsg;[dactyl|highlight] &#x0d; StatusWarningMsg;[dactyl|highlight]
-
-        Normal            /* Normal text */ \
-                          color: black   !important; background: white       !important; font-weight: normal !important;
-        StatusNormal      /* Normal text in the status line */ \
-                          color: inherit !important; background: transparent !important;
-        ErrorMsg          /* Error messages */ \
-                          color: white   !important; background: red         !important; font-weight: bold !important;
-        InfoMsg           /* Information messages */ \
-                          color: black   !important; background: white       !important;
-        StatusInfoMsg     /* Information messages in the status line */ \
-                          color: inherit !important; background: transparent !important;
-        LineNr            /* The line number of an error */ \
-                          color: orange  !important; background: white       !important;
-        ModeMsg           /* The mode indicator */ \
-                          color: black   !important; background: white       !important;
-        StatusModeMsg     /* The mode indicator in the status line */ \
-                          color: inherit !important; background: transparent !important; padding-right: 1em;
-        MoreMsg           /* The indicator that there is more text to view */ \
-                          color: green   !important; background: white       !important;
-        StatusMoreMsg                                background: transparent !important;
-        Message           /* A message as displayed in <ex>:messages</ex> */ \
-                          white-space: pre-wrap !important; min-width: 100%; width: 100%; padding-left: 4em; text-indent: -4em; display: block;
-        Message String    /* A message as displayed in <ex>:messages</ex> */ \
-                          white-space: pre-wrap;
-        NonText           /* The <em>~</em> indicators which mark blank lines in the completion list */ \
-                          color: blue; background: transparent !important;
-        *Preview          /* The completion preview displayed in the &tag.command-line; */ \
-                          color: gray;
-        Question          /* A prompt for a decision */ \
-                          color: green   !important; background: white       !important; font-weight: bold !important;
-        StatusQuestion    /* A prompt for a decision in the status line */ \
-                          color: green   !important; background: transparent !important;
-        WarningMsg        /* A warning message */ \
-                          color: red     !important; background: white       !important;
-        StatusWarningMsg  /* A warning message in the status line */ \
-                          color: red     !important; background: transparent !important;
-        Disabled          /* Disabled items */ \
-                          color: gray    !important;
-
-        CmdLine;>*;;FontFixed   /* The command line */ \
-                                padding: 1px !important;
-        CmdPrompt;.dactyl-commandline-prompt  /* The default styling form the command prompt */
-        CmdInput;.dactyl-commandline-command
-        CmdOutput         /* The output of commands executed by <ex>:run</ex> */ \
-                          white-space: pre;
-
-        CompGroup                      /* Item group in completion output */
-        CompGroup:not(:first-of-type)  margin-top: .5em;
-        CompGroup:last-of-type         padding-bottom: 1.5ex;
-
-        CompTitle            /* Completion row titles */ \
-                             color: magenta; background: white; font-weight: bold;
-        CompTitle>*          padding: 0 .5ex;
-        CompTitleSep         /* The element which separates the completion title from its results */ \
-                             height: 1px; background: magenta; background: -moz-linear-gradient(60deg, magenta, white);
-
-        CompMsg              /* The message which may appear at the top of a group of completion results */ \
-                             font-style: italic; margin-left: 16px;
-
-        CompItem             /* A single row of output in the completion list */
-        CompItem:nth-child(2n+1)    background: rgba(0, 0, 0, .04);
-        CompItem[selected]   /* A selected row of completion list */ \
-                             background: yellow;
-        CompItem>*           padding: 0 .5ex;
-
-        CompIcon             /* The favicon of a completion row */ \
-                             width: 16px; min-width: 16px; display: inline-block; margin-right: .5ex;
-        CompIcon>img         max-width: 16px; max-height: 16px; vertical-align: middle;
-
-        CompResult           /* The result column of the completion list */ \
-                             width: 36%; padding-right: 1%; overflow: hidden;
-        CompDesc             /* The description column of the completion list */ \
-                             color: gray; width: 62%; padding-left: 1em;
-
-        CompLess             /* The indicator shown when completions may be scrolled up */ \
-                             text-align: center; height: 0;    line-height: .5ex; padding-top: 1ex;
-        CompLess::after      /* The character of indicator shown when completions may be scrolled up */ \
-                             content: "⌃";
-
-        CompMore             /* The indicator shown when completions may be scrolled down */ \
-                             text-align: center; height: .5ex; line-height: .5ex; margin-bottom: -.5ex;
-        CompMore::after      /* The character of indicator shown when completions may be scrolled down */ \
-                             content: "⌄";
-
-        Dense              /* Arbitrary elements which should be packed densely together */\
-                           margin-top: 0; margin-bottom: 0;
-
-        EditorEditing;;*   /* Text fields for which an external editor is open */ \
-                           background-color: #bbb !important; -moz-user-input: none !important; -moz-user-modify: read-only !important;
-        EditorError;;*     /* Text fields briefly after an error has occurred running the external editor */ \
-                           background: red !important;
-        EditorBlink1;;*    /* Text fields briefly after successfully running the external editor, alternated with EditorBlink2 */ \
-                           background: yellow !important;
-        EditorBlink2;;*    /* Text fields briefly after successfully running the external editor, alternated with EditorBlink1 */
-
-        REPL                /* Read-Eval-Print-Loop output */ \
-                            overflow: auto; max-height: 40em;
-        REPL-R;;;Question   /* Prompts in REPL mode */
-        REPL-E              /* Evaled input in REPL mode */ \
-                            white-space: pre-wrap;
-        REPL-P              /* Evaled output in REPL mode */ \
-                            white-space: pre-wrap; margin-bottom: 1em;
-
-        Usage               /* Output from the :*usage commands */ \
-                            width: 100%;
-        UsageHead           /* Headings in output from the :*usage commands */
-        UsageBody           /* The body of listings in output from the :*usage commands */
-        UsageItem           /* Individual items in output from the :*usage commands */
-        UsageItem:nth-of-type(2n)    background: rgba(0, 0, 0, .04);
-
-        Indicator   /* The <em>#</em> and  <em>%</em> in the <ex>:buffers</ex> list */ \
-                    color: blue; width: 1.5em; text-align: center;
-        Filter      /* The matching text in a completion list */ \
-                    font-weight: bold;
-
-        Keyword     /* A bookmark keyword for a URL */ \
-                    color: red;
-        Tag         /* A bookmark tag for a URL */ \
-                    color: blue;
-
-        Link                        /* A link with additional information shown on hover */ \
-                                    position: relative; padding-right: 2em;
-        Link:not(:hover)>LinkInfo   opacity: 0; left: 0; width: 1px; height: 1px; overflow: hidden;
-        LinkInfo                    {
-            /* Information shown when hovering over a link */
-            color: black;
-            position: absolute;
-            left: 100%;
-            padding: 1ex;
-            margin: -1ex -1em;
-            background: rgba(255, 255, 255, .8);
-            border-radius: 1ex;
-        }
+    CSS: Class.Memoize(function () File.readURL("resource://dactyl-skin/global-styles.css")),
 
-        StatusLine;;;FontFixed  {
-            /* The status bar */
-            -moz-appearance: none !important;
-            font-weight: bold;
-            background: transparent !important;
-            border: 0px !important;
-            padding-right: 0px !important;
-            min-height: 18px !important;
-            text-shadow: none !important;
-        }
-        StatusLineNormal;[dactyl|highlight]    /* The status bar for an ordinary web page */ \
-                                               color: white !important; background: black   !important;
-        StatusLineBroken;[dactyl|highlight]    /* The status bar for a broken web page */ \
-                                               color: black !important; background: #FFa0a0 !important; /* light-red */
-        StatusLineSecure;[dactyl|highlight]    /* The status bar for a secure web page */ \
-                                               color: black !important; background: #a0a0FF !important; /* light-blue */
-        StatusLineExtended;[dactyl|highlight]  /* The status bar for a secure web page with an Extended Validation (EV) certificate */ \
-                                               color: black !important; background: #a0FFa0 !important; /* light-green */
-
-        !TabClose;.tab-close-button            /* The close button of a browser tab */ \
-                                               /* The close button of a browser tab */
-        !TabIcon;.tab-icon,.tab-icon-image     /* The icon of a browser tab */ \
-                                               /* The icon of a browser tab */
-        !TabText;.tab-text                     /* The text of a browser tab */
-        TabNumber                              /* The number of a browser tab, next to its icon */ \
-                                               font-weight: bold; margin: 0px; padding-right: .8ex; cursor: default;
-        TabIconNumber  {
-            /* The number of a browser tab, over its icon */
-            cursor: default;
-            width: 16px;
-            margin: 0 2px 0 -18px !important;
-            font-weight: bold;
-            color: white;
-            text-align: center;
-            text-shadow: black -1px 0 1px, black 0 1px 1px, black 1px 0 1px, black 0 -1px 1px;
-        }
-
-        Title       /* The title of a listing, including <ex>:pageinfo</ex>, <ex>:jumps</ex> */ \
-                    color: magenta; font-weight: bold;
-        URL         /* A URL */ \
-                    text-decoration: none; color: green; background: inherit;
-        URL:hover   text-decoration: underline; cursor: pointer;
-        URLExtra    /* Extra information about a URL */ \
-                    color: gray;
-
-        FrameIndicator;;* {
-            /* The styling applied to briefly indicate the active frame */
-            background-color: red;
-            opacity: 0.5;
-            z-index: 999999;
-            position: fixed;
-            top:      0;
-            bottom:   0;
-            left:     0;
-            right:    0;
-        }
-
-        Bell          /* &dactyl.appName;’s visual bell */ \
-                      background-color: black !important;
-
-        Hint;;* {
-            /* A hint indicator. See <ex>:help hints</ex> */
-            font:        bold 10px "Droid Sans Mono", monospace !important;
-            margin:      -.2ex;
-            padding:     0 0 0 1px;
-            outline:     1px solid rgba(0, 0, 0, .5);
-            background:  rgba(255, 248, 231, .8);
-            color:       black;
-        }
-        Hint[active];;*  background: rgba(255, 253, 208, .8);
-        Hint::after;;*   content: attr(text) !important;
-        HintElem;;*      /* The hintable element */ \
-                         background-color: yellow  !important; color: black !important;
-        HintActive;;*    /* The hint element of link which will be followed by <k name="CR"/> */ \
-                         background-color: #88FF00 !important; color: black !important;
-        HintImage;;*     /* The indicator which floats above hinted images */ \
-                         opacity: .5 !important;
-
-        Button                  /* A button widget */ \
-                                display: inline-block; font-weight: bold; cursor: pointer; color: black; text-decoration: none;
-        Button:hover            text-decoration: underline;
-        Button[collapsed]       visibility: collapse; width: 0;
-        Button::before          content: "["; color: gray; text-decoration: none !important;
-        Button::after           content: "]"; color: gray; text-decoration: none !important;
-        Button:not([collapsed]) ~ Button:not([collapsed])::before  content: "/[";
-
-        Buttons                 /* A group of buttons */
-
-        DownloadCell                    /* A table cell in the :downloads manager */ \
-                                        display: table-cell; padding: 0 1ex;
-
-        Downloads                       /* The :downloads manager */ \
-                                        display: table; margin: 0; padding: 0;
-        DownloadHead;;;CompTitle        /* A heading in the :downloads manager */ \
-                                        display: table-row;
-        DownloadHead>*;;;DownloadCell
-
-        Download                        /* A download in the :downloads manager */ \
-                                        display: table-row;
-        Download:not([active])          color: gray;
-        Download:nth-child(2n+1)        background: rgba(0, 0, 0, .04);
-
-        Download>*;;;DownloadCell
-        DownloadButtons                 /* A button group in the :downloads manager */
-        DownloadPercent                 /* The percentage column for a download */
-        DownloadProgress                /* The progress column for a download */
-        DownloadProgressHave            /* The completed portion of the progress column */
-        DownloadProgressTotal           /* The remaining portion of the progress column */
-        DownloadSource                  /* The download source column for a download */
-        DownloadState                   /* The download state column for a download */
-        DownloadTime                    /* The time remaining column for a download */
-        DownloadTitle                   /* The title column for a download */
-        DownloadTitle>Link>a         max-width: 48ex; overflow: hidden; display: inline-block;
-
-        AddonCell                    /* A cell in tell :addons manager */ \
-                                     display: table-cell; padding: 0 1ex;
-
-        Addons                       /* The :addons manager */ \
-                                     display: table; margin: 0; padding: 0;
-        AddonHead;;;CompTitle        /* A heading in the :addons manager */ \
-                                     display: table-row;
-        AddonHead>*;;;AddonCell
-
-        Addon                        /* An add-on in the :addons manager */ \
-                                     display: table-row;
-        Addon:nth-child(2n+1)        background: rgba(0, 0, 0, .04);
-
-        Addon>*;;;AddonCell
-        AddonButtons
-        AddonDescription
-        AddonName                    max-width: 48ex; overflow: hidden;
-        AddonStatus
-        AddonVersion
-
-        // </css>
-    ]]></>, /&#x0d;/g, "\n")),
-
-    helpCSS: UTF8(<><![CDATA[
-        // <css>
-        InlineHelpLink                              /* A help link shown in the command line or multi-line output area */ \
-                                                    font-size: inherit !important; font-family: inherit !important;
-
-        Help;;;FontProportional                     /* A help page */ \
-                                                    line-height: 1.4em;
-
-        HelpInclude                                 /* A help page included in the consolidated help listing */ \
-                                                    margin: 2em 0;
-
-        HelpArg;;;FontCode                          /* A required command argument indicator */ \
-                                                    color: #6A97D4;
-        HelpOptionalArg;;;FontCode                  /* An optional command argument indicator */ \
-                                                    color: #6A97D4;
-
-        HelpBody                                    /* The body of a help page */ \
-                                                    display: block; margin: 1em auto; max-width: 100ex; padding-bottom: 1em; margin-bottom: 4em; border-bottom-width: 1px;
-        HelpBorder;*;dactyl://help/*                /* The styling of bordered elements */ \
-                                                    border-color: silver; border-width: 0px; border-style: solid;
-        HelpCode;;;FontCode                         /* Code listings */ \
-                                                    display: block; white-space: pre; margin-left: 2em;
-        HelpTT;html|tt;dactyl://help/*;FontCode     /* Teletype text */
-
-        HelpDefault;;;FontCode                      /* The default value of a help item */ \
-                                                    display: inline-block; margin: -1px 1ex 0 0; white-space: pre; vertical-align: text-top;
-
-        HelpDescription                             /* The description of a help item */ \
-                                                    display: block; clear: right;
-        HelpDescription[short]                      clear: none;
-        HelpEm;html|em;dactyl://help/*              /* Emphasized text */ \
-                                                    font-weight: bold; font-style: normal;
-
-        HelpEx;;;FontCode                           /* An Ex command */ \
-                                                    display: inline-block; color: #527BBD;
-
-        HelpExample                                 /* An example */ \
-                                                    display: block; margin: 1em 0;
-        HelpExample::before                         content: "__MSG_help.Example__: "; font-weight: bold;
-
-        HelpInfo                                    /* Arbitrary information about a help item */ \
-                                                    display: block; width: 20em; margin-left: auto;
-        HelpInfoLabel                               /* The label for a HelpInfo item */ \
-                                                    display: inline-block; width: 6em;  color: magenta; font-weight: bold; vertical-align: text-top;
-        HelpInfoValue                               /* The details for a HelpInfo item */ \
-                                                    display: inline-block; width: 14em; text-decoration: none;             vertical-align: text-top;
-
-        HelpItem                                    /* A help item */ \
-                                                    display: block; margin: 1em 1em 1em 10em; clear: both;
-
-        HelpKey;;;FontCode                          /* A keyboard key specification */ \
-                                                    color: #102663;
-        HelpKeyword                                 /* A keyword */ \
-                                                    font-weight: bold; color: navy;
-
-        HelpLink;html|a;dactyl://help/*             /* A hyperlink */ \
-                                                    text-decoration: none !important;
-        HelpLink[href]:hover                        text-decoration: underline !important;
-        HelpLink[href^="mailto:"]::after            content: "✉"; padding-left: .2em;
-        HelpLink[rel=external] {
-            /* A hyperlink to an external resource */
-            /* Thanks, Wikipedia */
-            background: transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAMAAAC67D+PAAAAFVBMVEVmmcwzmcyZzP8AZswAZv////////9E6giVAAAAB3RSTlP///////8AGksDRgAAADhJREFUGFcly0ESAEAEA0Ei6/9P3sEcVB8kmrwFyni0bOeyyDpy9JTLEaOhQq7Ongf5FeMhHS/4AVnsAZubxDVmAAAAAElFTkSuQmCC) no-repeat scroll right center;
-            padding-right: 13px;
-        }
-
-        ErrorMsg HelpEx       color: inherit; background: inherit; text-decoration: underline;
-        ErrorMsg HelpKey      color: inherit; background: inherit; text-decoration: underline;
-        ErrorMsg HelpOption   color: inherit; background: inherit; text-decoration: underline;
-        ErrorMsg HelpTopic    color: inherit; background: inherit; text-decoration: underline;
-
-        HelpTOC               /* The Table of Contents for a help page */
-        HelpTOC>ol ol         margin-left: -1em;
-
-        HelpOrderedList;ol;dactyl://help/*                          /* Any ordered list */ \
-                                                                    margin: 1em 0;
-        HelpOrderedList1;ol[level="1"],ol;dactyl://help/*           /* A first-level ordered list */ \
-                                                                    list-style: outside decimal; display: block;
-        HelpOrderedList2;ol[level="2"],ol ol;dactyl://help/*        /* A second-level ordered list */ \
-                                                                    list-style: outside upper-alpha;
-        HelpOrderedList3;ol[level="3"],ol ol ol;dactyl://help/*     /* A third-level ordered list */ \
-                                                                    list-style: outside lower-roman;
-        HelpOrderedList4;ol[level="4"],ol ol ol ol;dactyl://help/*  /* A fourth-level ordered list */ \
-                                                                    list-style: outside decimal;
-
-        HelpList;html|ul;dactyl://help/*      /* An unordered list */ \
-                                              display: block; list-style-position: outside; margin: 1em 0;
-        HelpListItem;html|li;dactyl://help/*  /* A list item, ordered or unordered */ \
-                                              display: list-item;
-
-        HelpNote                                    /* The indicator for a note */ \
-                                                    color: red; font-weight: bold;
-
-        HelpOpt;;;FontCode                          /* An option name */ \
-                                                    color: #106326;
-        HelpOptInfo;;;FontCode                      /* Information about the type and default values for an option entry */ \
-                                                    display: block; margin-bottom: 1ex; padding-left: 4em;
-
-        HelpParagraph;html|p;dactyl://help/*        /* An ordinary paragraph */ \
-                                                    display: block; margin: 1em 0em;
-        HelpParagraph:first-child                   margin-top: 0;
-        HelpParagraph:last-child                    margin-bottom: 0;
-        HelpSpec;;;FontCode                         /* The specification for a help entry */ \
-                                                    display: block; margin-left: -10em; float: left; clear: left; color: #527BBD; margin-right: 1em;
-
-        HelpString;;;FontCode                       /* A quoted string */ \
-                                                    color: green; font-weight: normal;
-        HelpString::before                          content: '"';
-        HelpString::after                           content: '"';
-        HelpString[delim]::before                   content: attr(delim);
-        HelpString[delim]::after                    content: attr(delim);
-
-        HelpNews        /* A news item */           position: relative;
-        HelpNewsOld     /* An old news item */      opacity: .7;
-        HelpNewsNew     /* A new news item */       font-style: italic;
-        HelpNewsTag     /* The version tag for a news item */ \
-                        font-style: normal; position: absolute; left: 100%; padding-left: 1em; color: #527BBD; opacity: .6; white-space: pre;
-
-        HelpHead;html|h1,html|h2,html|h3,html|h4;dactyl://help/* {
-            /* Any help heading */
-            font-weight: bold;
-            color: #527BBD;
-            clear: both;
-        }
-        HelpHead1;html|h1;dactyl://help/* {
-            /* A first-level help heading */
-            margin: 2em 0 1em;
-            padding-bottom: .2ex;
-            border-bottom-width: 1px;
-            font-size: 2em;
-        }
-        HelpHead2;html|h2;dactyl://help/* {
-            /* A second-level help heading */
-            margin: 2em 0 1em;
-            padding-bottom: .2ex;
-            border-bottom-width: 1px;
-            font-size: 1.2em;
-        }
-        HelpHead3;html|h3;dactyl://help/* {
-            /* A third-level help heading */
-            margin: 1em 0;
-            padding-bottom: .2ex;
-            font-size: 1.1em;
-        }
-        HelpHead4;html|h4;dactyl://help/* {
-            /* A fourth-level help heading */
-        }
-
-        HelpTab;html|dl;dactyl://help/* {
-            /* A description table */
-            display: table;
-            width: 100%;
-            margin: 1em 0;
-            border-bottom-width: 1px;
-            border-top-width: 1px;
-            padding: .5ex 0;
-            table-layout: fixed;
-        }
-        HelpTabColumn;html|column;dactyl://help/*   display: table-column;
-        HelpTabColumn:first-child                   width: 25%;
-        HelpTabTitle;html|dt;dactyl://help/*;FontCode  /* The title column of description tables */ \
-                                                    display: table-cell; padding: .1ex 1ex; font-weight: bold;
-        HelpTabDescription;html|dd;dactyl://help/*  /* The description column of description tables */ \
-                                                    display: table-cell; padding: .3ex 1em; text-indent: -1em; border-width: 0px;
-        HelpTabDescription>*;;dactyl://help/*       text-indent: 0;
-        HelpTabRow;html|dl>html|tr;dactyl://help/*  /* Entire rows in description tables */ \
-                                                    display: table-row;
-
-        HelpTag;;;FontCode                          /* A help tag */ \
-                                                    display: inline-block; color: #527BBD; margin-left: 1ex; font-weight: normal;
-        HelpTags                                    /* A group of help tags */ \
-                                                    display: block; float: right; clear: right;
-        HelpTopic;;;FontCode                        /* A link to a help topic */ \
-                                                    color: #102663;
-        HelpType;;;FontCode                         /* An option type */ \
-                                                    color: #102663 !important; margin-right: 2ex;
-
-        HelpWarning                                 /* The indicator for a warning */ \
-                                                    color: red; font-weight: bold;
-
-        HelpXML;;;FontCode                          /* Highlighted XML */ \
-                                                    color: #C5F779; background-color: #444444; font-family: Terminus, Fixed, monospace;
-        HelpXMLBlock {                              white-space: pre; color: #C5F779; background-color: #444444;
-            border: 1px dashed #aaaaaa;
-            display: block;
-            margin-left: 2em;
-            font-family: Terminus, Fixed, monospace;
-        }
-        HelpXMLAttribute                            color: #C5F779;
-        HelpXMLAttribute::after                     color: #E5E5E5; content: "=";
-        HelpXMLComment                              color: #444444;
-        HelpXMLComment::before                      content: "<!--";
-        HelpXMLComment::after                       content: "-->";
-        HelpXMLProcessing                           color: #C5F779;
-        HelpXMLProcessing::before                   color: #444444; content: "<?";
-        HelpXMLProcessing::after                    color: #444444; content: "?>";
-        HelpXMLString                               color: #C5F779; white-space: pre;
-        HelpXMLString::before                       content: '"';
-        HelpXMLString::after                        content: '"';
-        HelpXMLNamespace                            color: #FFF796;
-        HelpXMLNamespace::after                     color: #777777; content: ":";
-        HelpXMLTagStart                             color: #FFF796; white-space: normal; display: inline-block; text-indent: -1.5em; padding-left: 1.5em;
-        HelpXMLTagEnd                               color: #71BEBE;
-        HelpXMLText                                 color: #E5E5E5;
-        // </css>
-    ]]></>)
+    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, {
@@ -922,7 +590,6 @@ config.INIT = update(Object.create(config.INIT), config.INIT, {
         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});
@@ -946,6 +613,6 @@ config.INIT = update(Object.create(config.INIT), config.INIT, {
 
 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:
index f5d976cae6dc030069d379599edd318ee66abf63..468193659893eb51ef2d84fead71f676b2ba1c92 100644 (file)
@@ -2,16 +2,16 @@
 //
 // 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", {
@@ -23,25 +23,42 @@ var Group = Class("Group", {
         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];
+
+        if (reason != "shutdown")
+            this.children.splice(0).forEach(this.contexts.closure.removeGroup);
     },
-    destroy: function destroy() {
+    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,
@@ -67,10 +84,21 @@ var Group = Class("Group", {
         });
     },
 
-    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;
@@ -115,13 +143,13 @@ var Contexts = Module("contexts", {
         },
 
         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))
@@ -179,29 +207,39 @@ var Contexts = Module("contexts", {
                                   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];
@@ -252,6 +290,74 @@ var Contexts = Module("contexts", {
         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,
 
     /**
@@ -271,9 +377,9 @@ var Contexts = Module("contexts", {
         return frame;
     },
 
-    groups: Class.memoize(function () this.matchingGroups(this.modules.buffer.uri)),
+    groups: Class.Memoize(function () this.matchingGroups()),
 
-    allGroups: Class.memoize(function () Object.create(this.groupsProto, {
+    allGroups: Class.Memoize(function () Object.create(this.groupsProto, {
         groups: { value: this.initializedGroups() }
     })),
 
@@ -281,10 +387,19 @@ var Contexts = Module("contexts", {
         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() {
@@ -310,6 +425,7 @@ var Contexts = Module("contexts", {
 
         if (replace) {
             util.trapErrors("cleanup", group);
+
             if (description)
                 group.description = description;
             if (filter)
@@ -321,7 +437,7 @@ var Contexts = Module("contexts", {
         return group;
     },
 
-    removeGroup: function removeGroup(name, filter) {
+    removeGroup: function removeGroup(name) {
         if (isObject(name)) {
             if (this.groupList.indexOf(name) === -1)
                 return;
@@ -401,7 +517,7 @@ var Contexts = Module("contexts", {
             /* 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);
@@ -450,6 +566,7 @@ var Contexts = Module("contexts", {
         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,
@@ -464,7 +581,7 @@ var Contexts = Module("contexts", {
         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]
     })
@@ -507,8 +624,19 @@ var Contexts = Module("contexts", {
                     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"]
@@ -680,6 +808,6 @@ 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 (file)
index 0000000..981ec9c
--- /dev/null
@@ -0,0 +1,1589 @@
+// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
+// Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
+//
+// This work is licensed for reuse under an MIT license. Details are
+// given in the LICENSE.txt file included with this file.
+/* use strict */
+
+Components.utils.import("resource://dactyl/bootstrap.jsm");
+defineModule("dom", {
+    exports: ["$", "DOM", "NS", "XBL", "XHTML", "XUL"]
+}, this);
+
+var XBL = Namespace("xbl", "http://www.mozilla.org/xbl");
+var XHTML = Namespace("html", "http://www.w3.org/1999/xhtml");
+var XUL = Namespace("xul", "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+var NS = Namespace("dactyl", "http://vimperator.org/namespaces/liberator");
+default xml namespace = XHTML;
+
+function BooleanAttribute(attr) ({
+    get: function (elem) elem.getAttribute(attr) == "true",
+    set: function (elem, val) {
+        if (val === "false" || !val)
+            elem.removeAttribute(attr);
+        else
+            elem.setAttribute(attr, true);
+    }
+});
+
+/**
+ * @class
+ *
+ * A jQuery-inspired DOM utility framework.
+ *
+ * Please note that while this currently implements an Array-like
+ * interface, this is *not a defined interface* and is very likely to
+ * change in the near future.
+ */
+var DOM = Class("DOM", {
+    init: function init(val, context, nodes) {
+        let self;
+        let length = 0;
+
+        if (nodes)
+            this.nodes = nodes;
+
+        if (context instanceof Ci.nsIDOMDocument)
+            this.document = context;
+
+        if (typeof val == "string")
+            val = context.querySelectorAll(val);
+
+        if (val == null)
+            ;
+        else if (typeof val == "xml" && context instanceof Ci.nsIDOMDocument)
+            this[length++] = DOM.fromXML(val, context, this.nodes);
+        else if (val instanceof Ci.nsIDOMNode || val instanceof Ci.nsIDOMWindow)
+            this[length++] = val;
+        else if ("length" in val)
+            for (let i = 0; i < val.length; i++)
+                this[length++] = val[i];
+        else if ("__iterator__" in val || isinstance(val, ["Iterator", "Generator"]))
+            for (let elem in val)
+                this[length++] = elem;
+        else
+            this[length++] = val;
+
+        this.length = length;
+        return self || this;
+    },
+
+    __iterator__: function __iterator__() {
+        for (let i = 0; i < this.length; i++)
+            yield this[i];
+    },
+
+    Empty: function Empty() this.constructor(null, this.document),
+
+    nodes: Class.Memoize(function () ({})),
+
+    get items() {
+        for (let i = 0; i < this.length; i++)
+            yield this.eq(i);
+    },
+
+    get document() this._document || this[0] && (this[0].ownerDocument || this[0].document || this[0]),
+    set document(val) this._document = val,
+
+    attrHooks: array.toObject([
+        ["", {
+            href: { get: function (elem) elem.href || elem.getAttribute("href") },
+            src:  { get: function (elem) elem.src || elem.getAttribute("src") },
+            checked: { get: function (elem) elem.hasAttribute("checked") ? elem.getAttribute("checked") == "true" : elem.checked,
+                       set: function (elem, val) { elem.setAttribute("checked", !!val); elem.checked = val } },
+            collapsed: BooleanAttribute("collapsed"),
+            disabled: BooleanAttribute("disabled"),
+            hidden: BooleanAttribute("hidden"),
+            readonly: BooleanAttribute("readonly")
+        }]
+    ]),
+
+    matcher: function matcher(sel) function (elem) elem.mozMatchesSelector && elem.mozMatchesSelector(sel),
+
+    each: function each(fn, self) {
+        let obj = self || this.Empty();
+        for (let i = 0; i < this.length; i++)
+            fn.call(self || update(obj, [this[i]]), this[i], i);
+        return this;
+    },
+
+    eachDOM: function eachDOM(val, fn, self) {
+        XML.prettyPrinting = XML.ignoreWhitespace = false;
+        if (isString(val))
+            val = XML(val);
+
+        if (typeof val == "xml")
+            return this.each(function (elem, i) {
+                fn.call(this, DOM.fromXML(val, elem.ownerDocument), elem, i);
+            }, self || this);
+
+        let dom = this;
+        function munge(val, container, idx) {
+            if (val instanceof Ci.nsIDOMRange)
+                return val.extractContents();
+            if (val instanceof Ci.nsIDOMNode)
+                return val;
+
+            if (typeof val == "xml") {
+                val = dom.constructor(val, dom.document);
+                if (container)
+                    container[idx] = val[0];
+            }
+
+            if (isObject(val) && "length" in val) {
+                let frag = dom.document.createDocumentFragment();
+                for (let i = 0; i < val.length; i++)
+                    frag.appendChild(munge(val[i], val, i));
+                return frag;
+            }
+            return val;
+        }
+
+        if (callable(val))
+            return this.each(function (elem, i) {
+                util.withProperErrors(fn, this, munge(val.call(this, elem, i)), elem, i);
+            }, self || this);
+
+        if (this.length)
+            util.withProperErrors(fn, self || this, munge(val), this[0], 0);
+        return this;
+    },
+
+    eq: function eq(idx) {
+        return this.constructor(this[idx >= 0 ? idx : this.length + idx]);
+    },
+
+    find: function find(val) {
+        return this.map(function (elem) elem.querySelectorAll(val));
+    },
+
+    findAnon: function findAnon(attr, val) {
+        return this.map(function (elem) elem.ownerDocument.getAnonymousElementByAttribute(elem, attr, val));
+    },
+
+    filter: function filter(val, self) {
+        let res = this.Empty();
+
+        if (!callable(val))
+            val = this.matcher(val);
+
+        this.constructor(Array.filter(this, val, self || this));
+        let obj = self || this.Empty();
+        for (let i = 0; i < this.length; i++)
+            if (val.call(self || update(obj, [this[i]]), this[i], i))
+                res[res.length++] = this[i];
+
+        return res;
+    },
+
+    is: function is(val) {
+        return this.some(this.matcher(val));
+    },
+
+    reverse: function reverse() {
+        Array.reverse(this);
+        return this;
+    },
+
+    all: function all(fn, self) {
+        let res = this.Empty();
+
+        this.each(function (elem) {
+            while(true) {
+                elem = fn.call(this, elem)
+                if (elem instanceof Ci.nsIDOMElement)
+                    res[res.length++] = elem;
+                else if (elem && "length" in elem)
+                    for (let i = 0; i < tmp.length; i++)
+                        res[res.length++] = tmp[j];
+                else
+                    break;
+            }
+        }, self || this);
+        return res;
+    },
+
+    map: function map(fn, self) {
+        let res = this.Empty();
+        let obj = self || this.Empty();
+
+        for (let i = 0; i < this.length; i++) {
+            let tmp = fn.call(self || update(obj, [this[i]]), this[i], i);
+            if (isObject(tmp) && "length" in tmp)
+                for (let j = 0; j < tmp.length; j++)
+                    res[res.length++] = tmp[j];
+            else if (tmp != null)
+                res[res.length++] = tmp;
+        }
+
+        return res;
+    },
+
+    slice: function eq(start, end) {
+        return this.constructor(Array.slice(this, start, end));
+    },
+
+    some: function some(fn, self) {
+        for (let i = 0; i < this.length; i++)
+            if (fn.call(self || this, this[i], i))
+                return true;
+        return false;
+    },
+
+    get parent() this.map(function (elem) elem.parentNode, this),
+
+    get offsetParent() this.map(function (elem) {
+        do {
+            var parent = elem.offsetParent;
+            if (parent instanceof Ci.nsIDOMElement && DOM(parent).position != "static")
+                return parent;
+        }
+        while (parent);
+    }, this),
+
+    get ancestors() this.all(function (elem) elem.parentNode),
+
+    get children() this.map(function (elem) Array.filter(elem.childNodes,
+                                                         function (e) e instanceof Ci.nsIDOMElement),
+                            this),
+
+    get contents() this.map(function (elem) elem.childNodes, this),
+
+    get siblings() this.map(function (elem) Array.filter(elem.parentNode.childNodes,
+                                                         function (e) e != elem && e instanceof Ci.nsIDOMElement),
+                            this),
+
+    get siblingsBefore() this.all(function (elem) elem.previousElementSibling),
+    get siblingsAfter() this.all(function (elem) elem.nextElementSibling),
+
+    get class() let (self = this) ({
+        toString: function () self[0].className,
+
+        get list() Array.slice(self[0].classList),
+        set list(val) self.attr("class", val.join(" ")),
+
+        each: function each(meth, arg) {
+            return self.each(function (elem) {
+                elem.classList[meth](arg);
+            })
+        },
+
+        add: function add(cls) this.each("add", cls),
+        remove: function remove(cls) this.each("remove", cls),
+        toggle: function toggle(cls, val, thisObj) {
+            if (callable(val))
+                return self.each(function (elem, i) {
+                    this.class.toggle(cls, val.call(thisObj || this, elem, i));
+                });
+            return this.each(val == null ? "toggle" : val ? "add" : "remove", cls);
+        },
+
+        has: function has(cls) this[0].classList.has(cls)
+    }),
+
+    get highlight() let (self = this) ({
+        toString: function () self.attrNS(NS, "highlight") || "",
+
+        get list() let (s = this.toString().trim()) s ? s.split(/\s+/) : [],
+        set list(val) {
+            let str = array.uniq(val).join(" ").trim();
+            self.attrNS(NS, "highlight", str || null);
+        },
+
+        has: function has(hl) ~this.list.indexOf(hl),
+
+        add: function add(hl) self.each(function () {
+            highlight.loaded[hl] = true;
+            this.highlight.list = this.highlight.list.concat(hl);
+        }),
+
+        remove: function remove(hl) self.each(function () {
+            this.highlight.list = this.highlight.list.filter(function (h) h != hl);
+        }),
+
+        toggle: function toggle(hl, val, thisObj) self.each(function (elem, i) {
+            let { highlight } = this;
+            let v = callable(val) ? val.call(thisObj || this, elem, i) : val;
+
+            highlight[(v == null ? highlight.has(hl) : !v) ? "remove" : "add"](hl)
+        }),
+    }),
+
+    get rect() this[0] instanceof Ci.nsIDOMWindow ? { width: this[0].scrollMaxX + this[0].innerWidth,
+                                                      height: this[0].scrollMaxY + this[0].innerHeight,
+                                                      get right() this.width + this.left,
+                                                      get bottom() this.height + this.top,
+                                                      top: -this[0].scrollY,
+                                                      left: -this[0].scrollX } :
+               this[0]                            ? this[0].getBoundingClientRect() : {},
+
+    get viewport() {
+        if (this[0] instanceof Ci.nsIDOMWindow)
+            return {
+                get width() this.right - this.left,
+                get height() this.bottom - this.top,
+                bottom: this[0].innerHeight,
+                right: this[0].innerWidth,
+                top: 0, left: 0
+            };
+
+        let r = this.rect;
+        return {
+            width: this[0].clientWidth,
+            height: this[0].clientHeight,
+            top: r.top + this[0].clientTop,
+            get bottom() this.top + this.height,
+            left: r.left + this[0].clientLeft,
+            get right() this.left + this.width
+        }
+    },
+
+    scrollPos: function scrollPos(left, top) {
+        if (arguments.length == 0) {
+            if (this[0] instanceof Ci.nsIDOMElement)
+                return { top: this[0].scrollTop, left: this[0].scrollLeft,
+                         height: this[0].scrollHeight, width: this[0].scrollWidth,
+                         innerHeight: this[0].clientHeight, innerWidth: this[0].innerWidth };
+
+            if (this[0] instanceof Ci.nsIDOMWindow)
+                return { top: this[0].scrollY, left: this[0].scrollX,
+                         height: this[0].scrollMaxY + this[0].innerHeight,
+                         width: this[0].scrollMaxX + this[0].innerWidth,
+                         innerHeight: this[0].innerHeight, innerWidth: this[0].innerWidth };
+
+            return null;
+        }
+        let func = callable(left) && left;
+
+        return this.each(function (elem, i) {
+            if (func)
+                ({ left, top }) = func.call(this, elem, i);
+
+            if (elem instanceof Ci.nsIDOMWindow)
+                elem.scrollTo(left == null ? elem.scrollX : left,
+                              top  == null ? elem.scrollY : top);
+            else {
+                if (left != null)
+                    elem.scrollLeft = left;
+                if (top != null)
+                    elem.scrollTop = top;
+            }
+        });
+    },
+
+    /**
+     * Returns true if the given DOM node is currently visible.
+     * @returns {boolean}
+     */
+    get isVisible() {
+        let style = this[0] && this.style;
+        return style && style.visibility == "visible" && style.display != "none";
+    },
+
+    get editor() {
+        if (!this.length)
+            return;
+
+        this[0] instanceof Ci.nsIDOMNSEditableElement;
+        try {
+            if (this[0].editor instanceof Ci.nsIEditor)
+                var editor = this[0].editor;
+        }
+        catch (e) {
+            util.reportError(e);
+        }
+
+        try {
+            if (!editor)
+                editor = this[0].QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
+                                .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIEditingSession)
+                                .getEditorForWindow(this[0]);
+        }
+        catch (e) {}
+
+        editor instanceof Ci.nsIPlaintextEditor;
+        editor instanceof Ci.nsIHTMLEditor;
+        return editor;
+    },
+
+    get isEditable() !!this.editor,
+
+    get isInput() isinstance(this[0], [Ci.nsIDOMHTMLInputElement,
+                                       Ci.nsIDOMHTMLTextAreaElement,
+                                       Ci.nsIDOMXULTextBoxElement])
+                    && this.isEditable,
+
+    /**
+     * Returns an object representing a Node's computed CSS style.
+     * @returns {Object}
+     */
+    get style() {
+        let node = this[0];
+        if (node instanceof Ci.nsIDOMWindow)
+            node = node.document;
+        if (node instanceof Ci.nsIDOMDocument)
+            node = node.documentElement;
+        while (node && !(node instanceof Ci.nsIDOMElement) && node.parentNode)
+            node = node.parentNode;
+
+        try {
+            var res = node.ownerDocument.defaultView.getComputedStyle(node, null);
+        }
+        catch (e) {}
+
+        if (res == null) {
+            util.dumpStack(_("error.nullComputedStyle", node));
+            Cu.reportError(Error(_("error.nullComputedStyle", node)));
+            return {};
+        }
+        return res;
+    },
+
+    /**
+     * Parses the fields of a form and returns a URL/POST-data pair
+     * that is the equivalent of submitting the form.
+     *
+     * @returns {object} An object with the following elements:
+     *      url: The URL the form points to.
+     *      postData: A string containing URL-encoded post data, if this
+     *                form is to be POSTed
+     *      charset: The character set of the GET or POST data.
+     *      elements: The key=value pairs used to generate query information.
+     */
+    // Nuances gleaned from browser.jar/content/browser/browser.js
+    get formData() {
+        function encode(name, value, param) {
+            param = param ? "%s" : "";
+            if (post)
+                return name + "=" + encodeComponent(value + param);
+            return encodeComponent(name) + "=" + encodeComponent(value) + param;
+        }
+
+        let field = this[0];
+        let form = field.form;
+        let doc = form.ownerDocument;
+
+        let charset = doc.characterSet;
+        let converter = services.CharsetConv(charset);
+        for each (let cs in form.acceptCharset.split(/\s*,\s*|\s+/)) {
+            let c = services.CharsetConv(cs);
+            if (c) {
+                converter = services.CharsetConv(cs);
+                charset = cs;
+            }
+        }
+
+        let uri = util.newURI(doc.baseURI.replace(/\?.*/, ""), charset);
+        let url = util.newURI(form.action, charset, uri).spec;
+
+        let post = form.method.toUpperCase() == "POST";
+
+        let encodeComponent = encodeURIComponent;
+        if (charset !== "UTF-8")
+            encodeComponent = function encodeComponent(str)
+                escape(converter.ConvertFromUnicode(str) + converter.Finish());
+
+        let elems = [];
+        if (field instanceof Ci.nsIDOMHTMLInputElement && field.type == "submit")
+            elems.push(encode(field.name, field.value));
+
+        for (let [, elem] in iter(form.elements))
+            if (elem.name && !elem.disabled) {
+                if (DOM(elem).isInput
+                        || /^(?:hidden|textarea)$/.test(elem.type)
+                        || elem.type == "submit" && elem == field
+                        || elem.checked && /^(?:checkbox|radio)$/.test(elem.type))
+                    elems.push(encode(elem.name, elem.value, elem === field));
+                else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
+                    for (let [, opt] in Iterator(elem.options))
+                        if (opt.selected)
+                            elems.push(encode(elem.name, opt.value));
+                }
+            }
+
+        if (post)
+            return { url: url, postData: elems.join('&'), charset: charset, elements: elems };
+        return { url: url + "?" + elems.join('&'), postData: null, charset: charset, elements: elems };
+    },
+
+    /**
+     * Generates an XPath expression for the given element.
+     *
+     * @returns {string}
+     */
+    get xpath() {
+        function quote(val) "'" + val.replace(/[\\']/g, "\\$&") + "'";
+        if (!(this[0] instanceof Ci.nsIDOMElement))
+            return null;
+
+        let res = [];
+        let doc = this.document;
+        for (let elem = this[0];; elem = elem.parentNode) {
+            if (!(elem instanceof Ci.nsIDOMElement))
+                res.push("");
+            else if (elem.id)
+                res.push("id(" + quote(elem.id) + ")");
+            else {
+                let name = elem.localName;
+                if (elem.namespaceURI && (elem.namespaceURI != XHTML || doc.xmlVersion))
+                    if (elem.namespaceURI in DOM.namespaceNames)
+                        name = DOM.namespaceNames[elem.namespaceURI] + ":" + name;
+                    else
+                        name = "*[local-name()=" + quote(name) + " and namespace-uri()=" + quote(elem.namespaceURI) + "]";
+
+                res.push(name + "[" + (1 + iter(DOM.XPath("./" + name, elem.parentNode)).indexOf(elem)) + "]");
+                continue;
+            }
+            break;
+        }
+
+        return res.reverse().join("/");
+    },
+
+    /**
+     * Returns a string or XML representation of this node.
+     *
+     * @param {boolean} color If true, return a colored, XML
+     *  representation of this node.
+     */
+    repr: function repr(color) {
+        XML.ignoreWhitespace = XML.prettyPrinting = false;
+
+        function namespaced(node) {
+            var ns = DOM.namespaceNames[node.namespaceURI] || /^(?:(.*?):)?/.exec(node.name)[0];
+            if (!ns)
+                return node.localName;
+            if (color)
+                return <><span highlight="HelpXMLNamespace">{ns}</span>{node.localName}</>
+            return ns + ":" + node.localName;
+        }
+
+        let res = [];
+        this.each(function (elem) {
+            try {
+                let hasChildren = elem.firstChild && (!/^\s*$/.test(elem.firstChild) || elem.firstChild.nextSibling)
+                if (color)
+                    res.push(<span highlight="HelpXML"><span highlight="HelpXMLTagStart">&lt;{
+                            namespaced(elem)} {
+                                template.map(array.iterValues(elem.attributes),
+                                    function (attr)
+                                        <span highlight="HelpXMLAttribute">{namespaced(attr)}</span> +
+                                        <span highlight="HelpXMLString">{attr.value}</span>,
+                                    <> </>)
+                            }{ !hasChildren ? "/>" : ">"
+                        }</span>{ !hasChildren ? "" : <>...</> +
+                            <span highlight="HtmlTagEnd">&lt;{namespaced(elem)}></span>
+                    }</span>);
+                else {
+                    let tag = "<" + [namespaced(elem)].concat(
+                        [namespaced(a) + "=" + template.highlight(a.value, true)
+                         for ([i, a] in array.iterItems(elem.attributes))]).join(" ");
+
+                    res.push(tag + (!hasChildren ? "/>" : ">...</" + namespaced(elem) + ">"));
+                }
+            }
+            catch (e) {
+                res.push({}.toString.call(elem));
+            }
+        }, this);
+        return template.map(res, util.identity, <>,</>);
+    },
+
+    attr: function attr(key, val) {
+        return this.attrNS("", key, val);
+    },
+
+    attrNS: function attrNS(ns, key, val) {
+        if (val !== undefined)
+            key = array.toObject([[key, val]]);
+
+        let hooks = this.attrHooks[ns] || {};
+
+        if (isObject(key))
+            return this.each(function (elem, i) {
+                for (let [k, v] in Iterator(key)) {
+                    if (callable(v))
+                        v = v.call(this, elem, i);
+
+                    if (Set.has(hooks, k) && hooks[k].set)
+                        hooks[k].set.call(this, elem, v, k);
+                    else if (v == null)
+                        elem.removeAttributeNS(ns, k);
+                    else
+                        elem.setAttributeNS(ns, k, v);
+                }
+            });
+
+        if (!this.length)
+            return null;
+
+        if (Set.has(hooks, key) && hooks[key].get)
+            return hooks[key].get.call(this, this[0], key);
+
+        if (!this[0].hasAttributeNS(ns, key))
+            return null;
+
+        return this[0].getAttributeNS(ns, key);
+    },
+
+    css: update(function css(key, val) {
+        if (val !== undefined)
+            key = array.toObject([[key, val]]);
+
+        if (isObject(key))
+            return this.each(function (elem) {
+                for (let [k, v] in Iterator(key))
+                    elem.style[css.property(k)] = v;
+            });
+
+        return this[0].style[css.property(key)];
+    }, {
+        name: function (property) property.replace(/[A-Z]/g, function (m0) "-" + m0.toLowerCase()),
+
+        property: function (name) name.replace(/-(.)/g, function (m0, m1) m1.toUpperCase())
+    }),
+
+    append: function append(val) {
+        return this.eachDOM(val, function (elem, target) {
+            target.appendChild(elem);
+        });
+    },
+
+    prepend: function prepend(val) {
+        return this.eachDOM(val, function (elem, target) {
+            target.insertBefore(elem, target.firstChild);
+        });
+    },
+
+    before: function before(val) {
+        return this.eachDOM(val, function (elem, target) {
+            target.parentNode.insertBefore(elem, target);
+        });
+    },
+
+    after: function after(val) {
+        return this.eachDOM(val, function (elem, target) {
+            target.parentNode.insertBefore(elem, target.nextSibling);
+        });
+    },
+
+    appendTo: function appendTo(elem) {
+        if (!(elem instanceof this.constructor))
+            elem = this.constructor(elem, this.document);
+        elem.append(this);
+        return this;
+    },
+
+    prependTo: function prependTo(elem) {
+        if (!(elem instanceof this.constructor))
+            elem = this.constructor(elem, this.document);
+        elem.prepend(this);
+        return this;
+    },
+
+    insertBefore: function insertBefore(elem) {
+        if (!(elem instanceof this.constructor))
+            elem = this.constructor(elem, this.document);
+        elem.before(this);
+        return this;
+    },
+
+    insertAfter: function insertAfter(elem) {
+        if (!(elem instanceof this.constructor))
+            elem = this.constructor(elem, this.document);
+        elem.after(this);
+        return this;
+    },
+
+    remove: function remove() {
+        return this.each(function (elem) {
+            if (elem.parentNode)
+                elem.parentNode.removeChild(elem);
+        }, this);
+    },
+
+    empty: function empty() {
+        return this.each(function (elem) {
+            while (elem.firstChild)
+                elem.removeChild(elem.firstChild);
+        }, this);
+    },
+
+    toggle: function toggle(val, self) {
+        if (callable(val))
+            return this.each(function (elem, i) {
+                this[val.call(self || this, elem, i) ? "show" : "hide"]();
+            });
+
+        if (arguments.length)
+            return this[val ? "show" : "hide"]();
+
+        let hidden = this.map(function (elem) elem.style.display == "none");
+        return this.each(function (elem, i) {
+            this[hidden[i] ? "show" : "hide"]();
+        });
+    },
+    hide: function hide() {
+        return this.each(function (elem) { elem.style.display = "none"; }, this);
+    },
+    show: function show() {
+        for (let i = 0; i < this.length; i++)
+            if (!this[i].dactylDefaultDisplay && this[i].style.display)
+                this[i].style.display = "";
+
+        this.each(function (elem) {
+            if (!elem.dactylDefaultDisplay)
+                elem.dactylDefaultDisplay = this.style.display;
+        });
+
+        return this.each(function (elem) {
+            elem.style.display = elem.dactylDefaultDisplay == "none" ? "block" : "";
+        }, this);
+    },
+
+    createContents: function createContents()
+        this.each(DOM.createContents, this),
+
+    isScrollable: function isScrollable(direction)
+        this.length && DOM.isScrollable(this[0], direction),
+
+    getSet: function getSet(args, get, set) {
+        if (!args.length)
+            return this[0] && get.call(this, this[0]);
+
+        let [fn, self] = args;
+        if (!callable(fn))
+            fn = function () args[0];
+
+        return this.each(function (elem, i) {
+            set.call(this, elem, fn.call(self || this, elem, i));
+        }, this);
+    },
+
+    html: function html(txt, self) {
+        return this.getSet(arguments,
+                           function (elem) elem.innerHTML,
+                           function (elem, val) { elem.innerHTML = val });
+    },
+
+    text: function text(txt, self) {
+        return this.getSet(arguments,
+                           function (elem) elem.textContent,
+                           function (elem, val) { elem.textContent = val });
+    },
+
+    val: function val(txt) {
+        return this.getSet(arguments,
+                           function (elem) elem.value,
+                           function (elem, val) { elem.value = val == null ? "" : val });
+    },
+
+    listen: function listen(event, listener, capture) {
+        if (isObject(event))
+            capture = listener;
+        else
+            event = array.toObject([[event, listener]]);
+
+        for (let [k, v] in Iterator(event))
+            event[k] = util.wrapCallback(v, true);
+
+        return this.each(function (elem) {
+            for (let [k, v] in Iterator(event))
+                elem.addEventListener(k, v, capture);
+        });
+    },
+    unlisten: function unlisten(event, listener, capture) {
+        if (isObject(event))
+            capture = listener;
+        else
+            event = array.toObject([[key, val]]);
+
+        return this.each(function (elem) {
+            for (let [k, v] in Iterator(event))
+                elem.removeEventListener(k, v.wrapper || v, capture);
+        });
+    },
+
+    dispatch: function dispatch(event, params, extraProps) {
+        this.canceled = false;
+        return this.each(function (elem) {
+            let evt = DOM.Event(this.document, event, params, elem);
+            if (!DOM.Event.dispatch(elem, evt, extraProps))
+                this.canceled = true;
+        }, this);
+    },
+
+    focus: function focus(arg, extra) {
+        if (callable(arg))
+            return this.listen("focus", arg, extra);
+
+        let elem = this[0];
+        let flags = arg || services.focus.FLAG_BYMOUSE;
+        try {
+            if (elem instanceof Ci.nsIDOMDocument)
+                elem = elem.defaultView;
+            if (elem instanceof Ci.nsIDOMElement)
+                services.focus.setFocus(elem, flags);
+            else if (elem instanceof Ci.nsIDOMWindow) {
+                services.focus.focusedWindow = elem;
+                if (services.focus.focusedWindow != elem)
+                    services.focus.clearFocus(elem);
+            }
+        }
+        catch (e) {
+            util.dump(elem);
+            util.reportError(e);
+        }
+        return this;
+    },
+    blur: function blur(arg, extra) {
+        if (callable(arg))
+            return this.listen("blur", arg, extra);
+        return this.each(function (elem) { elem.blur(); }, this);
+    },
+
+    /**
+     * Scrolls an element into view if and only if it's not already
+     * fully visible.
+     */
+    scrollIntoView: function scrollIntoView(alignWithTop) {
+        return this.each(function (elem) {
+            function getAlignment(viewport) {
+                if (alignWithTop !== undefined)
+                    return alignWithTop;
+                if (rect.bottom < viewport.top)
+                    return true;
+                if (rect.top > viewport.bottom)
+                    return false;
+                return Math.abs(rect.top) < Math.abs(viewport.bottom - rect.bottom)
+            }
+
+            let rect;
+            function fix(parent) {
+                if (!(parent[0] instanceof Ci.nsIDOMWindow)
+                        && parent.style.overflow == "visible")
+                    return;
+
+                ({ rect }) = DOM(elem);
+                let { viewport } = parent;
+                let isect = util.intersection(rect, viewport);
+
+                if (isect.height < Math.min(viewport.height, rect.height)) {
+                    let { top } = parent.scrollPos();
+                    if (getAlignment(viewport))
+                        parent.scrollPos(null, top - (viewport.top - rect.top));
+                    else
+                        parent.scrollPos(null, top - (viewport.bottom - rect.bottom));
+
+                }
+            }
+
+            for (let parent in this.ancestors.items)
+                fix(parent);
+
+            fix(DOM(this.document.defaultView));
+        });
+    },
+}, {
+    /**
+     * Creates an actual event from a pseudo-event object.
+     *
+     * The pseudo-event object (such as may be retrieved from
+     * DOM.Event.parse) should have any properties you want the event to
+     * have.
+     *
+     * @param {Document} doc The DOM document to associate this event with
+     * @param {Type} type The type of event (keypress, click, etc.)
+     * @param {Object} opts The pseudo-event. @optional
+     */
+    Event: Class("Event", {
+        init: function Event(doc, type, opts, target) {
+            const DEFAULTS = {
+                HTML: {
+                    type: type, bubbles: true, cancelable: false
+                },
+                Key: {
+                    type: type,
+                    bubbles: true, cancelable: true,
+                    view: doc.defaultView,
+                    ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
+                    keyCode: 0, charCode: 0
+                },
+                Mouse: {
+                    type: type,
+                    bubbles: true, cancelable: true,
+                    view: doc.defaultView,
+                    detail: 1,
+                    get screenX() this.view.mozInnerScreenX
+                                + Math.max(0, this.clientX + (DOM(target || opts.target).rect.left || 0)),
+                    get screenY() this.view.mozInnerScreenY
+                                + Math.max(0, this.clientY + (DOM(target || opts.target).rect.top || 0)),
+                    clientX: 0,
+                    clientY: 0,
+                    ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
+                    button: 0,
+                    relatedTarget: null
+                }
+            };
+
+            opts = opts || {};
+            var t = this.constructor.types[type] || "";
+            var evt = doc.createEvent(t + "Events");
+
+            let params = DEFAULTS[t || "HTML"];
+            let args = Object.keys(params);
+            update(params, this.constructor.defaults[type],
+                   iter.toObject([k, opts[k]] for (k in opts) if (k in params)));
+
+            evt["init" + t + "Event"].apply(evt, args.map(function (k) params[k]));
+            return evt;
+        }
+    }, {
+        init: function init() {
+            // NOTE: the order of ["Esc", "Escape"] or ["Escape", "Esc"]
+            //       matters, so use that string as the first item, that you
+            //       want to refer to within dactyl's source code for
+            //       comparisons like if (key == "<Esc>") { ... }
+            this.keyTable = {
+                add: ["Plus", "Add"],
+                back_space: ["BS"],
+                count: ["count"],
+                delete: ["Del"],
+                escape: ["Esc", "Escape"],
+                insert: ["Insert", "Ins"],
+                leader: ["Leader"],
+                left_shift: ["LT", "<"],
+                nop: ["Nop"],
+                pass: ["Pass"],
+                return: ["Return", "CR", "Enter"],
+                right_shift: [">"],
+                slash: ["/"],
+                space: ["Space", " "],
+                subtract: ["Minus", "Subtract"]
+            };
+
+            this.key_key = {};
+            this.code_key = {};
+            this.key_code = {};
+            this.code_nativeKey = {};
+
+            for (let list in values(this.keyTable))
+                for (let v in values(list)) {
+                    if (v.length == 1)
+                        v = v.toLowerCase();
+                    this.key_key[v.toLowerCase()] = v;
+                }
+
+            for (let [k, v] in Iterator(Ci.nsIDOMKeyEvent)) {
+                this.code_nativeKey[v] = k.substr(4);
+
+                k = k.substr(7).toLowerCase();
+                let names = [k.replace(/(^|_)(.)/g, function (m, n1, n2) n2.toUpperCase())
+                              .replace(/^NUMPAD/, "k")];
+
+                if (names[0].length == 1)
+                    names[0] = names[0].toLowerCase();
+
+                if (k in this.keyTable)
+                    names = this.keyTable[k];
+
+                this.code_key[v] = names[0];
+                for (let [, name] in Iterator(names)) {
+                    this.key_key[name.toLowerCase()] = name;
+                    this.key_code[name.toLowerCase()] = v;
+                }
+            }
+
+            // HACK: as Gecko does not include an event for <, we must add this in manually.
+            if (!("<" in this.key_code)) {
+                this.key_code["<"] = 60;
+                this.key_code["lt"] = 60;
+                this.code_key[60] = "lt";
+            }
+
+            return this;
+        },
+
+
+        code_key:       Class.Memoize(function (prop) this.init()[prop]),
+        code_nativeKey: Class.Memoize(function (prop) this.init()[prop]),
+        keyTable:       Class.Memoize(function (prop) this.init()[prop]),
+        key_code:       Class.Memoize(function (prop) this.init()[prop]),
+        key_key:        Class.Memoize(function (prop) this.init()[prop]),
+        pseudoKeys:     Set(["count", "leader", "nop", "pass"]),
+
+        /**
+         * Converts a user-input string of keys into a canonical
+         * representation.
+         *
+         * <C-A> maps to <C-a>, <C-S-a> maps to <C-S-A>
+         * <C- > maps to <C-Space>, <S-a> maps to A
+         * << maps to <lt><lt>
+         *
+         * <S-@> is preserved, as in Vim, to allow untypeable key-combinations
+         * in macros.
+         *
+         * canonicalKeys(canonicalKeys(x)) == canonicalKeys(x) for all values
+         * of x.
+         *
+         * @param {string} keys Messy form.
+         * @param {boolean} unknownOk Whether unknown keys are passed
+         *     through rather than being converted to <lt>keyname>.
+         *     @default false
+         * @returns {string} Canonical form.
+         */
+        canonicalKeys: function canonicalKeys(keys, unknownOk) {
+            if (arguments.length === 1)
+                unknownOk = true;
+            return this.parse(keys, unknownOk).map(this.closure.stringify).join("");
+        },
+
+        iterKeys: function iterKeys(keys) iter(function () {
+            let match, re = /<.*?>?>|[^<]/g;
+            while (match = re.exec(keys))
+                yield match[0];
+        }()),
+
+        /**
+         * Converts an event string into an array of pseudo-event objects.
+         *
+         * These objects can be used as arguments to {@link #stringify} or
+         * {@link DOM.Event}, though they are unlikely to be much use for other
+         * purposes. They have many of the properties you'd expect to find on a
+         * real event, but none of the methods.
+         *
+         * Also may contain two "special" parameters, .dactylString and
+         * .dactylShift these are set for characters that can never by
+         * typed, but may appear in mappings, for example <Nop> is passed as
+         * dactylString, and dactylShift is set when a user specifies
+         * <S-@> where @ is a non-case-changeable, non-space character.
+         *
+         * @param {string} keys The string to parse.
+         * @param {boolean} unknownOk Whether unknown keys are passed
+         *     through rather than being converted to <lt>keyname>.
+         *     @default false
+         * @returns {Array[Object]}
+         */
+        parse: function parse(input, unknownOk) {
+            if (isArray(input))
+                return array.flatten(input.map(function (k) this.parse(k, unknownOk), this));
+
+            if (arguments.length === 1)
+                unknownOk = true;
+
+            let out = [];
+            for (let match in util.regexp.iterate(/<.*?>?>|[^<]|<(?!.*>)/g, input)) {
+                let evt_str = match[0];
+
+                let evt_obj = { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false,
+                                keyCode: 0, charCode: 0, type: "keypress" };
+
+                if (evt_str.length == 1) {
+                    evt_obj.charCode = evt_str.charCodeAt(0);
+                    evt_obj._keyCode = this.key_code[evt_str[0].toLowerCase()];
+                    evt_obj.shiftKey = evt_str !== evt_str.toLowerCase();
+                }
+                else {
+                    let [match, modifier, keyname] = evt_str.match(/^<((?:[*12CASM⌘]-)*)(.+?)>$/i) || [false, '', ''];
+                    modifier = Set(modifier.toUpperCase());
+                    keyname = keyname.toLowerCase();
+                    evt_obj.dactylKeyname = keyname;
+                    if (/^u[0-9a-f]+$/.test(keyname))
+                        keyname = String.fromCharCode(parseInt(keyname.substr(1), 16));
+
+                    if (keyname && (unknownOk || keyname.length == 1 || /mouse$/.test(keyname) ||
+                                    this.key_code[keyname] || Set.has(this.pseudoKeys, keyname))) {
+                        evt_obj.globKey  ="*" in modifier;
+                        evt_obj.ctrlKey  ="C" in modifier;
+                        evt_obj.altKey   ="A" in modifier;
+                        evt_obj.shiftKey ="S" in modifier;
+                        evt_obj.metaKey  ="M" in modifier || "⌘" in modifier;
+                        evt_obj.dactylShift = evt_obj.shiftKey;
+
+                        if (keyname.length == 1) { // normal characters
+                            if (evt_obj.shiftKey)
+                                keyname = keyname.toUpperCase();
+
+                            evt_obj.dactylShift = evt_obj.shiftKey && keyname.toUpperCase() == keyname.toLowerCase();
+                            evt_obj.charCode = keyname.charCodeAt(0);
+                            evt_obj.keyCode = this.key_code[keyname.toLowerCase()];
+                        }
+                        else if (Set.has(this.pseudoKeys, keyname)) {
+                            evt_obj.dactylString = "<" + this.key_key[keyname] + ">";
+                        }
+                        else if (/mouse$/.test(keyname)) { // mouse events
+                            evt_obj.type = (/2-/.test(modifier) ? "dblclick" : "click");
+                            evt_obj.button = ["leftmouse", "middlemouse", "rightmouse"].indexOf(keyname);
+                            delete evt_obj.keyCode;
+                            delete evt_obj.charCode;
+                        }
+                        else { // spaces, control characters, and <
+                            evt_obj.keyCode = this.key_code[keyname];
+                            evt_obj.charCode = 0;
+                        }
+                    }
+                    else { // an invalid sequence starting with <, treat as a literal
+                        out = out.concat(this.parse("<lt>" + evt_str.substr(1)));
+                        continue;
+                    }
+                }
+
+                // TODO: make a list of characters that need keyCode and charCode somewhere
+                if (evt_obj.keyCode == 32 || evt_obj.charCode == 32)
+                    evt_obj.charCode = evt_obj.keyCode = 32; // <Space>
+                if (evt_obj.keyCode == 60 || evt_obj.charCode == 60)
+                    evt_obj.charCode = evt_obj.keyCode = 60; // <lt>
+
+                evt_obj.modifiers = (evt_obj.ctrlKey  && Ci.nsIDOMNSEvent.CONTROL_MASK)
+                                  | (evt_obj.altKey   && Ci.nsIDOMNSEvent.ALT_MASK)
+                                  | (evt_obj.shiftKey && Ci.nsIDOMNSEvent.SHIFT_MASK)
+                                  | (evt_obj.metaKey  && Ci.nsIDOMNSEvent.META_MASK);
+
+                out.push(evt_obj);
+            }
+            return out;
+        },
+
+        /**
+         * Converts the specified event to a string in dactyl key-code
+         * notation. Returns null for an unknown event.
+         *
+         * @param {Event} event
+         * @returns {string}
+         */
+        stringify: function stringify(event) {
+            if (isArray(event))
+                return event.map(function (e) this.stringify(e), this).join("");
+
+            if (event.dactylString)
+                return event.dactylString;
+
+            let key = null;
+            let modifier = "";
+
+            if (event.globKey)
+                modifier += "*-";
+            if (event.ctrlKey)
+                modifier += "C-";
+            if (event.altKey)
+                modifier += "A-";
+            if (event.metaKey)
+                modifier += "M-";
+
+            if (/^key/.test(event.type)) {
+                let charCode = event.type == "keyup" ? 0 : event.charCode; // Why? --Kris
+                if (charCode == 0) {
+                    if (event.keyCode in this.code_key) {
+                        key = this.code_key[event.keyCode];
+
+                        if (event.shiftKey && (key.length > 1 || event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
+                            modifier += "S-";
+                        else if (!modifier && key.length === 1)
+                            if (event.shiftKey)
+                                key = key.toUpperCase();
+                            else
+                                key = key.toLowerCase();
+
+                        if (!modifier && key.length == 1)
+                            return key;
+                    }
+                }
+                // [Ctrl-Bug] special handling of mysterious <C-[>, <C-\\>, <C-]>, <C-^>, <C-_> bugs (OS/X)
+                //            (i.e., cntrl codes 27--31)
+                // ---
+                // For more information, see:
+                //     [*] Referenced mailing list msg: http://www.mozdev.org/pipermail/pentadactyl/2008-May/001548.html
+                //     [*] Mozilla bug 416227: event.charCode in keypress handler has unexpected values on Mac for Ctrl with chars in "[ ] _ \"
+                //         https://bugzilla.mozilla.org/show_bug.cgi?id=416227
+                //     [*] Mozilla bug 432951: Ctrl+'foo' doesn't seem same charCode as Meta+'foo' on Cocoa
+                //         https://bugzilla.mozilla.org/show_bug.cgi?id=432951
+                // ---
+                //
+                // The following fixes are only activated if config.OS.isMacOSX.
+                // Technically, they prevent mappings from <C-Esc> (and
+                // <C-C-]> if your fancy keyboard permits such things<?>), but
+                // these <C-control> mappings are probably pathological (<C-Esc>
+                // certainly is on Windows), and so it is probably
+                // harmless to remove the config.OS.isMacOSX if desired.
+                //
+                else if (config.OS.isMacOSX && event.ctrlKey && charCode >= 27 && charCode <= 31) {
+                    if (charCode == 27) { // [Ctrl-Bug 1/5] the <C-[> bug
+                        key = "Esc";
+                        modifier = modifier.replace("C-", "");
+                    }
+                    else // [Ctrl-Bug 2,3,4,5/5] the <C-\\>, <C-]>, <C-^>, <C-_> bugs
+                        key = String.fromCharCode(charCode + 64);
+                }
+                // a normal key like a, b, c, 0, etc.
+                else if (charCode) {
+                    key = String.fromCharCode(charCode);
+
+                    if (!/^[^<\s]$/i.test(key) && key in this.key_code) {
+                        // a named charCode key (<Space> and <lt>) space can be shifted, <lt> must be forced
+                        if ((key.match(/^\s$/) && event.shiftKey) || event.dactylShift)
+                            modifier += "S-";
+
+                        key = this.code_key[this.key_code[key]];
+                    }
+                    else {
+                        // a shift modifier is only allowed if the key is alphabetical and used in a C-A-M- mapping in the uppercase,
+                        // or if the shift has been forced for a non-alphabetical character by the user while :map-ping
+                        if (key !== key.toLowerCase() && (event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
+                            modifier += "S-";
+                        if (/^\s$/.test(key))
+                            key = let (s = charCode.toString(16)) "U" + "0000".substr(4 - s.length) + s;
+                        else if (modifier.length == 0)
+                            return key;
+                    }
+                }
+                if (key == null) {
+                    if (event.shiftKey)
+                        modifier += "S-";
+                    key = this.key_key[event.dactylKeyname] || event.dactylKeyname;
+                }
+                if (key == null)
+                    return null;
+            }
+            else if (event.type == "click" || event.type == "dblclick") {
+                if (event.shiftKey)
+                    modifier += "S-";
+                if (event.type == "dblclick")
+                    modifier += "2-";
+                // TODO: triple and quadruple click
+
+                switch (event.button) {
+                case 0:
+                    key = "LeftMouse";
+                    break;
+                case 1:
+                    key = "MiddleMouse";
+                    break;
+                case 2:
+                    key = "RightMouse";
+                    break;
+                }
+            }
+
+            if (key == null)
+                return null;
+
+            return "<" + modifier + key + ">";
+        },
+
+
+        defaults: {
+            load:   { bubbles: false },
+            submit: { cancelable: true }
+        },
+
+        types: Class.Memoize(function () iter(
+            {
+                Mouse: "click mousedown mouseout mouseover mouseup dblclick " +
+                       "hover " +
+                       "popupshowing popupshown popuphiding popuphidden " +
+                       "contextmenu",
+                Key:   "keydown keypress keyup",
+                "":    "change command dactyl-input input submit " +
+                       "load unload pageshow pagehide DOMContentLoaded " +
+                       "resize scroll"
+            }
+        ).map(function ([k, v]) v.split(" ").map(function (v) [v, k]))
+         .flatten()
+         .toObject()),
+
+        /**
+         * Dispatches an event to an element as if it were a native event.
+         *
+         * @param {Node} target The DOM node to which to dispatch the event.
+         * @param {Event} event The event to dispatch.
+         */
+        dispatch: Class.Memoize(function ()
+            config.haveGecko("2b")
+                ? function dispatch(target, event, extra) {
+                    try {
+                        this.feedingEvent = extra;
+
+                        if (target instanceof Ci.nsIDOMElement)
+                            // This causes a crash on Gecko<2.0, it seems.
+                            return (target.ownerDocument || target.document || target).defaultView
+                                   .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
+                                   .dispatchDOMEventViaPresShell(target, event, true);
+                        else {
+                            target.dispatchEvent(event);
+                            return !event.getPreventDefault();
+                        }
+                    }
+                    catch (e) {
+                        util.reportError(e);
+                    }
+                    finally {
+                        this.feedingEvent = null;
+                    }
+                }
+                : function dispatch(target, event, extra) {
+                    try {
+                        this.feedingEvent = extra;
+                        target.dispatchEvent(update(event, extra));
+                    }
+                    finally {
+                        this.feedingEvent = null;
+                    }
+                })
+    }),
+
+    createContents: Class.Memoize(function () services.has("dactyl") && services.dactyl.createContents
+        || function (elem) {}),
+
+    isScrollable: Class.Memoize(function () services.has("dactyl") && services.dactyl.getScrollable
+        ? function (elem, dir) services.dactyl.getScrollable(elem) & (dir ? services.dactyl["DIRECTION_" + dir.toUpperCase()] : ~0)
+        : function (elem, dir) true),
+
+    /**
+     * The set of input element type attribute values that mark the element as
+     * an editable field.
+     */
+    editableInputs: Set(["date", "datetime", "datetime-local", "email", "file",
+                         "month", "number", "password", "range", "search",
+                         "tel", "text", "time", "url", "week"]),
+
+    /**
+     * Converts a given DOM Node, Range, or Selection to a string. If
+     * *html* is true, the output is HTML, otherwise it is presentation
+     * text.
+     *
+     * @param {nsIDOMNode | nsIDOMRange | nsISelection} node The node to
+     *      stringify.
+     * @param {boolean} html Whether the output should be HTML rather
+     *      than presentation text.
+     */
+    stringify: function stringify(node, html) {
+        if (node instanceof Ci.nsISelection && node.isCollapsed)
+            return "";
+
+        if (node instanceof Ci.nsIDOMNode) {
+            let range = node.ownerDocument.createRange();
+            range.selectNode(node);
+            node = range;
+        }
+        let doc = (node.getRangeAt ? node.getRangeAt(0) : node).startContainer;
+        doc = doc.ownerDocument || doc;
+
+        let encoder = services.HtmlEncoder();
+        encoder.init(doc, "text/unicode", encoder.OutputRaw|encoder.OutputPreformatted);
+        if (node instanceof Ci.nsISelection)
+            encoder.setSelection(node);
+        else if (node instanceof Ci.nsIDOMRange)
+            encoder.setRange(node);
+
+        let str = services.String(encoder.encodeToString());
+        if (html)
+            return str.data;
+
+        let [result, length] = [{}, {}];
+        services.HtmlConverter().convert("text/html", str, str.data.length*2, "text/unicode", result, length);
+        return result.value.QueryInterface(Ci.nsISupportsString).data;
+    },
+
+    /**
+     * Compiles a CSS spec and XPath pattern matcher based on the given
+     * list. List elements prefixed with "xpath:" are parsed as XPath
+     * patterns, while other elements are parsed as CSS specs. The
+     * returned function will, given a node, return an iterator of all
+     * descendants of that node which match the given specs.
+     *
+     * @param {[string]} list The list of patterns to match.
+     * @returns {function(Node)}
+     */
+    compileMatcher: function compileMatcher(list) {
+        let xpath = [], css = [];
+        for (let elem in values(list))
+            if (/^xpath:/.test(elem))
+                xpath.push(elem.substr(6));
+            else
+                css.push(elem);
+
+        return update(
+            function matcher(node) {
+                if (matcher.xpath)
+                    for (let elem in DOM.XPath(matcher.xpath, node))
+                        yield elem;
+
+                if (matcher.css)
+                    for (let [, elem] in iter(util.withProperErrors("querySelectorAll", node, matcher.css)))
+                        yield elem;
+            }, {
+                css: css.join(", "),
+                xpath: xpath.join(" | ")
+            });
+    },
+
+    /**
+     * Validates a list as input for {@link #compileMatcher}. Returns
+     * true if and only if every element of the list is a valid XPath or
+     * CSS selector.
+     *
+     * @param {[string]} list The list of patterns to test
+     * @returns {boolean} True when the patterns are all valid.
+     */
+    validateMatcher: function validateMatcher(list) {
+        return this.testValues(list, DOM.closure.testMatcher);
+    },
+
+    testMatcher: function testMatcher(value) {
+        let evaluator = services.XPathEvaluator();
+        let node = services.XMLDocument();
+        if (/^xpath:/.test(value))
+            util.withProperErrors("createExpression", evaluator, value.substr(6), DOM.XPath.resolver);
+        else
+            util.withProperErrors("querySelector", node, value);
+        return true;
+    },
+
+    /**
+     * Converts HTML special characters in *str* to the equivalent HTML
+     * entities.
+     *
+     * @param {string} str
+     * @returns {string}
+     */
+    escapeHTML: function escapeHTML(str) {
+        let map = { "'": "&apos;", '"': "&quot;", "%": "&#x25;", "&": "&amp;", "<": "&lt;", ">": "&gt;" };
+        return str.replace(/['"&<>]/g, function (m) map[m]);
+    },
+
+    /**
+     * Converts an E4X XML literal to a DOM node. Any attribute named
+     * highlight is present, it is transformed into dactyl:highlight,
+     * and the named highlight groups are guaranteed to be loaded.
+     *
+     * @param {Node} node
+     * @param {Document} doc
+     * @param {Object} nodes If present, nodes with the "key" attribute are
+     *     stored here, keyed to the value thereof.
+     * @returns {Node}
+     */
+    fromXML: function fromXML(node, doc, nodes) {
+        XML.ignoreWhitespace = XML.prettyPrinting = false;
+        if (typeof node === "string") // Sandboxes can't currently pass us XML objects.
+            node = XML(node);
+
+        if (node.length() != 1) {
+            let domnode = doc.createDocumentFragment();
+            for each (let child in node)
+                domnode.appendChild(fromXML(child, doc, nodes));
+            return domnode;
+        }
+
+        switch (node.nodeKind()) {
+        case "text":
+            return doc.createTextNode(String(node));
+        case "element":
+            let domnode = doc.createElementNS(node.namespace(), node.localName());
+
+            for each (let attr in node.@*::*)
+                if (attr.name() != "highlight")
+                    domnode.setAttributeNS(attr.namespace(), attr.localName(), String(attr));
+
+            for each (let child in node.*::*)
+                domnode.appendChild(fromXML(child, doc, nodes));
+            if (nodes && node.@key)
+                nodes[node.@key] = domnode;
+
+            if ("@highlight" in node)
+                highlight.highlightNode(domnode, String(node.@highlight), nodes || true);
+            return domnode;
+        default:
+            return null;
+        }
+    },
+
+    /**
+     * Evaluates an XPath expression in the current or provided
+     * document. It provides the xhtml, xhtml2 and dactyl XML
+     * namespaces. The result may be used as an iterator.
+     *
+     * @param {string} expression The XPath expression to evaluate.
+     * @param {Node} elem The context element.
+     * @param {boolean} asIterator Whether to return the results as an
+     *     XPath iterator.
+     * @param {object} namespaces Additional namespaces to recognize.
+     *     @optional
+     * @returns {Object} Iterable result of the evaluation.
+     */
+    XPath: update(
+        function XPath(expression, elem, asIterator, namespaces) {
+            try {
+                let doc = elem.ownerDocument || elem;
+
+                if (isArray(expression))
+                    expression = DOM.makeXPath(expression);
+
+                let resolver = XPath.resolver;
+                if (namespaces) {
+                    namespaces = update({}, DOM.namespaces, namespaces);
+                    resolver = function (prefix) namespaces[prefix] || null;
+                }
+
+                let result = doc.evaluate(expression, elem,
+                    resolver,
+                    asIterator ? Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE : Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
+                    null
+                );
+
+                return Object.create(result, {
+                    __iterator__: {
+                        value: asIterator ? function () { let elem; while ((elem = this.iterateNext())) yield elem; }
+                                          : function () { for (let i = 0; i < this.snapshotLength; i++) yield this.snapshotItem(i); }
+                    }
+                });
+            }
+            catch (e) {
+                throw e.stack ? e : Error(e);
+            }
+        },
+        {
+            resolver: function lookupNamespaceURI(prefix) (DOM.namespaces[prefix] || null)
+        }),
+
+    /**
+     * Returns an XPath union expression constructed from the specified node
+     * tests. An expression is built with node tests for both the null and
+     * XHTML namespaces. See {@link DOM.XPath}.
+     *
+     * @param nodes {Array(string)}
+     * @returns {string}
+     */
+    makeXPath: function makeXPath(nodes) {
+        return array(nodes).map(util.debrace).flatten()
+                           .map(function (node) /^[a-z]+:/.test(node) ? node : [node, "xhtml:" + node]).flatten()
+                           .map(function (node) "//" + node).join(" | ");
+    },
+
+    namespaces: {
+        xul: XUL.uri,
+        xhtml: XHTML.uri,
+        html: XHTML.uri,
+        xhtml2: "http://www.w3.org/2002/06/xhtml2",
+        dactyl: NS.uri
+    },
+
+    namespaceNames: Class.Memoize(function ()
+        iter(this.namespaces).map(function ([k, v]) [v, k]).toObject()),
+});
+
+Object.keys(DOM.Event.types).forEach(function (event) {
+    let name = event.replace(/-(.)/g, function (m, m1) m1.toUpperCase());
+    if (!Set.has(DOM.prototype, name))
+        DOM.prototype[name] =
+            function _event(arg, extra) {
+                return this[callable(arg) ? "listen" : "dispatch"](event, arg, extra);
+            };
+});
+
+var $ = DOM;
+
+endModule();
+
+// catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
+
+// vim: set sw=4 ts=4 et ft=javascript:
index faee6e22a7a1ce5a28bcda6019c2e99dc9c05a68..03ab19909e70014be9dc9d313c329331359d0581 100644 (file)
@@ -2,12 +2,11 @@
 //
 // 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);
@@ -20,9 +19,8 @@ var states = iter([v, k.slice(prefix.length).toLowerCase()]
 
 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 = {
@@ -74,7 +72,7 @@ var Download = Class("Download", {
 
     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"]),
@@ -172,7 +170,8 @@ var Download = Class("Download", {
             }
         }
 
-        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, "");
 
@@ -193,6 +192,14 @@ var Download = Class("Download", {
         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,
@@ -213,7 +220,7 @@ var DownloadList = Class("DownloadList",
         services.downloadManager.removeListener(this);
     },
 
-    message: Class.memoize(function () {
+    message: Class.Memoize(function () {
 
         util.xmlToDom(<table highlight="Downloads" key="list" xmlns={XHTML}>
                         <tr highlight="DownloadHead">
@@ -280,7 +287,7 @@ var DownloadList = Class("DownloadList",
             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)
     })),
 
@@ -316,16 +323,17 @@ var DownloadList = Class("DownloadList",
 
     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 = "";
 
index e2d24bb020495fa74ee45d713d963ac31957f679..2696630422d24a1d0309d5eb8a36e06fb4cd5bb8 100644 (file)
@@ -2,18 +2,18 @@
 //
 // 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);
 
-function equals(a, b) XPCNativeWrapper(a) == XPCNativeWrapper(b);
+this.lazyRequire("buffer", ["Buffer"]);
+this.lazyRequire("overlay", ["overlay"]);
 
-try {
+function equals(a, b) XPCNativeWrapper(a) == XPCNativeWrapper(b);
 
 /** @instance rangefinder */
 var RangeFinder = Module("rangefinder", {
@@ -25,13 +25,21 @@ var RangeFinder = Module("rangefinder", {
             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() {
@@ -40,20 +48,33 @@ var RangeFinder = Module("rangefinder", {
         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;
 
@@ -64,7 +85,7 @@ var RangeFinder = Module("rangefinder", {
         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")
@@ -80,8 +101,14 @@ var RangeFinder = Module("rangefinder", {
             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
@@ -93,17 +120,22 @@ var RangeFinder = Module("rangefinder", {
 
             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;
+        this.rangeFind.pattern = str;
+        return pattern;
     },
 
-    find: function (pattern, backwards) {
+    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);
@@ -111,7 +143,8 @@ var RangeFinder = Module("rangefinder", {
         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))
@@ -124,7 +157,7 @@ var RangeFinder = Module("rangefinder", {
                                                    | 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"])
@@ -132,26 +165,32 @@ var RangeFinder = Module("rangefinder", {
         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"])
@@ -163,7 +202,7 @@ var RangeFinder = Module("rangefinder", {
      * Highlights all occurrences of the last sought for string in the
      * current buffer.
      */
-    highlight: function () {
+    highlight: function highlight() {
         if (this.rangeFind)
             this.rangeFind.highlight();
     },
@@ -171,7 +210,7 @@ var RangeFinder = Module("rangefinder", {
     /**
      * Clears all find highlighting.
      */
-    clear: function () {
+    clear: function clear() {
         if (this.rangeFind)
             this.rangeFind.highlight(true);
     }
@@ -205,8 +244,9 @@ var RangeFinder = Module("rangefinder", {
     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);
             },
 
@@ -229,7 +269,7 @@ var RangeFinder = Module("rangefinder", {
             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,
@@ -257,7 +297,6 @@ var RangeFinder = Module("rangefinder", {
     },
     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",
@@ -279,6 +318,20 @@ var RangeFinder = Module("rangefinder", {
                 }
             });
 
+        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);
@@ -308,11 +361,11 @@ var RangeFinder = Module("rangefinder", {
  * 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;
+    init: function init(window, content, matchCase, backward, elementPath, regexp) {
+        this.window = util.weakReference(window);
+        this.content = content;
 
-        this.baseDocument = Cu.getWeakReference(this.content.document);
+        this.baseDocument = util.weakReference(this.content.document);
         this.elementPath = elementPath || null;
         this.reverse = Boolean(backward);
 
@@ -327,25 +380,18 @@ var RangeFind = Class("RangeFind", {
         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;
 
@@ -358,13 +404,15 @@ var RangeFind = Class("RangeFind", {
         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();
-        this.range.deselect();
-        this.range.descroll();
+        if (this.range) {
+            this.range.deselect();
+            this.range.descroll();
+        }
     },
 
     compareRanges: function compareRanges(r1, r2) {
@@ -400,8 +448,8 @@ var RangeFind = Class("RangeFind", {
 
     focus: function focus() {
         if (this.lastRange)
-            var node = util.evaluateXPath(RangeFind.selectNodePath,
-                                          this.lastRange.commonAncestorContainer).snapshotItem(0);
+            var node = DOM.XPath(RangeFind.selectNodePath,
+                                 this.lastRange.commonAncestorContainer).snapshotItem(0);
         if (node) {
             node.focus();
             // Re-highlight collapsed selection
@@ -448,7 +496,7 @@ var RangeFind = Class("RangeFind", {
         }
     },
 
-    indexIter: function (private_) {
+    indexIter: function indexIter(private_) {
         let idx = this.range.index;
         if (this.backward)
             var groups = [util.range(idx + 1, 0, -1), util.range(this.ranges.length, idx, -1)];
@@ -466,22 +514,38 @@ var RangeFind = Class("RangeFind", {
         }
     },
 
-    iter: function (word) {
-        let saved = ["lastRange", "lastString", "range"].map(function (s) [s, this[s]], this);
+    iter: function iter(word) {
+        let saved = ["lastRange", "lastString", "range", "regexp"].map(function (s) [s, this[s]], this);
+        let res;
         try {
-            this.range = this.ranges[0];
+            let regexp = this.regexp && word != util.regexp.escape(word);
             this.lastRange = null;
-            this.lastString = word;
-            var res;
-            while (res = this.find(null, this.reverse, true))
-                yield res;
+            this.regexp = false;
+            if (regexp) {
+                let re = RegExp(word, "gm" + this.flags);
+                for (this.range in array.iterValues(this.ranges)) {
+                    for (let match in util.regexp.iterate(re, DOM.stringify(this.range.range, true))) {
+                        let lastRange = this.lastRange;
+                        if (res = this.find(null, this.reverse, true))
+                            yield res;
+                        else
+                            this.lastRange = lastRange;
+                    }
+                }
+            }
+            else {
+                this.range = this.ranges[0];
+                this.lastString = word;
+                while (res = this.find(null, this.reverse, true))
+                    yield res;
+            }
         }
         finally {
             saved.forEach(function ([k, v]) this[k] = v, this);
         }
     },
 
-    makeFrameList: function (win) {
+    makeFrameList: function makeFrameList(win) {
         const self = this;
         win = win.top;
         let frames = [];
@@ -514,7 +578,7 @@ var RangeFind = Class("RangeFind", {
 
             for (let frame in array.iterValues(win.frames)) {
                 let range = doc.createRange();
-                if (util.computedStyle(frame.frameElement).visibility == "visible") {
+                if (DOM(frame.frameElement).style.visibility == "visible") {
                     range.selectNode(frame.frameElement);
                     pushRange(pageStart, RangeFind.endpoint(range, true));
                     pageStart = RangeFind.endpoint(range, false);
@@ -537,75 +601,53 @@ var RangeFind = Class("RangeFind", {
         return frames;
     },
 
-    reset: function () {
+    reset: function reset() {
         this.ranges = this.makeFrameList(this.content);
 
         this.startRange = this.selectedRange;
         this.startRange.collapse(!this.reverse);
         this.lastRange = this.selectedRange;
-        this.range = this.findRange(this.startRange);
+        this.range = this.findRange(this.startRange) || this.ranges[0];
+        util.assert(this.range, "Null range", false);
         this.ranges.first = this.range;
         this.ranges.forEach(function (range) range.save());
         this.forward = null;
         this.found = false;
     },
 
-    // This doesn't work yet.
-    resetCaret: function () {
-        let equal = RangeFind.equal;
-        let selection = this.win.getSelection();
-        if (selection.rangeCount == 0)
-            selection.addRange(this.pageStart);
-        function getLines() {
-            let orig = selection.getRangeAt(0);
-            function getRanges(forward) {
-                selection.removeAllRanges();
-                selection.addRange(orig);
-                let cur = orig;
-                while (true) {
-                    var last = cur;
-                    this.sel.lineMove(forward, false);
-                    cur = selection.getRangeAt(0);
-                    if (equal(cur, last))
-                        break;
-                    yield cur;
-                }
-            }
-            yield orig;
-            for (let range in getRanges(true))
-                yield range;
-            for (let range in getRanges(false))
-                yield range;
-        }
-        for (let range in getLines()) {
-            if (this.sel.checkVisibility(range.startContainer, range.startOffset, range.startOffset))
-                return range;
-        }
-        return null;
-    },
-
-    find: function (word, reverse, private_) {
+    find: function find(pattern, reverse, private_) {
         if (!private_ && this.lastRange && !RangeFind.equal(this.selectedRange, this.lastRange))
             this.reset();
 
         this.wrapped = false;
-        this.finder.findBackwards = reverse ? !this.reverse : this.reverse;
-        let again = word == null;
+        this.backward = reverse ? !this.reverse : this.reverse;
+        let again = pattern == null;
         if (again)
-            word = this.lastString;
+            pattern = this.lastString;
         if (!this.matchCase)
-            word = word.toLowerCase();
+            pattern = pattern.toLowerCase();
 
-        if (!again && (word === "" || word.indexOf(this.lastString) !== 0 || this.backward)) {
+        if (!again && (pattern === "" || pattern.indexOf(this.lastString) !== 0 || this.backward)) {
             if (!private_)
                 this.range.deselect();
-            if (word === "")
+            if (pattern === "")
                 this.range.descroll();
             this.lastRange = this.startRange;
             this.range = this.ranges.first;
         }
 
-        if (word == "")
+        let word = pattern;
+        let regexp = this.regexp && word != util.regexp.escape(word);
+
+        if (regexp)
+            try {
+                RegExp(pattern);
+            }
+            catch (e) {
+                pattern = "";
+            }
+
+        if (pattern == "")
             var range = this.startRange;
         else
             for (let i in this.indexIter(private_)) {
@@ -622,15 +664,33 @@ var RangeFind = Class("RangeFind", {
                 if (this.backward && !again)
                     start = RangeFind.endpoint(this.startRange, false);
 
+                if (regexp) {
+                    let range = this.range.range.cloneRange();
+                    range[this.backward ? "setEnd" : "setStart"](
+                        start.startContainer, start.startOffset);
+                    range = DOM.stringify(range);
+
+                    if (!this.backward)
+                        var match = RegExp(pattern, "m" + this.flags).exec(range);
+                    else {
+                        match = RegExp("[^]*(?:" + pattern + ")", "m" + this.flags).exec(range);
+                        if (match)
+                            match = RegExp(pattern + "$", this.flags).exec(match[0]);
+                    }
+                    if (!(match && match[0]))
+                        continue;
+                    word = match[0];
+                }
+
                 var range = this.finder.Find(word, this.range.range, start, this.range.range);
-                if (range)
+                if (range && DOM(range.commonAncestorContainer).isVisible)
                     break;
             }
 
         if (range)
             this.lastRange = range.cloneRange();
         if (!private_) {
-            this.lastString = word;
+            this.lastString = pattern;
             if (range == null) {
                 this.cancel();
                 this.found = false;
@@ -646,18 +706,18 @@ var RangeFind = Class("RangeFind", {
     get stale() this._stale || this.baseDocument.get() != this.content.document,
     set stale(val) this._stale = val,
 
-    addListeners: function () {
+    addListeners: function addListeners() {
         for (let range in array.iterValues(this.ranges))
             range.window.addEventListener("unload", this.closure.onUnload, true);
     },
-    purgeListeners: function () {
+    purgeListeners: function purgeListeners() {
         for (let range in array.iterValues(this.ranges))
             try {
                 range.window.removeEventListener("unload", this.closure.onUnload, true);
             }
             catch (e if e.result === Cr.NS_ERROR_FAILURE) {}
     },
-    onUnload: function (event) {
+    onUnload: function onUnload(event) {
         this.purgeListeners();
         if (this.highlighted)
             this.highlight(true);
@@ -665,14 +725,12 @@ var RangeFind = Class("RangeFind", {
     }
 }, {
     Range: Class("RangeFind.Range", {
-        init: function (range, index) {
+        init: function init(range, index) {
             this.index = index;
 
             this.range = range;
             this.document = range.startContainer.ownerDocument;
             this.window = this.document.defaultView;
-            this.docShell = this.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
-                                       .QueryInterface(Ci.nsIDocShell);
 
             if (this.selection == null)
                 return false;
@@ -680,9 +738,11 @@ var RangeFind = Class("RangeFind", {
             this.save();
         },
 
+        docShell: Class.Memoize(function () util.docShell(this.window)),
+
         intersects: function (range) RangeFind.intersects(this.range, range),
 
-        save: function () {
+        save: function save() {
             this.scroll = Point(this.window.pageXOffset, this.window.pageYOffset);
 
             this.initialSelection = null;
@@ -690,11 +750,11 @@ var RangeFind = Class("RangeFind", {
                 this.initialSelection = this.selection.getRangeAt(0);
         },
 
-        descroll: function () {
+        descroll: function descroll() {
             this.window.scrollTo(this.scroll.x, this.scroll.y);
         },
 
-        deselect: function () {
+        deselect: function deselect() {
             if (this.selection) {
                 this.selection.removeAllRanges();
                 if (this.initialSelection)
@@ -714,17 +774,19 @@ var RangeFind = Class("RangeFind", {
             }
         }
     }),
-    contains: function (range, r) {
+    contains: function contains(range, r, quiet) {
         try {
             return range.compareBoundaryPoints(range.START_TO_END, r) >= 0 &&
                    range.compareBoundaryPoints(range.END_TO_START, r) <= 0;
         }
         catch (e) {
-            util.reportError(e, true);
+            if (e.result != Cr.NS_ERROR_DOM_WRONG_DOCUMENT_ERR && !quiet)
+                util.reportError(e, true);
             return false;
         }
     },
-    intersects: function (range, r) {
+    containsNode: function containsNode(range, n, quiet) n.ownerDocument && this.contains(range, RangeFind.nodeRange(n), quiet),
+    intersects: function intersects(range, r) {
         try {
             return r.compareBoundaryPoints(range.START_TO_END, range) >= 0 &&
                    r.compareBoundaryPoints(range.END_TO_START, range) <= 0;
@@ -734,12 +796,12 @@ var RangeFind = Class("RangeFind", {
             return false;
         }
     },
-    endpoint: function (range, before) {
+    endpoint: function endpoint(range, before) {
         range = range.cloneRange();
         range.collapse(before);
         return range;
     },
-    equal: function (r1, r2) {
+    equal: function equal(r1, r2) {
         try {
             return !r1.compareBoundaryPoints(r1.START_TO_START, r2) && !r1.compareBoundaryPoints(r1.END_TO_END, r2);
         }
@@ -747,7 +809,7 @@ var RangeFind = Class("RangeFind", {
             return false;
         }
     },
-    nodeContents: function (node) {
+    nodeContents: function nodeContents(node) {
         let range = node.ownerDocument.createRange();
         try {
             range.selectNodeContents(node);
@@ -755,7 +817,7 @@ var RangeFind = Class("RangeFind", {
         catch (e) {}
         return range;
     },
-    nodeRange: function (node) {
+    nodeRange: function nodeRange(node) {
         let range = node.ownerDocument.createRange();
         try {
             range.selectNode(node);
@@ -763,7 +825,7 @@ var RangeFind = Class("RangeFind", {
         catch (e) {}
         return range;
     },
-    sameDocument: function (r1, r2) {
+    sameDocument: function sameDocument(r1, r2) {
         if (!(r1 && r2 && r1.endContainer.ownerDocument == r2.endContainer.ownerDocument))
             return false;
         try {
@@ -774,12 +836,18 @@ var RangeFind = Class("RangeFind", {
         }
         return true;
     },
-    selectNodePath: ["a", "xhtml:a", "*[@onclick]"].map(function (p) "ancestor-or-self::" + p).join(" | ")
+    selectNodePath: ["a", "xhtml:a", "*[@onclick]"].map(function (p) "ancestor-or-self::" + p).join(" | "),
+    union: function union(a, b) {
+        let start = a.compareBoundaryPoints(a.START_TO_START, b) < 0 ? a : b;
+        let end   = a.compareBoundaryPoints(a.END_TO_END, b) > 0 ? a : b;
+        let res   = start.cloneRange();
+        res.setEnd(end.endContainer, end.endOffset);
+        return res;
+    }
 });
 
-} 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); }
 
 endModule();
 
-
 // vim: set fdm=marker sw=4 ts=4 et ft=javascript:
diff --git a/common/modules/help.jsm b/common/modules/help.jsm
new file mode 100644 (file)
index 0000000..d12312b
--- /dev/null
@@ -0,0 +1,467 @@
+// 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("help", {
+    exports: ["help"],
+    require: ["cache", "dom", "protocol", "services", "util"]
+}, this);
+
+this.lazyRequire("completion", ["completion"]);
+this.lazyRequire("overlay", ["overlay"]);
+
+var HelpBuilder = Class("HelpBuilder", {
+    init: function init() {
+        try {
+            // The versions munger will need to access the tag map
+            // during this process and without this we'll get an
+            // infinite loop.
+            help._data = this;
+
+            this.files = {};
+            this.tags = {};
+            this.overlays = {};
+
+            // Scrape the list of help files from all.xml
+            this.tags["all"] = this.tags["all.xml"] = "all";
+            let files = this.findHelpFile("all").map(function (doc)
+                    [f.value for (f in DOM.XPath("//dactyl:include/@href", doc))]);
+
+            // Scrape the tags from the rest of the help files.
+            array.flatten(files).forEach(function (file) {
+                this.tags[file + ".xml"] = file;
+                this.findHelpFile(file).forEach(function (doc) {
+                    this.addTags(file, doc);
+                }, this);
+            }, this);
+        }
+        finally {
+            delete help._data;
+        }
+    },
+
+    toJSON: function toJSON() ({
+        files: this.files,
+        overlays: this.overlays,
+        tags: this.tags
+    }),
+
+    // Find the tags in the document.
+    addTags: function addTags(file, doc) {
+        for (let elem in DOM.XPath("//@tag|//dactyl:tags/text()|//dactyl:tag/text()", doc))
+                for (let tag in values((elem.value || elem.textContent).split(/\s+/)))
+            this.tags[tag] = file;
+    },
+
+    bases: ["dactyl://locale-local/", "dactyl://locale/", "dactyl://cache/help/"],
+
+    // Find help and overlay files with the given name.
+    findHelpFile: function findHelpFile(file) {
+        let result = [];
+        for (let base in values(this.bases)) {
+            let url = [base, file, ".xml"].join("");
+            let res = util.httpGet(url, { quiet: true });
+            if (res) {
+                if (res.responseXML.documentElement.localName == "document")
+                    this.files[file] = url;
+                if (res.responseXML.documentElement.localName == "overlay")
+                    this.overlays[file] = url;
+                result.push(res.responseXML);
+            }
+        }
+        return result;
+    }
+});
+
+var Help = Module("Help", {
+    init: function init() {
+        this.initialized = false;
+
+        function Loop(fn)
+            function (uri, path) {
+                if (!help.initialized)
+                    return RedirectChannel(uri.spec, uri, 2,
+                                           "Initializing. Please wait...");
+
+                return fn.apply(this, arguments);
+            }
+
+        update(services["dactyl:"].providers, {
+            "help": Loop(function (uri, path) help.files[path]),
+            "help-overlay": Loop(function (uri, path) help.overlays[path]),
+            "help-tag": Loop(function (uri, path) {
+                let tag = decodeURIComponent(path);
+                if (tag in help.files)
+                    return RedirectChannel("dactyl://help/" + tag, uri);
+                if (tag in help.tags)
+                    return RedirectChannel("dactyl://help/" + help.tags[tag] + "#" + tag.replace(/#/g, encodeURIComponent), uri);
+            })
+        });
+
+        cache.register("help.json", HelpBuilder);
+
+        cache.register("help/versions.xml", function () {
+            let NEWS = util.httpGet(config.addon.getResourceURI("NEWS").spec,
+                                    { mimeType: "text/plain;charset=UTF-8" })
+                           .responseText;
+
+            let re = util.regexp(UTF8(<![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 '<?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()
+        });
+    },
+
+    initialize: function initialize() {
+        help.initialized = true;
+    },
+
+    flush: function flush(entries, time) {
+        cache.flushEntry("help.json", time);
+
+        for (let entry in values(Array.concat(entries || [])))
+            cache.flushEntry(entry, time);
+    },
+
+    get data() this._data || cache.get("help.json"),
+
+    get files() this.data.files,
+    get overlays() this.data.overlays,
+    get tags() this.data.tags,
+
+    Local: function Local(dactyl, modules, window) ({
+        init: function init() {
+            dactyl.commands["dactyl.help"] = function (event) {
+                let elem = event.originalTarget;
+                help.help(elem.getAttribute("tag") || elem.textContent);
+            };
+        },
+
+        /**
+         * 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 && Set.has(help.files, topic))
+                return topic;
+            let items = modules.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;
+        },
+
+        /**
+         * 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" : modules.options["helpfile"];
+
+                if (Set.has(help.files, helpFile))
+                    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" });
+        },
+
+        exportHelp: 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(FILE, File.MODE_CREATE | File.MODE_WRONLY | File.MODE_TRUNCATE);
+
+                addURIEntry = function addURIEntry(file, uri)
+                    zip.addEntryChannel(PATH + file, TIME, 9,
+                        services.io.newChannel(uri, null, null), false);
+                addDataEntry = function addDataEntry(file, data) // Unideal to an extreme.
+                    addURIEntry(file, "data:text/plain;charset=UTF-8," + encodeURI(data));
+            }
+
+            let empty = Set("area base basefont br col frame hr img input isindex link meta param"
+                                .split(" "));
+            function fix(node) {
+                switch(node.nodeType) {
+                case Node.ELEMENT_NODE:
+                    if (isinstance(node, [HTMLBaseElement]))
+                        return;
+
+                    data.push("<"); data.push(node.localName);
+                    if (node instanceof HTMLHtmlElement)
+                        data.push(" xmlns=" + XHTML.uri.quote(),
+                                  " xmlns:dactyl=" + NS.uri.quote());
+
+                    for (let { name, value } in array.iterValues(node.attributes)) {
+                        if (name == "dactyl:highlight") {
+                            Set.add(styles, value);
+                            name = "class";
+                            value = "hl-" + value;
+                        }
+                        if (name == "href") {
+                            value = node.href || value;
+                            if (value.indexOf("dactyl://help-tag/") == 0) {
+                                let uri = services.io.newChannel(value, null, null).originalURI;
+                                value = uri.spec == value ? "javascript:;" : uri.path.substr(1);
+                            }
+                            if (!/^#|[\/](#|$)|^[a-z]+:/.test(value))
+                                value = value.replace(/(#|$)/, ".xhtml$1");
+                        }
+                        if (name == "src" && value.indexOf(":") > 0) {
+                            chromeFiles[value] = value.replace(/.*\//, "");
+                            value = value.replace(/.*\//, "");
+                        }
+
+                        data.push(" ", name, '="',
+                                  <>{value}</>.toXMLString().replace(/"/g, "&quot;"),
+                                  '"');
+                    }
+                    if (node.localName in empty)
+                        data.push(" />");
+                    else {
+                        data.push(">");
+                        if (node instanceof HTMLHeadElement)
+                            data.push(<link rel="stylesheet" type="text/css" href="help.css"/>.toXMLString());
+                        Array.map(node.childNodes, fix);
+                        data.push("</", node.localName, ">");
+                    }
+                    break;
+                case Node.TEXT_NODE:
+                    data.push(<>{node.textContent}</>.toXMLString());
+                }
+            }
+
+            let chromeFiles = {};
+            let styles = {};
+            for (let [file, ] in Iterator(help.files)) {
+                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(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();
+        }
+
+    })
+}, {
+}, {
+    commands: function init_commands(dactyl, modules, window) {
+        const { commands, completion, help } = modules;
+
+        [
+            {
+                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"));
+                    help.help(args.literalArg, consolidated);
+                }, {
+                    argCount: "?",
+                    bang: true,
+                    completer: function (context) completion.help(context, consolidated),
+                    literal: 0
+                });
+        });
+    },
+    completion: function init_completion(dactyl, modules, window) {
+        const { completion } = modules;
+
+        completion.help = function completion_help(context, consolidated) {
+            dactyl.initHelp();
+            context.title = ["Help"];
+            context.anchored = false;
+            context.completions = help.tags;
+            if (consolidated)
+                context.keys = { text: 0, description: function () "all" };
+        };
+    },
+    mappings: function init_mappings(dactyl, modules, window) {
+        const { help, mappings, modes } = modules;
+
+        mappings.add([modes.MAIN], ["<open-help>", "<F1>"],
+            "Open the introductory help page",
+            function () { help.help(); });
+
+        mappings.add([modes.MAIN], ["<open-single-help>", "<A-F1>"],
+            "Open the single, consolidated help page",
+            function () { modules.ex.helpall(); });
+    },
+    javascript: function init_javascript(dactyl, modules, window) {
+        modules.JavaScript.setCompleter([modules.help.exportHelp],
+            [function (context, args) overlay.activeModules.completion.file(context)]);
+    }
+});
+
+endModule();
+
+// vim: set fdm=marker sw=4 ts=4 et ft=javascript:
index 73b6b06ae7cf35ed166aa9b003e442d3285538bd..7d6cfaf03cf61a7cef814116d9128b535cc169b7 100644 (file)
@@ -2,15 +2,16 @@
 //
 // 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("highlight", {
     exports: ["Highlight", "Highlights", "highlight"],
-    require: ["services", "styles", "util"],
-    use: ["messages", "template"]
+    require: ["services", "util"]
 }, this);
 
+this.lazyRequire("styles", ["Styles", "styles"]);
+
 var Highlight = Struct("class", "selector", "sites",
                        "defaultExtends", "defaultValue",
                        "value", "extends", "agent",
@@ -152,7 +153,7 @@ var Highlights = Module("Highlight", {
 
         let highlight = this.highlight[key] || this._create(false, [key]);
 
-        let bases = extend || highlight.extend;
+        let bases = extend || highlight.extends;
         if (append) {
             newStyle = Styles.append(highlight.value || "", newStyle);
             bases = highlight.extends.concat(bases);
@@ -349,7 +350,7 @@ var Highlights = Module("Highlight", {
                              "text-align: center"],
                             ([h.class,
                               <span style={"text-align: center; line-height: 1em;" + h.value + style}>XXX</span>,
-                              template.map(h.extends, template.highlight),
+                              template.map(h.extends, function (s) template.highlight(s), <>,</>),
                               template.highlightRegexp(h.value, /\b[-\w]+(?=:)|\/\*.*?\*\//g,
                                                        function (match) <span highlight={match[0] == "/" ? "Comment" : "Key"}>{match}</span>)
                              ]
@@ -404,7 +405,10 @@ var Highlights = Module("Highlight", {
                     {
                         command: this.name,
                         arguments: [v.class],
-                        literalArg: v.value
+                        literalArg: v.value,
+                        options: {
+                            "-link": v.extends.length ? v.extends : undefined
+                        }
                     }
                     for (v in Iterator(highlight))
                     if (v.value != v.defaultValue)
index 63ac6061921a35d2d88c12a13d1f65c7712c458b..5eefcc17b6bb6aeaceecae247a49df9035a36846 100644 (file)
@@ -5,31 +5,33 @@
 //
 // 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("io", {
     exports: ["IO", "io"],
-    require: ["services"],
-    use: ["config", "messages", "storage", "styles", "template", "util"]
+    require: ["services"]
 }, this);
 
+this.lazyRequire("config", ["config"]);
+this.lazyRequire("contexts", ["Contexts", "contexts"]);
+
 // TODO: why are we passing around strings rather than file objects?
 /**
  * Provides a basic interface to common system I/O operations.
  * @instance io
  */
 var IO = Module("io", {
-    init: function () {
+    init: function init() {
         this._processDir = services.directory.get("CurWorkD", Ci.nsIFile);
         this._cwd = this._processDir.path;
         this._oldcwd = null;
-        this.config = config;
+        lazyRequire("config", ["config"], this);
     },
 
-    Local: function (dactyl, modules, window) let ({ io, plugins } = modules) ({
+    Local: function Local(dactyl, modules, window) let ({ io, plugins } = modules) ({
 
         init: function init() {
             this.config = modules.config;
@@ -164,25 +166,40 @@ var IO = Module("io", {
 
                     dactyl.echomsg(_("io.sourcing", filename.quote()), 2);
 
-                    let uri = services.io.newFileURI(file);
+                    let uri = file.URI;
+
+                    let sourceJSM = function sourceJSM() {
+                        context = contexts.Module(uri);
+                        dactyl.triggerObserver("io.source", context, file, file.lastModifiedTime);
+                    }
 
-                    // handle pure JavaScript files specially
-                    if (/\.js$/.test(filename)) {
+                    if (/\.js,$/.test(filename))
+                        sourceJSM();
+                    else if (/\.js$/.test(filename)) {
                         try {
                             var context = contexts.Script(file, params.group);
+                            if (Set.has(this._scriptNames, file.path))
+                                util.flushCache();
+
                             dactyl.loadScript(uri.spec, context);
-                            dactyl.helpInitialized = false;
+                            dactyl.triggerObserver("io.source", context, file, file.lastModifiedTime);
                         }
                         catch (e) {
-                            if (e.fileName)
-                                try {
-                                    e.fileName = util.fixURI(e.fileName);
-                                    if (e.fileName == uri.spec)
-                                        e.fileName = filename;
-                                    e.echoerr = <>{e.fileName}:{e.lineNumber}: {e}</>;
-                                }
-                                catch (e) {}
-                            throw e;
+                            if (e == Contexts) { // Hack;
+                                context.unload();
+                                sourceJSM();
+                            }
+                            else {
+                                if (e.fileName && !(e instanceof FailedAssertion))
+                                    try {
+                                        e.fileName = util.fixURI(e.fileName);
+                                        if (e.fileName == uri.spec)
+                                            e.fileName = filename;
+                                        e.echoerr = <>{e.fileName}:{e.lineNumber}: {e}</>;
+                                    }
+                                    catch (e) {}
+                                throw e;
+                            }
                         }
                     }
                     else if (/\.css$/.test(filename))
@@ -196,10 +213,10 @@ var IO = Module("io", {
                             group: context.GROUP,
                             line: 1
                         });
+                        dactyl.triggerObserver("io.source", context, file, file.lastModifiedTime);
                     }
 
-                    if (this._scriptNames.indexOf(file.path) == -1)
-                        this._scriptNames.push(file.path);
+                    Set.add(this._scriptNames, file.path);
 
                     dactyl.echomsg(_("io.sourcingEnd", filename.quote()), 2);
                     dactyl.log(_("dactyl.sourced", filename), 3);
@@ -207,7 +224,7 @@ var IO = Module("io", {
                     return context;
                 }
                 catch (e) {
-                    dactyl.reportError(e);
+                    util.reportError(e);
                     let message = _("io.sourcingError", e.echoerr || (file ? file.path : filename) + ": " + e);
                     if (!params.silent)
                         dactyl.echoerr(message);
@@ -267,7 +284,7 @@ var IO = Module("io", {
      * @property {function} File class.
      * @final
      */
-    File: Class.memoize(function () let (io = this)
+    File: Class.Memoize(function () let (io = this)
         Class("File", File, {
             init: function init(path, checkCWD)
                 init.supercall(this, path, (arguments.length < 2 || checkCWD) && io.cwd)
@@ -291,13 +308,13 @@ var IO = Module("io", {
      * @default $HOME.
      * @returns {nsIFile} The RC file or null if none is found.
      */
-    getRCFile: function (dir, always) {
+    getRCFile: function getRCFile(dir, always) {
         dir = this.File(dir || "~");
 
         let rcFile1 = dir.child("." + config.name + "rc");
         let rcFile2 = dir.child("_" + config.name + "rc");
 
-        if (util.OS.isWindows)
+        if (config.OS.isWindows)
             [rcFile1, rcFile2] = [rcFile2, rcFile1];
 
         if (rcFile1.exists() && rcFile1.isFile())
@@ -309,19 +326,21 @@ var IO = Module("io", {
         return null;
     },
 
-    // TODO: make secure
     /**
      * Creates a temporary file.
      *
      * @returns {File}
      */
-    createTempFile: function () {
-        let file = services.directory.get("TmpD", Ci.nsIFile);
-        file.append(this.config.tempFile);
+    createTempFile: function createTempFile(name) {
+        if (name instanceof Ci.nsIFile)
+            var file = name.clone();
+        else {
+            file = services.directory.get("TmpD", Ci.nsIFile);
+            file.append(this.config.tempFile + (name ? "." + name : ""));
+        }
         file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, octal(600));
 
-        Cc["@mozilla.org/uriloader/external-helper-app-service;1"]
-            .getService(Ci.nsPIExternalAppLauncher).deleteTemporaryFileOnExit(file);
+        services.externalApp.deleteTemporaryFileOnExit(file);
 
         return File(file);
     },
@@ -335,9 +354,12 @@ var IO = Module("io", {
      */
     isJarURL: function isJarURL(url) {
         try {
-            let uri = util.newURI(util.fixURI(url));
+            let uri = util.newURI(url);
+            if (uri instanceof Ci.nsIJARURI)
+                return uri;
+
             let channel = services.io.newChannelFromURI(uri);
-            channel.cancel(Cr.NS_BINDING_ABORTED);
+            try { channel.cancel(Cr.NS_BINDING_ABORTED); } catch (e) {}
             if (channel instanceof Ci.nsIJARChannel)
                 return channel.URI.QueryInterface(Ci.nsIJARURI);
         }
@@ -372,7 +394,7 @@ var IO = Module("io", {
         }
     },
 
-    readHeredoc: function (end) {
+    readHeredoc: function readHeredoc(end) {
         return "";
     },
 
@@ -386,14 +408,15 @@ var IO = Module("io", {
      * name and searched for in turn.
      *
      * @param {string} bin The name of the executable to find.
+     * @returns {File|null}
      */
-    pathSearch: function (bin) {
+    pathSearch: function pathSearch(bin) {
         if (bin instanceof File || File.isAbsolutePath(bin))
             return this.File(bin);
 
-        let dirs = services.environment.get("PATH").split(util.OS.isWindows ? ";" : ":");
+        let dirs = services.environment.get("PATH").split(config.OS.isWindows ? ";" : ":");
         // Windows tries the CWD first TODO: desirable?
-        if (util.OS.isWindows)
+        if (config.OS.isWindows)
             dirs = [io.cwd].concat(dirs);
 
         for (let [, dir] in Iterator(dirs))
@@ -406,7 +429,7 @@ var IO = Module("io", {
 
                 // TODO: couldn't we just palm this off to the start command?
                 // automatically try to add the executable path extensions on windows
-                if (util.OS.isWindows) {
+                if (config.OS.isWindows) {
                     let extensions = services.environment.get("PATHEXT").split(";");
                     for (let [, extension] in Iterator(extensions)) {
                         file = dir.child(bin + extension);
@@ -425,7 +448,7 @@ var IO = Module("io", {
      * @param {File|string} program The program to run.
      * @param {[string]} args An array of arguments to pass to *program*.
      */
-    run: function (program, args, blocking) {
+    run: function run(program, args, blocking, self) {
         args = args || [];
 
         let file = this.pathSearch(program);
@@ -445,7 +468,7 @@ var IO = Module("io", {
                     function () {
                         if (!process.isRunning) {
                             timer.cancel();
-                            util.trapErrors(blocking);
+                            util.trapErrors(blocking, self, process.exitValue);
                         }
                     },
                     100, services.Timer.TYPE_REPEATING_SLACK);
@@ -469,12 +492,14 @@ var IO = Module("io", {
      *
      * @param {string} command The command to run.
      * @param {string} input Any input to be provided to the command on stdin.
-     * @returns {object}
+     * @param {function(object)} callback A callback to be called when
+     *      the command completes. @optional
+     * @returns {object|null}
      */
-    system: function (command, input) {
+    system: function system(command, input, callback) {
         util.dactyl.echomsg(_("io.callingShell", command), 4);
 
-        function escape(str) '"' + str.replace(/[\\"$]/g, "\\$&") + '"';
+        let { shellEscape } = util.closure;
 
         return this.withTempFiles(function (stdin, stdout, cmd) {
             if (input instanceof File)
@@ -482,33 +507,41 @@ var IO = Module("io", {
             else if (input)
                 stdin.write(input);
 
+            function result(status, output) ({
+                __noSuchMethod__: function (meth, args) this.output[meth].apply(this.output, args),
+                valueOf: function () this.output,
+                output: output.replace(/^(.*)\n$/, "$1"),
+                returnValue: status,
+                toString: function () this.output
+            });
+
+            function async(status) {
+                let output = stdout.read();
+                [stdin, stdout, cmd].forEach(function (f) f.exists() && f.remove(false));
+                callback(result(status, output));
+            }
+
             let shell = io.pathSearch(storage["options"].get("shell").value);
             let shcf = storage["options"].get("shellcmdflag").value;
             util.assert(shell, _("error.invalid", "'shell'"));
 
             if (isArray(command))
-                command = command.map(escape).join(" ");
+                command = command.map(shellEscape).join(" ");
 
             // TODO: implement 'shellredir'
-            if (util.OS.isWindows && !/sh/.test(shell.leafName)) {
+            if (config.OS.isWindows && !/sh/.test(shell.leafName)) {
                 command = "cd /D " + this.cwd.path + " && " + command + " > " + stdout.path + " 2>&1" + " < " + stdin.path;
-                var res = this.run(shell, shcf.split(/\s+/).concat(command), true);
+                var res = this.run(shell, shcf.split(/\s+/).concat(command), callback ? async : true);
             }
             else {
-                cmd.write("cd " + escape(this.cwd.path) + "\n" +
-                        ["exec", ">" + escape(stdout.path), "2>&1", "<" + escape(stdin.path),
-                         escape(shell.path), shcf, escape(command)].join(" "));
-                res = this.run("/bin/sh", ["-e", cmd.path], true);
+                cmd.write("cd " + shellEscape(this.cwd.path) + "\n" +
+                        ["exec", ">" + shellEscape(stdout.path), "2>&1", "<" + shellEscape(stdin.path),
+                         shellEscape(shell.path), shcf, shellEscape(command)].join(" "));
+                res = this.run("/bin/sh", ["-e", cmd.path], callback ? async : true);
             }
 
-            return {
-                __noSuchMethod__: function (meth, args) this.output[meth].apply(this.output, args),
-                valueOf: function () this.output,
-                output: stdout.read().replace(/^(.*)\n$/, "$1"),
-                returnValue: res,
-                toString: function () this.output
-            };
-        }) || "";
+            return callback ? true : result(res, stdout.read());
+        }, this, true);
     },
 
     /**
@@ -522,8 +555,8 @@ var IO = Module("io", {
      * @returns {boolean} false if temp files couldn't be created,
      *     otherwise, the return value of *func*.
      */
-    withTempFiles: function (func, self, checked) {
-        let args = array(util.range(0, func.length)).map(this.closure.createTempFile).array;
+    withTempFiles: function withTempFiles(func, self, checked, ext) {
+        let args = array(util.range(0, func.length)).map(bind("createTempFile", this, ext)).array;
         try {
             if (!args.every(util.identity))
                 return false;
@@ -531,7 +564,7 @@ var IO = Module("io", {
         }
         finally {
             if (!checked || res !== true)
-                args.forEach(function (f) f && f.remove(false));
+                args.forEach(function (f) f.remove(false));
         }
         return res;
     }
@@ -544,7 +577,7 @@ var IO = Module("io", {
         const rtpvar = config.idName + "_RUNTIME";
         let rtp = services.environment.get(rtpvar);
         if (!rtp) {
-            rtp = "~/" + (util.OS.isWindows ? "" : ".") + config.name;
+            rtp = "~/" + (config.OS.isWindows ? "" : ".") + config.name;
             services.environment.set(rtpvar, rtp);
         }
         return rtp;
@@ -555,7 +588,7 @@ var IO = Module("io", {
      */
     PATH_SEP: deprecated("File.PATH_SEP", { get: function PATH_SEP() File.PATH_SEP })
 }, {
-    commands: function (dactyl, modules, window) {
+    commands: function init_commands(dactyl, modules, window) {
         const { commands, completion, io } = modules;
 
         commands.add(["cd", "chd[ir]"],
@@ -632,7 +665,7 @@ var IO = Module("io", {
         commands.add(["mks[yntax]"],
             "Generate a Vim syntax file",
             function (args) {
-                let runtime = util.OS.isWindows ? "~/vimfiles/" : "~/.vim/";
+                let runtime = config.OS.isWindows ? "~/vimfiles/" : "~/.vim/";
                 let file = io.File(runtime + "syntax/" + config.name + ".vim");
                 if (args.length)
                     file = io.File(args[0]);
@@ -780,12 +813,13 @@ unlet s:cpo_save
         commands.add(["scrip[tnames]"],
             "List all sourced script names",
             function () {
-                if (!io._scriptNames.length)
+                let names = Object.keys(io._scriptNames);
+                if (!names.length)
                     dactyl.echomsg(_("command.scriptnames.none"));
                 else
                     modules.commandline.commandOutput(
                         template.tabular(["<SNR>", "Filename"], ["text-align: right; padding-right: 1em;"],
-                            ([i + 1, file] for ([i, file] in Iterator(io._scriptNames)))));
+                            ([i + 1, file] for ([i, file] in Iterator(names)))));
 
             },
             { argCount: "0" });
@@ -813,12 +847,12 @@ unlet s:cpo_save
                 if (args.bang)
                     arg = "!" + arg;
 
-                // NOTE: Vim doesn't replace ! preceded by 2 or more backslashes and documents it - desirable?
-                // pass through a raw bang when escaped or substitute the last command
-
-                // This is an asinine and irritating feature when we have searchable
+                // This is an asinine and irritating "feature" when we have searchable
                 // command-line history. --Kris
                 if (modules.options["banghist"]) {
+                    // NOTE: Vim doesn't replace ! preceded by 2 or more backslashes and documents it - desirable?
+                    // pass through a raw bang when escaped or substitute the last command
+
                     // replaceable bang and no previous command?
                     dactyl.assert(!/((^|[^\\])(\\\\)*)!/.test(arg) || io._lastRunCommand,
                         _("command.run.noPrevious"));
@@ -846,7 +880,7 @@ unlet s:cpo_save
                 literal: 0
             });
     },
-    completion: function (dactyl, modules, window) {
+    completion: function init_completion(dactyl, modules, window) {
         const { completion, io } = modules;
 
         completion.charset = function (context) {
@@ -873,7 +907,7 @@ unlet s:cpo_save
         completion.environment = function environment(context) {
             context.title = ["Environment Variable", "Value"];
             context.generate = function ()
-                io.system(util.OS.isWindows ? "set" : "env")
+                io.system(config.OS.isWindows ? "set" : "env")
                   .output.split("\n")
                   .filter(function (line) line.indexOf("=") > 0)
                   .map(function (line) line.match(/([^=]+)=(.*)/).slice(1));
@@ -943,7 +977,7 @@ unlet s:cpo_save
         completion.shellCommand = function shellCommand(context) {
             context.title = ["Shell Command", "Path"];
             context.generate = function () {
-                let dirNames = services.environment.get("PATH").split(util.OS.isWindows ? ";" : ":");
+                let dirNames = services.environment.get("PATH").split(config.OS.pathListSep);
                 let commands = [];
 
                 for (let [, dirName] in Iterator(dirNames)) {
@@ -957,7 +991,7 @@ unlet s:cpo_save
             };
         };
 
-        completion.addUrlCompleter("f", "Local files", function (context, full) {
+        completion.addUrlCompleter("file", "Local files", function (context, full) {
             let match = util.regexp(<![CDATA[
                 ^
                 (?P<prefix>
@@ -974,7 +1008,7 @@ unlet s:cpo_save
                 if (!match.path) {
                     context.key = match.proto;
                     context.advance(match.proto.length);
-                    context.generate = function () util.chromePackages.map(function (p) [p, match.proto + p + "/"]);
+                    context.generate = function () config.chromePackages.map(function (p) [p, match.proto + p + "/"]);
                 }
                 else if (match.scheme === "chrome") {
                     context.key = match.prefix;
@@ -988,13 +1022,13 @@ unlet s:cpo_save
             }
             if (!match || match.scheme === "resource" && match.path)
                 if (/^(\.{0,2}|~)\/|^file:/.test(context.filter)
-                    || util.OS.isWindows && /^[a-z]:/i.test(context.filter)
+                    || config.OS.isWindows && /^[a-z]:/i.test(context.filter)
                     || util.getFile(context.filter)
                     || io.isJarURL(context.filter))
                     completion.file(context, full);
         });
     },
-    javascript: function (dactyl, modules, window) {
+    javascript: function init_javascript(dactyl, modules, window) {
         modules.JavaScript.setCompleter([File, File.expandPath],
             [function (context, obj, args) {
                 context.quote[2] = "";
@@ -1013,11 +1047,11 @@ unlet s:cpo_save
             input: true
         });
     },
-    options: function (dactyl, modules, window) {
+    options: function init_options(dactyl, modules, window) {
         const { completion, options } = modules;
 
         var shell, shellcmdflag;
-        if (util.OS.isWindows) {
+        if (config.OS.isWindows) {
             shell = "cmd.exe";
             shellcmdflag = "/c";
         }
@@ -1028,7 +1062,7 @@ unlet s:cpo_save
 
         options.add(["banghist", "bh"],
             "Replace occurrences of ! with the previous command when executing external commands",
-            "boolean", true);
+            "boolean", false);
 
         options.add(["fileencoding", "fenc"],
             "The character encoding used when reading and writing files",
@@ -1064,7 +1098,7 @@ unlet s:cpo_save
             "string", shellcmdflag,
             {
                 getter: function (value) {
-                    if (this.hasChanged || !util.OS.isWindows)
+                    if (this.hasChanged || !config.OS.isWindows)
                         return value;
                     return /sh/.test(options["shell"]) ? "-c" : "/c";
                 }
index e85b8ca41f3002ae3151226b519139c254ab5a64..a4d1ab6bf435024c35d1cb68e0ce64bcaf133414 100644 (file)
@@ -2,7 +2,7 @@
 //
 // 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 */
 
 let { getOwnPropertyNames } = Object;
 
@@ -10,8 +10,7 @@ try {
 
 Components.utils.import("resource://dactyl/bootstrap.jsm");
 defineModule("javascript", {
-    exports: ["JavaScript", "javascript"],
-    use: ["messages", "services", "template", "util"]
+    exports: ["JavaScript", "javascript"]
 }, this);
 
 let isPrototypeOf = Object.prototype.isPrototypeOf;
@@ -44,13 +43,13 @@ var JavaScript = Module("javascript", {
         }
     }),
 
-    globals: Class.memoize(function () [
+    globals: Class.Memoize(function () [
        [this.modules.userContext, /*L*/"Global Variables"],
        [this.modules, "modules"],
        [this.window, "window"]
     ]),
 
-    toplevel: Class.memoize(function () this.modules.jsmodules),
+    toplevel: Class.Memoize(function () this.modules.jsmodules),
 
     lazyInit: true,
 
@@ -103,7 +102,7 @@ var JavaScript = Module("javascript", {
 
         let completions = [k for (k in this.iter(obj, toplevel))];
         if (obj === this.modules) // Hack.
-            completions = completions.concat([k for (k in this.iter(this.modules.jsmodules, toplevel))]);
+            completions = array.uniq(completions.concat([k for (k in this.iter(this.modules.jsmodules, toplevel))]));
         return completions;
     },
 
@@ -593,8 +592,10 @@ var JavaScript = Module("javascript", {
 
         // Wait for a keypress before completing when there's no key
         if (!this.context.tabPressed && key == "" && obj.length > 1) {
+            let message = this.context.message || "";
             this.context.waitingForTab = true;
-            this.context.message = _("completion.waitingForKeyPress");
+            this.context.message = <>{message}
+                                     {_("completion.waitingForKeyPress")}</>;
             return null;
         }
 
@@ -612,17 +613,17 @@ var JavaScript = Module("javascript", {
         return null;
     },
 
-    magicalNames: Class.memoize(function () Object.getOwnPropertyNames(Cu.Sandbox(this.window), true).sort()),
+    magicalNames: Class.Memoize(function () Object.getOwnPropertyNames(Cu.Sandbox(this.window), true).sort()),
 
     /**
      * A list of properties of the global object which are not
      * enumerable by any standard method.
      */
-    globalNames: Class.memoize(function () let (self = this) array.uniq([
-        "Array", "ArrayBuffer", "AttributeName", "Boolean", "Components",
+    globalNames: Class.Memoize(function () let (self = this) array.uniq([
+        "Array", "ArrayBuffer", "AttributeName", "Audio", "Boolean", "Components",
         "CSSFontFaceStyleDecl", "CSSGroupRuleRuleList", "CSSNameSpaceRule",
-        "CSSRGBColor", "CSSRect", "ComputedCSSStyleDeclaration", "Date",
-        "Error", "EvalError", "Float32Array", "Float64Array", "Function",
+        "CSSRGBColor", "CSSRect", "ComputedCSSStyleDeclaration", "Date", "Error",
+        "EvalError", "File", "Float32Array", "Float64Array", "Function",
         "HTMLDelElement", "HTMLInsElement", "HTMLSpanElement", "Infinity",
         "InnerModalContentWindow", "InnerWindow", "Int16Array", "Int32Array",
         "Int8Array", "InternalError", "Iterator", "JSON", "KeyboardEvent",
@@ -700,7 +701,7 @@ var JavaScript = Module("javascript", {
         modes.addMode("REPL", {
             description: "JavaScript Read Eval Print Loop",
             bases: [modes.COMMAND_LINE],
-            displayName: Class.memoize(function () this.name)
+            displayName: Class.Memoize(function () this.name)
         });
     },
     commandline: function initCommandLine(dactyl, modules, window) {
@@ -718,7 +719,7 @@ var JavaScript = Module("javascript", {
 
                 try {
                     var result = dactyl.userEval(js, this.context);
-                    var xml = util.objectToString(result, true);
+                    var xml = result === undefined ? "" : util.objectToString(result, true);
                 }
                 catch (e) {
                     util.reportError(e);
@@ -746,7 +747,7 @@ var JavaScript = Module("javascript", {
 
             count: 0,
 
-            message: Class.memoize(function () {
+            message: Class.Memoize(function () {
                 default xml namespace = XHTML;
                 util.xmlToDom(<div highlight="REPL" key="rootNode"/>,
                               this.document, this);
@@ -762,7 +763,7 @@ var JavaScript = Module("javascript", {
                 init.supercall(this);
 
                 let self = this;
-                let sandbox = isinstance(context, ["Sandbox"]);
+                let sandbox = true || isinstance(context, ["Sandbox"]);
 
                 this.context = modules.newContext(context, !sandbox);
                 this.js = modules.JavaScript();
diff --git a/common/modules/main.jsm b/common/modules/main.jsm
new file mode 100644 (file)
index 0000000..2740b66
--- /dev/null
@@ -0,0 +1,357 @@
+// 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 */
+
+try {
+
+Components.utils.import("resource://dactyl/bootstrap.jsm");
+defineModule("main", {
+    exports: ["ModuleBase"],
+    require: ["config", "overlay", "services", "util"]
+}, this);
+
+var BASE = "resource://dactyl-content/";
+
+/**
+ * @class ModuleBase
+ * The base class for all modules.
+ */
+var ModuleBase = Class("ModuleBase", {
+    /**
+     * @property {[string]} A list of module prerequisites which
+     * must be initialized before this module is loaded.
+     */
+    requires: [],
+
+    toString: function () "[module " + this.constructor.className + "]"
+});
+
+var Modules = function Modules(window) {
+    /**
+     * @constructor Module
+     *
+     * Constructs a new ModuleBase class and makes arrangements for its
+     * initialization. Arguments marked as optional must be either
+     * entirely elided, or they must have the exact type specified.
+     * Loading semantics are as follows:
+     *
+     *  - A module is guaranteed not to be initialized before any of its
+     *    prerequisites as listed in its {@see ModuleBase#requires} member.
+     *  - A module is considered initialized once it's been instantiated,
+     *    its {@see Class#init} method has been called, and its
+     *    instance has been installed into the top-level {@see modules}
+     *    object.
+     *  - Once the module has been initialized, its module-dependent
+     *    initialization functions will be called as described hereafter.
+     * @param {string} name The module's name as it will appear in the
+     *     top-level {@see modules} object.
+     * @param {ModuleBase} base The base class for this module.
+     *     @optional
+     * @param {Object} prototype The prototype for instances of this
+     *     object. The object itself is copied and not used as a prototype
+     *     directly.
+     * @param {Object} classProperties The class properties for the new
+     *     module constructor.
+     *     @optional
+     * @param {Object} moduleInit The module initialization functions
+     *     for the new module. Each function is called as soon as the
+     *     named module has been initialized. The constructors are
+     *     guaranteed to be called in the same order that the dependent
+     *     modules were initialized.
+     *     @optional
+     *
+     * @returns {function} The constructor for the resulting module.
+     */
+    function Module(name) {
+        let args = Array.slice(arguments);
+
+        var base = ModuleBase;
+        if (callable(args[1]))
+            base = args.splice(1, 1)[0];
+
+        let [, prototype, classProperties, moduleInit] = args;
+        prototype._metaInit_ = function () {
+            delete module.prototype._metaInit_;
+            Class.replaceProperty(modules, module.className, this);
+        };
+        const module = Class(name, base, prototype, classProperties);
+
+        module.INIT = moduleInit || {};
+        module.modules = modules;
+        module.prototype.INIT = module.INIT;
+        module.requires = prototype.requires || [];
+        Module.list.push(module);
+        Module.constructors[name] = module;
+        return module;
+    }
+    Module.list = [];
+    Module.constructors = {};
+
+    const create = window.Object.create || (function () {
+        window.__dactyl_eval_string = "(function (proto) ({ __proto__: proto }))";
+        JSMLoader.loadSubScript(BASE + "eval.js", window);
+
+        let res = window.__dactyl_eval_result;
+        delete window.__dactyl_eval_string;
+        delete window.__dactyl_eval_result;
+        return res;
+    })();
+
+
+    const BASES = [BASE, "resource://dactyl-local-content/"];
+
+    const jsmodules = { NAME: "jsmodules" };
+    const modules = update(create(jsmodules), {
+        yes_i_know_i_should_not_report_errors_in_these_branches_thanks: [],
+
+        jsmodules: jsmodules,
+
+        get content() this.config.browser.contentWindow || window.content,
+
+        window: window,
+
+        Module: Module,
+
+        load: function load(script) {
+            for (let [i, base] in Iterator(BASES)) {
+                try {
+                    JSMLoader.loadSubScript(base + script + ".js", modules, "UTF-8");
+                    return;
+                }
+                catch (e) {
+                    if (typeof e !== "string") {
+                        util.dump("Trying: " + (base + script + ".js") + ":");
+                        util.reportError(e);
+                    }
+                }
+            }
+            try {
+                require(jsmodules, script);
+            }
+            catch (e) {
+                util.dump("Loading script " + script + ":");
+                util.reportError(e);
+            }
+        },
+
+        newContext: function newContext(proto, normal) {
+            if (normal)
+                return create(proto);
+
+            if (services.has("dactyl") && services.dactyl.createGlobal)
+                var sandbox = services.dactyl.createGlobal();
+            else
+                sandbox = Components.utils.Sandbox(window, { sandboxPrototype: proto || modules,
+                                                             wantXrays: false });
+
+            // Hack:
+            sandbox.Object = jsmodules.Object;
+            sandbox.File = jsmodules.File;
+            sandbox.Math = jsmodules.Math;
+            sandbox.__proto__ = proto || modules;
+            return sandbox;
+        },
+
+        get ownPropertyValues() array.compact(
+                Object.getOwnPropertyNames(this)
+                      .map(function (name) Object.getOwnPropertyDescriptor(this, name).value, this)),
+
+        get moduleList() this.ownPropertyValues.filter(function (mod) mod instanceof this.ModuleBase || mod.isLocalModule, this)
+    });
+
+    modules.plugins = create(modules);
+    modules.modules = modules;
+    return modules;
+}
+
+config.loadStyles();
+
+overlay.overlayWindow(Object.keys(config.overlays), function _overlay(window) ({
+    ready: function onInit(document) {
+        const modules = Modules(window);
+        modules.moduleManager = this;
+        this.modules = modules;
+
+        window.dactyl = { modules: modules };
+
+        defineModule.time("load", null, function _load() {
+            config.modules.global
+                  .forEach(function (name) defineModule.time("load", name, require, null, modules.jsmodules, name));
+
+            config.modules.window
+                  .forEach(function (name) defineModule.time("load", name, modules.load, modules, name));
+        }, this);
+    },
+
+    load: function onLoad(document) {
+        let self = this;
+
+        var { modules, Module } = this.modules;
+        delete window.dactyl;
+
+        this.startTime = Date.now();
+        this.deferredInit = { load: {} };
+        this.seen = {};
+        this.loaded = {};
+        modules.loaded = this.loaded;
+
+        defineModule.modules.forEach(function defModule(mod) {
+            let names = Set(Object.keys(mod.INIT));
+            if ("init" in mod.INIT)
+                Set.add(names, "init");
+
+            keys(names).forEach(function (name) { self.deferInit(name, mod.INIT, mod); });
+        });
+        this.modules = modules;
+
+        this.scanModules();
+        this.initDependencies("init");
+
+        modules.config.scripts.forEach(modules.load);
+
+        this.scanModules();
+
+        defineModule.modules.forEach(function defModule({ lazyInit, constructor: { className } }) {
+            if (!lazyInit) {
+                Class.replaceProperty(modules, className, modules[className]);
+                this.initDependencies(className);
+            }
+            else
+                modules.__defineGetter__(className, function () {
+                    let module = modules.jsmodules[className];
+                    Class.replaceProperty(modules, className, module);
+                    if (module.reallyInit)
+                        module.reallyInit(); // :(
+
+                    if (!module.lazyDepends)
+                        self.initDependencies(className);
+                    return module;
+                });
+        }, this);
+    },
+
+    cleanup: function cleanup(window) {
+        overlay.windows = overlay.windows.filter(function (w) w != window);
+    },
+
+    unload: function unload(window) {
+        for each (let mod in this.modules.moduleList.reverse()) {
+            mod.stale = true;
+
+            if ("destroy" in mod)
+                util.trapErrors("destroy", mod);
+        }
+    },
+
+    visible: function visible(window) {
+        // Module.list.forEach(load);
+        this.initDependencies("load");
+        this.modules.times = update({}, defineModule.times);
+
+        defineModule.loadLog.push("Loaded in " + (Date.now() - this.startTime) + "ms");
+
+        overlay.windows = array.uniq(overlay.windows.concat(window), true);
+    },
+
+    loadModule: function loadModule(module, prereq, frame) {
+        let { loaded, seen } = this;
+        let { Module, modules } = this.modules;
+
+        if (isString(module)) {
+            if (!Module.constructors.hasOwnProperty(module))
+                modules.load(module);
+            module = Module.constructors[module];
+        }
+
+        try {
+            if (Set.has(loaded, module.className))
+                return;
+
+            if (Set.add(seen, module.className))
+                throw Error("Module dependency loop.");
+
+            for (let dep in values(module.requires))
+                this.loadModule(Module.constructors[dep], module.className);
+
+            defineModule.loadLog.push(
+                "Load" + (isString(prereq) ? " " + prereq + " dependency: " : ": ")
+                    + module.className);
+
+            if (frame && frame.filename)
+                defineModule.loadLog.push("  from: " + util.fixURI(frame.filename) + ":" + frame.lineNumber);
+
+            let obj = defineModule.time(module.className, "init", module);
+            Class.replaceProperty(modules, module.className, obj);
+
+            Set.add(loaded, module.className);
+
+            if (loaded.dactyl && obj.signals)
+                modules.dactyl.registerObservers(obj);
+
+            if (!module.lazyDepends)
+                this.initDependencies(module.className);
+        }
+        catch (e) {
+            util.dump("Loading " + (module && module.className) + ":");
+            util.reportError(e);
+        }
+        return modules[module.className];
+    },
+
+    deferInit: function deferInit(name, INIT, mod) {
+        let { modules } = this.modules;
+
+        let init = this.deferredInit[name] || {};
+        this.deferredInit[name] = init;
+
+        let className = mod.className || mod.constructor.className;
+
+        init[className] = function callee() {
+            function finish() {
+                this.currentDependency = className;
+                defineModule.time(className, name, INIT[name], mod,
+                                  modules.dactyl, modules, window);
+            }
+            if (!callee.frobbed) {
+                callee.frobbed = true;
+                if (modules[name] instanceof Class)
+                    modules[name].withSavedValues(["currentDependency"], finish);
+                else
+                    finish.call({});
+            }
+        };
+
+        INIT[name].require = function (name) { init[name](); };
+    },
+
+    scanModules: function scanModules() {
+        let self = this;
+        let { Module, modules } = this.modules;
+
+        Module.list.forEach(function frobModule(mod) {
+            if (!mod.frobbed) {
+                modules.__defineGetter__(mod.className, function () {
+                    delete modules[mod.className];
+                    return self.loadModule(mod.className, null, Components.stack.caller);
+                });
+                Object.keys(mod.prototype.INIT)
+                      .forEach(function (name) { self.deferInit(name, mod.prototype.INIT, mod); });
+            }
+            mod.frobbed = true;
+        });
+    },
+
+    initDependencies: function initDependencies(name, parents) {
+        for (let [k, v] in Iterator(this.deferredInit[name] || {}))
+            if (!parents || ~parents.indexOf(k))
+                util.trapErrors(v);
+    }
+}));
+
+endModule();
+
+} 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:
index f1f4de1324a3d02103e7313294775671135ef629..bca366992cf0b3f49a5752a97fcb4ae3ab20a26d 100644 (file)
@@ -2,7 +2,7 @@
 //
 // 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 {
 
@@ -12,25 +12,18 @@ defineModule("messages", {
     require: ["services", "util"]
 }, this);
 
-// TODO: Lazy instantiation
 var Messages = Module("messages", {
 
     init: function init(name) {
         let self = this;
-        name = name || "messages";
-
-        this.bundles = array.uniq([JSMLoader.getTarget("dactyl://locale/" + name + ".properties"),
-                                   JSMLoader.getTarget("dactyl://locale-local/" + name + ".properties"),
-                                   "resource://dactyl-locale/en-US/" + name + ".properties",
-                                   "resource://dactyl-locale-local/en-US/" + name + ".properties"])
-                            .map(services.stringBundle.createBundle)
-                            .filter(function (bundle) { try { bundle.getSimpleEnumeration(); return true; } catch (e) { return false; } });
+        this.name = name || "messages";
 
         this._ = Class("_", String, {
             init: function _(message) {
                 this.args = arguments;
             },
-            message: Class.memoize(function () {
+            instance: {},
+            message: Class.Memoize(function () {
                 let message = this.args[0];
 
                 if (this.args.length > 1) {
@@ -42,27 +35,35 @@ var Messages = Module("messages", {
             valueOf: function valueOf() this.message,
             toString: function toString() this.message
         });
-
-        let seen = {};
-        for (let { key } in this.iterate()) {
-            if (!Set.add(seen, key))
-                this._[key] = this[key] = {
-                    __noSuchMethod__: function __(prop, args) self._.apply(self, [prop].concat(args))
-                };
-        }
     },
 
-    iterate: function () let (bundle = this.bundles[0])
-        iter(prop.QueryInterface(Ci.nsIPropertyElement) for (prop in iter(bundle.getSimpleEnumeration()))),
-
     cleanup: function cleanup() {
         services.stringBundle.flushBundles();
     },
 
+    bundles: Class.Memoize(function ()
+        array.uniq([JSMLoader.getTarget("dactyl://locale/" + this.name + ".properties"),
+                    JSMLoader.getTarget("dactyl://locale-local/" + this.name + ".properties"),
+                    "resource://dactyl-locale/en-US/" + this.name + ".properties",
+                    "resource://dactyl-locale-local/en-US/" + this.name + ".properties"])
+             .map(services.stringBundle.createBundle)
+             .filter(function (bundle) { try { bundle.getSimpleEnumeration(); return true; } catch (e) { return false; } })),
+
+    iterate: function () {
+        let seen = {};
+        for (let bundle in values(this.bundles))
+            for (let { key, value } in iter(bundle.getSimpleEnumeration(), Ci.nsIPropertyElement))
+                if (!Set.add(seen, key))
+                    yield [key, value];
+    },
+
     get: function get(value, default_) {
         for (let bundle in values(this.bundles))
             try {
-                return bundle.GetStringFromName(value);
+                let res = bundle.GetStringFromName(value);
+                if (res.slice(0, 2) == "+ ")
+                    return res.slice(2).replace(/\s+/g, " ");
+                return res;
             }
             catch (e) {}
 
@@ -75,7 +76,10 @@ var Messages = Module("messages", {
     format: function format(value, args, default_) {
         for (let bundle in values(this.bundles))
             try {
-                return bundle.formatStringFromName(value, args, args.length);
+                let res = bundle.formatStringFromName(value, args, args.length);
+                if (res.slice(0, 2) == "+ ")
+                    return res.slice(2).replace(/\s+/g, " ");
+                return res;
             }
             catch (e) {}
 
@@ -83,8 +87,51 @@ var Messages = Module("messages", {
         if (arguments.length < 3) // Do *not* localize these strings
             util.reportError(Error("Invalid locale string: " + value));
         return arguments.length > 2 ? default_ : value;
-    }
+    },
 
+    /**
+     * Exports known localizable strings to a properties file.
+     *
+     * @param {string|nsIFile} {file} The file to which to export
+     *      the strings.
+     */
+    export: function export_(file) {
+        let { Buffer, commands, hints, io, mappings, modes, options, sanitizer } = overlay.activeModules;
+        file = io.File(file);
+
+        function properties(base, iter_, prop) iter(function _properties() {
+            function key() [base, obj.identifier || obj.name].concat(Array.slice(arguments)).join(".").replace(/[\\:=]/g, "\\$&");
+
+            prop = prop || "description";
+            for (var obj in iter_) {
+                if (!obj.hive || obj.hive.name !== "user") {
+                    yield key(prop) + " = " + obj[prop];
+
+                    if (iter_.values)
+                        for (let [k, v] in isArray(obj.values) ? array.iterValues(obj.values) : iter(obj.values))
+                            yield key("values", k) + " = " + v;
+
+                    for (let opt in values(obj.options))
+                        yield key("options", opt.names[0]) + " = " + opt.description;
+
+                    if (obj.deprecated)
+                        yield key("deprecated") + " = " + obj.deprecated;
+                }
+            }
+        }()).toArray();
+
+        file.write(
+            array(commands.allHives.map(function (h) properties("command", h)))
+                          .concat(modes.all.map(function (m)
+                              properties("map", values(mappings.builtin.getStack(m)
+                                                               .filter(function (map) map.modes[0] == m)))))
+                          .concat(properties("mode", values(modes.all.filter(function (m) !m.hidden))))
+                          .concat(properties("option", options))
+                          .concat(properties("hintmode", values(hints.modes), "prompt"))
+                          .concat(properties("pageinfo", values(Buffer.pageInfo), "title"))
+                          .concat(properties("sanitizeitem", values(sanitizer.itemMap)))
+                .flatten().uniq().join("\n"));
+    }
 }, {
     Localized: Class("Localized", Class.Property, {
         init: function init(prop, obj) {
@@ -108,7 +155,10 @@ var Messages = Module("messages", {
                         function getter(key, default_) function getter() messages.get([name, key].join("."), default_);
 
                         if (value != null) {
-                            var name = [this.constructor.className.toLowerCase(), this.identifier || this.name, prop].join(".");
+                            var name = [this.constructor.className.toLowerCase(),
+                                        this.identifier || this.name,
+                                        prop].join(".");
+
                             if (!isObject(value))
                                 value = messages.get(name, value);
                             else if (isArray(value))
@@ -137,12 +187,17 @@ var Messages = Module("messages", {
     })
 }, {
     javascript: function initJavascript(dactyl, modules, window) {
-        modules.JavaScript.setCompleter([this._, this.get, this.format], [
-            function (context) {
-                context.keys = { text: "key", description: "value" };
-                return messages.iterate();
-            }
+        let { JavaScript } = modules;
+
+        JavaScript.setCompleter([this._, this.get, this.format], [
+            function (context) messages.iterate()
         ]);
+
+        JavaScript.setCompleter([this.export],
+            [function (context, obj, args) {
+                context.quote[2] = "";
+                modules.completion.file(context, true);
+            }]);
     }
 });
 
index a09fef257b3dc149ab1b8a53c1b20b054e2e0984..512e304f789b800506c8fe4be6fa6e13f6d6f223 100644 (file)
@@ -4,17 +4,18 @@
 //
 // 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("options", {
     exports: ["Option", "Options", "ValueError", "options"],
-    require: ["messages", "storage"],
-    use: ["commands", "completion", "config", "prefs", "services", "styles", "template", "util"]
+    require: ["contexts", "messages", "storage"]
 }, this);
 
+this.lazyRequire("config", ["config"]);
+
 /** @scope modules */
 
 let ValueError = Class("ValueError", ErrorBase);
@@ -47,32 +48,13 @@ var Option = Class("Option", {
     init: function init(modules, names, description, defaultValue, extraInfo) {
         this.modules = modules;
         this.name = names[0];
-        this.names = names;
         this.realNames = names;
         this.description = description;
 
         if (extraInfo)
             this.update(extraInfo);
 
-        if (Set.has(this.modules.config.defaults, this.name))
-            defaultValue = this.modules.config.defaults[this.name];
-
-        if (defaultValue !== undefined) {
-            if (this.type === "string")
-                defaultValue = Commands.quote(defaultValue);
-
-            if (isObject(defaultValue))
-                defaultValue = iter(defaultValue).map(function (val) val.map(Option.quote).join(":")).join(",");
-
-            if (isArray(defaultValue))
-                defaultValue = defaultValue.map(Option.quote).join(",");
-
-            this.defaultValue = this.parse(defaultValue);
-        }
-
-        // add no{option} variant of boolean {option} to this.names
-        if (this.type == "boolean")
-            this.names = array([name, "no" + name] for (name in values(names))).flatten().array;
+        this._defaultValue = defaultValue;
 
         if (this.globalValue == undefined && !this.initialValue)
             this.globalValue = this.defaultValue;
@@ -101,8 +83,15 @@ var Option = Class("Option", {
     },
 
     /** @property {value} The option's global value. @see #scope */
-    get globalValue() { try { return options.store.get(this.name, {}).value; } catch (e) { util.reportError(e); throw e; } },
-    set globalValue(val) { options.store.set(this.name, { value: val, time: Date.now() }); },
+    get globalValue() {
+        let val = options.store.get(this.name, {}).value;
+        if (val != null)
+            return val;
+        return this.globalValue = this.defaultValue;
+    },
+    set globalValue(val) {
+        options.store.set(this.name, { value: val, time: Date.now() });
+    },
 
     /**
      * Returns *value* as an array of parsed values if the option type is
@@ -268,8 +257,9 @@ var Option = Class("Option", {
 
     /** @property {string} The option's canonical name. */
     name: null,
+
     /** @property {[string]} All names by which this option is identified. */
-    names: null,
+    names: Class.Memoize(function () this.realNames),
 
     /**
      * @property {string} The option's data type. One of:
@@ -330,7 +320,32 @@ var Option = Class("Option", {
      *     unless the option is explicitly set either interactively or in an RC
      *     file or plugin.
      */
-    defaultValue: null,
+    defaultValue: Class.Memoize(function () {
+        let defaultValue = this._defaultValue;
+        delete this._defaultValue;
+
+        if (Set.has(this.modules.config.optionDefaults, this.name))
+            defaultValue = this.modules.config.optionDefaults[this.name];
+
+        if (defaultValue == null && this.getter)
+            defaultValue = this.getter();
+
+        if (defaultValue == undefined)
+            return null;
+
+        if (this.type === "string")
+            defaultValue = Commands.quote(defaultValue);
+
+        if (isArray(defaultValue))
+            defaultValue = defaultValue.map(Option.quote).join(",");
+        else if (isObject(defaultValue))
+            defaultValue = iter(defaultValue).map(function (val) val.map(Option.quote).join(":")).join(",");
+
+        if (isArray(defaultValue))
+            defaultValue = defaultValue.map(Option.quote).join(",");
+
+        return this.parse(defaultValue);
+    }),
 
     /**
      * @property {function} The function called when the option value is read.
@@ -425,6 +440,7 @@ var Option = Class("Option", {
         let re = util.regexp(Option.dequote(val), flags);
         re.bang = bang;
         re.result = result !== undefined ? result : !bang;
+        re.key = re.bang + Option.quote(util.regexp.getSource(re), /^!|:/);
         re.toString = function () Option.unparseRegexp(this, keepQuotes);
         return re;
     },
@@ -597,6 +613,23 @@ var Option = Class("Option", {
             return null;
         },
 
+        string: function string(operator, values, scope, invert) {
+            if (invert)
+                return values[(values.indexOf(this.value) + 1) % values.length];
+
+            switch (operator) {
+            case "+":
+                return this.value + values;
+            case "-":
+                return this.value.replace(values, "");
+            case "^":
+                return values + this.value;
+            case "=":
+                return values;
+            }
+            return null;
+        },
+
         stringmap: function stringmap(operator, values, scope, invert) {
             let res = update({}, this.value);
 
@@ -654,23 +687,7 @@ var Option = Class("Option", {
         get regexplist() this.stringlist,
         get regexpmap() this.stringlist,
         get sitelist() this.stringlist,
-        get sitemap() this.stringlist,
-
-        string: function string(operator, values, scope, invert) {
-            if (invert)
-                return values[(values.indexOf(this.value) + 1) % values.length];
-            switch (operator) {
-            case "+":
-                return this.value + values;
-            case "-":
-                return this.value.replace(values, "");
-            case "^":
-                return values + this.value;
-            case "=":
-                return values;
-            }
-            return null;
-        }
+        get sitemap() this.stringlist
     },
 
     validIf: function validIf(test, error) {
@@ -686,23 +703,31 @@ var Option = Class("Option", {
      * @param {value|[string]} values The value or array of values to validate.
      * @returns {boolean}
      */
-    validateCompleter: function validateCompleter(values) {
-        if (this.values)
-            var acceptable = this.values.array || this.values;
-        else {
+    validateCompleter: function validateCompleter(vals) {
+        function completions(extra) {
             let context = CompletionContext("");
-            acceptable = context.fork("", 0, this, this.completer);
-            if (!acceptable)
-                acceptable = context.allItems.items.map(function (item) [item.text]);
+            return context.fork("", 0, this, this.completer, extra) ||
+                   context.allItems.items.map(function (item) [item.text]);
+        };
+
+        if (isObject(vals) && !isArray(vals)) {
+            let k = values(completions.call(this, { values: {} })).toObject();
+            let v = values(completions.call(this, { value: "" })).toObject();
+            return Object.keys(vals).every(Set.has(k)) && values(vals).every(Set.has(v));
         }
 
+        if (this.values)
+            var acceptable = this.values.array || this.values;
+        else
+            acceptable = completions.call(this);
+
         if (isArray(acceptable))
             acceptable = Set(acceptable.map(function ([k]) k));
 
         if (this.type === "regexpmap" || this.type === "sitemap")
-            return Array.concat(values).every(function (re) Set.has(acceptable, re.result));
+            return Array.concat(vals).every(function (re) Set.has(acceptable, re.result));
 
-        return Array.concat(values).every(Set.has(acceptable));
+        return Array.concat(vals).every(Set.has(acceptable));
     },
 
     types: {}
@@ -745,6 +770,23 @@ var Option = Class("Option", {
     EXPORTED_SYMBOLS.push(class_.className);
 }, this);
 
+update(BooleanOption.prototype, {
+    names: Class.Memoize(function ()
+                array.flatten([[name, "no" + name] for (name in values(this.realNames))]))
+});
+
+var OptionHive = Class("OptionHive", Contexts.Hive, {
+    init: function init(group) {
+        init.supercall(this, group);
+        this.values = {};
+        this.has = Set.has(this.values);
+    },
+
+    add: function add(names, description, type, defaultValue, extraInfo) {
+        return this.modules.options.add(names, description, type, defaultValue, extraInfo);
+    }
+});
+
 /**
  * @instance options
  */
@@ -752,6 +794,12 @@ var Options = Module("options", {
     Local: function Local(dactyl, modules, window) let ({ contexts } = modules) ({
         init: function init() {
             const self = this;
+
+            update(this, {
+                hives: contexts.Hives("options", Class("OptionHive", OptionHive, { modules: modules })),
+                user: contexts.hives.options.user
+            });
+
             this.needInit = [];
             this._options = [];
             this._optionMap = {};
@@ -764,17 +812,23 @@ var Options = Module("options", {
                     opt.set(opt.globalValue, Option.SCOPE_GLOBAL, true);
             }, window);
 
-            services["dactyl:"].pages["options.dtd"] = function () [null,
+            modules.cache.register("options.dtd", function ()
                 util.makeDTD(
                     iter(([["option", o.name, "default"].join("."),
                            o.type === "string" ? o.defaultValue.replace(/'/g, "''") :
-                           o.value === true    ? "on"  :
-                           o.value === false   ? "off" : o.stringDefaultValue]
+                           o.defaultValue === true  ? "on"  :
+                           o.defaultValue === false ? "off" : o.stringDefaultValue]
                           for (o in self)),
 
                          ([["option", o.name, "type"].join("."), o.type] for (o in self)),
 
-                         config.dtd))];
+                         config.dtd)));
+        },
+
+        signals: {
+            "io.source": function ioSource(context, file, modTime) {
+                cache.flushEntry("options.dtd", modTime);
+            }
         },
 
         dactyl: dactyl,
@@ -811,8 +865,10 @@ var Options = Module("options", {
                             option.pre = "no";
                         option.default = (opt.defaultValue ? "" : "no") + opt.name;
                     }
-                    else if (isArray(opt.value))
-                        option.value = <>={template.map(opt.value, function (v) template.highlight(String(v)), <>,<span style="width: 0; display: inline-block"> </span></>)}</>;
+                    else if (isArray(opt.value) && opt.type != "charlist")
+                        option.value = <>={template.map(opt.value,
+                            function (v) template.highlight(String(v)),
+                            <>,<span style="width: 0; display: inline-block"> </span></>)}</>;
                     else
                         option.value = <>={template.highlight(opt.stringValue)}</>;
                     yield option;
@@ -842,6 +898,12 @@ var Options = Module("options", {
         add: function add(names, description, type, defaultValue, extraInfo) {
             const self = this;
 
+            if (!util.isDactyl(Components.stack.caller))
+                deprecated.warn(add, "options.add", "group.options.add");
+
+            util.assert(type in Option.types, _("option.noSuchType", type),
+                        false);
+
             if (!extraInfo)
                 extraInfo = {};
 
@@ -891,7 +953,7 @@ var Options = Module("options", {
     setPref: deprecated("prefs.set", function setPref() prefs.set.apply(prefs, arguments)),
     withContext: deprecated("prefs.withContext", function withContext() prefs.withContext.apply(prefs, arguments)),
 
-    cleanupPrefs: Class.memoize(function () localPrefs.Branch("cleanup.option.")),
+    cleanupPrefs: Class.Memoize(function () config.prefs.Branch("cleanup.option.")),
 
     cleanup: function cleanup(reason) {
         if (~["disable", "uninstall"].indexOf(reason))
@@ -969,7 +1031,8 @@ var Options = Module("options", {
         res.optionValue = res.option.get(res.scope);
 
         try {
-            res.values = res.option.parse(res.value);
+            if (!res.invert || res.option.type != "number") // Hack.
+                res.values = res.option.parse(res.value);
         }
         catch (e) {
             res.error = e;
@@ -1060,11 +1123,12 @@ var Options = Module("options", {
                     }
                     else {
                         var [matches, name, postfix, valueGiven, operator, value] =
-                            arg.match(/^\s*?([^=]+?)([?&!])?\s*(([-+^]?)=(.*))?\s*$/);
+                            arg.match(/^\s*?((?:[^=\\']|\\.|'[^']*')+?)([?&!])?\s*(([-+^]?)=(.*))?\s*$/);
                         reset = (postfix == "&");
                         invertBoolean = (postfix == "!");
                     }
 
+                    name = Option.dequote(name);
                     if (name == "all" && reset)
                         modules.commandline.input(_("pref.prompt.resetAll", config.host) + " ",
                             function (resp) {
@@ -1442,6 +1506,25 @@ var Options = Module("options", {
             context.advance(Option._splitAt);
             context.filter = Option.dequote(context.filter);
 
+            function val(obj) {
+                if (isArray(opt.defaultValue)) {
+                    let val = array.nth(obj, function (re) re.key == extra.key, 0);
+                    return val && val.result;
+                }
+                if (Set.has(opt.defaultValue, extra.key))
+                    return obj[extra.key];
+            }
+
+            if (extra.key && extra.value != null) {
+                context.fork("default", 0, this, function (context) {
+                    context.completions = [
+                            [val(opt.value), _("option.currentValue")],
+                            [val(opt.defaultValue), _("option.defaultValue")]
+                    ].filter(function (f) f[0] !== "" && f[0] != null);
+                });
+                context = context.fork("stuff", 0);
+            }
+
             context.title = ["Option Value"];
             context.quote = Commands.complQuote[Option._quote] || Commands.complQuote[""];
             // Not Vim compatible, but is a significant enough improvement
@@ -1469,7 +1552,7 @@ var Options = Module("options", {
     },
     javascript: function initJavascript(dactyl, modules, window) {
         const { options, JavaScript } = modules;
-        JavaScript.setCompleter(options.get, [function () ([o.name, o.description] for (o in options))]);
+        JavaScript.setCompleter(Options.prototype.get, [function () ([o.name, o.description] for (o in options))]);
     },
     sanitizer: function initSanitizer(dactyl, modules, window) {
         const { sanitizer } = modules;
index 08c2829d774415219aefdf05a627d3b61199bed5..0ccf502c00984159200124271c2ff193df84b416 100644 (file)
 //
 // 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("overlay", {
-    exports: ["ModuleBase"],
-    require: ["config", "io", "services", "util"]
+    exports: ["overlay"],
+    require: ["util"]
 }, this);
 
-/**
- * @class ModuleBase
- * The base class for all modules.
- */
-var ModuleBase = Class("ModuleBase", {
-    /**
-     * @property {[string]} A list of module prerequisites which
-     * must be initialized before this module is loaded.
-     */
-    requires: [],
+var getAttr = function getAttr(elem, ns, name)
+    elem.hasAttributeNS(ns, name) ? elem.getAttributeNS(ns, name) : null;
+var setAttr = function setAttr(elem, ns, name, val) {
+    if (val == null)
+        elem.removeAttributeNS(ns, name);
+    else
+        elem.setAttributeNS(ns, name, val);
+}
 
-    toString: function () "[module " + this.constructor.className + "]"
-});
+var Overlay = Class("Overlay", {
+    init: function init(window) {
+        this.window = window;
+    },
 
-var Overlay = Module("Overlay", {
-    init: function init() {
-        services["dactyl:"]; // Hack. Force module initialization.
-
-        config.loadStyles();
-
-        util.overlayWindow(config.overlayChrome, function overlay(window) ({
-            init: function onInit(document) {
-                /**
-                 * @constructor Module
-                 *
-                 * Constructs a new ModuleBase class and makes arrangements for its
-                 * initialization. Arguments marked as optional must be either
-                 * entirely elided, or they must have the exact type specified.
-                 * Loading semantics are as follows:
-                 *
-                 *  - A module is guaranteed not to be initialized before any of its
-                 *    prerequisites as listed in its {@see ModuleBase#requires} member.
-                 *  - A module is considered initialized once it's been instantiated,
-                 *    its {@see Class#init} method has been called, and its
-                 *    instance has been installed into the top-level {@see modules}
-                 *    object.
-                 *  - Once the module has been initialized, its module-dependent
-                 *    initialization functions will be called as described hereafter.
-                 * @param {string} name The module's name as it will appear in the
-                 *     top-level {@see modules} object.
-                 * @param {ModuleBase} base The base class for this module.
-                 *     @optional
-                 * @param {Object} prototype The prototype for instances of this
-                 *     object. The object itself is copied and not used as a prototype
-                 *     directly.
-                 * @param {Object} classProperties The class properties for the new
-                 *     module constructor.
-                 *     @optional
-                 * @param {Object} moduleInit The module initialization functions
-                 *     for the new module. Each function is called as soon as the named module
-                 *     has been initialized, but after the module itself. The constructors are
-                 *     guaranteed to be called in the same order that the dependent modules
-                 *     were initialized.
-                 *     @optional
-                 *
-                 * @returns {function} The constructor for the resulting module.
-                 */
-                function Module(name) {
-                    let args = Array.slice(arguments);
-
-                    var base = ModuleBase;
-                    if (callable(args[1]))
-                        base = args.splice(1, 1)[0];
-                    let [, prototype, classProperties, moduleInit] = args;
-                    const module = Class(name, base, prototype, classProperties);
-
-                    module.INIT = moduleInit || {};
-                    module.modules = modules;
-                    module.prototype.INIT = module.INIT;
-                    module.requires = prototype.requires || [];
-                    Module.list.push(module);
-                    Module.constructors[name] = module;
-                    return module;
-                }
-                Module.list = [];
-                Module.constructors = {};
+    cleanups: Class.Memoize(function () []),
+    objects: Class.Memoize(function () ({})),
 
-                const BASE = "resource://dactyl-content/";
+    get doc() this.window.document,
 
-                const create = window.Object.create || (function () {
-                    window.__dactyl_eval_string = "(function (proto) ({ __proto__: proto }))";
-                    JSMLoader.loadSubScript(BASE + "eval.js", window);
+    get win() this.window,
 
-                    let res = window.__dactyl_eval_result;
-                    delete window.__dactyl_eval_string;
-                    delete window.__dactyl_eval_result;
-                    return res;
-                })();
+    $: function $(sel, node) DOM(sel, node || this.doc),
 
-                const jsmodules = { NAME: "jsmodules" };
-                const modules = update(create(jsmodules), {
-                    yes_i_know_i_should_not_report_errors_in_these_branches_thanks: [],
+    cleanup: function cleanup(window, reason) {
+        for (let fn in values(this.cleanups))
+            util.trapErrors(fn, this, window, reason);
+    }
+});
 
-                    jsmodules: jsmodules,
 
-                    get content() this.config.browser.contentWindow || window.content,
+var Overlay = Module("Overlay", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), {
+    init: function init() {
+        util.addObserver(this);
+        this.overlays = {};
 
-                    window: window,
+        this.onWindowVisible = [];
+    },
 
-                    Module: Module,
+    id: Class.Memoize(function () config.addon.id),
 
-                    load: function load(script) {
-                        for (let [i, base] in Iterator(prefix)) {
-                            try {
-                                JSMLoader.loadSubScript(base + script + ".js", modules, "UTF-8");
-                                return;
-                            }
-                            catch (e) {
-                                if (typeof e !== "string") {
-                                    util.dump("Trying: " + (base + script + ".js") + ":");
-                                    util.reportError(e);
-                                }
-                            }
-                        }
-                        try {
-                            require(jsmodules, script);
-                        }
-                        catch (e) {
-                            util.dump("Loading script " + script + ":");
-                            util.reportError(e);
+    /**
+     * 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) {
+        let doc = target.ownerDocument || target.document || target;
+        let listeners = this.getData(doc, "listeners");
+
+        if (!isObject(event))
+            var [self, events] = [null, array.toObject([[event, callback]])];
+        else
+            [self, events] = [event, event[callback || "events"]];
+
+        for (let [event, callback] in Iterator(events)) {
+            let args = [util.weakReference(target),
+                        event,
+                        util.wrapCallback(callback, self),
+                        capture,
+                        allowUntrusted];
+
+            target.addEventListener.apply(target, args.slice(1));
+            listeners.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) {
+        let doc = target.ownerDocument || target.document || target;
+        let listeners = this.getData(doc, "listeners");
+        if (event === true)
+            target = null;
+
+        this.setData(doc, "listeners", listeners.filter(function (args) {
+            if (target == null || args[0].get() == target && args[1] == event && args[2].wrapped == callback && args[3] == capture) {
+                args[0].get().removeEventListener.apply(args[0].get(), args.slice(1));
+                return false;
+            }
+            return !args[0].get();
+        }));
+    },
+
+    cleanup: function cleanup(reason) {
+        for (let doc in util.iterDocuments()) {
+            for (let elem in values(this.getData(doc, "overlayElements")))
+                if (elem.parentNode)
+                    elem.parentNode.removeChild(elem);
+
+            for (let [elem, ns, name, orig, value] in values(this.getData(doc, "overlayAttributes")))
+                if (getAttr(elem, ns, name) === value)
+                    setAttr(elem, ns, name, orig);
+
+            for (let callback in values(this.getData(doc, "cleanup")))
+                util.trapErrors(callback, doc, reason);
+
+            this.unlisten(doc, true);
+
+            delete doc[this.id];
+            delete doc.defaultView[this.id];
+        }
+    },
+
+    observers: {
+        "toplevel-window-ready": function (window, data) {
+            let listener = util.wrapCallback(function listener(event) {
+                if (event.originalTarget === window.document) {
+                    window.removeEventListener("DOMContentLoaded", listener.wrapper, true);
+                    window.removeEventListener("load", listener.wrapper, true);
+                    overlay._loadOverlays(window);
+                }
+            });
+
+            window.addEventListener("DOMContentLoaded", listener, true);
+            window.addEventListener("load", listener, true);
+        },
+        "chrome-document-global-created": function (window, uri) { this.observe(window, "toplevel-window-ready", null); },
+        "content-document-global-created": function (window, uri) { this.observe(window, "toplevel-window-ready", null); },
+        "xul-window-visible": function () {
+            if (this.onWindowVisible)
+                this.onWindowVisible.forEach(function (f) f.call(this), this);
+            this.onWindowVisible = null;
+        }
+    },
+
+    getData: function getData(obj, key, constructor) {
+        let { id } = this;
+
+        if (!(id in obj && obj[id]))
+            obj[id] = {};
+
+        if (arguments.length == 1)
+            return obj[id];
+
+        if (obj[id][key] === undefined)
+            if (constructor === undefined || callable(constructor))
+                obj[id][key] = (constructor || Array)();
+            else
+                obj[id][key] = constructor;
+
+        return obj[id][key];
+    },
+
+    setData: function setData(obj, key, val) {
+        let { id } = this;
+
+        if (!(id in obj))
+            obj[id] = {};
+
+        return obj[id][key] = val;
+    },
+
+    overlayWindow: function (url, fn) {
+        if (url instanceof Ci.nsIDOMWindow)
+            overlay._loadOverlay(url, fn);
+        else {
+            Array.concat(url).forEach(function (url) {
+                if (!this.overlays[url])
+                    this.overlays[url] = [];
+                this.overlays[url].push(fn);
+            }, this);
+
+            for (let doc in util.iterDocuments())
+                if (~["interactive", "complete"].indexOf(doc.readyState)) {
+                    this.observe(doc.defaultView, "xul-window-visible");
+                    this._loadOverlays(doc.defaultView);
+                }
+                else {
+                    if (!this.onWindowVisible)
+                        this.onWindowVisible = [];
+                    this.observe(doc.defaultView, "toplevel-window-ready");
+                }
+        }
+    },
+
+    _loadOverlays: function _loadOverlays(window) {
+        let overlays = this.getData(window, "overlays");
+
+        for each (let obj in overlay.overlays[window.document.documentURI] || []) {
+            if (~overlays.indexOf(obj))
+                continue;
+            overlays.push(obj);
+            this._loadOverlay(window, obj(window));
+        }
+    },
+
+    _loadOverlay: function _loadOverlay(window, obj) {
+        let doc = window.document;
+        let elems = this.getData(doc, "overlayElements");
+        let attrs = this.getData(doc, "overlayAttributes");
+
+        function insert(key, fn) {
+            if (obj[key]) {
+                let iterator = Iterator(obj[key]);
+                if (!isObject(obj[key]))
+                    iterator = ([elem.@id, elem.elements(), elem.@*::*.(function::name() != "id")] for each (elem in obj[key]));
+
+                for (let [elem, xml, attr] in iterator) {
+                    if (elem = doc.getElementById(elem)) {
+                        let node = DOM.fromXML(xml, doc, obj.objects);
+                        if (!(node instanceof Ci.nsIDOMDocumentFragment))
+                            elems.push(node);
+                        else
+                            for (let n in array.iterValues(node.childNodes))
+                                elems.push(n);
+
+                        fn(elem, node);
+                        for each (let attr in attr || []) {
+                            let ns = attr.namespace(), name = attr.localName();
+                            attrs.push([elem, ns, name, getAttr(elem, ns, name), String(attr)]);
+                            if (attr.name() != "highlight")
+                                elem.setAttributeNS(ns, name, String(attr));
+                            else
+                                highlight.highlightNode(elem, String(attr));
                         }
-                    },
-
-                    newContext: function newContext(proto, normal) {
-                        if (normal)
-                            return create(proto);
-                        let sandbox = Components.utils.Sandbox(window, { sandboxPrototype: proto || modules, wantXrays: false });
-                        // Hack:
-                        sandbox.Object = jsmodules.Object;
-                        sandbox.Math = jsmodules.Math;
-                        sandbox.__proto__ = proto || modules;
-                        return sandbox;
-                    },
-
-                    get ownPropertyValues() array.compact(
-                            Object.getOwnPropertyNames(this)
-                                  .map(function (name) Object.getOwnPropertyDescriptor(this, name).value, this)),
-
-                    get moduleList() this.ownPropertyValues.filter(function (mod) mod instanceof this.ModuleBase || mod.isLocalModule, this)
-                });
-                modules.plugins = create(modules);
-                modules.modules = modules;
-                window.dactyl = { modules: modules };
-
-                let prefix = [BASE, "resource://dactyl-local-content/"];
-
-                defineModule.time("load", null, function _load() {
-                    ["addons",
-                     "base",
-                     "io",
-                     "commands",
-                     "completion",
-                     "config",
-                     "contexts",
-                     "downloads",
-                     "finder",
-                     "highlight",
-                     "javascript",
-                     "messages",
-                     "options",
-                     "overlay",
-                     "prefs",
-                     "sanitizer",
-                     "services",
-                     "storage",
-                     "styles",
-                     "template",
-                     "util"
-                    ].forEach(function (name) defineModule.time("load", name, require, null, jsmodules, name));
-
-                    ["dactyl",
-                     "modes",
-                     "commandline",
-                     "abbreviations",
-                     "autocommands",
-                     "buffer",
-                     "editor",
-                     "events",
-                     "hints",
-                     "mappings",
-                     "marks",
-                     "mow",
-                     "statusline"
-                     ].forEach(function (name) defineModule.time("load", name, modules.load, modules, name));
-                }, this);
-            },
-            load: function onLoad(document) {
-                // This is getting to be horrible. --Kris
-
-                var { modules, Module } = window.dactyl.modules;
-                delete window.dactyl;
-
-                const start = Date.now();
-                const deferredInit = { load: {} };
-                const seen = Set();
-                const loaded = Set();
-                modules.loaded = loaded;
-
-                function load(module, prereq, frame) {
-                    if (isString(module)) {
-                        if (!Module.constructors.hasOwnProperty(module))
-                            modules.load(module);
-                        module = Module.constructors[module];
                     }
+                }
+            }
+        }
+
+        insert("before", function (elem, dom) elem.parentNode.insertBefore(dom, elem));
+        insert("after", function (elem, dom) elem.parentNode.insertBefore(dom, elem.nextSibling));
+        insert("append", function (elem, dom) elem.appendChild(dom));
+        insert("prepend", function (elem, dom) elem.insertBefore(dom, elem.firstChild));
+        if (obj.ready)
+            util.trapErrors("ready", obj, window);
+
+        function load(event) {
+            util.trapErrors("load", obj, window, event);
+            if (obj.visible)
+                if (!event || !overlay.onWindowVisible || window != util.topWindow(window))
+                    util.trapErrors("visible", obj, window);
+                else
+                    overlay.onWindowVisible.push(function () { obj.visible(window) });
+        }
+
+        if (obj.load)
+            if (doc.readyState === "complete")
+                load();
+            else
+                window.addEventListener("load", util.wrapCallback(function onLoad(event) {
+                    if (event.originalTarget === doc) {
+                        window.removeEventListener("load", onLoad.wrapper, true);
+                        load(event);
+                    }
+                }), true);
 
-                    try {
-                        if (module.className in loaded)
-                            return;
-                        if (module.className in seen)
-                            throw Error("Module dependency loop.");
-                        Set.add(seen, module.className);
-
-                        for (let dep in values(module.requires))
-                            load(Module.constructors[dep], module.className);
+        if (obj.unload || obj.cleanup)
+            this.listen(window, "unload", function unload(event) {
+                if (event.originalTarget === doc) {
+                    overlay.unlisten(window, "unload", unload);
+                    if (obj.unload)
+                        util.trapErrors("unload", obj, window, event);
 
-                        defineModule.loadLog.push("Load" + (isString(prereq) ? " " + prereq + " dependency: " : ": ") + module.className);
-                        if (frame && frame.filename)
-                            defineModule.loadLog.push(" from: " + util.fixURI(frame.filename) + ":" + frame.lineNumber);
+                    if (obj.cleanup)
+                        util.trapErrors("cleanup", obj, window, "unload", event);
+                }
+            });
 
-                        let obj = defineModule.time(module.className, "init", module);
-                        Class.replaceProperty(modules, module.className, obj);
-                        loaded[module.className] = true;
+        if (obj.cleanup)
+            this.getData(doc, "cleanup").push(bind("cleanup", obj, window));
+    },
 
-                        if (loaded.dactyl && obj.signals)
-                            modules.dactyl.registerObservers(obj);
+    /**
+     * Overlays an object with the given property overrides. Each
+     * property in *overrides* is added to *object*, replacing any
+     * original value. Functions in *overrides* are augmented with the
+     * new properties *super*, *supercall*, and *superapply*, in the
+     * same manner as class methods, so that they may call their
+     * overridden counterparts.
+     *
+     * @param {object} object The object to overlay.
+     * @param {object} overrides An object containing properties to
+     *      override.
+     * @returns {function} A function which, when called, will remove
+     *      the overlay.
+     */
+    overlayObject: function (object, overrides) {
+        let original = Object.create(object);
+        overrides = update(Object.create(original), overrides);
+
+        Object.getOwnPropertyNames(overrides).forEach(function (k) {
+            let orig, desc = Object.getOwnPropertyDescriptor(overrides, k);
+            if (desc.value instanceof Class.Property)
+                desc = desc.value.init(k) || desc.value;
+
+            if (k in object) {
+                for (let obj = object; obj && !orig; obj = Object.getPrototypeOf(obj))
+                    if (orig = Object.getOwnPropertyDescriptor(obj, k))
+                        Object.defineProperty(original, k, orig);
+
+                if (!orig)
+                    if (orig = Object.getPropertyDescriptor(object, k))
+                        Object.defineProperty(original, k, orig);
+            }
 
-                        frob(module.className);
-                    }
-                    catch (e) {
-                        util.dump("Loading " + (module && module.className) + ":");
-                        util.reportError(e);
+            // Guard against horrible add-ons that use eval-based monkey
+            // patching.
+            let value = desc.value;
+            if (callable(desc.value)) {
+
+                delete desc.value;
+                delete desc.writable;
+                desc.get = function get() value;
+                desc.set = function set(val) {
+                    if (!callable(val) || Function.prototype.toString(val).indexOf(sentinel) < 0)
+                        Class.replaceProperty(this, k, val);
+                    else {
+                        let package_ = util.newURI(Components.stack.caller.filename).host;
+                        util.reportError(Error(_("error.monkeyPatchOverlay", package_)));
+                        util.dactyl.echoerr(_("error.monkeyPatchOverlay", package_));
                     }
-                    return modules[module.className];
-                }
-
-                function deferInit(name, INIT, mod) {
-                    let init = deferredInit[name] = deferredInit[name] || {};
-                    let className = mod.className || mod.constructor.className;
+                };
+            }
 
-                    init[className] = function callee() {
-                        if (!callee.frobbed)
-                            defineModule.time(className, name, INIT[name], mod,
-                                              modules.dactyl, modules, window);
-                        callee.frobbed = true;
-                    };
+            try {
+                Object.defineProperty(object, k, desc);
 
-                    INIT[name].require = function (name) { init[name](); };
+                if (callable(value)) {
+                    var sentinel = "(function DactylOverlay() {}())"
+                    value.toString = function toString() toString.toString.call(this).replace(/\}?$/, sentinel + "; $&");
+                    value.toSource = function toSource() toSource.toSource.call(this).replace(/\}?$/, sentinel + "; $&");
                 }
-
-                function frobModules() {
-                    Module.list.forEach(function frobModule(mod) {
-                        if (!mod.frobbed) {
-                            modules.__defineGetter__(mod.className, function () {
-                                delete modules[mod.className];
-                                return load(mod.className, null, Components.stack.caller);
-                            });
-                            Object.keys(mod.prototype.INIT)
-                                  .forEach(function (name) { deferInit(name, mod.prototype.INIT, mod); });
-                        }
-                        mod.frobbed = true;
-                    });
+            }
+            catch (e) {
+                try {
+                    if (value) {
+                        object[k] = value;
+                        return;
+                    }
                 }
-                defineModule.modules.forEach(function defModule(mod) {
-                    let names = Set(Object.keys(mod.INIT));
-                    if ("init" in mod.INIT)
-                        Set.add(names, "init");
-
-                    keys(names).forEach(function (name) { deferInit(name, mod.INIT, mod); });
-                });
-
-                function frob(name) { values(deferredInit[name] || {}).forEach(call); }
-
-                frobModules();
-                frob("init");
-                modules.config.scripts.forEach(modules.load);
-                frobModules();
+                catch (f) {}
+                util.reportError(e);
+            }
+        }, this);
 
-                defineModule.modules.forEach(function defModule({ lazyInit, constructor: { className } }) {
-                    if (!lazyInit) {
-                        frob(className);
-                        Class.replaceProperty(modules, className, modules[className]);
+        return function unwrap() {
+            for each (let k in Object.getOwnPropertyNames(original))
+                if (Object.getOwnPropertyDescriptor(object, k).configurable)
+                    Object.defineProperty(object, k, Object.getOwnPropertyDescriptor(original, k));
+                else {
+                    try {
+                        object[k] = original[k];
                     }
-                    else
-                        modules.__defineGetter__(className, function () {
-                            delete modules[className];
-                            frob(className);
-                            return modules[className] = modules[className];
-                        });
-                });
+                    catch (e) {}
+                }
+        };
+    },
 
-                // Module.list.forEach(load);
-                frob("load");
-                modules.times = update({}, defineModule.times);
+    get activeModules() this.activeWindow && this.activeWindow.dactyl.modules,
 
-                defineModule.loadLog.push("Loaded in " + (Date.now() - start) + "ms");
+    get modules() this.windows.map(function (w) w.dactyl.modules),
 
-                modules.events.listen(window, "unload", function onUnload() {
-                    window.removeEventListener("unload", onUnload.wrapped, false);
+    /**
+     * The most recently active dactyl window.
+     */
+    get activeWindow() this.windows[0],
 
-                    for each (let mod in modules.moduleList.reverse()) {
-                        mod.stale = true;
+    set activeWindow(win) this.windows = [win].concat(this.windows.filter(function (w) w != win)),
 
-                        if ("destroy" in mod)
-                            util.trapErrors("destroy", mod);
-                    }
-                }, false);
-            }
-        }));
-    }
+    /**
+     * A list of extant dactyl windows.
+     */
+    windows: Class.Memoize(function () [])
 });
 
 endModule();
index 13500685dbb10318a2925b33be7ced0a96db680f..f8e7e403713ddeac9d6e2c2cecd91d87157d3285 100644 (file)
@@ -4,17 +4,18 @@
 //
 // 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("prefs", {
     exports: ["Prefs", "localPrefs", "prefs"],
-    require: ["services", "util"],
-    use: ["config", "messages", "template"]
+    require: ["services", "util"]
 }, this);
 
+this.lazyRequire("messages", ["_"]);
+
 var Prefs = Module("prefs", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), {
     ORIGINAL: "extensions.dactyl.original.",
     RESTORE: "extensions.dactyl.restore.",
@@ -44,7 +45,7 @@ var Prefs = Module("prefs", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference])
 
     cleanup: function cleanup(reason) {
         if (this.defaults != this)
-            this.defaults.cleanup();
+            this.defaults.cleanup(reason);
 
         this._observers = {};
         if (this.observe) {
@@ -62,7 +63,7 @@ var Prefs = Module("prefs", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference])
                 this.branches.saved.resetBranch();
             }
 
-            if (reason == "uninstall" && this == prefs)
+            if (reason == "uninstall")
                 localPrefs.resetBranch();
         }
     },
@@ -112,7 +113,7 @@ var Prefs = Module("prefs", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference])
 
         if (!this._observers[pref])
             this._observers[pref] = [];
-        this._observers[pref].push(!strong ? Cu.getWeakReference(callback) : { get: function () callback });
+        this._observers[pref].push(!strong ? util.weakReference(callback) : { get: function () callback });
     },
 
     /**
@@ -198,10 +199,18 @@ var Prefs = Module("prefs", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference])
      */
     getNames: function getNames(branch) this.branch.getChildList(branch || "", { value: 0 }),
 
+    /**
+     * Returns true if the current branch has the given preference.
+     *
+     * @param {string} name The preference name.
+     * @returns {boolean}
+     */
+    has: function get(name) this.branch.getPrefType(name) != 0,
+
     _checkSafe: function _checkSafe(name, message, value) {
         let curval = this.get(name, null);
 
-        if (this.branches.original.get(name) == null)
+        if (this.branches.original.get(name) == null && !this.branches.saved.has(name))
             this.branches.original.set(name, curval, true);
 
         if (arguments.length > 2 && curval === value)
diff --git a/common/modules/protocol.jsm b/common/modules/protocol.jsm
new file mode 100644 (file)
index 0000000..98d8e3b
--- /dev/null
@@ -0,0 +1,255 @@
+// 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("protocol", {
+    exports: ["LocaleChannel", "Protocol", "RedirectChannel", "StringChannel", "XMLChannel"],
+    require: ["services", "util"]
+}, this);
+
+var systemPrincipal = Cc["@mozilla.org/systemprincipal;1"].getService(Ci.nsIPrincipal);
+
+function Channel(url, orig, noErrorChannel, unprivileged) {
+    try {
+        if (url == null)
+            return noErrorChannel ? null : NetError(orig);
+
+        if (url instanceof Ci.nsIChannel)
+            return url;
+
+        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 = services.io.newURI(url, null, null);
+        return (new XMLChannel(uri, null, noErrorChannel)).channel;
+    }
+    catch (e) {
+        util.reportError(e);
+        util.dump(url);
+        throw e;
+    }
+}
+function NetError(orig, error) {
+    return services.InterfacePointer({
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannel]),
+
+        name: orig.spec,
+
+        URI: orig,
+
+        originalURI: orig,
+
+        asyncOpen: function () { throw error || Cr.NS_ERROR_FILE_NOT_FOUND },
+
+        open: function () { throw error || Cr.NS_ERROR_FILE_NOT_FOUND }
+    }).data.QueryInterface(Ci.nsIChannel);
+}
+function RedirectChannel(to, orig, time, message) {
+    let html = <html><head><meta http-equiv="Refresh" content={(time || 0) + ";" + to}/></head>
+                     <body><h2 style="text-align: center">{message || ""}</h2></body></html>.toXMLString();
+    return StringChannel(html, "text/html", services.io.newURI(to, null, null));
+}
+
+function Protocol(scheme, classID, contentBase) {
+    function Protocol() {
+        ProtocolBase.call(this);
+    }
+    Protocol.prototype = {
+        __proto__: ProtocolBase.prototype,
+
+        classID: Components.ID(classID),
+
+        scheme: scheme,
+
+        contentBase: contentBase,
+
+        _xpcom_factory: JSMLoader.Factory(Protocol),
+    };
+    return Protocol;
+}
+
+function ProtocolBase() {
+    this.wrappedJSObject = this;
+
+    this.pages = {};
+    this.providers = {
+        "content": function (uri, path) this.pages[path] || this.contentBase + path,
+
+        "data": function (uri) {
+            var channel = services.io.newChannel(uri.path.replace(/^\/(.*)(?:#.*)?/, "data:$1"),
+                                                 null, null);
+
+            channel.contentCharset = "UTF-8";
+            channel.owner = systemPrincipal;
+            channel.originalURI = uri;
+            return channel;
+        }
+    };
+}
+ProtocolBase.prototype = {
+    get contractID()        services.PROTOCOL + this.scheme,
+    get classDescription()  this.scheme + " utility protocol",
+    QueryInterface:         XPCOMUtils.generateQI([Ci.nsIProtocolHandler]),
+
+    purge: function purge() {
+        for (let doc in util.iterDocuments())
+            try {
+                if (doc.documentURIObject.scheme == this.scheme)
+                    doc.defaultView.close();
+            }
+            catch (e) {
+                util.reportError(e);
+            }
+    },
+
+    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) {
+        if (baseURI && (!(baseURI instanceof Ci.nsIURL) || baseURI.host === "data"))
+            baseURI = null;
+        return services.URL(services.URL.URLTYPE_AUTHORITY,
+                            this.defaultPort, spec, charset, baseURI);
+    },
+
+    newChannel: function newChannel(uri) {
+        try {
+            uri.QueryInterface(Ci.nsIURL);
+
+            let path = decodeURIComponent(uri.filePath.substr(1));
+            if (uri.host in this.providers)
+                return Channel(this.providers[uri.host].call(this, uri, path),
+                               uri);
+
+            return NetError(uri);
+        }
+        catch (e) {
+            util.reportError(e);
+            throw e;
+        }
+    }
+};
+
+function LocaleChannel(pkg, locale, path, orig) {
+    for each (let locale in [locale, "en-US"])
+        for each (let sep in "-/") {
+            var channel = Channel(["resource:/", pkg + sep + locale, path].join("/"), orig, true);
+            if (channel)
+                return channel;
+        }
+
+    return NetError(orig);
+}
+
+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, noErrorChannel, unprivileged) {
+    try {
+        var channel = services.io.newChannelFromURI(uri);
+        var channelStream = channel.open();
+    }
+    catch (e) {
+        this.channel = noErrorChannel ? null : NetError(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";
+    if (!unprivileged)
+    this.channel.owner = systemPrincipal;
+
+    let stream = services.InputStream(channelStream);
+    let [, pre, doctype, url, extra, open, post] = util.regexp(<![CDATA[
+            ^ ([^]*?)
+            (?:
+                (<!DOCTYPE \s+ \S+ \s+) (?:SYSTEM \s+ "([^"]*)" | ((?:[^[>\s]|\s[^[])*))
+                (\s+ \[)?
+                ([^]*)
+            )?
+            $
+        ]]>, "x").exec(stream.read(4096));
+    this.writes.push(pre);
+    if (doctype) {
+        this.writes.push(doctype + (extra || "") + " [\n");
+        if (url)
+            this.addChannel(url);
+
+        if (!open)
+            this.writes.push("\n]");
+
+        for (let [, pre, url] in util.regexp.iterate(/([^]*?)(?:%include\s+"([^"]*)";|$)/gy, post)) {
+            this.writes.push(pre);
+            if (url)
+                this.addChannel(url);
+        }
+    }
+    this.writes.push(channelStream);
+
+    this.writeNext();
+}
+XMLChannel.prototype = {
+    QueryInterface:   XPCOMUtils.generateQI([Ci.nsIRequestObserver]),
+
+    addChannel: function addChannel(url) {
+        try {
+            this.writes.push(services.io.newChannel(url, null, this.uri).open());
+        }
+        catch (e) {
+            util.reportError(e);
+        }
+    },
+
+    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();
+    }
+};
+
+endModule();
+
+// vim: set fdm=marker sw=4 ts=4 et ft=javascript:
index e240424dba36f7af3ee308008d01adbbc65670c8..efc0f2de61dca23b76a4ad8ed6ba81de5c59df51 100644 (file)
@@ -3,7 +3,7 @@
 //
 // 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 */
 
 // TODO:
 //   - fix Sanitize autocommand
 // FIXME:
 //   - finish 1.9.0 support if we're going to support sanitizing in Melodactyl
 
-try {
-
 Components.utils.import("resource://dactyl/bootstrap.jsm");
 defineModule("sanitizer", {
     exports: ["Range", "Sanitizer", "sanitizer"],
-    use: ["config"],
-    require: ["messages", "prefs", "services", "storage", "template", "util"]
+    require: ["config", "prefs", "services", "util"]
 }, this);
 
+this.lazyRequire("messages", ["_"]);
+this.lazyRequire("storage", ["storage"]);
+this.lazyRequire("template", ["teplate"]);
+
 let tmp = {};
 JSMLoader.loadSubScript("chrome://browser/content/sanitize.js", tmp);
 tmp.Sanitizer.prototype.__proto__ = Class.prototype;
@@ -56,7 +57,7 @@ var Item = Class("SanitizeItem", {
     shouldSanitize: function (shutdown) (!shutdown || this.builtin || this.persistent) &&
         prefs.get(shutdown ? this.shutdownPref : this.pref)
 }, {
-    PREFIX: localPrefs.branch.root,
+    PREFIX: config.prefs.branch.root,
     BRANCH: "privacy.cpd.",
     SHUTDOWN_BRANCH: "privacy.clearOnShutdown."
 });
@@ -216,7 +217,7 @@ var Sanitizer = Module("sanitizer", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakRef
                         }
                     </>
                 },
-                init: function (win) {
+                ready: function ready(win) {
                     let elem =  win.document.getElementById("itemList");
                     elem.setAttribute("rows", elem.itemCount);
                     win.Sanitizer = Class("Sanitizer", win.Sanitizer, {
@@ -291,8 +292,8 @@ var Sanitizer = Module("sanitizer", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakRef
         }
     },
 
-    get ranAtShutdown()    localPrefs.get("didSanitizeOnShutdown"),
-    set ranAtShutdown(val) localPrefs.set("didSanitizeOnShutdown", Boolean(val)),
+    get ranAtShutdown()    config.prefs.get("didSanitizeOnShutdown"),
+    set ranAtShutdown(val) config.prefs.set("didSanitizeOnShutdown", Boolean(val)),
     get runAtShutdown()    prefs.get("privacy.sanitize.sanitizeOnShutdown"),
     set runAtShutdown(val) prefs.set("privacy.sanitize.sanitizeOnShutdown", Boolean(val)),
 
@@ -348,7 +349,7 @@ var Sanitizer = Module("sanitizer", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakRef
         session: 8
     },
 
-    UNPERMS: Class.memoize(function () iter(this.PERMS).map(Array.reverse).toObject()),
+    UNPERMS: Class.Memoize(function () iter(this.PERMS).map(Array.reverse).toObject()),
 
     COMMANDS: {
         unset:   /*L*/"Unset",
@@ -369,17 +370,17 @@ var Sanitizer = Module("sanitizer", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakRef
     prefToArg: function (pref) pref.replace(/.*\./, "").toLowerCase(),
 
     iterCookies: function iterCookies(host) {
-        let iterator = host ? services.cookies.getCookiesFromHost(host)
-                            : services.cookies;
-        for (let c in iter(iterator))
-            yield c.QueryInterface(Ci.nsICookie2);
+        for (let c in iter(services.cookies, Ci.nsICookie2))
+            if (!host || util.isSubdomain(c.rawHost, host) ||
+                    c.host[0] == "." && c.host.length < host.length
+                        && host.indexOf(c.host) == host.length - c.host.length)
+                yield c;
+
     },
     iterPermissions: function iterPermissions(host) {
-        for (let p in iter(services.permissions)) {
-            p.QueryInterface(Ci.nsIPermission);
+        for (let p in iter(services.permissions, Ci.nsIPermission))
             if (!host || util.isSubdomain(p.host, host))
                 yield p;
-        }
     }
 }, {
     load: function (dactyl, modules, window) {
@@ -388,16 +389,18 @@ var Sanitizer = Module("sanitizer", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakRef
         sanitizer.ranAtShutdown = false;
     },
     autocommands: function (dactyl, modules, window) {
+        const { autocommands } = modules;
+
         storage.addObserver("private-mode",
             function (key, event, value) {
-                modules.autocommands.trigger("PrivateMode", { state: value });
+                autocommands.trigger("PrivateMode", { state: value });
             }, window);
         storage.addObserver("sanitizer",
             function (key, event, value) {
                 if (event == "domain")
-                    modules.autocommands.trigger("SanitizeDomain", { domain: value });
+                    autocommands.trigger("SanitizeDomain", { domain: value });
                 else if (!value[1])
-                    modules.autocommands.trigger("Sanitize", { name: event.substr("clear-".length), domain: value[1] });
+                    autocommands.trigger("Sanitize", { name: event.substr("clear-".length), domain: value[1] });
             }, window);
     },
     commands: function (dactyl, modules, window) {
@@ -407,6 +410,9 @@ var Sanitizer = Module("sanitizer", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakRef
             function (args) {
                 dactyl.assert(!modules.options['private'], _("command.sanitize.privateMode"));
 
+                if (args["-host"] && !args.length && !args.bang)
+                    args[0] = "all";
+
                 let timespan = args["-timespan"] || modules.options["sanitizetimespan"];
 
                 let range = Range();
@@ -415,17 +421,15 @@ var Sanitizer = Module("sanitizer", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakRef
                     match ? 1000 * (Date.now() - 1000 * parseInt(num, 10) * { m: 60, h: 3600, d: 3600 * 24, w: 3600 * 24 * 7 }[unit])
                           : (timespan[0] == "s" ? sanitizer.sessionStart : null);
 
-                let items = args.slice();
-                if (args["-host"] && !args.length)
-                    args[0] = "all";
-
-                if (args.bang) {
+                let opt = modules.options.get("sanitizeitems");
+                if (args.bang)
                     dactyl.assert(args.length == 0, _("error.trailingCharacters"));
-                    items = Object.keys(sanitizer.itemMap).filter(
-                        function (k) modules.options.get("sanitizeitems").has(k));
+                else {
+                    dactyl.assert(opt.validator(args), _("error.invalidArgument"));
+                    opt = { __proto__: opt, value: args.slice() };
                 }
-                else
-                    dactyl.assert(modules.options.get("sanitizeitems").validator(items), _("error.invalidArgument"));
+
+                let items = Object.keys(sanitizer.itemMap).slice(1).filter(opt.has, opt);
 
                 function sanitize(items) {
                     sanitizer.range = range.native;
@@ -441,13 +445,12 @@ var Sanitizer = Module("sanitizer", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakRef
                         sanitizer.sanitize(items, range);
                 }
 
-                if (items.indexOf("all") >= 0)
+                if (array.nth(opt.value, function (i) i == "all" || /^!/.test(i), 0) == "all" && !args["-host"])
                     modules.commandline.input(_("sanitize.prompt.deleteAll") + " ",
                         function (resp) {
                             if (resp.match(/^y(es)?$/i)) {
-                                items = Object.keys(sanitizer.itemMap).filter(function (k) items.indexOf(k) === -1);
                                 sanitize(items);
-                                dactyl.echo(_("command.sanitize.allDeleted"));
+                                dactyl.echomsg(_("command.sanitize.allDeleted"));
                             }
                             else
                                 dactyl.echo(_("command.sanitize.noneDeleted"));
@@ -593,9 +596,19 @@ var Sanitizer = Module("sanitizer", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakRef
             "stringlist", "all",
             {
                 get values() values(sanitizer.itemMap).toArray(),
-                has: modules.Option.has.toggleAll,
+
+                completer: function completer(context, extra) {
+                    if (context.filter[0] == "!")
+                        context.advance(1);
+                    return completer.superapply(this, arguments);
+                },
+
+                has: function has(val)
+                    let (res = array.nth(this.value, function (v) v == "all" || v.replace(/^!/, "") == val, 0))
+                        res && !/^!/.test(res),
+
                 validator: function (values) values.length &&
-                    values.every(function (val) val === "all" || Set.has(sanitizer.itemMap, val))
+                    values.every(function (val) val === "all" || Set.has(sanitizer.itemMap, val.replace(/^!/, "")))
             });
 
         options.add(["sanitizeshutdown", "ss"],
@@ -638,13 +651,13 @@ var Sanitizer = Module("sanitizer", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakRef
                     "1d":      "Past day",
                     "1w":      "Past week"
                 },
-                validator: function (value) /^(a(ll)?|s(ession)|\d+[mhdw])$/.test(value)
+                validator: bind("test", /^(a(ll)?|s(ession)|\d+[mhdw])$/)
             });
 
         options.add(["cookies", "ck"],
             "The default mode for newly added cookie permissions",
             "stringlist", "session",
-            { get values() iter(Sanitizer.COMMANDS) });
+            { get values() Sanitizer.COMMANDS });
 
         options.add(["cookieaccept", "ca"],
             "When to accept cookies",
@@ -694,6 +707,6 @@ var Sanitizer = Module("sanitizer", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakRef
 
 endModule();
 
-} catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}
+// catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}
 
 // vim: set fdm=marker sw=4 ts=4 et ft=javascript:
index b3372ddf5377db31298b81679c7590c226fe8442..d47da247ba409eb1149614f9568096fd7830fddf 100644 (file)
@@ -2,61 +2,71 @@
 //
 // 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 {
 
 var global = this;
 Components.utils.import("resource://dactyl/bootstrap.jsm");
 defineModule("services", {
-    exports: ["services"],
-    use: ["util"]
+    exports: ["services"]
 }, this);
 
 /**
  * A lazily-instantiated XPCOM class and service cache.
  */
 var Services = Module("Services", {
+    ABOUT: "@mozilla.org/network/protocol/about;1?what=",
+    AUTOCOMPLETE: "@mozilla.org/autocomplete/search;1?name=",
+    PROTOCOL: "@mozilla.org/network/protocol;1?name=",
+
     init: function () {
         this.services = {};
 
         this.add("annotation",          "@mozilla.org/browser/annotation-service;1",        "nsIAnnotationService");
         this.add("appShell",            "@mozilla.org/appshell/appShellService;1",          "nsIAppShellService");
         this.add("appStartup",          "@mozilla.org/toolkit/app-startup;1",               "nsIAppStartup");
-        this.add("autoCompleteSearch",  "@mozilla.org/autocomplete/search;1?name=history",  "nsIAutoCompleteSearch");
         this.add("bookmarks",           "@mozilla.org/browser/nav-bookmarks-service;1",     "nsINavBookmarksService");
         this.add("bootstrap",           "@dactyl.googlecode.com/base/bootstrap");
         this.add("browserSearch",       "@mozilla.org/browser/search-service;1",            "nsIBrowserSearchService");
         this.add("cache",               "@mozilla.org/network/cache-service;1",             "nsICacheService");
         this.add("charset",             "@mozilla.org/charset-converter-manager;1",         "nsICharsetConverterManager");
         this.add("chromeRegistry",      "@mozilla.org/chrome/chrome-registry;1",            "nsIXULChromeRegistry");
+        this.add("clipboard",           "@mozilla.org/widget/clipboard;1",                  "nsIClipboard");
+        this.add("clipboardHelper",     "@mozilla.org/widget/clipboardhelper;1",            "nsIClipboardHelper");
         this.add("commandLineHandler",  "@mozilla.org/commandlinehandler/general-startup;1?type=dactyl");
         this.add("console",             "@mozilla.org/consoleservice;1",                    "nsIConsoleService");
-        this.add("dactyl:",             "@mozilla.org/network/protocol;1?name=dactyl");
+        this.add("contentPrefs",        "@mozilla.org/content-pref/service;1",              "nsIContentPrefService");
+        this.add("dactyl",              "@dactyl.googlecode.com/extra/utils",               "dactylIUtils");
+        this.add("dactyl:",             this.PROTOCOL + "dactyl");
         this.add("debugger",            "@mozilla.org/js/jsd/debugger-service;1",           "jsdIDebuggerService");
         this.add("directory",           "@mozilla.org/file/directory_service;1",            "nsIProperties");
         this.add("downloadManager",     "@mozilla.org/download-manager;1",                  "nsIDownloadManager");
         this.add("environment",         "@mozilla.org/process/environment;1",               "nsIEnvironment");
         this.add("extensionManager",    "@mozilla.org/extensions/manager;1",                "nsIExtensionManager");
+        this.add("externalApp",         "@mozilla.org/uriloader/external-helper-app-service;1", "nsPIExternalAppLauncher")
         this.add("externalProtocol",    "@mozilla.org/uriloader/external-protocol-service;1", "nsIExternalProtocolService");
         this.add("favicon",             "@mozilla.org/browser/favicon-service;1",           "nsIFaviconService");
-        this.add("file:",               "@mozilla.org/network/protocol;1?name=file",        "nsIFileProtocolHandler");
+        this.add("file:",               this.PROTOCOL + "file",                             "nsIFileProtocolHandler");
         this.add("focus",               "@mozilla.org/focus-manager;1",                     "nsIFocusManager");
         this.add("history",             "@mozilla.org/browser/global-history;2",
-                 ["nsIBrowserHistory", "nsIGlobalHistory3", "nsINavHistoryService", "nsPIPlacesDatabase"]);
+                 ["nsIBrowserHistory", "nsIGlobalHistory2", "nsINavHistoryService", "nsPIPlacesDatabase"]);
         this.add("io",                  "@mozilla.org/network/io-service;1",                "nsIIOService");
         this.add("json",                "@mozilla.org/dom/json;1",                          "nsIJSON", "createInstance");
         this.add("listeners",           "@mozilla.org/eventlistenerservice;1",              "nsIEventListenerService");
         this.add("livemark",            "@mozilla.org/browser/livemark-service;2",          "nsILivemarkService");
+        this.add("messages",            "@mozilla.org/globalmessagemanager;1",              "nsIChromeFrameMessageManager");
         this.add("mime",                "@mozilla.org/mime;1",                              "nsIMIMEService");
         this.add("observer",            "@mozilla.org/observer-service;1",                  "nsIObserverService");
         this.add("pref",                "@mozilla.org/preferences-service;1",               ["nsIPrefBranch2", "nsIPrefService"]);
         this.add("privateBrowsing",     "@mozilla.org/privatebrowsing;1",                   "nsIPrivateBrowsingService");
         this.add("profile",             "@mozilla.org/toolkit/profile-service;1",           "nsIToolkitProfileService");
-        this.add("resource:",           "@mozilla.org/network/protocol;1?name=resource",    ["nsIProtocolHandler", "nsIResProtocolHandler"]);
+        this.add("resource:",           this.PROTOCOL + "resource",                         ["nsIProtocolHandler", "nsIResProtocolHandler"]);
         this.add("runtime",             "@mozilla.org/xre/runtime;1",                       ["nsIXULAppInfo", "nsIXULRuntime"]);
         this.add("rdf",                 "@mozilla.org/rdf/rdf-service;1",                   "nsIRDFService");
+        this.add("security",            "@mozilla.org/scriptsecuritymanager;1",             "nsIScriptSecurityManager");
         this.add("sessionStore",        "@mozilla.org/browser/sessionstore;1",              "nsISessionStore");
+        this.add("spell",               "@mozilla.org/spellchecker/engine;1",               "mozISpellCheckingEngine");
         this.add("stringBundle",        "@mozilla.org/intl/stringbundle;1",                 "nsIStringBundleService");
         this.add("stylesheet",          "@mozilla.org/content/style-sheet-service;1",       "nsIStyleSheetService");
         this.add("subscriptLoader",     "@mozilla.org/moz/jssubscript-loader;1",            "mozIJSSubScriptLoader");
@@ -70,53 +80,61 @@ var Services = Module("Services", {
         this.add("zipReader",           "@mozilla.org/libjar/zip-reader-cache;1",           "nsIZipReaderCache");
 
         this.addClass("CharsetConv",  "@mozilla.org/intl/scriptableunicodeconverter", "nsIScriptableUnicodeConverter", "charset");
+        this.addClass("CharsetStream","@mozilla.org/intl/converter-input-stream;1",   ["nsIConverterInputStream",
+                                                                                       "nsIUnicharLineInputStream"], "init");
+        this.addClass("ConvOutStream","@mozilla.org/intl/converter-output-stream;1", "nsIConverterOutputStream", "init", false);
         this.addClass("File",         "@mozilla.org/file/local;1",                 "nsILocalFile");
+        this.addClass("FileInStream", "@mozilla.org/network/file-input-stream;1",  "nsIFileInputStream", "init", false);
+        this.addClass("FileOutStream","@mozilla.org/network/file-output-stream;1", "nsIFileOutputStream", "init", false);
         this.addClass("Find",         "@mozilla.org/embedcomp/rangefind;1",        "nsIFind");
+        this.addClass("FormData",     "@mozilla.org/files/formdata;1",             "nsIDOMFormData");
         this.addClass("HtmlConverter","@mozilla.org/widget/htmlformatconverter;1", "nsIFormatConverter");
         this.addClass("HtmlEncoder",  "@mozilla.org/layout/htmlCopyEncoder;1",     "nsIDocumentEncoder");
+        this.addClass("InterfacePointer", "@mozilla.org/supports-interface-pointer;1", "nsISupportsInterfacePointer", "data");
         this.addClass("InputStream",  "@mozilla.org/scriptableinputstream;1",      "nsIScriptableInputStream", "init");
         this.addClass("Persist",      "@mozilla.org/embedding/browser/nsWebBrowserPersist;1", "nsIWebBrowserPersist");
         this.addClass("Pipe",         "@mozilla.org/pipe;1",                       "nsIPipe", "init");
         this.addClass("Process",      "@mozilla.org/process/util;1",               "nsIProcess", "init");
+        this.addClass("Pump",         "@mozilla.org/network/input-stream-pump;1",  "nsIInputStreamPump", "init")
         this.addClass("StreamChannel","@mozilla.org/network/input-stream-channel;1",
-                      ["nsIChannel", "nsIInputStreamChannel", "nsIRequest"], "setURI");
+                      ["nsIInputStreamChannel", "nsIChannel"], "setURI");
         this.addClass("StreamCopier", "@mozilla.org/network/async-stream-copier;1","nsIAsyncStreamCopier", "init");
         this.addClass("String",       "@mozilla.org/supports-string;1",            "nsISupportsString", "data");
         this.addClass("StringStream", "@mozilla.org/io/string-input-stream;1",     "nsIStringInputStream", "data");
         this.addClass("Transfer",     "@mozilla.org/transfer;1",                   "nsITransfer", "init");
+        this.addClass("Transferable", "@mozilla.org/widget/transferable;1",        "nsITransferable");
         this.addClass("Timer",        "@mozilla.org/timer;1",                      "nsITimer", "initWithCallback");
-        this.addClass("Xmlhttp",      "@mozilla.org/xmlextras/xmlhttprequest;1",   "nsIXMLHttpRequest");
+        this.addClass("URL",          "@mozilla.org/network/standard-url;1",       ["nsIStandardURL", "nsIURL"], "init");
+        this.addClass("Xmlhttp",      "@mozilla.org/xmlextras/xmlhttprequest;1",   "nsIXMLHttpRequest", "open");
         this.addClass("XPathEvaluator", "@mozilla.org/dom/xpath-evaluator;1",      "nsIDOMXPathEvaluator");
         this.addClass("XMLDocument",  "@mozilla.org/xml/xml-document;1",           ["nsIDOMXMLDocument", "nsIDOMNodeSelector"]);
-        this.addClass("ZipReader",    "@mozilla.org/libjar/zip-reader;1",          "nsIZipReader", "open");
-        this.addClass("ZipWriter",    "@mozilla.org/zipwriter;1",                  "nsIZipWriter");
+        this.addClass("ZipReader",    "@mozilla.org/libjar/zip-reader;1",          "nsIZipReader", "open", false);
+        this.addClass("ZipWriter",    "@mozilla.org/zipwriter;1",                  "nsIZipWriter", "open", false);
     },
     reinit: function () {},
 
-    _create: function (classes, ifaces, meth, init, args) {
+    _create: function (name, args) {
         try {
-            let res = Cc[classes][meth || "getService"]();
-            if (!ifaces)
-                return res["wrapped" + "JSObject"]; // Kill stupid validator warning
-            Array.concat(ifaces).forEach(function (iface) res.QueryInterface(Ci[iface]));
-            if (init && args.length) {
-                try {
-                    var isCallable = callable(res[init]);
-                }
-                catch (e) {} // Ugh.
-
-                if (isCallable)
-                    res[init].apply(res, args);
+            var service = this.services[name];
+
+            let res = Cc[service.class][service.method || "getService"]();
+            if (!service.interfaces.length)
+                return res.wrappedJSObject || res;
+
+            service.interfaces.forEach(function (iface) res.QueryInterface(Ci[iface]));
+            if (service.init && args.length) {
+                if (service.callable)
+                    res[service.init].apply(res, args);
                 else
-                    res[init] = args[0];
+                    res[service.init] = args[0];
             }
             return res;
         }
-        catch (e) {
+        catch (e if service.quiet !== false) {
             if (typeof util !== "undefined")
                 util.reportError(e);
             else
-                dump("dactyl: Service creation failed for '" + classes + "': " + e + "\n" + (e.stack || Error(e).stack));
+                dump("dactyl: Service creation failed for '" + service.class + "': " + e + "\n" + (e.stack || Error(e).stack));
             return null;
         }
     },
@@ -133,10 +151,10 @@ var Services = Module("Services", {
      */
     add: function (name, class_, ifaces, meth) {
         const self = this;
-        this.services[name] = { class: class_, interfaces: Array.concat(ifaces || []) };
+        this.services[name] = { method: meth, class: class_, interfaces: Array.concat(ifaces || []) };
         if (name in this && ifaces && !this.__lookupGetter__(name) && !(this[name] instanceof Ci.nsISupports))
             throw TypeError();
-        memoize(this, name, function () self._create(class_, ifaces, meth));
+        memoize(this, name, function () self._create(name));
     },
 
     /**
@@ -144,14 +162,19 @@ var Services = Module("Services", {
      *
      * @param {string} name The class's cache key.
      * @param {string} class_ The class's contract ID.
-     * @param {nsISupports|[nsISupports]} ifaces The interface or array of
+     * @param {string|[string]} ifaces The interface or array of
      *     interfaces implemented by this class.
-     * @param {string} init Name of a property or method used to initialise the
-     *     class. See {@link #_create}.
+     * @param {string} init Name of a property or method used to initialize the
+     *     class.
      */
-    addClass: function (name, class_, ifaces, init) {
+    addClass: function (name, class_, ifaces, init, quiet) {
         const self = this;
-        this[name] = function () self._create(class_, ifaces, "createInstance", init, arguments);
+        this.services[name] = { class: class_, interfaces: Array.concat(ifaces || []), method: "createInstance", init: init, quiet: quiet };
+        if (init)
+            memoize(this.services[name], "callable",
+                    function () callable(XPCOMShim(this.interfaces)[this.init]));
+
+        this[name] = function () self._create(name, arguments);
         update.apply(null, [this[name]].concat([Ci[i] for each (i in Array.concat(ifaces))]));
         return this[name];
     },
@@ -161,14 +184,14 @@ var Services = Module("Services", {
      *
      * @param {string} name The class's cache key.
      */
-    create: function (name) this[name[0].toUpperCase() + name.substr(1)],
+    create: deprecated("services.*name*()", function create(name) this[util.capitalize(name)]()),
 
     /**
      * Returns the cached service with the specified name.
      *
      * @param {string} name The service's cache key.
      */
-    get: function (name) this[name],
+    get: deprecated("services.*name*", function get(name) this[name]),
 
     /**
      * Returns true if the given service is available.
@@ -177,11 +200,6 @@ var Services = Module("Services", {
      */
     has: function (name) Set.has(this.services, name) && this.services[name].class in Cc &&
         this.services[name].interfaces.every(function (iface) iface in Ci)
-}, {
-}, {
-    javascript: function (dactyl, modules) {
-        modules.JavaScript.setCompleter(this.get, [function () [[k, v] for ([k, v] in Iterator(services)) if (v instanceof Ci.nsISupports)]]);
-    }
 });
 
 endModule();
index 896daf4461189867811231b5c916dba25887f93b..664124e883d6df3cb22d7ba826f49a09c0d19322 100644 (file)
@@ -2,7 +2,7 @@
 //
 // 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("storage", {
@@ -10,17 +10,25 @@ defineModule("storage", {
     require: ["services", "util"]
 }, this);
 
+this.lazyRequire("config", ["config"]);
+this.lazyRequire("io", ["IO"]);
+
 var win32 = /^win(32|nt)$/i.test(services.runtime.OS);
 var myObject = JSON.parse("{}").constructor;
 
 function loadData(name, store, type) {
     try {
-        let data = storage.infoPath.child(name).read();
-        let result = JSON.parse(data);
-        if (result instanceof type)
-            return result;
+        let file = storage.infoPath.child(name);
+        if (file.exists()) {
+            let data = file.read();
+            let result = JSON.parse(data);
+            if (result instanceof type)
+                return result;
+        }
+    }
+    catch (e) {
+        util.reportError(e);
     }
-    catch (e) {}
 }
 
 function saveData(obj) {
@@ -71,10 +79,13 @@ var ArrayStore = Class("ArrayStore", StoreBase, {
 
     get length() this._object.length,
 
-    set: function set(index, value) {
+    set: function set(index, value, quiet) {
         var orig = this._object[index];
         this._object[index] = value;
-        this.fireEvent("change", index);
+        if (!quiet)
+            this.fireEvent("change", index);
+
+        return orig;
     },
 
     push: function push(value) {
@@ -82,12 +93,32 @@ var ArrayStore = Class("ArrayStore", StoreBase, {
         this.fireEvent("push", this._object.length);
     },
 
-    pop: function pop(value) {
-        var res = this._object.pop();
-        this.fireEvent("pop", this._object.length);
+    pop: function pop(value, ord) {
+        if (ord == null)
+            var res = this._object.pop();
+        else
+            res = this._object.splice(ord, 1)[0];
+
+        this.fireEvent("pop", this._object.length, ord);
+        return res;
+    },
+
+    shift: function shift(value) {
+        var res = this._object.shift();
+        this.fireEvent("shift", this._object.length);
         return res;
     },
 
+    insert: function insert(value, ord) {
+        if (ord == 0)
+            this._object.unshift(value);
+        else
+            this._object = this._object.slice(0, ord)
+                               .concat([value])
+                               .concat(this._object.slice(ord));
+        this.fireEvent("insert", this._object.length, ord);
+    },
+
     truncate: function truncate(length, fromEnd) {
         var res = this._object.length;
         if (this._object.length > length) {
@@ -164,16 +195,26 @@ var Storage = Module("Storage", {
                 this[key].timer.flush();
             delete this[key];
         }
-        for (let ary in values(this.observers))
-            for (let obj in values(ary))
-                if (obj.ref && obj.ref.get())
-                    delete obj.ref.get().dactylStorageRefs;
 
         this.keys = {};
         this.observers = {};
     },
 
-    exists: function exists(name) this.infoPath.child(name).exists(),
+    infoPath: Class.Memoize(function ()
+        File(IO.runtimePath.replace(/,.*/, ""))
+            .child("info").child(config.profileName)),
+
+    exists: function exists(key) this.infoPath.child(key).exists(),
+
+    remove: function remove(key) {
+        if (this.exists(key)) {
+            if (this[key] && this[key].timer)
+                this[key].timer.flush();
+            delete this[key];
+            delete this.keys[key];
+            this.infoPath.child(key).remove(false);
+        }
+    },
 
     newObject: function newObject(key, constructor, params) {
         if (params == null || !isObject(params))
@@ -201,10 +242,9 @@ var Storage = Module("Storage", {
 
     addObserver: function addObserver(key, callback, ref) {
         if (ref) {
-            if (!ref.dactylStorageRefs)
-                ref.dactylStorageRefs = [];
-            ref.dactylStorageRefs.push(callback);
-            var callbackRef = Cu.getWeakReference(callback);
+            let refs = overlay.getData(ref, "storage-refs");
+            refs.push(callback);
+            var callbackRef = util.weakReference(callback);
         }
         else {
             callbackRef = { get: function () callback };
@@ -227,7 +267,9 @@ var Storage = Module("Storage", {
 
     removeDeadObservers: function () {
         for (let [key, ary] in Iterator(this.observers)) {
-            this.observers[key] = ary = ary.filter(function (o) o.callback.get() && (!o.ref || o.ref.get() && o.ref.get().dactylStorageRefs));
+            this.observers[key] = ary = ary.filter(function (o) o.callback.get()
+                                                             && (!o.ref || o.ref.get()
+                                                                        && overlay.getData(o.ref.get(), "storage-refs", null)));
             if (!ary.length)
                 delete this.observers[key];
         }
@@ -273,14 +315,8 @@ var Storage = Module("Storage", {
         skipXpcom: function skipXpcom(key, val) val instanceof Ci.nsISupports ? null : val
     }
 }, {
-    init: function init(dactyl, modules) {
-        init.superapply(this, arguments);
-        storage.infoPath = File(modules.IO.runtimePath.replace(/,.*/, ""))
-                             .child("info").child(dactyl.profileName);
-    },
-
     cleanup: function (dactyl, modules, window) {
-        delete window.dactylStorageRefs;
+        overlay.setData(window, "storage-refs", null);
         this.removeDeadObservers();
     }
 });
@@ -292,11 +328,18 @@ var Storage = Module("Storage", {
  * @param {nsIFile|string} path Expanded according to {@link IO#expandPath}
  * @param {boolean} checkPWD Whether to allow expansion relative to the
  *          current directory. @default true
+ * @param {string} charset The charset of the file. @default File.defaultEncoding
  */
 var File = Class("File", {
-    init: function (path, checkPWD) {
+    init: function (path, checkPWD, charset) {
         let file = services.File();
 
+        if (charset)
+            this.charset = charset;
+
+        if (path instanceof Ci.nsIFileURL)
+            path = path.file;
+
         if (path instanceof Ci.nsIFile)
             file = path.clone();
         else if (/file:\/\//.test(path))
@@ -320,10 +363,16 @@ var File = Class("File", {
         return self;
     },
 
+    charset: Class.Memoize(function () File.defaultEncoding),
+
     /**
      * @property {nsIFileURL} Returns the nsIFileURL object for this file.
      */
-    get URI() services.io.newFileURI(this),
+    URI: Class.Memoize(function () {
+        let uri = services.io.newFileURI(this).QueryInterface(Ci.nsIFileURL);
+        uri.QueryInterface(Ci.nsIMutable).mutable = false;
+        return uri;
+    }),
 
     /**
      * Iterates over the objects in this directory.
@@ -347,19 +396,24 @@ var File = Class("File", {
         return f;
     },
 
+    /**
+     * Returns an iterator for all lines in a file.
+     */
+    get lines() File.readLines(services.FileInStream(this, -1, 0, 0),
+                               this.charset),
+
     /**
      * Reads this file's entire contents in "text" mode and returns the
      * content as a string.
      *
      * @param {string} encoding The encoding from which to decode the file.
-     *          @default options["fileencoding"]
+     *          @default #charset
      * @returns {string}
      */
     read: function (encoding) {
-        let ifstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream);
-        ifstream.init(this, -1, 0, 0);
+        let ifstream = services.FileInStream(this, -1, 0, 0);
 
-        return File.readStream(ifstream, encoding);
+        return File.readStream(ifstream, encoding || this.charset);
     },
 
     /**
@@ -408,20 +462,17 @@ var File = Class("File", {
      *     permissions if the file exists.
      * @default 0644
      * @param {string} encoding The encoding to used to write the file.
-     * @default options["fileencoding"]
+     * @default #charset
      */
     write: function (buf, mode, perms, encoding) {
-        let ofstream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
         function getStream(defaultChar) {
-            let stream = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream);
-            stream.init(ofstream, encoding, 0, defaultChar);
-            return stream;
+            return services.ConvOutStream(ofstream, encoding, 0, defaultChar);
         }
         if (buf instanceof File)
             buf = buf.read();
 
         if (!encoding)
-            encoding = File.defaultEncoding;
+            encoding = this.charset;
 
         if (mode == ">>")
             mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_APPEND;
@@ -433,7 +484,7 @@ var File = Class("File", {
         if (!this.exists()) // OCREAT won't create the directory
             this.create(this.NORMAL_FILE_TYPE, perms);
 
-        ofstream.init(this, mode, perms, 0);
+        let ofstream = services.FileOutStream(this, mode, perms, 0);
         try {
             var ocstream = getStream(0);
             ocstream.writeString(buf);
@@ -510,15 +561,15 @@ var File = Class("File", {
     /**
      * @property {string} The current platform's path separator.
      */
-    PATH_SEP: Class.memoize(function () {
+    PATH_SEP: Class.Memoize(function () {
         let f = services.directory.get("CurProcD", Ci.nsIFile);
         f.append("foo");
         return f.path.substr(f.parent.path.length, 1);
     }),
 
-    pathSplit: Class.memoize(function () util.regexp("(?:/|" + util.regexp.escape(this.PATH_SEP) + ")", "g")),
+    pathSplit: Class.Memoize(function () util.regexp("(?:/|" + util.regexp.escape(this.PATH_SEP) + ")", "g")),
 
-    DoesNotExist: function (path, error) ({
+    DoesNotExist: function DoesNotExist(path, error) ({
         path: path,
         exists: function () false,
         __noSuchMethod__: function () { throw error || Error("Does not exist"); }
@@ -541,7 +592,7 @@ var File = Class("File", {
      * @param {boolean} relative Whether the path is relative or absolute.
      * @returns {string}
      */
-    expandPath: function (path, relative) {
+    expandPath: function expandPath(path, relative) {
         function getenv(name) services.environment.get(name);
 
         // expand any $ENV vars - this is naive but so is Vim and we like to be compatible
@@ -577,11 +628,18 @@ var File = Class("File", {
 
     expandPathList: function (list) list.map(this.expandPath),
 
-    readStream: function (ifstream, encoding) {
+    readURL: function readURL(url, encoding) {
+        let channel = services.io.newChannel(url, null, null);
+        channel.contentType = "text/plain";
+        return this.readStream(channel.open(), encoding);
+    },
+
+    readStream: function readStream(ifstream, encoding) {
         try {
-            var icstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream);
-            icstream.init(ifstream, encoding || File.defaultEncoding, 4096, // 4096 bytes buffering
-                          Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
+            var icstream = services.CharsetStream(
+                    ifstream, encoding || File.defaultEncoding, 4096, // buffer size
+                    services.CharsetStream.DEFAULT_REPLACEMENT_CHARACTER);
+
             let buffer = [];
             let str = {};
             while (icstream.readString(4096, str) != 0)
@@ -594,7 +652,24 @@ var File = Class("File", {
         }
     },
 
-    isAbsolutePath: function (path) {
+    readLines: function readLines(ifstream, encoding) {
+        try {
+            var icstream = services.CharsetStream(
+                    ifstream, encoding || File.defaultEncoding, 4096, // buffer size
+                    services.CharsetStream.DEFAULT_REPLACEMENT_CHARACTER);
+
+            var value = {};
+            while (icstream.readLine(value))
+                yield value.value;
+        }
+        finally {
+            icstream.close();
+            ifstream.close();
+        }
+    },
+
+
+    isAbsolutePath: function isAbsolutePath(path) {
         try {
             services.File().initWithPath(path);
             return true;
@@ -604,7 +679,7 @@ var File = Class("File", {
         }
     },
 
-    joinPaths: function (head, tail, cwd) {
+    joinPaths: function joinPaths(head, tail, cwd) {
         let path = this(head, cwd);
         try {
             // FIXME: should only expand environment vars and normalize path separators
@@ -621,6 +696,6 @@ var File = Class("File", {
 
 endModule();
 
-// catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}
+// catch(e){ dump(e + "\n" + (e.stack || Error().stack)); Components.utils.reportError(e) }
 
 // vim: set fdm=marker sw=4 sts=4 et ft=javascript:
index 70608b9fb170eca5cddec2992b490b093028bb25..9ee7f6286f581ca56f5b6630946c4c13a8cdb2cc 100644 (file)
@@ -2,13 +2,12 @@
 //
 // 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("styles", {
     exports: ["Style", "Styles", "styles"],
-    require: ["services", "util"],
-    use: ["contexts", "messages", "template"]
+    require: ["services", "util"]
 }, this);
 
 function cssUri(css) "chrome-data:text/css," + encodeURI(css);
@@ -74,11 +73,11 @@ update(Sheet.prototype, {
             return preamble + css;
 
         let selectors = filter.map(function (part)
-                                    !/^(?:[a-z-]+[:*]|[a-z-.]+$)/i.test(part) ? "regexp(" + (".*(?:" + part + ").*").quote() + ")" :
+                                    !/^(?:[a-z-]+[:*]|[a-z-.]+$)/i.test(part) ? "regexp(" + Styles.quote(".*(?:" + part + ").*") + ")" :
                                        (/[*]$/.test(part)   ? "url-prefix" :
                                         /[\/:]/.test(part)  ? "url"
                                                             : "domain")
-                                       + '("' + part.replace(/"/g, "%22").replace(/\*$/, "") + '")')
+                                       + '(' + Styles.quote(part.replace(/\*$/, "")) + ')')
                               .join(",\n               ");
 
         return preamble + "@-moz-document " + selectors + " {\n\n" + css + "\n\n}\n";
@@ -97,7 +96,7 @@ var Hive = Class("Hive", {
     get modifiable() this.name !== "system",
 
     addRef: function (obj) {
-        this.refs.push(Cu.getWeakReference(obj));
+        this.refs.push(util.weakReference(obj));
         this.dropRef(null);
     },
     dropRef: function (obj) {
@@ -254,12 +253,14 @@ var Styles = Module("Styles", {
         this.cleanup();
         this.allSheets = {};
 
-        services["dactyl:"].providers["style"] = function styleProvider(uri) {
-            let id = /^\/(\d*)/.exec(uri.path)[1];
-            if (Set.has(styles.allSheets, id))
-                return ["text/css", styles.allSheets[id].fullCSS];
-            return null;
-        };
+        update(services["dactyl:"].providers, {
+            "style": function styleProvider(uri, path) {
+                let id = parseInt(path);
+                if (Set.has(styles.allSheets, id))
+                    return ["text/css", styles.allSheets[id].fullCSS];
+                return null;
+            }
+        });
     },
 
     cleanup: function cleanup() {
@@ -434,6 +435,7 @@ var Styles = Module("Styles", {
         else
             test = function test(uri) { try { return util.isSubdomain(uri.host, filter); } catch (e) { return false; } };
         test.toString = function toString() filter;
+        test.key = filter;
         if (arguments.length < 2)
             return test;
         return test(arguments[1]);
@@ -529,7 +531,17 @@ var Styles = Module("Styles", {
                 | [^;}\s]+
             )
         ]]>, "gix", this)
-    })
+    }),
+
+    /**
+     * Quotes a string for use in CSS stylesheets.
+     *
+     * @param {string} str
+     * @returns {string}
+     */
+    quote: function quote(str) {
+        return '"' + str.replace(/([\\"])/g, "\\$1").replace(/\n/g, "\\00000a") + '"';
+    },
 }, {
     commands: function (dactyl, modules, window) {
         const { commands, contexts, styles } = modules;
@@ -616,9 +628,10 @@ var Styles = Module("Styles", {
                                     command: "style",
                                     arguments: [style.sites.join(",")],
                                     literalArg: style.css,
-                                    options: update(
-                                        hive.name == "user" ? {} : { "-group": hive.name },
-                                        style.name ? { "-name": style.name } : {})
+                                    options: {
+                                        "-group": hive.name == "user" ? undefined : hive.name,
+                                        "-name": style.name || undefined
+                                    }
                                 })))
                         .flatten().array
             });
@@ -701,7 +714,7 @@ var Styles = Module("Styles", {
             }));
     },
     completion: function (dactyl, modules, window) {
-        const names = Array.slice(util.computedStyle(window.document.createElement("div")));
+        const names = Array.slice(DOM(<div/>, window.document).style);
         modules.completion.css = function (context) {
             context.title = ["CSS Property"];
             context.keys = { text: function (p) p + ":", description: function () "" };
@@ -716,7 +729,7 @@ var Styles = Module("Styles", {
         };
     },
     javascript: function (dactyl, modules, window) {
-        modules.JavaScript.setCompleter(["get", "add", "remove", "find"].map(function (m) styles.user[m]),
+        modules.JavaScript.setCompleter(["get", "add", "remove", "find"].map(function (m) Hive.prototype[m]),
             [ // Prototype: (name, filter, css, index)
                 function (context, obj, args) this.names,
                 function (context, obj, args) Styles.completeSite(context, window.content),
@@ -730,8 +743,10 @@ var Styles = Module("Styles", {
         template.highlightCSS = function highlightCSS(css) {
             XML.prettyPrinting = XML.ignoreWhitespace = false;
 
-            return this.highlightRegexp(css, patterns.property, function (match) <>{
-                match.preSpace}{template.filter(match.name)}: {
+            return this.highlightRegexp(css, patterns.property, function (match) {
+                if (!match.length)
+                    return <></>;
+                return <>{match.preSpace}{template.filter(match.name)}: {
 
                     template.highlightRegexp(match.value, patterns.token, function (match) {
                         if (match.function)
@@ -748,7 +763,7 @@ var Styles = Module("Styles", {
                     })
 
                 }{ match.postSpace }</>
-            )
+            })
         }
     },
 });
index 6e18dc506c676e085153eebcfb9f338d1dfe68be..61c64cdeae079190451c9041454b4f2477a3fd4c 100644 (file)
@@ -2,13 +2,13 @@
 //
 // 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 */
 
+let global = this;
 Components.utils.import("resource://dactyl/bootstrap.jsm");
 defineModule("template", {
     exports: ["Binding", "Template", "template"],
-    require: ["util"],
-    use: ["messages", "services"]
+    require: ["util"]
 }, this);
 
 default xml namespace = XHTML;
@@ -22,7 +22,7 @@ var Binding = Class("Binding", {
         Object.defineProperties(node, this.constructor.properties);
 
         for (let [event, handler] in values(this.constructor.events))
-            node.addEventListener(event, handler, false);
+            node.addEventListener(event, util.wrapCallback(handler, true), false);
     },
 
     set collapsed(collapsed) {
@@ -57,7 +57,7 @@ var Binding = Class("Binding", {
         }
     },
 
-    events: Class.memoize(function () {
+    events: Class.Memoize(function () {
         let res = [];
         for (let obj in this.bindings)
             if (Object.getOwnPropertyDescriptor(obj, "events"))
@@ -66,7 +66,7 @@ var Binding = Class("Binding", {
         return res;
     }),
 
-    properties: Class.memoize(function () {
+    properties: Class.Memoize(function () {
         let res = {};
         for (let obj in this.bindings)
             for (let prop in properties(obj)) {
@@ -153,9 +153,11 @@ var Template = Module("Template", {
 
                 let obj = params.eventTarget;
                 let events = obj[this.getAttribute("events") || "events"];
+                if (Set.has(events, "input"))
+                    events["dactyl-input"] = events["input"];
 
                 for (let [event, handler] in Iterator(events))
-                    node.addEventListener(event, obj.closure(handler), false);
+                    node.addEventListener(event, util.wrapCallback(obj.closure(handler), true), false);
             }
         })
     },
@@ -171,7 +173,7 @@ var Template = Module("Template", {
                     <>&#xa0;</>)
                 })&#xa0;</span>
         }
-        <a xmlns:dactyl={NS} identifier={item.id || ""} dactyl:command={item.command || ""}
+        <a xmlns:dactyl={NS} identifier={item.id == null ? "" : item.id} dactyl:command={item.command || ""}
            href={item.item.url} highlight="URL">{text || ""}</a>
     </>,
 
@@ -204,7 +206,7 @@ var Template = Module("Template", {
     },
 
     helpLink: function (token, text, type) {
-        if (!services["dactyl:"].initialized)
+        if (!help.initialized)
             util.dactyl.initHelp();
 
         let topic = token; // FIXME: Evil duplication!
@@ -213,7 +215,7 @@ var Template = Module("Template", {
         else if (/^n_/.test(topic))
             topic = topic.slice(2);
 
-        if (services["dactyl:"].initialized && !Set.has(services["dactyl:"].HELP_TAGS, topic))
+        if (help.initialized && !Set.has(help.tags, topic))
             return <span highlight={type || ""}>{text || token}</span>;
 
         XML.ignoreWhitespace = false; XML.prettyPrinting = false;
@@ -224,7 +226,7 @@ var Template = Module("Template", {
         return <a highlight={"InlineHelpLink " + type} tag={topic} href={"dactyl://help-tag/" + topic} dactyl:command="dactyl.help" xmlns:dactyl={NS}>{text || topic}</a>;
     },
     HelpLink: function (token) {
-        if (!services["dactyl:"].initialized)
+        if (!help.initialized)
             util.dactyl.initHelp();
 
         let topic = token; // FIXME: Evil duplication!
@@ -233,7 +235,7 @@ var Template = Module("Template", {
         else if (/^n_/.test(topic))
             topic = topic.slice(2);
 
-        if (services["dactyl:"].initialized && !Set.has(services["dactyl:"].HELP_TAGS, topic))
+        if (help.initialized && !Set.has(help.tags, topic))
             return <>{token}</>;
 
         XML.ignoreWhitespace = false; XML.prettyPrinting = false;
@@ -256,13 +258,32 @@ var Template = Module("Template", {
         })(), template[help ? "HelpLink" : "helpLink"]);
     },
 
+    // Fixes some strange stack rewinds on NS_ERROR_OUT_OF_MEMORY
+    // exceptions that we can't catch.
+    stringify: function stringify(arg) {
+        if (!callable(arg))
+            return String(arg);
+
+        try {
+            this._sandbox.arg = arg;
+            return Cu.evalInSandbox("String(arg)", this._sandbox);
+        }
+        finally {
+            this._sandbox.arg = null;
+        }
+    },
+
+    _sandbox: Class.Memoize(function () Cu.Sandbox(global, { wantXrays: false })),
+
     // if "processStrings" is true, any passed strings will be surrounded by " and
     // any line breaks are displayed as \n
-    highlight: function highlight(arg, processStrings, clip) {
+    highlight: function highlight(arg, processStrings, clip, bw) {
         XML.ignoreWhitespace = false; XML.prettyPrinting = false;
         // some objects like window.JSON or getBrowsers()._browsers need the try/catch
         try {
-            let str = clip ? util.clip(String(arg), clip) : String(arg);
+            let str = this.stringify(arg);
+            if (clip)
+                str = util.clip(str, clip);
             switch (arg == null ? "undefined" : typeof arg) {
             case "number":
                 return <span highlight="Number">{str}</span>;
@@ -273,17 +294,21 @@ var Template = Module("Template", {
             case "boolean":
                 return <span highlight="Boolean">{str}</span>;
             case "function":
-                // Vim generally doesn't like /foo*/, because */ looks like a comment terminator.
-                // Using /foo*(:?)/ instead.
+                if (arg instanceof Ci.nsIDOMElement) // wtf?
+                    return util.objectToString(arg, !bw);
+
+                str = str.replace("/* use strict */ \n", "/* use strict */ ");
                 if (processStrings)
                     return <span highlight="Function">{str.replace(/\{(.|\n)*(?:)/g, "{ ... }")}</span>;
                     <>}</>; /* Vim */
+                arg = String(arg).replace("/* use strict */ \n", "/* use strict */ ");
                 return <>{arg}</>;
             case "undefined":
                 return <span highlight="Null">{arg}</span>;
             case "object":
                 if (arg instanceof Ci.nsIDOMElement)
-                    return util.objectToString(arg, false);
+                    return util.objectToString(arg, !bw);
+
                 // for java packages value.toString() would crash so badly
                 // that we cannot even try/catch it
                 if (/^\[JavaPackage.*\]$/.test(arg))
@@ -302,7 +327,10 @@ var Template = Module("Template", {
         }
     },
 
-    highlightFilter: function highlightFilter(str, filter, highlight) {
+    highlightFilter: function highlightFilter(str, filter, highlight, isURI) {
+        if (isURI)
+            str = util.losslessDecodeURI(str);
+
         return this.highlightSubstrings(str, (function () {
             if (filter.length == 0)
                 return;
@@ -364,6 +392,8 @@ var Template = Module("Template", {
         return <table>
                 <tr style="text-align: left;" highlight="Title">
                     <th colspan="2">{_("title.Jump")}</th>
+                    <th>{_("title.HPos")}</th>
+                    <th>{_("title.VPos")}</th>
                     <th>{_("title.Title")}</th>
                     <th>{_("title.URI")}</th>
                 </tr>
@@ -372,6 +402,8 @@ var Template = Module("Template", {
                     <tr>
                         <td class="indicator">{idx == index ? ">" : ""}</td>
                         <td>{Math.abs(idx - index)}</td>
+                        <td>{val.offset ? val.offset.x : ""}</td>
+                        <td>{val.offset ? val.offset.y : ""}</td>
                         <td style="width: 250px; max-width: 500px; overflow: hidden;">{val.title}</td>
                         <td><a href={val.URI.spec} highlight="URL jump-list">{util.losslessDecodeURI(val.URI.spec)}</a></td>
                     </tr>)
index 7941ebf587249f94d4a90979432023c773cbe67a..9cc4a49e99779bb3f33bb891c3b54ae1ec2d0a96 100644 (file)
@@ -4,23 +4,17 @@
 //
 // 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");
-    let frag=1;
 defineModule("util", {
-    exports: ["frag", "FailedAssertion", "Math", "NS", "Point", "Util", "XBL", "XHTML", "XUL", "util"],
-    require: ["services"],
-    use: ["commands", "config", "highlight", "messages", "storage", "template"]
+    exports: ["DOM", "$", "FailedAssertion", "Math", "NS", "Point", "Util", "XBL", "XHTML", "XUL", "util"],
+    require: ["dom", "services"]
 }, 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;
+this.lazyRequire("overlay", ["overlay"]);
 
 var FailedAssertion = Class("FailedAssertion", ErrorBase, {
     init: function init(message, level, noTrace) {
@@ -34,63 +28,71 @@ var FailedAssertion = Class("FailedAssertion", ErrorBase, {
     noTrace: true
 });
 
-var Point = Struct("x", "y");
+var Point = Struct("Point", "x", "y");
 
-var wrapCallback = function wrapCallback(fn) {
-    fn.wrapper = function wrappedCallback () {
-        try {
-            return fn.apply(this, arguments);
-        }
-        catch (e) {
-            util.reportError(e);
-            return undefined;
-        }
-    };
+var wrapCallback = function wrapCallback(fn, isEvent) {
+    if (!fn.wrapper)
+        fn.wrapper = function wrappedCallback() {
+            try {
+                let res = fn.apply(this, arguments);
+                if (isEvent && res === false) {
+                    arguments[0].preventDefault();
+                    arguments[0].stopPropagation();
+                }
+                return res;
+            }
+            catch (e) {
+                util.reportError(e);
+                return undefined;
+            }
+        };
     fn.wrapper.wrapped = fn;
     return fn.wrapper;
 }
 
-var getAttr = function getAttr(elem, ns, name)
-    elem.hasAttributeNS(ns, name) ? elem.getAttributeNS(ns, name) : null;
-var setAttr = function setAttr(elem, ns, name, val) {
-    if (val == null)
-        elem.removeAttributeNS(ns, name);
-    else
-        elem.setAttributeNS(ns, name, val);
-}
-
 var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), {
     init: function () {
         this.Array = array;
 
         this.addObserver(this);
-        this.overlays = {};
+        this.windows = [];
     },
 
-    cleanup: function cleanup() {
-        for (let { document: doc } in iter(services.windowMediator.getEnumerator(null))) {
-            for (let elem in values(doc.dactylOverlayElements || []))
-                if (elem.parentNode)
-                    elem.parentNode.removeChild(elem);
+    activeWindow: deprecated("overlay.activeWindow", { get: function activeWindow() overlay.activeWindow }),
+    overlayObject: deprecated("overlay.overlayObject", { get: function overlayObject() overlay.closure.overlayObject }),
+    overlayWindow: deprecated("overlay.overlayWindow", { get: function overlayWindow() overlay.closure.overlayWindow }),
+
+    compileMatcher: deprecated("DOM.compileMatcher", { get: function compileMatcher() DOM.compileMatcher }),
+    computedStyle: deprecated("DOM#style", function computedStyle(elem) DOM(elem).style),
+    domToString: deprecated("DOM.stringify", { get: function domToString() DOM.stringify }),
+    editableInputs: deprecated("DOM.editableInputs", { get: function editableInputs(elem) DOM.editableInputs }),
+    escapeHTML: deprecated("DOM.escapeHTML", { get: function escapeHTML(elem) DOM.escapeHTML }),
+    evaluateXPath: deprecated("DOM.XPath",
+        function evaluateXPath(path, elem, asIterator) DOM.XPath(path, elem || util.activeWindow.content.document, asIterator)),
+    isVisible: deprecated("DOM#isVisible", function isVisible(elem) DOM(elem).isVisible),
+    makeXPath: deprecated("DOM.makeXPath", { get: function makeXPath(elem) DOM.makeXPath }),
+    namespaces: deprecated("DOM.namespaces", { get: function namespaces(elem) DOM.namespaces }),
+    namespaceNames: deprecated("DOM.namespaceNames", { get: function namespaceNames(elem) DOM.namespaceNames }),
+    parseForm: deprecated("DOM#formData", function parseForm(elem) values(DOM(elem).formData).toArray()),
+    scrollIntoView: deprecated("DOM#scrollIntoView", function scrollIntoView(elem, alignWithTop) DOM(elem).scrollIntoView(alignWithTop)),
+    validateMatcher: deprecated("DOM.validateMatcher", { get: function validateMatcher() DOM.validateMatcher }),
 
-            for (let [elem, ns, name, orig, value] in values(doc.dactylOverlayAttributes || []))
-                if (getAttr(elem, ns, name) === value)
-                    setAttr(elem, ns, name, orig);
+    map: deprecated("iter.map", function map(obj, fn, self) iter(obj).map(fn, self).toArray()),
+    writeToClipboard: deprecated("dactyl.clipboardWrite", function writeToClipboard(str, verbose) util.dactyl.clipboardWrite(str, verbose)),
+    readFromClipboard: deprecated("dactyl.clipboardRead", function readFromClipboard() util.dactyl.clipboardRead(false)),
 
-            delete doc.dactylOverlayElements;
-            delete doc.dactylOverlayAttributes;
-            delete doc.dactylOverlays;
-        }
-    },
+    chromePackages: deprecated("config.chromePackages", { get: function chromePackages() config.chromePackages }),
+    haveGecko: deprecated("config.haveGecko", { get: function haveGecko() config.closure.haveGecko }),
+    OS: deprecated("config.OS", { get: function OS() config.OS }),
 
-    // FIXME: Only works for Pentadactyl
-    get activeWindow() services.windowMediator.getMostRecentWindow("navigator:browser"),
     dactyl: update(function dactyl(obj) {
         if (obj)
             var global = Class.objectGlobal(obj);
+
         return {
             __noSuchMethod__: function (meth, args) {
-                let win = util.activeWindow;
+                let win = overlay.activeWindow;
+
                 var dactyl = global && global.dactyl || win && win.dactyl;
                 if (!dactyl)
                     return null;
@@ -119,8 +121,10 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         if (!obj.observers)
             obj.observers = obj.observe;
 
+        let cleanup = ["dactyl-cleanup-modules", "quit-application"];
+
         function register(meth) {
-            for (let target in Set(["dactyl-cleanup-modules", "quit-application"].concat(Object.keys(obj.observers))))
+            for (let target in Set(cleanup.concat(Object.keys(obj.observers))))
                 try {
                     services.observer[meth](obj, target, true);
                 }
@@ -130,7 +134,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         Class.replaceProperty(obj, "observe",
             function (subject, target, data) {
                 try {
-                    if (target == "quit-application" || target == "dactyl-cleanup-modules")
+                    if (~cleanup.indexOf(target))
                         register("removeObserver");
                     if (obj.observers[target])
                         obj.observers[target].call(obj, subject, data);
@@ -161,6 +165,14 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         return condition;
     },
 
+    /**
+     * CamelCases a -non-camel-cased identifier name.
+     *
+     * @param {string} name The name to mangle.
+     * @returns {string} The mangled name.
+     */
+    camelCase: function camelCase(name) String.replace(name, /-(.)/g, function (m, m1) m1.toUpperCase()),
+
     /**
      * Capitalizes the first character of the given string.
      * @param {string} str The string to capitalize
@@ -192,55 +204,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         return RegExp("[" + util.regexp.escape(list) + "]");
     },
 
-    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) {
-                if (jar.hasEntry("chrome.manifest"))
-                    process(File.readStream(jar.getInputStream("chrome.manifest")));
-                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 a shallow copy of *obj*.
      *
@@ -415,16 +378,27 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
                 if (Set.has(defaults, name))
                     stack.top.elements.push(quote(defaults[name]));
                 else {
+                    let index = idx;
                     if (idx) {
                         idx = Number(idx) - 1;
                         stack.top.elements.push(update(
-                            function (obj) obj[name] != null && idx in obj[name] ? quote(obj[name][idx]) : Set.has(obj, name) ? "" : unknown(full),
-                            { test: function (obj) obj[name] != null && idx in obj[name] && obj[name][idx] !== false && (!flags.e || obj[name][idx] != "") }));
+                            function (obj) obj[name] != null && idx in obj[name] ? quote(obj[name][idx])
+                                                                                 : Set.has(obj, name) ? "" : unknown(full),
+                            {
+                                test: function (obj) obj[name] != null && idx in obj[name]
+                                                  && obj[name][idx] !== false
+                                                  && (!flags.e || obj[name][idx] != "")
+                            }));
                     }
                     else {
                         stack.top.elements.push(update(
-                            function (obj) obj[name] != null ? quote(obj[name]) : Set.has(obj, name) ? "" : unknown(full),
-                            { test: function (obj) obj[name] != null && obj[name] !== false && (!flags.e || obj[name] != "") }));
+                            function (obj) obj[name] != null ? quote(obj[name])
+                                                             : Set.has(obj, name) ? "" : unknown(full),
+                            {
+                                test: function (obj) obj[name] != null
+                                                  && obj[name] !== false
+                                                  && (!flags.e || obj[name] != "")
+                            }));
                     }
 
                     for (let elem in array.iterValues(stack))
@@ -439,80 +413,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         return stack.top;
     },
 
-    /**
-     * 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 util.evaluateXPath(matcher.xpath, node))
-                        yield elem;
-
-                if (matcher.css)
-                    for (let [, elem] in iter(node.querySelectorAll(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) {
-        let evaluator = services.XPathEvaluator();
-        let node = services.XMLDocument();
-        return this.testValues(list, function (value) {
-            if (/^xpath:/.test(value))
-                evaluator.createExpression(value.substr(6), util.evaluateXPath.resolver);
-            else
-                node.querySelector(value);
-            return true;
-        });
-    },
-
-    /**
-     * Returns an object representing a Node's computed CSS style.
-     *
-     * @param {Node} node
-     * @returns {Object}
-     */
-    computedStyle: function computedStyle(node) {
-        while (!(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;
-    },
-
     /**
      * Converts any arbitrary string into an URI object. Returns null on
      * failure.
@@ -522,7 +422,9 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      */
     createURI: function createURI(str) {
         try {
-            return services.urifixup.createFixupURI(str, services.urifixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP);
+            let uri = services.urifixup.createFixupURI(str, services.urifixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP);
+            uri instanceof Ci.nsIURL;
+            return uri;
         }
         catch (e) {
             return null;
@@ -539,55 +441,68 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      * @returns [string] The resulting strings.
      */
     debrace: function debrace(pattern) {
-        if (isArray(pattern)) {
+        try {
+            if (isArray(pattern)) {
+                // Jägermonkey hates us.
+                let obj = ({
+                    res: [],
+                    rec: function rec(acc) {
+                        let vals;
+
+                        while (isString(vals = pattern[acc.length]))
+                            acc.push(vals);
+
+                        if (acc.length == pattern.length)
+                            this.res.push(acc.join(""))
+                        else
+                            for (let val in values(vals))
+                                this.rec(acc.concat(val));
+                    }
+                });
+                obj.rec([]);
+                return obj.res;
+            }
+
+            if (pattern.indexOf("{") == -1)
+                return [pattern];
+
             let res = [];
-            let rec = function rec(acc) {
-                let vals;
 
-                while (isString(vals = pattern[acc.length]))
-                    acc.push(vals);
+            let split = function split(pattern, re, fn, dequote) {
+                let end = 0, match, res = [];
+                while (match = re.exec(pattern)) {
+                    end = match.index + match[0].length;
+                    res.push(match[1]);
+                    if (fn)
+                        fn(match);
+                }
+                res.push(pattern.substr(end));
+                return res.map(function (s) util.dequote(s, dequote));
+            }
+
+            let patterns = [];
+            let substrings = split(pattern, /((?:[^\\{]|\\.)*)\{((?:[^\\}]|\\.)*)\}/gy,
+                function (match) {
+                    patterns.push(split(match[2], /((?:[^\\,]|\\.)*),/gy,
+                        null, ",{}"));
+                }, "{}");
 
-                if (acc.length == pattern.length)
-                    res.push(acc.join(""))
+            let rec = function rec(acc) {
+                if (acc.length == patterns.length)
+                    res.push(array(substrings).zip(acc).flatten().join(""));
                 else
-                    for (let val in values(vals))
-                        rec(acc.concat(val));
+                    for (let [, pattern] in Iterator(patterns[acc.length]))
+                        rec(acc.concat(pattern));
             }
             rec([]);
             return res;
         }
-
-        if (pattern.indexOf("{") == -1)
-            return [pattern];
-
-        function split(pattern, re, fn, dequote) {
-            let end = 0, match, res = [];
-            while (match = re.exec(pattern)) {
-                end = match.index + match[0].length;
-                res.push(match[1]);
-                if (fn)
-                    fn(match);
-            }
-            res.push(pattern.substr(end));
-            return res.map(function (s) util.dequote(s, dequote));
-        }
-        let patterns = [];
-        let substrings = split(pattern, /((?:[^\\{]|\\.)*)\{((?:[^\\}]|\\.)*)\}/gy,
-            function (match) {
-                patterns.push(split(match[2], /((?:[^\\,]|\\.)*),/gy,
-                    null, ",{}"));
-            }, "{}");
-
-        let res = [];
-        function rec(acc) {
-            if (acc.length == patterns.length)
-                res.push(array(substrings).zip(acc).flatten().join(""));
-            else
-                for (let [, pattern] in Iterator(patterns[acc.length]))
-                    rec(acc.concat(pattern));
+        catch (e if e.message && ~e.message.indexOf("res is undefined")) {
+            // prefs.safeSet() would be reset on :rehash
+            prefs.set("javascript.options.methodjit.chrome", false);
+            util.dactyl.warn(_(UTF8("error.damnYouJägermonkey")));
+            return [];
         }
-        rec([]);
-        return res;
     },
 
     /**
@@ -602,42 +517,15 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         pattern.replace(/\\(.)/, function (m0, m1) chars.indexOf(m1) >= 0 ? m1 : m0),
 
     /**
-     * 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.
+     * Returns the nsIDocShell for the given window.
+     *
+     * @param {Window} win The window for which to get the docShell.
+     * @returns {nsIDocShell}
      */
-    domToString: function (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;
-    },
+     docShell: function docShell(win)
+            win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
+               .QueryInterface(Ci.nsIDocShell),
 
     /**
      * Prints a message to the console. If *msg* is an object it is pretty
@@ -675,25 +563,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         util.dump((arguments.length == 0 ? "Stack" : msg) + "\n" + stack + "\n");
     },
 
-    /**
-     * 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 HTML special characters in *str* to the equivalent HTML
-     * entities.
-     *
-     * @param {string} str
-     * @returns {string}
-     */
-    escapeHTML: function escapeHTML(str) {
-        return str.replace(/&/g, "&amp;").replace(/</g, "&lt;");
-    },
-
     /**
      * Escapes quotes, newline and tab characters in *str*. The returned string
      * is delimited by *delimiter* or " if *delimiter* is not specified.
@@ -709,69 +578,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         return delimiter + str.replace(/([\\'"])/g, "\\$1").replace("\n", "\\n", "g").replace("\t", "\\t", "g") + delimiter;
     },
 
-    /**
-     * 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.
-     * @default The current document.
-     * @param {boolean} asIterator Whether to return the results as an
-     *     XPath iterator.
-     * @returns {Object} Iterable result of the evaluation.
-     */
-    evaluateXPath: update(
-        function evaluateXPath(expression, elem, asIterator) {
-            try {
-                if (!elem)
-                    elem = util.activeWindow.content.document;
-                let doc = elem.ownerDocument || elem;
-                if (isArray(expression))
-                    expression = util.makeXPath(expression);
-
-                let result = doc.evaluate(expression, elem,
-                    evaluateXPath.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) ({
-                    xul: XUL.uri,
-                    xhtml: XHTML.uri,
-                    xhtml2: "http://www.w3.org/2002/06/xhtml2",
-                    dactyl: NS.uri
-                }[prefix] || null)
-        }),
-
-    extend: function extend(dest) {
-        Array.slice(arguments, 1).filter(util.identity).forEach(function (src) {
-            for (let [k, v] in Iterator(src)) {
-                let get = src.__lookupGetter__(k),
-                    set = src.__lookupSetter__(k);
-                if (!get && !set)
-                    dest[k] = v;
-                if (get)
-                    dest.__defineGetter__(k, get);
-                if (set)
-                    dest.__defineSetter__(k, set);
-            }
-        });
-        return dest;
-    },
-
     /**
      * Converts *bytes* to a pretty printed data size string.
      *
@@ -845,13 +651,16 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
     getFile: function getFile(uri) {
         try {
             if (isString(uri))
-                uri = util.newURI(util.fixURI(uri));
+                uri = util.newURI(uri);
 
             if (uri instanceof Ci.nsIFileURL)
                 return File(uri.file);
 
+            if (uri instanceof Ci.nsIFile)
+                return File(uri);
+
             let channel = services.io.newChannelFromURI(uri);
-            channel.cancel(Cr.NS_BINDING_ABORTED);
+            try { channel.cancel(Cr.NS_BINDING_ABORTED); } catch (e) {}
             if (channel instanceof Ci.nsIFileChannel)
                 return File(channel.file);
         }
@@ -873,15 +682,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         return null;
     },
 
-    /**
-     * Returns true if the current Gecko runtime is of the given version
-     * or greater.
-     *
-     * @param {string} ver The required version.
-     * @returns {boolean}
-     */
-    haveGecko: function (ver) services.versionCompare.compare(services.runtime.platformVersion, ver) >= 0,
-
     /**
      * Sends a synchronous or asynchronous HTTP request to *url* and returns
      * the XMLHttpRequest object. If *callback* is specified the request is
@@ -889,7 +689,32 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      * argument.
      *
      * @param {string} url
-     * @param {function(XMLHttpRequest)} callback
+     * @param {object} params Optional parameters for this request:
+     *    method: {string} The request method. @default "GET"
+     *
+     *    params: {object} Parameters to append to *url*'s query string.
+     *    data: {*} POST data to send to the server. Ordinary objects
+     *              are converted to FormData objects, with one datum
+     *              for each property/value pair.
+     *
+     *    onload:   {function(XMLHttpRequest, Event)} The request's load event handler.
+     *    onerror:  {function(XMLHttpRequest, Event)} The request's error event handler.
+     *    callback: {function(XMLHttpRequest, Event)} An event handler
+     *              called for either error or load events.
+     *
+     *    background: {boolean} Whether to perform the request in the
+     *                background. @default true
+     *
+     *    mimeType: {string} Override the response mime type with the
+     *              given value.
+     *    responseType: {string} Override the type of the "response"
+     *                  property.
+     *
+     *    user: {string} The user name to send via HTTP Authentication.
+     *    pass: {string} The password to send via HTTP Authentication.
+     *
+     *    quiet: {boolean} If true, don't report errors.
+     *
      * @returns {XMLHttpRequest}
      */
     httpGet: function httpGet(url, callback, self) {
@@ -899,24 +724,50 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
 
         try {
             let xmlhttp = services.Xmlhttp();
-            xmlhttp.mozBackgroundRequest = true;
+            xmlhttp.mozBackgroundRequest = Set.has(params, "background") ? params.background : true;
 
             let async = params.callback || params.onload || params.onerror;
             if (async) {
-                xmlhttp.onload = function handler(event) { util.trapErrors(params.onload || params.callback, params, xmlhttp, event) };
-                xmlhttp.onerror = function handler(event) { util.trapErrors(params.onerror || params.callback, params, xmlhttp, event) };
+                xmlhttp.addEventListener("load",  function handler(event) { util.trapErrors(params.onload  || params.callback, params, xmlhttp, event) }, false);
+                xmlhttp.addEventListener("error", function handler(event) { util.trapErrors(params.onerror || params.callback, params, xmlhttp, event) }, false);
+            }
+
+
+            if (isObject(params.params)) {
+                let data = [encodeURIComponent(k) + "=" + encodeURIComponent(v)
+                            for ([k, v] in iter(params.params))];
+                let uri = util.newURI(url);
+                uri.query += (uri.query ? "&" : "") + data.join("&");
+
+                url = uri.spec;
+            }
+
+            if (isObject(params.data) && !(params.data instanceof Ci.nsISupports)) {
+                let data = services.FormData();
+                for (let [k, v] in iter(params.data))
+                    data.append(k, v);
+                params.data = data;
             }
+
+
             if (params.mimeType)
                 xmlhttp.overrideMimeType(params.mimeType);
 
             xmlhttp.open(params.method || "GET", url, async,
                          params.user, params.pass);
 
-            xmlhttp.send(null);
+            if (params.responseType)
+                xmlhttp.responseType = params.responseType;
+
+            if (params.notificationCallbacks)
+                xmlhttp.channel.notificationCallbacks = params.notificationCallbacks;
+
+            xmlhttp.send(params.data);
             return xmlhttp;
         }
         catch (e) {
-            util.dactyl.log(_("error.cantOpen", String.quote(url), e), 1);
+            if (!params.quiet)
+                util.reportError(e);
             return null;
         }
     },
@@ -951,7 +802,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      * @param {nsIStackFrame} frame
      * @returns {boolean}
      */
-    isDactyl: Class.memoize(function () {
+    isDactyl: Class.Memoize(function () {
         let base = util.regexp.escape(Components.stack.filename.replace(/[^\/]+$/, ""));
         let re = RegExp("^(?:.* -> )?(?:resource://dactyl(?!-content/eval.js)|" + base + ")\\S+$");
         return function isDactyl(frame) re.test(frame.filename);
@@ -966,24 +817,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      */
     isDomainURL: function isDomainURL(url, domain) util.isSubdomain(util.getHost(url), domain),
 
-    /** 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 && !this.isMacOSX,
-        /** @property {RegExp} A RegExp which matches illegal characters in path components. */
-        get illegalCharacters() this.isWindows ? /[<>:"/\\|?*\x00-\x1f]/g : /\//g
-    }),
-
     /**
      * Returns true if *host* is a subdomain of *domain*.
      *
@@ -998,26 +831,18 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         return idx > -1 && idx + domain.length == host.length && (idx == 0 || host[idx - 1] == ".");
     },
 
-    /**
-     * Returns true if the given DOM node is currently visible.
-     *
-     * @param {Node} node
-     * @returns {boolean}
-     */
-    isVisible: function (node) {
-        let style = util.computedStyle(node);
-        return style.visibility == "visible" && style.display != "none";
-    },
-
     /**
      * Iterates over all currently open documents, including all
      * top-level window and sub-frames thereof.
      */
-    iterDocuments: function iterDocuments() {
+    iterDocuments: function iterDocuments(types) {
+        types = types ? types.map(function (s) "type" + util.capitalize(s))
+                      : ["typeChrome", "typeContent"];
+
         let windows = services.windowMediator.getXULWindowEnumerator(null);
         while (windows.hasMoreElements()) {
             let window = windows.getNext().QueryInterface(Ci.nsIXULWindow);
-            for each (let type in ["typeChrome", "typeContent"]) {
+            for each (let type in types) {
                 let docShells = window.docShell.getDocShellEnumerator(Ci.nsIDocShellTreeItem[type],
                                                                       Ci.nsIDocShell.ENUMERATE_FORWARDS);
                 while (docShells.hasMoreElements())
@@ -1030,7 +855,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
     },
 
     // ripped from Firefox; modified
-    unsafeURI: Class.memoize(function () util.regexp(String.replace(<![CDATA[
+    unsafeURI: Class.Memoize(function () util.regexp(String.replace(<![CDATA[
             [
                 \s
                 // Invisible characters (bug 452979)
@@ -1055,21 +880,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
                 catch (e) {
                     return url;
                 }
-            }, this).join("%25");
-    },
-
-    /**
-     * 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 Buffer#evaluateXPath}.
-     *
-     * @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(" | ");
+            }, this).join("%25").replace(/[\s.,>)]$/, encodeURIComponent);
     },
 
     /**
@@ -1089,18 +900,25 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
                                   "'>"].join(""))
           .join("\n"),
 
-    map: deprecated("iter.map", function map(obj, fn, self) iter(obj).map(fn, self).toArray()),
-    writeToClipboard: deprecated("dactyl.clipboardWrite", function writeToClipboard(str, verbose) util.dactyl.clipboardWrite(str, verbose)),
-    readFromClipboard: deprecated("dactyl.clipboardRead", function readFromClipboard() util.dactyl.clipboardRead(false)),
-
     /**
      * Converts a URI string into a URI object.
      *
      * @param {string} uri
      * @returns {nsIURI}
      */
-    // FIXME: createURI needed too?
-    newURI: function newURI(uri, charset, base) this.withProperErrors("newURI", services.io, uri, charset, base),
+    newURI: function newURI(uri, charset, base) {
+        if (uri instanceof Ci.nsIURI)
+            var res = uri.clone();
+        else {
+            let idx = uri.lastIndexOf(" -> ");
+            if (~idx)
+                uri = uri.slice(idx + 4);
+
+            res = this.withProperErrors("newURI", services.io, uri, charset, base);
+        }
+        res instanceof Ci.nsIURL;
+        return res;
+    },
 
     /**
      * Removes leading garbage prepended to URIs by the subscript
@@ -1130,48 +948,12 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         if (!isObject(object))
             return String(object);
 
-        function namespaced(node) {
-            var ns = NAMESPACES[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;
-        }
-
         if (object instanceof Ci.nsIDOMElement) {
-            const NAMESPACES = array.toObject([
-                [NS, "dactyl"],
-                [XHTML, "html"],
-                [XUL, "xul"]
-            ]);
             let elem = object;
             if (elem.nodeType == elem.TEXT_NODE)
                 return elem.data;
 
-            try {
-                let hasChildren = elem.firstChild && (!/^\s*$/.test(elem.firstChild) || elem.firstChild.nextSibling)
-                if (color)
-                    return <span highlight="HelpXMLBlock"><span highlight="HelpXMLTagStart">&lt;{
-                            namespaced(elem)} {
-                                template.map(array.iterValues(elem.attributes),
-                                    function (attr)
-                                        <span highlight="HelpXMLAttribute">{namespaced(attr)}</span> +
-                                        <span highlight="HelpXMLString">{attr.value}</span>,
-                                    <> </>)
-                            }{ !hasChildren ? "/>" : ">"
-                        }</span>{ !hasChildren ? "" : <>...</> +
-                            <span highlight="HtmlTagEnd">&lt;{namespaced(elem)}></span>
-                    }</span>;
-
-                let tag = "<" + [namespaced(elem)].concat(
-                    [namespaced(a) + "=" + template.highlight(a.value, true)
-                     for ([i, a] in array.iterItems(elem.attributes))]).join(" ");
-                return tag + (!hasChildren ? "/>" : ">...</" + namespaced(elem) + ">");
-            }
-            catch (e) {
-                return {}.toString.call(elem);
-            }
+            return DOM(elem).repr(color);
         }
 
         try { // for window.JSON
@@ -1192,7 +974,11 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
                 object = Iterator(object);
                 hasValue = false;
             }
-            for (let i in object) {
+            let keyIter = object;
+            if ("__iterator__" in object && !callable(object.__iterator__))
+                keyIter = keys(object)
+
+            for (let i in keyIter) {
                 let value = <![CDATA[<no value>]]>;
                 try {
                     value = object[i];
@@ -1201,20 +987,24 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
                 if (!hasValue) {
                     if (isArray(i) && i.length == 2)
                         [i, value] = i;
-                    else
+                    else {
                         var noVal = true;
+                        value = i;
+                    }
                 }
 
-                value = template.highlight(value, true, 150);
+                value = template.highlight(value, true, 150, !color);
                 let key = <span highlight="Key">{i}</span>;
                 if (!isNaN(i))
                     i = parseInt(i);
                 else if (/^[A-Z_]+$/.test(i))
                     i = "";
-                keys.push([i, <>{key}{noVal ? "" : <>: {value}</>}&#x0a;</>]);
+                keys.push([i, <>{noVal ? value : <>{key}: {value}</>}&#x0a;</>]);
             }
         }
-        catch (e) {}
+        catch (e) {
+            util.reportError(e);
+        }
 
         function compare(a, b) {
             if (!isNaN(a[0]) && !isNaN(b[0]))
@@ -1256,250 +1046,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         "dactyl-purge": function () {
             this.rehashing = 1;
         },
-
-        "toplevel-window-ready": function (window, data) {
-            window.addEventListener("DOMContentLoaded", wrapCallback(function listener(event) {
-                if (event.originalTarget === window.document) {
-                    window.removeEventListener("DOMContentLoaded", listener.wrapper, true);
-                    util._loadOverlays(window);
-                }
-            }), true);
-        },
-        "chrome-document-global-created": function (window, uri) { this.observe(window, "toplevel-window-ready", null); },
-        "content-document-global-created": function (window, uri) { this.observe(window, "toplevel-window-ready", null); }
-    },
-
-    _loadOverlays: function _loadOverlays(window) {
-        if (!window.dactylOverlays)
-            window.dactylOverlays = [];
-
-        for each (let obj in util.overlays[window.document.documentURI] || []) {
-            if (window.dactylOverlays.indexOf(obj) >= 0)
-                continue;
-            window.dactylOverlays.push(obj);
-            this._loadOverlay(window, obj(window));
-        }
-    },
-
-    _loadOverlay: function _loadOverlay(window, obj) {
-        let doc = window.document;
-        if (!doc.dactylOverlayElements) {
-            doc.dactylOverlayElements = [];
-            doc.dactylOverlayAttributes = [];
-        }
-
-        function overlay(key, fn) {
-            if (obj[key]) {
-                let iterator = Iterator(obj[key]);
-                if (!isObject(obj[key]))
-                    iterator = ([elem.@id, elem.elements(), elem.@*::*.(function::name() != "id")] for each (elem in obj[key]));
-
-                for (let [elem, xml, attr] in iterator) {
-                    if (elem = doc.getElementById(elem)) {
-                        let node = util.xmlToDom(xml, doc, obj.objects);
-                        if (!(node instanceof Ci.nsIDOMDocumentFragment))
-                            doc.dactylOverlayElements.push(node);
-                        else
-                            for (let n in array.iterValues(node.childNodes))
-                                doc.dactylOverlayElements.push(n);
-
-                        fn(elem, node);
-                        for each (let attr in attr || []) {
-                            let ns = attr.namespace(), name = attr.localName();
-                            doc.dactylOverlayAttributes.push([elem, ns, name, getAttr(elem, ns, name), String(attr)]);
-                            if (attr.name() != "highlight")
-                                elem.setAttributeNS(ns, name, String(attr));
-                            else
-                                highlight.highlightNode(elem, String(attr));
-                        }
-                    }
-                }
-            }
-        }
-
-        overlay("before", function (elem, dom) elem.parentNode.insertBefore(dom, elem));
-        overlay("after", function (elem, dom) elem.parentNode.insertBefore(dom, elem.nextSibling));
-        overlay("append", function (elem, dom) elem.appendChild(dom));
-        overlay("prepend", function (elem, dom) elem.insertBefore(dom, elem.firstChild));
-        if (obj.init)
-            obj.init(window);
-
-        if (obj.load)
-            if (doc.readyState === "complete")
-                obj.load(window);
-            else
-                doc.addEventListener("load", wrapCallback(function load(event) {
-                    if (event.originalTarget === event.target) {
-                        doc.removeEventListener("load", load.wrapper, true);
-                        obj.load(window, event);
-                    }
-                }), true);
-    },
-
-    /**
-     * Overlays an object with the given property overrides. Each
-     * property in *overrides* is added to *object*, replacing any
-     * original value. Functions in *overrides* are augmented with the
-     * new properties *super*, *supercall*, and *superapply*, in the
-     * same manner as class methods, so that they man call their
-     * overridden counterparts.
-     *
-     * @param {object} object The object to overlay.
-     * @param {object} overrides An object containing properties to
-     *      override.
-     * @returns {function} A function which, when called, will remove
-     *      the overlay.
-     */
-    overlayObject: function (object, overrides) {
-        let original = Object.create(object);
-        overrides = update(Object.create(original), overrides);
-
-        Object.getOwnPropertyNames(overrides).forEach(function (k) {
-            let orig, desc = Object.getOwnPropertyDescriptor(overrides, k);
-            if (desc.value instanceof Class.Property)
-                desc = desc.value.init(k) || desc.value;
-
-            if (k in object) {
-                for (let obj = object; obj && !orig; obj = Object.getPrototypeOf(obj))
-                    if (orig = Object.getOwnPropertyDescriptor(obj, k))
-                        Object.defineProperty(original, k, orig);
-
-                if (!orig)
-                    if (orig = Object.getPropertyDescriptor(object, k))
-                        Object.defineProperty(original, k, orig);
-            }
-
-            // Guard against horrible add-ons that use eval-based monkey
-            // patching.
-            let value = desc.value;
-            if (callable(desc.value)) {
-
-                delete desc.value;
-                delete desc.writable;
-                desc.get = function get() value;
-                desc.set = function set(val) {
-                    if (!callable(val) || Function.prototype.toString(val).indexOf(sentinel) < 0)
-                        Class.replaceProperty(this, k, val);
-                    else {
-                        let package_ = util.newURI(util.fixURI(Components.stack.caller.filename)).host;
-                        util.reportError(Error(_("error.monkeyPatchOverlay", package_)));
-                        util.dactyl.echoerr(_("error.monkeyPatchOverlay", package_));
-                    }
-                };
-            }
-
-            try {
-                Object.defineProperty(object, k, desc);
-
-                if (callable(value)) {
-                    let sentinel = "(function DactylOverlay() {}())"
-                    value.toString = function toString() toString.toString.call(this).replace(/\}?$/, sentinel + "; $&");
-                    value.toSource = function toSource() toSource.toSource.call(this).replace(/\}?$/, sentinel + "; $&");
-                }
-            }
-            catch (e) {
-                try {
-                    if (value) {
-                        object[k] = value;
-                        return;
-                    }
-                }
-                catch (f) {}
-                util.reportError(e);
-            }
-        }, this);
-
-        return function unwrap() {
-            for each (let k in Object.getOwnPropertyNames(original))
-                if (Object.getOwnPropertyDescriptor(object, k).configurable)
-                    Object.defineProperty(object, k, Object.getOwnPropertyDescriptor(original, k));
-                else {
-                    try {
-                        object[k] = original[k];
-                    }
-                    catch (e) {}
-                }
-        };
-    },
-
-    overlayWindow: function (url, fn) {
-        if (url instanceof Ci.nsIDOMWindow)
-            util._loadOverlay(url, fn);
-        else {
-            Array.concat(url).forEach(function (url) {
-                if (!this.overlays[url])
-                    this.overlays[url] = [];
-                this.overlays[url].push(fn);
-            }, this);
-
-            for (let doc in util.iterDocuments())
-                if (["interactive", "complete"].indexOf(doc.readyState) >= 0)
-                    this._loadOverlays(doc.defaultView);
-                else
-                    this.observe(doc.defaultView, "toplevel-window-ready");
-        }
-    },
-
-    /**
-     * Parses the fields of a form and returns a URL/POST-data pair
-     * that is the equivalent of submitting the form.
-     *
-     * @param {nsINode} field One of the fields of the given form.
-     * @returns {array}
-     */
-    // Nuances gleaned from browser.jar/content/browser/browser.js
-    parseForm: function parseForm(field) {
-        function encode(name, value, param) {
-            param = param ? "%s" : "";
-            if (post)
-                return name + "=" + encodeComponent(value + param);
-            return encodeComponent(name) + "=" + encodeComponent(value) + param;
-        }
-
-        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 (Set.has(util.editableInputs, elem.type)
-                        || /^(?: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, elems.join('&'), charset, elems];
-        return [url + "?" + elems.join('&'), null, charset, elems];
     },
 
     /**
@@ -1572,7 +1118,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         expr = String.replace(expr, /\\(.)/, function (m, m1) {
             if (m1 === "c")
                 flags = flags.replace(/i/g, "") + "i";
-            else if (m === "C")
+            else if (m1 === "C")
                 flags = flags.replace(/i/g, "");
             else
                 return m;
@@ -1652,6 +1198,16 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         }())
     }),
 
+    /**
+     * Flushes the startup or jar cache.
+     */
+    flushCache: function flushCache(file) {
+        if (file)
+            services.observer.notifyObservers(file, "flush-cache-entry", "");
+        else
+            services.observer.notifyObservers(null, "startupcache-invalidate", "");
+    },
+
     /**
      * Reloads dactyl in entirety by disabling the add-on and
      * re-enabling it.
@@ -1659,7 +1215,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
     rehash: function (args) {
         storage.session.commandlineArgs = args;
         this.timeout(function () {
-            services.observer.notifyObservers(null, "startupcache-invalidate", "");
+            this.flushCache();
             this.rehashing = true;
             let addon = config.addon;
             addon.userDisabled = true;
@@ -1668,22 +1224,21 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
     },
 
     errorCount: 0,
-    errors: Class.memoize(function () []),
+    errors: Class.Memoize(function () []),
     maxErrors: 15,
     /**
      * Reports an error to the Error Console and the standard output,
      * along with a stack trace and other relevant information. The
      * error is appended to {@see #errors}.
      */
-    reportError: function (error) {
+    reportError: function reportError(error) {
         if (error.noTrace)
             return;
 
         if (isString(error))
             error = Error(error);
 
-        if (Cu.reportError)
-            Cu.reportError(error);
+        Cu.reportError(error);
 
         try {
             this.errorCount++;
@@ -1693,6 +1248,8 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
                 stack: <>{util.stackLines(String(error.stack || Error().stack)).join("\n").replace(/^/mg, "\t")}</>
             });
 
+            services.console.logStringMessage(obj.stack);
+
             this.errors.push([new Date, obj + "\n" + obj.stack]);
             this.errors = this.errors.slice(-this.maxErrors);
             this.errors.toString = function () [k + "\n" + v for ([k, v] in array.iterValues(this))].join("\n\n");
@@ -1736,19 +1293,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         return ary.filter(function (h) h.length >= base.length);
     },
 
-    /**
-     * Scrolls an element into view if and only if it's not already
-     * fully visible.
-     *
-     * @param {Node} elem The element to make visible.
-     */
-    scrollIntoView: function scrollIntoView(elem, alignWithTop) {
-        let win = elem.ownerDocument.defaultView;
-        let rect = elem.getBoundingClientRect();
-        if (!(rect && rect.bottom <= win.innerHeight && rect.top >= 0 && rect.left < win.innerWidth && rect.right > 0))
-            elem.scrollIntoView(arguments.length > 1 ? alignWithTop : Math.abs(rect.top) < Math.abs(win.innerHeight - rect.bottom));
-    },
-
     /**
      * Returns the selection controller for the given window.
      *
@@ -1760,6 +1304,12 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
            .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay)
            .QueryInterface(Ci.nsISelectionController),
 
+    /**
+     * Escapes a string against shell meta-characters and argument
+     * separators.
+     */
+    shellEscape: function shellEscape(str) '"' + String.replace(str, /[\\"$]/g, "\\$&") + '"',
+
     /**
      * Suspend execution for at least *delay* milliseconds. Functions by
      * yielding execution to the next item in the main event queue, and
@@ -1958,7 +1508,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
             return func.apply(self || this, Array.slice(arguments, 2));
         }
         catch (e) {
-            util.reportError(e);
+            this.reportError(e);
             return undefined;
         }
     },
@@ -2020,6 +1570,15 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         return res.filter(function (h) !Set.add(seen, h.spec));
     },
 
+    /**
+     * Like Cu.getWeakReference, but won't crash if you pass null.
+     */
+    weakReference: function weakReference(jsval) {
+        if (jsval == null)
+            return { get: function get() null };
+        return Cu.getWeakReference(jsval);
+    },
+
     /**
      * Wraps native exceptions thrown by the called function so that a
      * proper stack trace may be retrieved from them.
@@ -2037,55 +1596,12 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         }
     },
 
-    /**
-     * 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}
-     */
-    xmlToDom: function xmlToDom(node, doc, nodes) {
-        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(xmlToDom(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(xmlToDom(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;
-        }
-    }
+    xmlToDom: function () DOM.fromXML.apply(DOM, arguments)
 }, {
     Array: array
 });
 
+
 /**
  * Math utility methods.
  * @singleton
index 8a513932eb5daa8469db249eca1b32eb69d8acd7..4abfceefa0c549488f3471a4ce8a77fc2307439a 100644 (file)
@@ -4,7 +4,7 @@ BEGIN {
         chrome = suffix
 }
 { content = $1 ~ /^(content|skin|locale|resource)$/ }
-content && $NF ~ /^[a-z]/ { $NF = "/" name "/" $NF }
+content && $NF ~ /^[a-z]|^\.\// { $NF = "/" name "/" $NF }
 content {
     sub(/^\.\./, "", $NF);
     if (isjar)
@@ -13,6 +13,7 @@ content {
            $NF = chrome $NF
 }
 {
+    gsub(/\/\.\//, "/")
     sub("^\\.\\./common/", "", $NF)
     print
 }
index f4db095c309284542d3473cd0a1553300854e196..53d3dc29ba6a260ae6e078fab52df58c70caf1ce 100644 (file)
@@ -68,6 +68,10 @@ input[type=file][dactyl|highlight~=HintElem] {
     line-height: 1.5em !important;
 }
 
+.completion-items-container {
+    overflow: hidden;
+}
+
 .td-span {
     display: inline-block;
     overflow: visible;
@@ -191,39 +195,11 @@ statusbarpanel {
 
 /* MOW */
 
-.dactyl-completions,
-#dactyl-multiline-output,
-#dactyl-multiline-input {
-    background-color: white;
-    color: black;
-}
-
-.dactyl-completions-content,
-#dactyl-multiline-output-content,
-#dactyl-multiline-input {
-    white-space: pre;
-    font-family: -moz-fixed;
-    margin: 0px;
-}
-
 #dactyl-commandline-prompt *,
 #dactyl-commandline-command {
     font: inherit;
 }
 
-.dactyl-completions-content table,
-#dactyl-multiline-output-content table {
-    white-space: inherit;
-    border-spacing: 0px;
-}
-
-.dactyl-completions-content td,
-#dactyl-multiline-output-content td,
-.dactyl-completions-content th,
-#dactyl-multiline-output-content th {
-    padding: 0px 2px;
-}
-
 /* for Teledactyl's composer */
 #content-frame, #appcontent {
     border: 0px;
diff --git a/common/skin/global-styles.css b/common/skin/global-styles.css
new file mode 100644 (file)
index 0000000..ef99b86
--- /dev/null
@@ -0,0 +1,321 @@
+
+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]>*
+    CmdErrorMsg;[dactyl|highlight]
+    CmdInfoMsg;[dactyl|highlight]
+    CmdModeMsg;[dactyl|highlight]
+    CmdMoreMsg;[dactyl|highlight]
+    CmdNormal;[dactyl|highlight]
+    CmdQuestion;[dactyl|highlight]
+    CmdWarningMsg;[dactyl|highlight]
+
+    StatusCmdLine;[dactyl|highlight]>*
+    StatusErrorMsg;[dactyl|highlight]
+    StatusInfoMsg;[dactyl|highlight]
+    StatusModeMsg;[dactyl|highlight]
+    StatusMoreMsg;[dactyl|highlight]
+    StatusNormal;[dactyl|highlight]
+    StatusQuestion;[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;
+
+MOW;;;FontFixed,Normal         /* The Multiline Output Window */ \
+                               margin: 0; white-space: pre;
+
+MOW table                      white-space: inherit; border-spacing: 0px;
+MOW :-moz-any(td, th)          padding: 0px 2px;
+
+Comp;;;FontFixed,Normal        /* The completion window */ \
+                               margin: 0; border-top: 1px solid black;
+
+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 */ \
+                   -moz-user-input: none !important; -moz-user-modify: read-only !important; \
+                   background-color: #bbb !important;
+*EditorEditing>*;;*  background-color: #bbb !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" link="false"/> */ \
+                 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:not([active])          color: #888;
+Addon:nth-child(2n+1)        background: rgba(0, 0, 0, .04);
+
+Addon>*;;;AddonCell
+AddonButtons
+AddonDescription
+AddonName                    max-width: 48ex; overflow: hidden;
+AddonStatus
+AddonVersion
+
+// vim:se sts=4 sw=4 et ft=css:
diff --git a/common/skin/help-styles.css b/common/skin/help-styles.css
new file mode 100644 (file)
index 0000000..60c8a2e
--- /dev/null
@@ -0,0 +1,215 @@
+
+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;
+
+HelpXMLBase;;;FontCode  {
+    white-space: pre;
+    color: #C5F779;
+    background-color: #444444;
+    font-family: Terminus, Fixed, monospace;
+}
+HelpXML;;;HelpXMLBase  {
+    /* Highlighted XML */
+    display: inline-block;
+    border: 1px dashed #aaaaaa;
+}
+HelpXMLBlock;;;HelpXMLBase {
+    display: block;
+    margin-left: 2em;
+    border: 1px dashed #aaaaaa;
+}
+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;
+
+CompItem HelpXMLTagStart                    white-space: pre;
+
+// vim:se sts=4 sw=4 et ft=css:
diff --git a/common/tests/functional/testDOMEvents.js b/common/tests/functional/testDOMEvents.js
new file mode 100644 (file)
index 0000000..75d33a4
--- /dev/null
@@ -0,0 +1,92 @@
+"use strict";
+
+var utils = require("utils");
+const { module } = utils;
+
+var { interfaces: Ci } = Components;
+var { nsIDOMKeyEvent: KeyEvent } = Ci;
+
+var controller, dactyl;
+var dactyllib = module("dactyl");
+var jumlib = module("resource://mozmill/modules/jum.js");
+
+var setupModule = function (module) {
+    controller = mozmill.getBrowserController();
+    dactyl = new dactyllib.Controller(controller);
+};
+var teardownModule = function (module) {
+    dactyl.teardown();
+}
+
+var keyDefaults = {
+    keyCode: 0,
+    charCode: 0,
+    altKey: false,
+    ctrlKey: false,
+    metaKey: false,
+    shiftKey: false
+};
+
+var keyMap = {
+    "a": {
+        charCode: "a".charCodeAt(0)
+    },
+    "A": {
+        charCode: "A".charCodeAt(0),
+        shiftKey: true
+    },
+    "<C-a>": {
+        aliases: ["<C-A>"],
+        charCode: "a".charCodeAt(0),
+        ctrlKey: true,
+    },
+    "<C-S-A>": {
+        aliases: ["<C-S-a>"],
+        charCode: "A".charCodeAt(0),
+        ctrlKey: true,
+        shiftKey: true,
+    },
+    "<Return>": {
+        aliases: ["<CR>"],
+        keyCode: KeyEvent.DOM_VK_RETURN
+    },
+    "<S-Return>": {
+        aliases: ["<S-CR>"],
+        keyCode: KeyEvent.DOM_VK_RETURN,
+        shiftKey: true
+    },
+    "<Space>": {
+        aliases: [" ", "< >"],
+        charCode: " ".charCodeAt(0)
+    }
+};
+
+function testKeys() {
+    let { DOM, update } = dactyl.modules;
+
+    for (let [name, object] in Iterator(keyMap)) {
+        for each (let key in (object.aliases || []).concat(name)) {
+            dactyl.assertNoErrors(function () {
+                let result = DOM.Event.parse(key);
+                jumlib.assertEquals(result.length, 1);
+
+                for (let [k, v] in Iterator(keyDefaults))
+                    if (k != "keyCode" || "keyCode" in object || result.keyCode == 0) // TODO
+                        jumlib.assertEquals(result[0][k],
+                                            k in object ? object[k] : v,
+                                            name + ":" + key + ":" + k);
+
+                jumlib.assertEquals(DOM.Event.stringify(result[0]),
+                                    name);
+            });
+        }
+
+        jumlib.assertEquals(name,
+                            DOM.Event.stringify(
+                                update({ type: "keypress" },
+                                       keyDefaults,
+                                       object)));
+    }
+}
+
+// vim: sw=4 ts=8 et:
index dd0afe96aa69d69f8a2af1f7ade654509980be1d..912545ba0266c94181163d2ad1aa953b64178304 100644 (file)
@@ -21,10 +21,17 @@ var setupTest = function (test) {
 
 function urlTarget(url) Services.io.newChannel(url, null, null).name;
 
-__defineGetter__("doesNotExist", function () {
-    delete this.doesNotExist;
-    return this.doesNotExist = urlTarget("dactyl://help-tag/non-existent-help-tag-url-thingy");
-});
+function urlExists(url) {
+    try {
+        let chan = Services.io.newChannel(url);
+        chan.open();
+        try { chan.cancel(Cr.NS_BINDING_ABORTED) } catch (e) {}
+        return true;
+    }
+    catch (e) {
+        return false;
+    }
+}
 
 const HELP_FILES = ["all", "tutorial", "intro", "starting", "browsing",
     "buffer", "cmdline", "editing", "options", "pattern", "tabs", "hints",
@@ -70,7 +77,7 @@ var testExHelpCommand_PageTagArg_OpensHelpPageContainingTag = function () {
 
         let links = controller.tabs.activeTab.querySelectorAll("a[href^='dactyl:']");
 
-        let missing = Array.filter(links, function (link) urlTarget(link.href) === doesNotExist)
+        let missing = Array.filter(links, function (link) urlExists(link.href))
                            .map(function (link) link.textContent + " -> " + link.href);
 
         utils.assertEqual("testHelpCommands.assertNoDeadLinks", 0, missing.length,
index 89301320f766f619c95b91b18da0295391df5aaf..3a84001b5a3a5534e92152dc2121c9ac5483da97 100644 (file)
@@ -33,7 +33,9 @@ function testCompleters() {
     for (var option in dactyl.modules.options)
         for (var [, value] in Iterator([""].concat(options[option.name] || []))) {
             dump("OPT COMP " + option.name + " " + value + "\n");
-            dactyl.testCompleter(option, "completer", value, "Option '" + option.name + "' completer failed");
+            dactyl.testCompleter(dactyl.modules.completion, "optionValue", value,
+                                 "Option '" + option.name + "' completer failed",
+                                 option.name);
         }
 }
 
diff --git a/melodactyl/components/protocols.js b/melodactyl/components/protocols.js
deleted file mode 120000 (symlink)
index 7c25b74..0000000
+++ /dev/null
@@ -1 +0,0 @@
-../../common/components/protocols.js
\ No newline at end of file
index 0213a574dfb222c1025ff61fa143dbd39e8b0e41..273a80cd5dc91f59364a4f1fbba20fe620221a0e 100644 (file)
@@ -182,8 +182,6 @@ const Config = Module("config", ConfigBase, {
         song: "song"
     }, this.__proto__.completers)),
 
-    hasTabbrowser: true,
-
     removeTab: function (tab) {
         if (config.tabbrowser.mTabs.length > 1)
             config.tabbrowser.removeTab(tab);
index af99705228447e8fdfec3def18571027bd407176..a9b1526b2233d880ef9da7c1ea878a87d183b0cd 100644 (file)
@@ -1,4 +1,4 @@
-1.0b7pre:
+1.0rc1:
     • Extensive Firefox 4 support, including:
       - Fully restartless. Can now be installed, uninstalled,
         enabled, disabled, and upgraded without restarting Firefox.
         everywhere from command-line and message history to option
         values and cookies. [b1]
     • New and much more powerful incremental search implementation.
-      Improvements over the standard Firefox find include: [b1]
+      Improvements over the standard Firefox find include:
       - Starts at the cursor position in the currently selected
         frame, unlike Firefox, which always starts at the start of
-        the first frame.
+        the first frame. [b1]
       - Returns the cursor and viewport to their original position
-        on cancel.
+        on cancel. [b1]
       - Backtracks to the first successful match after pressing
-        backspace.
-      - Supports reverse incremental search.
-      - Input boxes are not focused when matches are highlighted.
+        backspace. [b1]
+      - Supports reverse incremental search. [b1]
+      - Input boxes are not focused when matches are highlighted. [b1]
+      - Crude regular expression search is supported. [b8]
+      - New searches now start within the current viewport where possible. [b8]
+    • Text editing improvements, including:
+      - Added t_gu, t_gU, and v_o mappings. [b8]
+      - Added t_<C-a> and t_<C-a> mappings. [b8]
+      - Added o_c, o_d, and o_y mappings. [b8]
+      - Added register and basic kill ring support, t_" and
+        I_<C-'>/I_<C-"> mappings, and :registers command. [b8]
+      - Added operator modes and proper first class motion maps. [b8]
+      - Improved undo support for most mappings. [b8]
+    • General completion improvements
+      - Greatly improved completion rendering performance, especially
+        while scrolling. [b8]
+      - Added c_<C-f>, c_<C-b>, c_<C-Tab>, and c_<C-S-Tab> for scrolling
+        the completion list in increments larger than one line. [b8]
+      - Improved handling of asynchronous completions, including: [b8]
+        + Pressing <Return> after tabbing past the end of already received
+          completions will execute the command after the desired result has
+          arrived.
+        + Tabbing past the end of available completions more reliably selects
+          the desired completion when it is available.
+        + Late arriving completion results no longer interfere with typing.
+        + It is now possible to skip past the end of incomplete completion
+          groups via the c_<C-f> and c_<C-Tab> keys.
+      - JavaScript completion improvements, including: [b2]
+        + The prototype of the function whose arguments are currently
+          being typed is displayed during completion.
+        + Non-enumerable global properties are now completed for the
+          global object, including XMLHttpRequest and encodeURI.
+      - The concept of completion contexts is now exposed to the user
+        (see :contexts), allowing for powerful and fine-grained
+        completion system customization. [b1]
     • Ex command parsing improvements, including:
       - Multiple Ex commands may now be separated by | [b1]
       - Commands can continue over multiple lines in RC files by
         prefixing the continuation lines with a \ [b3]
       - The \ character is no longer treated specially within single
         quotes, i.e., 'fo\o''bar' ⇒ fo\o'bar [b1]
+    • IMPORTANT: Command script files now use the *.penta file extension. [b2]
+    • IMPORTANT: Plugins are now loaded from the 'plugins/'
+      directory in 'runtimepath' rather than 'plugin/'. [b1]
     • The command line is now hidden by default. Added c, C, and M to
       'guioptions'. [b4]
     • Hints mode improvements, including:
       - Added ;a and ;S modes for creating bookmarks and search keywords. [b4][b3]
       - Added 'hintkeys' option. [b2]
       - Added "transliterated" option to 'hintmatching'. [b1]
-    • General completion improvements
-      - JavaScript completion improvements, including: [b2]
-        + The prototype of the function whose arguments are currently
-          being typed is displayed during completion.
-        + Non-enumerable global properties are now completed for the
-          global object, including XMLHttpRequest and encodeURI.
-      - The concept of completion contexts is now exposed to the user
-        (see :contexts), allowing for powerful and fine-grained
-        completion system customization. [b1]
     • The external editor can now be configured to open to a given
       line number and column, used for opening source links and
       editing input fields with i_<C-i>. See :h 'editor'. [b4]
     • Improved [macro-string] support, including automatic elision
       of optional elements, and array subscripts. [b4][b7]
+    • Added -pentadactyl-remote command-line option. [b8]
+    • Moved the smooth-scroll plugin to the core. [b8]
+    • Improvements to marks:
+      - Marks are now stored as line and column ordinals rather than percentages. [b8]
+      - Marks now store the marked element and ensure its visibility when followed. [b8]
     • Mapping changes:
       - It's now possible to map keys in many more modes, including
         Hint, Multi-line Output, and Menu. [b4]
+      - <C-o> and <C-i> now behave more like Vim. [b8]
+      - n_G now uses 'linenumbers' to determine destination if possible. [b8]
+      - Add n_s and n_S. [b8]
+      - Added Operator mode for motion maps, per Vim. [b8]
       - Added site-specific mapping groups and related command
         changes. [b6]
       - Added 'timeout' and 'timeoutlen' options. [b6]
-      - Added n_{, n_}, n_[, and n_] mappings. [b7]
+      - Added n_{, n_}, n_[ and n_] mappings. [b7]
+      - Added n_g], n_[d and n_]d. [b8]
+      - Added <C-t> to open the next mapping in a new tab. [b8]
       - Added <A-b> to execute a builtin mapping. [b6]
       - Added <A-m>l and <A-m>s to aid in the construction of
         macros. [b6]
         :listoptions and :listcommands, providing more powerful and
         consistent interactive help facility (improvements include
         listing keys for modes other than Normal, filtering the output,
-        and linking to source code locations). [b4]
+        and linking to source code definitions). [b4]
       - :downloads now opens a download list in the multi-line output
         buffer. Added -sort flag. [b6][b7]
       - :style now supports regexp site-filters on Firefox 6+. [b7]
       - :qa closes only the current window, per Vim. [b7]
+      - Added :background command. [b8]
+      - Removed :bwipeout and :bunload aliases. Changed :bdelete and
+        :tabclose semantics slightly. The latter now only operates on
+        visible tabs. [b8]
+      - Added -id flag to :bmark command and changed updating semantics. [b8]
       - Added :exit command. [b7]
       - Added :dlclear command. [b7]
       - :extensions has been replaced with a more powerful :addons. [b6]
       - :command now accepts comma-separated alternative command names. [b4]
       - :command -complete custom now also accepts a completions array, see
         :h :command-completion-custom. [b4]
+      - Removed :beep. [b2]
+      - Removed :edit, :tabedit, and :winedit aliases. [b2]
+      - Removed :play. [b2]
     • Improvements to :style and :highlight:
       - Added -link flag to :highlight. [b4]
       - Added -agent flag to :style. [b2]
       - Boolean options no longer accept an argument. [b4]
       - 'cdpath' and 'runtimepath' no longer treat ",,"
         specially. Use "." instead. [b2]
+      - 'complete' is now a [stringlist] rather than a [charlist]
+        and supports native autocomplete providers. [b8]
       - 'extendedhinttags' is now a [regexpmap] rather than a
         string. [b2]
       - 'guioptions' default value has changed. [b4][b7]
         variable. [b4]
       - 'passkeys' is now a [sitemap] with key chain support rather
         than a [regexpmap]. [b6]
+      - The precise format of 'sanitizeitems' has changed slightly. [b8]
       - 'showmode' is now a [regexplist]. [b6]
       - 'showstatuslinks' and 'showtabline' are now [string] options. [b4]
-    • IMPORTANT: Command script files now use the *.penta file extension. [b2]
-    • IMPORTANT: Plugins are now loaded from the 'plugins/'
-      directory in 'runtimepath' rather than 'plugin/'. [b1]
-    • Option changes:
+    • Other option changes:
       - Added [stringmap], [regexplist], and [regexpmap] option
         types. [b1]
       - Added [sitelist] and [sitemap] option types. [b6]
       - Added 'banghist' option. [b1]
       - Added 'cookies', 'cookieaccept', and 'cookielifetime' options. [b3]
       - Added 'downloadsort' option. [b7]
+      - Added 'findflags' option. [b8]
       - Replaced 'focuscontent' with 'strictfocus'. [b1]
       - Made 'strictfocus' a [sitemap]. [b7]
       - 'complete' now defaults to "slf" but file completion only
         triggers when the URL begins as above. [b1]
       - Added 'jumptags' option. [b7]
+      - Added 'linenumbers' option. [b8]
       - Added 's' flag to 'pageinfo' and changed default value. [b7]
       - Added 'passkeys' option. [b3]
       - Added 'passunknown' option. [b7]
       - Changed 'urlseparator' default value to "|". [b3]
-      - Added "passwords" and "venkman" dialogs to :dialog. [b2]
+      - Added "passwords", and "venkman" dialogs to :dialog. [b2][b8]
+      - Added 'scrollsteps' and 'scrolltime' options. [b8]
+      - Added 'spelllang' option. [b8]
       - Make 'showmode' a [stringlist] option. [b7]
       - Added 'wildanchor' option. [b2]
+      - Added 'yankshort' option. [b8]
     • Added BookmarkChange, BookmarkRemove autocommands. [b2]
     • Removed the :source line at the end of files generated by
       :mkpentadactylrc. [b2]
       - The help system is newly modularized and features significant
         updates, rewrites, and formatting improvements. [b1]
       - Added <A-F1> to open the single, consolidated help page. [b5]
-    • Removed :beep. [b2]
-    • Removed :edit, :tabedit, and :winedit aliases. [b2]
-    • Removed :play. [b2]
 
 # vim:set ft=conf sts=2 sw=2 et fo=tcn2 tw=68:
index 809af63aa9e647252150bbab645d7deb85124828..af56b4f1ff28ddb9831832fb99b8374921e980a1 100644 (file)
@@ -12,36 +12,21 @@ BUGS:
 - RC file is sourced once per window
 
 FEATURES:
-9 <C-o>/<C-i> should work as in Vim (i.e., save page positions as well as
-  locations in the history list).
+9 Add more tests.
 9 clean up error message codes and document
-9 option groups, including buffer-local and site-specific
-9 proper motion maps
+9 option groups
+9 global, window-local, tab-local, buffer-local, script-local groups
+9 add [count] support to :b* and :tab* commands where missing
 8 wherever possible: get rid of dialogs and ask console-like dialog questions
   or write error prompts directly on the webpage or with :echo()
-8 add search capability to MOW
-8 registers
-8 Document Caret and Visual modes.
-8 replace global variables with plugin scoped user options
-8 adaptive timeout for auto-completions, :set completions can be updated more often than
-  :open foo
 8 add support for filename special characters such as %
 8 :redir and 'verbosefile'
-8 middleclick in content == p, and if command line is open, paste there the clipboard buffer
-8 all search commands should start searching from the top of the visible viewport
 8 Add information to dactyl/HACKING file about testing and optimization
 7 describe-key command (prompt for a key and display its binding with documentation)
-7 Specify all INSERT mode mappings rather than falling through to the host apps
-  mystery meat mappings.
 7 use ctrl-n/p in insert mode for word completion
-7 implement QuickFix window based on ItemList
-7 [d could go to the last domain in the history stack. So if I browse from
-  google to another page and click 10 links there, [d would take me back to the google page
-  opera's fast forward does something like this
 7 make an option to disable session saving by default when you close Firefox
 7 :grep support (needs location list)
 6 :mksession
-6 add [count] support to :b* and :tab* commands where missing
 6 check/correct spellings in insert mode with some mappings
 6 add more autocommands (TabClose, TabOpen, TabChanged etc)
 6 Use ctrl-w+j/k/w to switch between sidebar, content, preview window
diff --git a/pentadactyl/components/protocols.js b/pentadactyl/components/protocols.js
deleted file mode 120000 (symlink)
index 7c25b74..0000000
+++ /dev/null
@@ -1 +0,0 @@
-../../common/components/protocols.js
\ No newline at end of file
diff --git a/pentadactyl/config.json b/pentadactyl/config.json
new file mode 100644 (file)
index 0000000..844c5f4
--- /dev/null
@@ -0,0 +1,86 @@
+{
+    "name": "pentadactyl",
+    "appName": "Pentadactyl",
+    "idName": "PENTADACTYL",
+    "host": "Firefox",
+    "hostbin": "firefox",
+
+    "autocommands": {
+        "BookmarkAdd":      "Triggered after a page is bookmarked",
+        "BookmarkChange":   "Triggered after a page's bookmark is changed",
+        "BookmarkRemove":   "Triggered after a page's bookmark is removed",
+        "ColorScheme":      "Triggered after a color scheme has been loaded",
+        "DOMLoad":          "Triggered when a page's DOM content has fully loaded",
+        "DownloadPost":     "Triggered when a download has completed",
+        "Fullscreen":       "Triggered when the browser's fullscreen state changes",
+        "LocationChange":   "Triggered when changing tabs or when navigation to a new location",
+        "PageLoadPre":      "Triggered after a page load is initiated",
+        "PageLoad":         "Triggered when a page gets (re)loaded/opened",
+        "PrivateMode":      "Triggered when private browsing mode is activated or deactivated",
+        "Sanitize":         "Triggered when a sanitizeable item is cleared",
+        "ShellCmdPost":     "Triggered after executing a shell command with :!cmd",
+        "Enter":            "Triggered after Firefox starts",
+        "LeavePre":         "Triggered before exiting Firefox, just before destroying each module",
+        "Leave":            "Triggered before exiting Firefox"
+    },
+
+    "option-defaults": {
+        "complete":     "search,location,file",
+        "guioptions":   "bCrs",
+        "showtabline":  "always",
+        "titlestring":  "Pentadactyl"
+    },
+
+    "features": [
+        "sanitizer",
+        "windows"
+    ],
+
+    "guioptions": {
+        "m": ["Menubar",      ["toolbar-menubar"]],
+        "T": ["Toolbar",      ["nav-bar"]],
+        "B": ["Bookmark bar", ["PersonalToolbar"]]
+    },
+
+    "overlays": {
+        "chrome://browser/content/browser.xul": {
+            "completers": {
+                "sidebar": "sidebar",
+                "window": "window"
+            },
+
+            "features": [
+                "bookmarks",
+                "hints",
+                "history",
+                "marks",
+                "quickmarks",
+                "session",
+                "tabbrowser",
+                "tabs",
+                "tabs_undo"
+            ],
+
+            "ids": {
+                "command-container": "browser-bottombox"
+            },
+
+            "scripts": [
+                "browser",
+                "bookmarkcache",
+                "bookmarks",
+                "history",
+                "quickmarks",
+                "sanitizer",
+                "tabs"
+            ]
+        }
+    },
+
+    "sidebars": {
+        "viewAddons":       ["Add-ons",     "A", "chrome://mozapps/content/extensions/extensions.xul"],
+        "viewConsole":      ["Console",     "C", "chrome://global/content/console.xul"],
+        "viewDownloads":    ["Downloads",   "D", "chrome://mozapps/content/downloads/downloads.xul"],
+        "viewPreferences":  ["Preferences", "P", "about:config"]
+    }
+}
index 6c52b2ca60db7b95f7b387b9f5824ed7553def43..c9ae10b3823e1d16d5f0f60657b148d3593fa893 100644 (file)
@@ -7,19 +7,9 @@
 "use strict";
 
 var Config = Module("config", ConfigBase, {
-    name: "pentadactyl",
-    appName: "Pentadactyl",
-    idName: "PENTADACTYL",
-    host: "Firefox",
-    hostbin: "firefox",
-
-    commandContainer: "browser-bottombox",
-
     Local: function Local(dactyl, modules, window)
         let ({ config } = modules) ({
 
-        completers: Class.memoize(function () update({ sidebar: "sidebar", window: "window" }, this.__proto__.completers)),
-
         dialogs: {
             about: ["About Firefox",
                 function () { window.openDialog("chrome://browser/content/aboutDialog.xul", "_blank", "chrome,dialog,modal,centerscreen"); }],
@@ -42,11 +32,9 @@ var Config = Module("config", ConfigBase, {
                 function () { window.inspectDOMDocument(window.content.document); },
                 function () "inspectDOMDocument" in window],
             downloads: ["Manage Downloads",
-                function () { window.toOpenWindowByType("Download:Manager", "chrome://mozapps/content/downloads/downloads.xul", "chrome,dialog=no,resizable"); }],
+                function () { window.BrowserDownloadsUI(); }],
             history: ["List your history",
                 function () { window.openDialog("chrome://browser/content/history/history-panel.xul", "History", "dialog,centerscreen,width=600,height=600"); }],
-            import: ["Import Preferences, Bookmarks, History, etc. from other browsers",
-                function () { window.BrowserImport(); }],
             openfile: ["Open the file selector dialog",
                 function () { window.BrowserOpenFileWindow(); }],
             pageinfo: ["Show information about the current page",
@@ -98,69 +86,10 @@ var Config = Module("config", ConfigBase, {
             }
             catch (e) {}
 
-            return prefix + ".tmp";
+            return prefix + ".txt";
         }
-    }),
-
-    overlayChrome: ["chrome://browser/content/browser.xul"],
-
-    styleableChrome: ["chrome://browser/content/browser.xul"],
-
-    autocommands: {
-        BookmarkAdd: "Triggered after a page is bookmarked",
-        BookmarkChange: "Triggered after a page's bookmark is changed",
-        BookmarkRemove: "Triggered after a page's bookmark is removed",
-        ColorScheme: "Triggered after a color scheme has been loaded",
-        DOMLoad: "Triggered when a page's DOM content has fully loaded",
-        DownloadPost: "Triggered when a download has completed",
-        Fullscreen: "Triggered when the browser's fullscreen state changes",
-        LocationChange: "Triggered when changing tabs or when navigation to a new location",
-        PageLoadPre: "Triggered after a page load is initiated",
-        PageLoad: "Triggered when a page gets (re)loaded/opened",
-        PrivateMode: "Triggered when private browsing mode is activated or deactivated",
-        Sanitize: "Triggered when a sanitizeable item is cleared",
-        ShellCmdPost: "Triggered after executing a shell command with :!cmd",
-        Enter: "Triggered after Firefox starts",
-        LeavePre: "Triggered before exiting Firefox, just before destroying each module",
-        Leave: "Triggered before exiting Firefox"
-    },
-
-    defaults: {
-        complete: "slf",
-        guioptions: "bCrs",
-        showtabline: "always",
-        titlestring: "Pentadactyl"
-    },
-
-    features: Set([
-        "bookmarks", "hints", "history", "marks", "quickmarks", "sanitizer",
-        "session", "tabs", "tabs_undo", "windows"
-    ]),
-
-    guioptions: {
-        m: ["Menubar",      ["toolbar-menubar"]],
-        T: ["Toolbar",      ["nav-bar"]],
-        B: ["Bookmark bar", ["PersonalToolbar"]]
-    },
-
-    hasTabbrowser: true,
-
-    scripts: [
-        "browser",
-        "bookmarkcache",
-        "bookmarks",
-        "history",
-        "quickmarks",
-        "sanitizer",
-        "tabs"
-    ],
+    })
 
-    sidebars: {
-        viewAddons:      ["Add-ons",     "A", "chrome://mozapps/content/extensions/extensions.xul"],
-        viewConsole:     ["Console",     "C", "chrome://global/content/console.xul"],
-        viewDownloads:   ["Downloads",   "D", "chrome://mozapps/content/downloads/downloads.xul"],
-        viewPreferences: ["Preferences", "P", "about:config"]
-    }
 }, {
 }, {
     commands: function (dactyl, modules, window) {
@@ -242,8 +171,8 @@ var Config = Module("config", ConfigBase, {
         commands.add(["wind[ow]"],
             "Execute a command and tell it to output in a new window",
             function (args) {
-                dactyl.withSavedValues(["forceNewWindow"], function () {
-                    this.forceNewWindow = true;
+                dactyl.withSavedValues(["forceTarget"], function () {
+                    this.forceTarget = dactyl.NEW_WINDOW;
                     this.execute(args[0], null, true);
                 });
             },
@@ -278,55 +207,12 @@ var Config = Module("config", ConfigBase, {
         const { CompletionContext, bookmarkcache, completion } = modules;
         const { document } = window;
 
-        var searchRunning = null; // only until Firefox fixes https://bugzilla.mozilla.org/show_bug.cgi?id=510589
         completion.location = function location(context) {
-            if (!services.autoCompleteSearch)
-                return;
-
-            if (searchRunning) {
-                searchRunning.completions = searchRunning.completions;
-                searchRunning.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 = modules.bookmarks.format;
-            context.keys.extra = function (item) (bookmarkcache.get(item.url) || {}).extra;
+            completion.autocomplete("history", context);
             context.title = ["Smart Completions"];
-
-            context.cancel = function () {
-                this.incomplete = false;
-                if (searchRunning === this) {
-                    services.autoCompleteSearch.stopSearch();
-                    searchRunning = null;
-                }
-            };
-
-            services.autoCompleteSearch.startSearch(context.filter, "", context.result, {
-                onSearchResult: function onSearchResult(search, result) {
-                    if (result.searchResult <= result.RESULT_SUCCESS)
-                        searchRunning = 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
-            });
-            searchRunning = context;
         };
 
-        completion.addUrlCompleter("l",
+        completion.addUrlCompleter("location",
             "Firefox location bar entries (bookmarks and history sorted in an intelligent way)",
             completion.location);
 
@@ -345,17 +231,13 @@ var Config = Module("config", ConfigBase, {
     mappings: function initMappings(dactyl, modules, window) {
         const { Events, mappings, modes } = modules;
         mappings.add([modes.NORMAL],
-                     ["<Return>", "<Space>", "<Up>", "<Down>"],
+                     ["<Return>", "<Up>", "<Down>"],
                      "Handled by " + config.host,
                      function () Events.PASS_THROUGH);
     },
-    modes: function (dactyl, modules, window) {
-        const { modes } = modules;
-        config.modes.forEach(function (mode) { modes.addMode.apply(this, mode); });
-    },
     options: function (dactyl, modules, window) {
         modules.options.add(["online"],
-            "Set the 'work offline' option",
+            "Enables or disables offline mode",
             "boolean", true,
             {
                 setter: function (value) {
diff --git a/pentadactyl/icon.png b/pentadactyl/icon.png
new file mode 100644 (file)
index 0000000..862a3bd
Binary files /dev/null and b/pentadactyl/icon.png differ
diff --git a/pentadactyl/icon16.png b/pentadactyl/icon16.png
new file mode 100644 (file)
index 0000000..bdd74b7
Binary files /dev/null and b/pentadactyl/icon16.png differ
diff --git a/pentadactyl/icon64.png b/pentadactyl/icon64.png
new file mode 100644 (file)
index 0000000..dd248bb
Binary files /dev/null and b/pentadactyl/icon64.png differ
index 45de1fa19cb82d5df112f3eb1bd83a8cd18b240d..2a257ecbc477a91718714a401557ef7fc9deaa0a 100644 (file)
@@ -5,10 +5,9 @@
         em:id="pentadactyl@dactyl.googlecode.com"
         em:type="2"
         em:name="Pentadactyl"
-        em:version="1.0b7.1"
+        em:version="1.0rc1"
         em:description="Firefox for Vim and Links addicts"
         em:homepageURL="http://dactyl.sourceforge.net/pentadactyl"
-        em:iconURL="resource://dactyl-local-skin/icon.png"
         em:bootstrap="true">
 
         <em:creator>Kris Maglione, Doug Kearns</em:creator>
@@ -32,7 +31,7 @@
             <Description
                 em:id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"
                 em:minVersion="3.6"
-                em:maxVersion="8.*"/>
+                em:maxVersion="11.*"/>
         </em:targetApplication>
     </Description>
 </RDF>
index 1a88ee5581d57d9a17dc63c30593b0c014aee213..01f0752005af7466e52621135596ca376779e7db 100644 (file)
@@ -9,11 +9,10 @@
     xmlns="&xmlns.dactyl;"
     xmlns:html="&xmlns.html;">
 
-<!-- Initial revision: Sun Jun  8 10:07:05 UTC 2008 (penryu) -->
 <h1 tag="tutorial">Quick-start tutorial</h1>
 
 <note>
-    This is a quickstart tutorial to help new users get up and running
+    This is a quick-start tutorial to help new users get up and running
     in &dactyl.appName;. It is not intended as a full reference explaining all
     features.
 </note>
 <p>
     where <k name="CR"/> represents pressing the <k name="Enter" link="false"/>
     or <k name="Return" link="false"/> key. If you're a veteran Vim user, this
-    may look familiar. It should.
+    should look familiar.
 </p>
 
 <p>
     However, in this author's opinion, the best way to get familiar with
-    &dactyl.appName; is to leave these disabled for now. (The above action can be
-    reversed with <se opt="go" op="&amp;"/><k name="CR"/>) You can look at the entry
-    for <o>guioptions</o> in <t>options</t> for more information on this.
+    &dactyl.appName; is to leave these disabled for now. (The above action can
+    be reversed with <se opt="go" op="&amp;"/><k name="CR"/>) You can have a
+    look at the <o>guioptions</o> help entry for more information on this.
 </p>
 
 <h2 tag="modal">&dactyl.appName;'s modal interface</h2>
@@ -47,7 +46,7 @@
     &dactyl.appName;'s power, like Vim's, comes from its modal interface. Keys have
     different meanings depending on which mode the browser is in. &dactyl.appName; has
     several modes, but the 2 most important are <em>Normal</em> mode and
-    <em>Command Line</em> mode.
+    <em>Command Line</em> mode (see <t>modes</t> for the full picture).
 </p>
 
 <p>
@@ -62,9 +61,9 @@
 </p>
 
 <p>
-    To return to Normal mode from Command Line mode, type <k name="Esc"/>. Pressing
-    <k name="Esc"/> will also return you to Normal mode from most other modes in
-    &dactyl.appName;.
+    To return to Normal mode from Command Line mode, type <k name="Esc"/>.
+    Pressing <k name="Esc"/> will also return you to Normal mode from most
+    other &dactyl.appName; modes.
 </p>
 
 <h2 tag="getting-help">Getting help</h2>
 
 <h2 tag="living-mouseless">Mouseless</h2>
 
-<em>â\80\93 or how I learned to stop worrying and love the 80+ buttons I already have.</em>
+<em>â\80\94 or how I learned to stop worrying and love the 80+ buttons I already have.</em>
 
 <p>
     The efficiency of &dactyl.appName;, as with the legendary editor it was inspired by,
     <dd>
         scroll window left/right
     </dd>
-    <dt><k name="Space"/>/<k name="C-b"/></dt>
+    <dt><k name="C-f"/>/<k name="C-b"/></dt>
     <dd>
         scroll down/up by one page
     </dd>
 <h2 tag="history-navigation tab-navigation">History and tabs</h2>
 
 <p>
-    History navigation (e.g., <em>Back</em>, <em>Forward</em>) are done similarly to
+    History navigation (e.g., <em>Back</em>, <em>Forward</em>) is done similarly to
     scrolling.
 </p>
 
 <dl>
+    <dt><k>H</k>/<k>L</k></dt>
+    <dd>
+        move back/forward in the current tab's history
+    </dd>
     <dt><k name="C-o"/>/<k name="C-i"/></dt>
     <dd>
-        move Back/Forward in the current window/tab's history, respectively
+        move back/forward in the current tab's jump list
     </dd>
 </dl>
 
 <p>
-    Move between tabs using these keystrokes which may also be familiar to tabbing
-    Vimmers.
+    Move between tabs using these keystrokes which may also be familiar to
+    tabbing Vimmers:
 </p>
 
 <dl>
 </dl>
 
 <p>
-    To open a web page in a new tab, use the <ex>:tabopen <a>url</a></ex>. To open a URL in
+    To open a web page in a new tab, use <ex>:tabopen <a>url</a></ex>. To open a URL in
     the current tab, use <ex>:open</ex>. The Normal mode mappings <k>t</k> and <k>o</k>,
     respectively, map to these commands, so the following pairs of sequences are
     equivalent:
 </p>
 
 <p>
-    The answer is <em>hints</em>. Activating hints displays a number next to every link
-    &dactyl.appName; can find. To follow the link, simply type the number corresponding
-    to the hint.
+    The answer is <t>hints</t>. Activating hints displays a number next to
+    every link (or other element, depending on the mode) &dactyl.appName; can
+    find. To act on the element (e.g., follow or save a link), simply type
+    the number corresponding to the hint.
 </p>
 
 <p>
 
 <p>
     Whichever way you choose to indicate your target link, once &dactyl.appName; has
-    highlighted the link you want, simply hit <k name="CR"/> to open it.
+    highlighted the link you want, simply hit <k name="CR" link="false"/> to open it.
 </p>
 
 <p>
     upper-case <k>F</k> will open it in a new tab.
 </p>
 
+<p>
+    Extended hint modes, started by <k>;</k> or <k>g;</k>, provide a richer way
+    to interact with various elements, not limited to following links.
+</p>
+
 <p>
     To test it, try this link: <link topic="&dactyl.apphome;">&dactyl.appName; Homepage</link>.
     Activate Hints mode with <k>f</k> or <k>F</k> to highlight all currently
     visible links. Then start typing the text of the link. The link should be
     uniquely identified soon, and &dactyl.appName; will open it. Once you're
-    done, remember to use <k name="C-o"/> (<em>History Back</em>) or <k>d</k>
+    done, remember to use <k>H</k> (<em>History Back</em>) or <k>d</k>
     (<em>Delete Buffer</em>) to return here, depending on which key you used to
     activate Hints mode.
 </p>
 
-<h2 tag="common-issues">Common issues</h2>
-
-<p>
-    Say you get half-way done typing in a new URL, only to remember that you've
-    already got that page open in the previous tab. Your command line might look
-    something like this:
-</p>
-
-<code><ex>:open my.partial.url/fooba</ex></code>
-
-<p>
-    You can exit the command line and access the already loaded page with the
-    following:
-</p>
-
-<code><k name="Esc"/></code>
-
-<h2 tag="pentadactylrc">Saving for posterity—<tt>pentadactylrc</tt></h2>
+<h2 tag="saving-customization">Saving for posterity—<tt>pentadactylrc</tt></h2>
 
 <p>
     Once you get &dactyl.appName; set up with your desired options, maps, and commands,
     you'll probably want them to be available the next time you open &dactyl.appName;.
-    Continuing the Vim theme, this is done with a <tt>pentadactylrc</tt> file.
+    Continuing the Vim theme, this is done with a <tt><t>pentadactylrc</t></tt> file.
 </p>
 
 <p>
 <dl>
     <dt><ex>:xall</ex></dt>
     <dd>
-        command to quit and save the current browsing session for next time; the default.
+        quit and save the current browsing session for next time; the default
     </dd>
-    <dt><ex>:qall</ex></dt>
+    <dt><ex>:exit</ex></dt>
     <dd>
-        command to quit <em>without</em> saving the session
+        quit <em>without</em> saving the session
     </dd>
     <dt><k>ZZ</k></dt>
     <dd>
     </dd>
     <dt><k>ZQ</k></dt>
     <dd>
-        Normal mode mapping equivalent to <ex>:qall</ex>
+        Normal mode mapping equivalent to <ex>:exit</ex>
     </dd>
 </dl>
 
 <h2 tag="whither-&dactyl.host;">Where did &dactyl.host; go?</h2>
 
 <p>
-    You might feel pretty disoriented now. Don't worry. This is still &dactyl.host;
-    underneath. Here are some ways &dactyl.appName; allows &dactyl.host; to shine through. See
-    the <ex>:help</ex> for these commands and mappings for more information on how to
-    make the best use of them.
+    You might feel pretty disoriented now. Don't worry. This is still
+    &dactyl.host; underneath. Here are some ways &dactyl.appName; allows
+    &dactyl.host; to shine through (see the <ex>:help</ex> for these commands
+    and mappings for more information on how to make the best use of them):
 </p>
 
 <dl>
 </dl>
 
 <p>
-    Feel free to explore at this point. If you use the <ex>:tabopen</ex> command,
-    remember to use the <k>gt</k>/<k>gT</k> mappings to get back to this page. If
-    using the <ex>:open</ex> command, use the history keys (e.g., <k>H</k>) to return.
-    If you get hopelessly lost, just type <ex>:help<k name="CR"/></ex> and click the
-    <em>Tutorial</em> link to return.
+    Feel free to explore at this point. If you get hopelessly lost, just type
+    <ex>:help<k name="CR"/></ex> and click the <em>Quick-start tutorial</em>
+    link to return here.
 </p>
 
 <!-- TODO: other sections? -->
 <h2 tag="removal">Get me out of here!</h2>
 
 <p>
-    If you've given it a fair shot and determined … TODO
+    If you've given it a fair shot and determined that &dactyl.appName; is not
+    for you after all, you might want to disable it.
 </p>
 
 <p>
-    The &dactyl.appName; way to do this is with the command <ex>:addons</ex>. Issuing this
-    command brings up the &dactyl.host; Add-ons dialog window; you can then remove it as
-    normal, selecting &dactyl.appName; from the list and clicking (yes, clicking)
-    <em>Uninstall</em>.
+    The &dactyl.appName; way to do this is with the command <ex>:addons</ex>,
+    which displays a list of all installed extensions. You can use hints or
+    mouse to click on <em>Off</em> or <em>Del</em> to disable or remove
+    &dactyl.appName;, respectively.
 </p>
 
 <p>
     Alternatively, you can do this the old-fashioned way: re-enable the menubar,
-    as above, with <se opt="go" op="+=">m</se>, and select <em>Add-ons</em> from the <em>Tools</em> menu.
+    as above, with <se opt="go" op="+=">m</se>, and select <em>Add-ons</em>
+    from the <em>Tools</em> menu. You can also use <ex>:dialog addons</ex> to
+    get to the interface.
 </p>
 
 <h2 tag="support">I'm interested… but lost!</h2>
 
 <p>
-    &dactyl.appName; has an energetic and growing user base. If you've run into a problem
-    that you can't seem to solve with &dactyl.appName;, or if you think you might have
-    found a bug, please let us know! There is support available on the
-    <link topic="http://code.google.com/p/dactyl/w/list?q=label%3Aproject-&dactyl.name;">wiki</link>
+    &dactyl.appName; has an energetic and growing user base. If you've run into
+    a problem that you can't seem to solve with &dactyl.appName;, or if you
+    think you might have found a bug, please let us know! There is support
+    available on the <link topic="&dactyl.list.href;">mailing list</link>
+    (mirrored on <link topic="http://dir.gmane.org/gmane.comp.mozilla.firefox.pentadactyl">Gmane</link>)
     or in the <link topic="irc://irc.oftc.net/pentadactyl">#pentadactyl</link> IRC
-    channel on <link topic="http://oftc.net/">OFTC</link>.
+    channel on <link topic="http://oftc.net/">OFTC</link>. See also
+    <t>contact</t>.
 </p>
 
 <p>
     hear from you as well. Developers work on &dactyl.appName; whenever possible, but we
     are neither infinite nor omnipotent; please bear with us. If you can't wait for
     us to get around to it, rest assured patches are welcome! See the
-    <link topic="developer">developer</link> page for more information.
+    <t>developer</t> page for more information.
 </p>
 
 </document>
diff --git a/teledactyl/components/protocols.js b/teledactyl/components/protocols.js
deleted file mode 120000 (symlink)
index 7c25b74..0000000
+++ /dev/null
@@ -1 +0,0 @@
-../../common/components/protocols.js
\ No newline at end of file
diff --git a/teledactyl/config.json b/teledactyl/config.json
new file mode 100644 (file)
index 0000000..59825b6
--- /dev/null
@@ -0,0 +1,71 @@
+{
+    "name": "teledactyl",
+    "appName": "Teledactyl",
+    "idName": "TELEDACTYL",
+    "host": "Thunderbird",
+    "hostbin": "thunderbird",
+
+    "autocommands": {
+        "DOMLoad": "Triggered when a page's DOM content has fully loaded",
+        "FolderLoad": "Triggered after switching folders in Thunderbird",
+        "PageLoadPre": "Triggered after a page load is initiated",
+        "PageLoad": "Triggered when a page gets (re)loaded/opened",
+        "Enter": "Triggered after Thunderbird starts",
+        "Leave": "Triggered before exiting Thunderbird",
+        "LeavePre": "Triggered before exiting Thunderbird"
+    },
+
+    "guioptions": {
+        "m": ["MenuBar", ["mail-toolbar-menubar2"]],
+        "T": ["Toolbar", ["mail-bar2"]]
+    },
+
+    "option-defaults": {
+        "complete": "f",
+        "showtabline": 1,
+        "titlestring": "Teledactyl"
+    },
+
+    "overlays": {
+        "chrome://messenger/content/messenger.xul": {
+            "features": [
+                "hints",
+                "mail",
+                "marks",
+                "addressbook",
+                "tabs"
+            ],
+
+            "guioptions": {
+                "f": ["Folder list",        ["folderPaneBox", "folderpane_splitter"]],
+                "F": ["Folder list header", ["folderPaneHeader"]]
+            },
+
+            "option-defaults": {
+                "guioptions": "bCfrs"
+            },
+
+            "scripts": [
+                "addressbook",
+                "mail",
+                "tabs"
+            ]
+        },
+
+        "chrome://messenger/content/messengercompose/messengercompose.xul": {
+            "is-compose-window": true,
+
+            "features": [
+                "addressbook"
+            ],
+
+            "option-defaults": {
+                "guioptions": "bCrs"
+            },
+
+            "scripts": [
+                "compose/compose"
+            ]
+        }
+    }
+}
diff --git a/teledactyl/content/compose/dactyl.xul b/teledactyl/content/compose/dactyl.xul
deleted file mode 100644 (file)
index bcdb613..0000000
+++ /dev/null
@@ -1,98 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<!-- ***** BEGIN LICENSE BLOCK ***** {{{
-// 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.
-}}} ***** END LICENSE BLOCK ***** -->
-
-<?xml-stylesheet href="chrome://dactyl/skin/dactyl.css" type="text/css"?>
-<!DOCTYPE overlay SYSTEM "dactyl.dtd" [
-    <!ENTITY dactyl.content "chrome://dactyl/content/">
-]>
-
-<overlay id="dactyl"
-    xmlns:dactyl="http://vimperator.org/namespaces/liberator"
-    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-    xmlns:nc="http://home.netscape.com/NC-rdf#"
-    xmlns:html="http://www.w3.org/1999/xhtml"
-    xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
-
-    <script type="application/x-javascript;version=1.8" src="&dactyl.content;dactyl-overlay.js"/>
-
-    <window id="&dactyl.mainWindow;">
-
-        <keyset id="mainKeyset">
-            <key id="key_open_vimbar" key=":" oncommand="dactyl.modules.commandline.open(':', '', dactyl.modules.modes.EX);" modifiers=""/>
-            <key id="key_stop" keycode="VK_ESCAPE" oncommand="dactyl.modules.events.onEscape();"/>
-            <!-- other keys are handled inside the event loop in events.js -->
-        </keyset>
-
-        <popupset>
-            <panel id="dactyl-visualbell" dactyl:highlight="Bell"/>
-        </popupset>
-
-        <!--this notifies us also of focus events in the XUL
-            from: http://developer.mozilla.org/en/docs/XUL_Tutorial:Updating_Commands !-->
-        <commandset id="onPentadactylFocus"
-            commandupdater="true"
-            events="focus"
-            oncommandupdate="if (dactyl.modules.events != undefined) dactyl.modules.events.onFocusChange(event);"/>
-        <commandset id="onPentadactylSelect"
-            commandupdater="true"
-            events="select"
-            oncommandupdate="if (dactyl.modules.events != undefined) dactyl.modules.events.onSelectionChange(event);"/>
-
-        <!-- As of Firefox 3.1pre, <iframe>.height changes do not seem to have immediate effect,
-             therefore we need to put them into a <vbox> for which that works just fine -->
-        <vbox class="dactyl-container" hidden="false" collapsed="true">
-            <iframe id="dactyl-multiline-output" src="chrome://dactyl/content/buffer.xhtml"
-                flex="1" hidden="false" collapsed="false"
-                onclick="dactyl.modules.commandline.onMultilineOutputEvent(event)"/>
-        </vbox>
-
-        <vbox class="dactyl-container" hidden="false" collapsed="true">
-            <iframe id="dactyl-completions" src="chrome://dactyl/content/buffer.xhtml"
-                flex="1" hidden="false" collapsed="false"
-                onclick="dactyl.modules.commandline.onMultilineOutputEvent(event)"/>
-        </vbox>
-
-        <stack orient="horizontal" align="stretch" class="dactyl-container" dactyl:highlight="CmdLine">
-            <textbox class="plain" id="dactyl-message" flex="1" readonly="true" dactyl:highlight="Normal"/>
-            <hbox id="dactyl-commandline" hidden="false" collapsed="true" class="dactyl-container" dactyl:highlight="Normal">
-                <label class="plain" id="dactyl-commandline-prompt"  flex="0" crop="end" value="" collapsed="true"/>
-                <textbox class="plain" id="dactyl-commandline-command" flex="1" type="timed" timeout="100"
-                    oninput="dactyl.modules.commandline.onEvent(event);"
-                    onkeyup="dactyl.modules.commandline.onEvent(event);"
-                    onfocus="dactyl.modules.commandline.onEvent(event);"
-                    onblur="dactyl.modules.commandline.onEvent(event);"/>
-            </hbox>
-        </stack>
-
-        <vbox class="dactyl-container" hidden="false" collapsed="false">
-            <textbox id="dactyl-multiline-input" class="plain" flex="1" rows="1" hidden="false" collapsed="true" multiline="true"
-                onkeypress="dactyl.modules.commandline.onMultilineInputEvent(event);"
-                oninput="dactyl.modules.commandline.onMultilineInputEvent(event);"
-                onblur="dactyl.modules.commandline.onMultilineInputEvent(event);"/>
-        </vbox>
-
-    </window>
-
-    <statusbar id="status-bar" dactyl:highlight="StatusLine">
-        <hbox insertbefore="&dactyl.statusBefore;" insertafter="&dactyl.statusAfter;"
-              id="dactyl-statusline" flex="1" hidden="false" align="center">
-            <textbox class="plain" id="dactyl-statusline-field-url" readonly="false" flex="1" crop="end"/>
-            <label class="plain" id="dactyl-statusline-field-inputbuffer"    flex="0"/>
-            <label class="plain" id="dactyl-statusline-field-progress"       flex="0"/>
-            <label class="plain" id="dactyl-statusline-field-tabcount"       flex="0"/>
-            <label class="plain" id="dactyl-statusline-field-bufferposition" flex="0"/>
-        </hbox>
-        <!-- just hide them since other elements expect them -->
-        <statusbarpanel id="statusbar-display" hidden="true"/>
-        <statusbarpanel id="statusbar-progresspanel" hidden="true"/>
-    </statusbar>
-
-</overlay>
-
-<!-- vim: set fdm=marker sw=4 ts=4 et: -->
index 3a883ae9f7f0d8b879b39fa89d98e6ab09db7424..0fae1d292864d6bfdb329063630d936d0c9d788b 100644 (file)
@@ -5,12 +5,6 @@
 "use strict";
 
 var Config = Module("config", ConfigBase, {
-    name: "teledactyl",
-    appName: "Teledactyl",
-    idName: "TELEDACTYL",
-    host: "Thunderbird",
-    hostbin: "thunderbird",
-
     Local: function Local(dactyl, modules, window)
         let ({ config } = modules, { document } = window) {
         init: function init() {
@@ -27,8 +21,6 @@ var Config = Module("config", ConfigBase, {
                 tabmail && tabmail.tabInfo.length ? tabmail.getBrowserForSelectedTab()
                                                   : document.getElementById("messagepane"),
 
-        get commandContainer() document.documentElement.id,
-
         tabbrowser: {
             __proto__: Class.makeClosure.call(window.document.getElementById("tabmail")),
             get mTabContainer() this.tabContainer,
@@ -47,16 +39,12 @@ var Config = Module("config", ConfigBase, {
             }
         },
 
-        get hasTabbrowser() !this.isComposeWindow,
-
         get tabStip() this.tabbrowser.tabContainer,
 
-        get isComposeWindow() window.wintype == "msgcompose",
-
         get mainWidget() this.isComposeWindow ? document.getElementById("content-frame") : window.GetThreadTree(),
 
-        get mainWindowId() this.isComposeWindow ? "msgcomposeWindow" : "messengerWindow",
         get browserModes() [modules.modes.MESSAGE],
+
         get mailModes() [modules.modes.NORMAL],
 
         // NOTE: as I don't use TB I have no idea how robust this is. --djk
@@ -83,7 +71,7 @@ var Config = Module("config", ConfigBase, {
                 dactyl.beep();
         },
 
-        completers: Class.memoize(function () update({ mailfolder: "mailFolder" }, this.__proto__.completers)),
+        completers: Class.Memoize(function () update({ mailfolder: "mailFolder" }, this.__proto__.completers)),
 
         dialogs: {
             about: ["About Thunderbird",
@@ -115,8 +103,13 @@ var Config = Module("config", ConfigBase, {
         focusChange: function focusChange(win) {
             const { modes } = modules;
 
+            if (win.top == window)
+                return;
+
             // we switch to -- MESSAGE -- mode for Teledactyl when the main HTML widget gets focus
-            if (win && win.document instanceof Ci.nsIHTMLDocument || dactyl.focus instanceof Ci.nsIHTMLAnchorElement) {
+            if (win && win.document instanceof Ci.nsIDOMHTMLDocument
+                    || dactyl.focusedElement instanceof Ci.nsIDOMHTMLAnchorElement) {
+
                 if (this.isComposeWindow)
                     modes.set(modes.INSERT, modes.TEXT_EDIT);
                 else if (dactyl.mode != modes.MESSAGE)
@@ -125,54 +118,6 @@ var Config = Module("config", ConfigBase, {
         }
     },
 
-    autocommands: {
-        DOMLoad: "Triggered when a page's DOM content has fully loaded",
-        FolderLoad: "Triggered after switching folders in Thunderbird",
-        PageLoadPre: "Triggered after a page load is initiated",
-        PageLoad: "Triggered when a page gets (re)loaded/opened",
-        Enter: "Triggered after Thunderbird starts",
-        Leave: "Triggered before exiting Thunderbird",
-        LeavePre: "Triggered before exiting Thunderbird"
-    },
-
-    defaults: {
-        guioptions: "bCfrs",
-        complete: "f",
-        showtabline: 1,
-        titlestring: "Teledactyl"
-    },
-
-    /*** optional options, there are checked for existence and a fallback provided  ***/
-    features: Class.memoize(function () Set(
-        this.isComposeWindow ? ["addressbook"]
-                             : ["hints", "mail", "marks", "addressbook", "tabs"])),
-
-    guioptions: {
-        m: ["MenuBar",            ["mail-toolbar-menubar2"]],
-        T: ["Toolbar" ,           ["mail-bar2"]],
-        f: ["Folder list",        ["folderPaneBox", "folderpane_splitter"]],
-        F: ["Folder list header", ["folderPaneHeader"]]
-    },
-
-    // they are sorted by relevance, not alphabetically
-    helpFiles: ["intro.html", "version.html"],
-
-    modes: [
-        ["MESSAGE", { char: "m" }],
-        ["COMPOSE"]
-    ],
-
-    get scripts() this.isComposeWindow ? ["compose/compose"] : [
-        "addressbook",
-        "mail",
-        "tabs",
-    ],
-
-    overlayChrome: ["chrome://messenger/content/messenger.xul",
-                      "chrome://messenger/content/messengercompose/messengercompose.xul"],
-    styleableChrome: ["chrome://messenger/content/messenger.xul",
-                      "chrome://messenger/content/messengercompose/messengercompose.xul"],
-
     // to allow Vim to :set ft=mail automatically
     tempFile: "teledactyl.eml"
 }, {
index 3b67b4500441d7e808e0ecd6935922e6fbdcb09f..cdfe22d4b6ad4ba6d4b138945673281d6fedb6ce 100644 (file)
@@ -179,9 +179,7 @@ var Mail = Module("mail", {
 
         params.type = Ci.nsIMsgCompType.New;
 
-        const msgComposeService = Cc["@mozilla.org/messengercompose;1"].getService();
-        msgComposeService = msgComposeService.QueryInterface(Ci.nsIMsgComposeService);
-        msgComposeService.OpenComposeWindowWithParams(null, params);
+        services.compose.OpenComposeWindowWithParams(null, params);
     },
 
     // returns an array of nsIMsgFolder objects
@@ -861,8 +859,8 @@ var Mail = Module("mail", {
     },
     services: function initServices(dactyl, modules, window) {
         services.add("smtp", "@mozilla.org/messengercompose/smtp;1", Ci.nsISmtpService);
+        services.add("compose", "@mozilla.org/messengercompose;1", "nsIMsgComposeService");
     },
-
     modes: function initModes(dactyl, modules, window) {
         modes.addMode("MESSAGE", {
             char: "m",