From 64b12db9907f714376450f32b1da27ece196b08b Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Sun, 23 Jan 2011 17:37:59 -0500 Subject: [PATCH 01/36] Begin replacing the key processing rat's nest. --HG-- branch : key-processing --- common/content/commandline.js | 4 +- common/content/events.js | 457 ++++++++++++++++++---------------- common/content/mappings.js | 18 +- common/content/modes.js | 20 +- common/modules/base.jsm | 2 +- 5 files changed, 260 insertions(+), 241 deletions(-) diff --git a/common/content/commandline.js b/common/content/commandline.js index f1ec7d79..a838e4bd 100644 --- a/common/content/commandline.js +++ b/common/content/commandline.js @@ -1115,9 +1115,9 @@ var CommandLine = Module("commandline", { } // TODO: Wouldn't multiple handlers be cleaner? --djk - if (event.type == "click" && (event.target instanceof HTMLAnchorElement || - event.originalTarget.hasAttributeNS(NS, "command"))) { + if (event.type == "click" && event.target instanceof HTMLAnchorElement) { + util.dump(event.getPreventDefault(), event.target); if (event.getPreventDefault()) return; diff --git a/common/content/events.js b/common/content/events.js index 1784f64e..02a1b734 100644 --- a/common/content/events.js +++ b/common/content/events.js @@ -8,11 +8,219 @@ /** @scope modules */ +var ProcessorStack = Class("ProcessorStack", { + init: function (mode, hives, keyModes) { + this.main = mode.main; + this.actions = []; + this.buffer = ""; + this.events = []; + + this.processors = keyModes.map(function (m) hives.map(function (h) KeyProcessor(m, h))) + .flatten().array; + + for (let [i, input] in Iterator(this.processors)) { + let params = input.main == mode.main ? mode.params : input.main.params; + if (params.preExecute) + input.preExecute = params.preExecute; + if (params.postExecute) + input.postExecute = params.postExecute; + if (params.onEvent && input.hive === mappings.builtin) + input.fallthrough = function (event) { + return params.onEvent(event) === false ? Events.KILL : Events.PASS; + }; + } + }, + + process: function process(event) { + function dbg() {} + + let key = events.toString(event); + this.events.push(event); + + this.buffer += key; + + let actions = []; + let processors = []; + + dbg("\n\n"); + dbg("KEY: " + key + " skipmap: " + event.skipmap + " macro: " + event.isMacro); + + for (let [i, input] in Iterator(this.processors)) { + let res = input.process(event); + if (res !== Events.ABORT) + var result = res; + + dbg("RES: " + input + " " + (callable(res) ? {}.toString.call(res) : res)); + + if (res === Events.KILL) + break; + + buffer = buffer || input.inputBuffer; + + if (callable(res)) + actions.push(res); + + if (isinstance(res, KeyProcessor)) + processors.push(res); + if (res === Events.WAIT || input.waiting) + processors.push(input); + } + + dbg("RESULT: " + (callable(result) ? {}.toString.call(result) : result) + " " + event.getPreventDefault()); + dbg("ACTIONS: " + actions.length + " " + this.actions.length); + dbg("PROCESSORS:", processors); + + if (!processors.some(function (p) p.main.ownsBuffer)) + statusline.updateInputBuffer(processors.length ? this.buffer : ""); + + this.actions = actions.concat(this.actions); + + if (result === Events.KILL) + this.actions = []; + else if (!this.actions.length) + for (let input in values(this.processors)) + if (input.fallthrough) { + if (result === Events.KILL) + break; + result = dactyl.trapErrors(input.fallthrough, input, event); + } + + this.processors = processors; + + if (processors.length) + result = Events.KILL; + else if (this.actions.length) { + if (actions.length == 0) + dactyl.beep(); + result = this.actions[0]() === Events.PASS ? Events.PASS : Events.KILL; + } + else if (result !== Events.KILL && processors.some(function (p) !p.main.passUnknown)) { + result = Events.KILL; + dactyl.beep(); + } + else if (result === undefined) + result = Events.PASS; + + if (result !== Events.PASS) + Events.kill(event); + + if (result === Events.PASS || result === Events.ABORT) + this.events.filter(function (e) e.getPreventDefault()) + .forEach(function (event, i) { + if (event.originalTarget) { + let evt = events.create(event.originalTarget.ownerDocument, event.type, event); + events.dispatch(event.originalTarget, evt, { skipmap: true, isMacro: true }); + } + else if (i > 0) + events.onKeyPress(event); + }); + return this.processors.length == 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) + let (self = this, args = arguments) + function execute() { + if (self.preExecute) + self.preExecute.apply(self, args); + let res = map.execute.apply(map, Array.slice(args, 1)); + 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"); + else if (modes.replaying && !events.waitForPageLoad()) + return Events.KILL; + + return this.execute(map, { count: this.count, command: this.command, events: this.events }); + } + + if (!this.waiting) + return this.main.input ? 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; + }, + + 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); + } +}); + /** * @instance events */ var Events = Module("events", { init: function () { + const self = this; + util.overlayWindow(window, { append: @@ -32,7 +240,6 @@ var Events = Module("events", { this._currentMacro = ""; this._macroKeys = []; this._lastMacro = ""; - this._processors = []; this.sessionListeners = []; @@ -103,6 +310,10 @@ var Events = Module("events", { this.addSessionListener(window, "popuphidden", this.onPopupHidden, true); this.addSessionListener(window, "popupshown", this.onPopupShown, true); this.addSessionListener(window, "resize", this.onResize, true); + + dactyl.registerObserver("modeChange", function () { + delete this.processor; + }); }, destroy: function () { @@ -863,15 +1074,6 @@ var Events = Module("events", { onKeyPress: function onKeyPress(event) { event.dactylDefaultPrevented = event.getPreventDefault(); - function kill(event) { - event.preventDefault(); - event.stopPropagation(); - } - - function shouldPass() - (!dactyl.focusedElement || events.isContentNode(dactyl.focusedElement)) && - options.get("passkeys").has(events.toString(event)); - let duringFeed = this.duringFeed || []; this.duringFeed = []; try { @@ -906,16 +1108,14 @@ var Events = Module("events", { else duringFeed.push(event); - return kill(event); + return Events.kill(event); } - let mode = modes.getStack(0); - if (event.dactylMode) - mode = Modes.StackElement(event.dactylMode); + if (!this.processor) { + let mode = modes.getStack(0); + if (event.dactylMode) + mode = Modes.StackElement(event.dactylMode); - let processors = this._processors; - this._processors = []; - if (!processors.length) { let ignore = false; let overrideMode = null; @@ -930,7 +1130,7 @@ var Events = Module("events", { mode.params.mainMode = modes.getStack(2).main; ignore = Events.isEscape(key); } - else if (shouldPass()) + else if (events.shouldPass(event)) mode.params.mainMode = modes.getStack(1).main; else ignore = true; @@ -938,7 +1138,7 @@ var Events = Module("events", { if (ignore && !Events.isEscape(key)) modes.pop(); } - else if (!event.isMacro && !event.noremap && shouldPass()) + else if (!event.isMacro && !event.noremap && events.shouldPass(event)) ignore = true; if (ignore) @@ -957,78 +1157,14 @@ var Events = Module("events", { let hives = mappings.hives.slice(event.noremap ? -1 : 0); - processors = keyModes.map(function (m) hives.map(function (h) Events.KeyProcessor(m, h))) - .flatten().array; - - for (let [i, input] in Iterator(processors)) { - let params = input.main == mode.main ? mode.params : input.main.params; - if (params.preExecute) - input.preExecute = params.preExecute; - if (params.postExecute) - input.postExecute = params.postExecute; - if (params.onEvent && input.hive === mappings.builtin) - input.fallthrough = function (event) { - return params.onEvent(event) === false ? Events.KILL : Events.PASS; - }; - } + this.processor = ProcessorStack(mode, hives, keyModes); } - let refeed, buffer, waiting = 0, action; - for (let input in values(processors)) { - var res = input.process(event); - waiting += res == Events.WAIT; - buffer = buffer || input.inputBuffer; - if (callable(res)) - action = action || res; + let processor = this.processor; + this.processor = null; + if (!processor.process(event)) + this.processor = processor; - if (isArray(res) && !waiting) - refeed = res; - if (res === Events.KILL) - break; - } - - if (!refeed || refeed.length == 1) - for (let input in values(processors)) - if (input.fallthrough) { - if (res === Events.KILL) - break; - res = dactyl.trapErrors(input.fallthrough, input, event); - } - - if (!processors.some(function (p) p.main.ownsBuffer)) - statusline.updateInputBuffer(buffer); - - if (waiting) { - res = Events.KILL; - this._processors = processors; - } - else if (action) - res = action(res) === Events.PASS ? Events.PASS : Events.KILL; - - if (res !== Events.KILL && (mode.main & (modes.TEXT_EDIT | modes.VISUAL))) { - res = Events.KILL; - dactyl.beep(); - } - - if (res !== Events.PASS && !isArray(res)) - refeed = null; - - if (refeed && refeed[0] && (!refeed[0].getPreventDefault() || refeed[0].dactylDefaultPrevented)) { - res = Events.PASS; - refeed.shift(); - } - - if (res !== Events.PASS) - kill(event); - - if (refeed) - for (let [i, event] in Iterator(refeed)) - if (event.originalTarget) { - let evt = events.create(event.originalTarget.ownerDocument, event.type, event); - events.dispatch(event.originalTarget, evt, { skipmap: true, isMacro: true }); - } - else if (i > 0) - events.onKeyPress(event); } catch (e) { dactyl.reportError(e); @@ -1053,15 +1189,11 @@ var Events = Module("events", { // before we get a chance to process our key bindings on the // "keypress" event. - function shouldPass() // FIXME. - (!dactyl.focusedElement || events.isContentNode(dactyl.focusedElement)) && - options.get("passkeys").has(events.toString(event)); - if (modes.main == modes.PASS_THROUGH || modes.main == modes.QUOTE && modes.getStack(1).main !== modes.PASS_THROUGH - && !shouldPass() || - !modes.passThrough && shouldPass()) + && !events.shouldPass(event) || + !modes.passThrough && events.shouldPass(event)) return; if (!Events.isInputElement(dactyl.focusedElement)) @@ -1110,137 +1242,17 @@ var Events = Module("events", { else if (modes.main == modes.CARET) modes.push(modes.VISUAL); } - } + }, + + shouldPass: function shouldPass(event) + (!dactyl.focusedElement || events.isContentNode(dactyl.focusedElement)) && + options.get("passkeys").has(events.toString(event)) }, { + ABORT: {}, KILL: true, PASS: false, WAIT: null, - - KeyProcessor: Class("KeyProcessor", { - init: function init(main, hive) { - this.main = main; - this.events = []; - this.hive = hive; - }, - - get toStringParams() [this.main.name, this.hive.name], - - buffer: "", // partial command storage - pendingMotionMap: null, // e.g. "d{motion}" if we wait for a motion of the "d" command - pendingArgMap: null, // pending map storage for commands like m{a-z} - count: null, // parsed count from the input buffer - motionCount: null, - - append: function append(event) { - this.events.push(event); - this.buffer += events.toString(event); - return this.events; - }, - - process: function process(event) { - function kill(event) { - event.stopPropagation(); - event.preventDefault(); - } - - let res = this.onKeyPress(event); - - if (res != Events.WAIT) - this.inputBuffer = ""; - else { - let motionMap = (this.pendingMotionMap && this.pendingMotionMap.names[0]) || ""; - this.inputBuffer = motionMap + this.buffer; - } - - return res; - }, - - execute: function execute(map) - let (self = this, args = arguments) - function execute() { - if (self.preExecute) - self.preExecute.apply(self, args); - let res = map.execute.apply(map, Array.slice(args, 1)); - if (self.postExecute) // To do: get rid of self. - self.postExecute.apply(self, args); - return res; - }, - - onKeyPress: function onKeyPress(event) { - // This all needs to go. It's horrible. --Kris - - let key = events.toString(event); - let [, countStr, command] = /^((?:[1-9][0-9]*)?)(.*)/.exec(this.buffer + key); - - var map = this.hive.get(this.main, command); - - let candidates = this.hive.getCandidates(this.main, command); - if (candidates == 0 && !map) { - [map] = this.pendingMap || []; - this.pendingMap = null; - if (map && map.arg) - this.pendingArgMap = [map, command]; - } - - // counts must be at the start of a complete mapping (10j -> go 10 lines down) - if (countStr && !command) { - // no count for insert mode mappings - if (!this.main.count) - return this.append(event); - else if (this.main.input) - return Events.PASS; - else - this.append(event); - } - else if (this.pendingArgMap) { - let [map, command] = this.pendingArgMap; - if (!Events.isEscape(key)) - return this.execute(map, null, this.count, key, command); - return Events.KILL; - } - else if (!event.skipmap && map && candidates == 0) { - this.pendingMap = null; - - let count = this.pendingMotionMap ? "motionCount" : "count"; - this[count] = parseInt(countStr, 10); - - if (isNaN(this[count])) - this[count] = null; - - if (map.arg) { - this.append(event); - this.pendingArgMap = [map, command]; - } - else if (this.pendingMotionMap) { - let [map, command] = this.pendingMotionMap; - if (!Events.isEscape(key)) - return this.execute(map, command, this.motionCount || this.count, null, command); - return Events.KILL; - } - else if (map.motion) { - this.buffer = ""; - this.pendingMotionMap = [map, command]; - } - else { - if (modes.replaying && !events.waitForPageLoad()) - return Events.KILL; - - return this.execute(map, null, this.count, null, command); - } - } - else if (!event.skipmap && this.hive.getCandidates(this.main, command) > 0) { - this.append(event); - this.pendingMap = [map, command]; - } - else { - this.append(event); - return this.events; - } - return Events.WAIT; - } - }), - isEscape: function isEscape(event) let (key = isString(event) ? event : events.toString(event)) key === "" || key === "", @@ -1252,6 +1264,11 @@ var Events = Module("events", { HTMLTextAreaElement, Ci.nsIDOMXULTreeElement, Ci.nsIDOMXULTextBoxElement]) || elem instanceof Window && Editor.getEditor(elem); + }, + + kill: function kill(event) { + event.stopPropagation(); + event.preventDefault(); } }, { commands: function () { diff --git a/common/content/mappings.js b/common/content/mappings.js index 124aab47..e9364c91 100644 --- a/common/content/mappings.js +++ b/common/content/mappings.js @@ -95,22 +95,20 @@ var Map = Class("Map", { /** * Execute the action for this mapping. * - * @param {string} motion The motion argument if accepted by this mapping. - * E.g. "w" for "dw" - * @param {number} count The associated count. E.g. "5" for "5j" - * @default -1 - * @param {string} argument The normal argument if accepted by this - * mapping. E.g. "a" for "ma" + * @param {object} args The arguments object for the given mapping. */ - execute: function (motion, count, argument, command) { - let args = { count: count, arg: argument, motion: motion, command: command }; + execute: function (args) { + if (!isObject(args)) // Backwards compatibility :( + args = iter(["motion", "count", "arg", "command"]) + .map(function ([i, prop]) [prop, this[i]], arguments) + .toObject() let self = this; function repeat() self.action(args) if (this.names[0] != ".") // FIXME: Kludge. mappings.repeat = repeat; - dactyl.assert(!this.executing, "Attempt to execute mapping recursively"); + dactyl.assert(!this.executing, "Attempt to execute mapping recursively: " + args.command); this.executing = true; let res = dactyl.trapErrors(repeat); this.executing = false; @@ -382,7 +380,7 @@ var Mappings = Module("mappings", { * @returns {Map} */ get: function get(mode, cmd) { - return this.hives.nth(function (h) h.get(mode, command), 0); + return this.hives.nth(function (h) h.get(mode, cmd), 0); }, /** diff --git a/common/content/modes.js b/common/content/modes.js index a32011af..023e9ca5 100644 --- a/common/content/modes.js +++ b/common/content/modes.js @@ -60,9 +60,10 @@ var Modes = Module("modes", { this.addMode("VISUAL", { char: "v", description: "Active when text is selected", + display: function () "VISUAL" + (this._extended & modes.LINE ? " LINE" : ""), bases: [this.COMMAND], ownsFocus: true, - display: function () "VISUAL" + (this._extended & modes.LINE ? " LINE" : "") + passUnknown: false }, { leave: function (stack, newMode) { if (newMode.main == modes.CARET) { @@ -98,7 +99,8 @@ var Modes = Module("modes", { char: "t", description: "Vim-like editing of input elements", bases: [this.COMMAND], - ownsFocus: true + ownsFocus: true, + passUnknown: false }); this.addMode("OUTPUT_MULTILINE", { description: "Active when the multi-line output buffer is open", @@ -432,11 +434,7 @@ var Modes = Module("modes", { get bases() this.input ? [modes.INPUT] : [modes.MAIN], - get toStringParams() [this.name], - - valueOf: function () this.id, - - count: true, + get count() !this.input, get description() this._display, @@ -450,7 +448,13 @@ var Modes = Module("modes", { input: false, - get mask() this + passUnknown: false, + + get mask() this, + + get toStringParams() [this.name], + + valueOf: function () this.id }, { _id: 0 }), diff --git a/common/modules/base.jsm b/common/modules/base.jsm index 071688eb..caaab70b 100644 --- a/common/modules/base.jsm +++ b/common/modules/base.jsm @@ -427,7 +427,7 @@ function isinstance(object, interfaces) { if (objproto.toString.call(object) === "[object " + interfaces[i] + "]") return true; } - else if ("isinstance" in object && object.isinstance !== isinstance) { + else if (typeof object === "object" && "isinstance" in object && object.isinstance !== isinstance) { if (object.isinstance(interfaces[i])) return true; } From e83708cea4c7c0519fb1891fade94b5bb03b8d53 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Sun, 23 Jan 2011 18:11:45 -0500 Subject: [PATCH 02/36] Fix bug in last commit. --HG-- branch : key-processing --- common/content/events.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/content/events.js b/common/content/events.js index 02a1b734..a9994e9c 100644 --- a/common/content/events.js +++ b/common/content/events.js @@ -312,7 +312,7 @@ var Events = Module("events", { this.addSessionListener(window, "resize", this.onResize, true); dactyl.registerObserver("modeChange", function () { - delete this.processor; + delete self.processor; }); }, From 2b6fdc861b38ef304c45267b6e03109fb73575b6 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Sun, 23 Jan 2011 18:20:11 -0500 Subject: [PATCH 03/36] Fix arbitrary limit on the number of substrings to highlight in template.highlightSubstrings. --HG-- branch : key-processing --- common/modules/template.jsm | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/common/modules/template.jsm b/common/modules/template.jsm index 1cd8764a..36847f6d 100644 --- a/common/modules/template.jsm +++ b/common/modules/template.jsm @@ -182,10 +182,12 @@ var Template = Module("Template", { str = String(str).replace(" ", "\u00a0"); let s = <>; let start = 0; - let n = 0; + let n = 0, _i; for (let [i, length, args] in iter) { - if (n++ > 50) // Prevent infinite loops. + if (i == _i || i < _i) break; + _i = i; + XML.ignoreWhitespace = false; s += <>{str.substring(start, i)}; s += highlight.apply(this, Array.concat(args || str.substr(i, length))); @@ -227,14 +229,13 @@ var Template = Module("Template", { linkifyHelp: function linkifyHelp(str, help) { let re = util.regexp( ) - (?=[[!,;./\s]|$) + (?P
 [/\s]|^)
+            (?P '[\w-]+' | :(?:[\w-]+|!) | (?:._)?<[\w-]+> )
+            (?=      [[!,;./\s]|$)
         ]]>, "g");
         return this.highlightSubstrings(str, (function () {
-            let res;
-            while ((res = re.exec(str)) && res[2].length)
-                yield [res.index + res[1].length, res[2].length];
+            for (let res in re.iterate(str))
+                yield [res.index + res.pre.length, res.tag.length];
         })(), template[help ? "HelpLink" : "helpLink"]);
     },
 

From d7efe3374b6fd526d7799ce04aa29cdcad1a9a93 Mon Sep 17 00:00:00 2001
From: Kris Maglione 
Date: Sun, 23 Jan 2011 20:07:50 -0500
Subject: [PATCH 04/36] Fix error when closing last about:blank tab on FF36.
 Closes issue #299.

--HG--
branch : key-processing
---
 common/content/buffer.js      | 7 +------
 pentadactyl/content/config.js | 2 +-
 2 files changed, 2 insertions(+), 7 deletions(-)

diff --git a/common/content/buffer.js b/common/content/buffer.js
index 21008477..6a030a2b 100644
--- a/common/content/buffer.js
+++ b/common/content/buffer.js
@@ -425,12 +425,7 @@ var Buffer = Module("buffer", {
     /**
      * @property {string} The current top-level document's URL.
      */
-    get URL() {
-        let str = String(content.location.href);
-        for (let [k, v] in Iterator(util.newURI(content.location.href)))
-            str[k] = v;
-        return str;
-    },
+    get URL() update(content.location.href, util.newURI(content.location.href)),
 
     /**
      * @property {number} The buffer's height in pixels.
diff --git a/pentadactyl/content/config.js b/pentadactyl/content/config.js
index bed5b294..3c98bca7 100644
--- a/pentadactyl/content/config.js
+++ b/pentadactyl/content/config.js
@@ -85,7 +85,7 @@ var Config = Module("config", ConfigBase, {
             if (this.tabbrowser.mTabs.length > 1)
                 this.tabbrowser.removeTab(tab);
             else {
-                if (modules.buffer.URL != "about:blank" || window.getWebNavigation().sessionHistory.count > 0) {
+                if (modules.buffer.uri.spec !== "about:blank" || window.getWebNavigation().sessionHistory.count > 0) {
                     dactyl.open("about:blank", dactyl.NEW_BACKGROUND_TAB);
                     this.tabbrowser.removeTab(tab);
                 }

From 590e8d22b5ec3169006d6fed621b438a02983183 Mon Sep 17 00:00:00 2001
From: Kris Maglione 
Date: Sun, 23 Jan 2011 20:30:16 -0500
Subject: [PATCH 05/36] Muck with event listener registration.

--HG--
branch : key-processing
---
 common/content/events.js | 458 +++++++++++++++++++--------------------
 1 file changed, 227 insertions(+), 231 deletions(-)

diff --git a/common/content/events.js b/common/content/events.js
index a9994e9c..186464aa 100644
--- a/common/content/events.js
+++ b/common/content/events.js
@@ -112,7 +112,7 @@ var ProcessorStack = Class("ProcessorStack", {
                         events.dispatch(event.originalTarget, evt, { skipmap: true, isMacro: true });
                     }
                     else if (i > 0)
-                        events.onKeyPress(event);
+                        events.events.keypress.call(events, event);
                 });
         return this.processors.length == 0;
     }
@@ -299,17 +299,8 @@ var Events = Module("events", {
         }
 
         this._activeMenubar = false;
-        this.addSessionListener(window, "DOMMenuBarActive", this.onDOMMenuBarActive, true);
-        this.addSessionListener(window, "DOMMenuBarInactive", this.onDOMMenuBarInactive, true);
-        this.addSessionListener(window, "blur", this.onBlur, true);
-        this.addSessionListener(window, "focus", this.onFocus, true);
-        this.addSessionListener(window, "keydown", this.onKeyUpOrDown, true);
-        this.addSessionListener(window, "keypress", this.onKeyPress, true);
-        this.addSessionListener(window, "keyup", this.onKeyUpOrDown, true);
-        this.addSessionListener(window, "mousedown", this.onMouseDown, true);
-        this.addSessionListener(window, "popuphidden", this.onPopupHidden, true);
-        this.addSessionListener(window, "popupshown", this.onPopupShown, true);
-        this.addSessionListener(window, "resize", this.onResize, true);
+        for (let [event, callback] in Iterator(this.events))
+            this.addSessionListener(window, event, callback, true);
 
         dactyl.registerObserver("modeChange", function () {
             delete self.processor;
@@ -507,7 +498,7 @@ var Events = Module("events", {
                     if (!evt_obj.dactylString && !evt_obj.dactylShift && !mode)
                         events.dispatch(dactyl.focusedElement || buffer.focusedFrame, event, evt);
                     else
-                        events.onKeyPress(event);
+                        events.events.keypress.call(events, event);
                 }
 
                 if (!this.feedingKeys)
@@ -908,16 +899,6 @@ var Events = Module("events", {
         return buffer.loaded;
     },
 
-    onDOMMenuBarActive: function () {
-        this._activeMenubar = true;
-        modes.add(modes.MENU);
-    },
-
-    onDOMMenuBarInactive: function () {
-        this._activeMenubar = false;
-        modes.remove(modes.MENU);
-    },
-
     /**
      * Ensures that the currently focused element is visible and blurs
      * it if it's not.
@@ -934,65 +915,240 @@ var Events = Module("events", {
         }
     },
 
-    onBlur: function onFocus(event) {
-        if (event.originalTarget instanceof Window && services.focus.activeWindow == null) {
-            // Deals with circumstances where, after the main window
-            // blurs while a collapsed frame has focus, re-activating
-            // the main window does not restore focus and we lose key
-            // input.
-            services.focus.clearFocus(window);
-            document.commandDispatcher.focusedWindow = content;
-        }
-    },
+    events: {
+        DOMMenuBarActive: function () {
+            this._activeMenubar = true;
+            modes.add(modes.MENU);
+        },
 
-    // TODO: Merge with onFocusChange
-    onFocus: function onFocus(event) {
-        let elem = event.originalTarget;
-        if (elem instanceof Element) {
+        DOMMenuBarInactive: function () {
+            this._activeMenubar = false;
+            modes.remove(modes.MENU);
+        },
+
+        blur: function onBlur(event) {
+            if (event.originalTarget instanceof Window && services.focus.activeWindow == null) {
+                // Deals with circumstances where, after the main window
+                // blurs while a collapsed frame has focus, re-activating
+                // the main window does not restore focus and we lose key
+                // input.
+                services.focus.clearFocus(window);
+                document.commandDispatcher.focusedWindow = content;
+            }
+        },
+
+        // TODO: Merge with onFocusChange
+        focus: function onFocus(event) {
+            let elem = event.originalTarget;
+            if (elem instanceof Element) {
+                let win = elem.ownerDocument.defaultView;
+
+                if (event.target instanceof Ci.nsIDOMXULTextBoxElement)
+                    for (let e = elem; e instanceof Element; e = e.parentNode)
+                        if (util.computedStyle(e).visibility !== "visible") {
+                            elem.blur();
+                            break;
+                        }
+
+                if (events.isContentNode(elem) && !buffer.focusAllowed(elem)
+                    && !(services.focus.getLastFocusMethod(win) & 0x7000)
+                    && isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement, Window])) {
+                    if (elem.frameElement)
+                        dactyl.focusContent(true);
+                    else if (!(elem instanceof Window) || Editor.getEditor(elem))
+                        elem.blur();
+                }
+            }
+        },
+
+        /*
+        onFocus: function onFocus(event) {
+            let elem = event.originalTarget;
+            if (!(elem instanceof Element))
+                return;
             let win = elem.ownerDocument.defaultView;
 
-            if (event.target instanceof Ci.nsIDOMXULTextBoxElement)
-                for (let e = elem; e instanceof Element; e = e.parentNode)
-                    if (util.computedStyle(e).visibility !== "visible") {
+            try {
+                util.dump(elem, services.focus.getLastFocusMethod(win) & (0x7000));
+                if (buffer.focusAllowed(win))
+                    win.dactylLastFocus = elem;
+                else if (isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement])) {
+                    if (win.dactylLastFocus)
+                        dactyl.focus(win.dactylLastFocus);
+                    else
                         elem.blur();
-                        break;
+                }
+            }
+            catch (e) {
+                util.dump(win, String(elem.ownerDocument), String(elem.ownerDocument && elem.ownerDocument.defaultView));
+                util.reportError(e);
+            }
+        },
+        */
+
+        // this keypress handler gets always called first, even if e.g.
+        // the command-line has focus
+        // TODO: ...help me...please...
+        keypress: function onKeyPress(event) {
+            event.dactylDefaultPrevented = event.getPreventDefault();
+
+            let duringFeed = this.duringFeed || [];
+            this.duringFeed = [];
+            try {
+                if (this.feedingEvent && [!(k in event) || event[k] === v for ([k, v] in Iterator(this.feedingEvent))].every(util.identity)) {
+                    for (let [k, v] in Iterator(this.feedingEvent))
+                        if (!(k in event))
+                            event[k] = v;
+                    this.feedingEvent = null;
+                }
+
+                let key = events.toString(event);
+                if (!key)
+                     return null;
+
+                if (modes.recording && (!this._input || !mappings.user.hasMap(modes.main, this._input.buffer + key)))
+                    events._macroKeys.push(key);
+
+                // feedingKeys needs to be separate from interrupted so
+                // we can differentiate between a recorded 
+                // interrupting whatever it's started and a real 
+                // interrupting our playback.
+                if (events.feedingKeys && !event.isMacro) {
+                    if (!event.originalTarget)
+                        util.dumpStack();
+                    if (key == "") {
+                        events.feedingKeys = false;
+                        if (modes.replaying) {
+                            modes.replaying = false;
+                            this.timeout(function () { dactyl.echomsg("Canceled playback of macro '" + this._lastMacro + "'"); }, 100);
+                        }
                     }
+                    else
+                        duringFeed.push(event);
+
+                    return Events.kill(event);
+                }
+
+                if (!this.processor) {
+                    let mode = modes.getStack(0);
+                    if (event.dactylMode)
+                        mode = Modes.StackElement(event.dactylMode);
+
+                    let ignore = false;
+                    let overrideMode = null;
+
+                    // menus have their own command handlers
+                    if (modes.extended & modes.MENU)
+                        overrideMode = modes.MENU;
+
+                    if (modes.main == modes.PASS_THROUGH)
+                        ignore = !Events.isEscape(key) && key != "";
+                    else if (modes.main == modes.QUOTE) {
+                        if (modes.getStack(1).main == modes.PASS_THROUGH) {
+                            mode.params.mainMode = modes.getStack(2).main;
+                            ignore = Events.isEscape(key);
+                        }
+                        else if (events.shouldPass(event))
+                            mode.params.mainMode = modes.getStack(1).main;
+                        else
+                            ignore = true;
+
+                        if (ignore && !Events.isEscape(key))
+                            modes.pop();
+                    }
+                    else if (!event.isMacro && !event.noremap && events.shouldPass(event))
+                        ignore = true;
+
+                    if (ignore)
+                        return null;
+
+                    if (key == "")
+                        util.interrupted = true;
+
+                    if (config.ignoreKeys[key] & mode.main)
+                        return null;
+
+                    if (overrideMode)
+                        var keyModes = array([overrideMode]);
+                    else
+                        keyModes = array([mode.params.keyModes, mode.main, mode.main.allBases]).flatten().compact();
+
+                    let hives = mappings.hives.slice(event.noremap ? -1 : 0);
+
+                    this.processor = ProcessorStack(mode, hives, keyModes);
+                }
+
+                let processor = this.processor;
+                this.processor = null;
+                if (!processor.process(event))
+                    this.processor = processor;
 
-            if (events.isContentNode(elem) && !buffer.focusAllowed(elem)
-                && !(services.focus.getLastFocusMethod(win) & 0x7000)
-                && isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement, Window])) {
-                if (elem.frameElement)
-                    dactyl.focusContent(true);
-                else if (!(elem instanceof Window) || Editor.getEditor(elem))
-                    elem.blur();
             }
-        }
-    },
-
-    /*
-    onFocus: function onFocus(event) {
-        let elem = event.originalTarget;
-        if (!(elem instanceof Element))
-            return;
-        let win = elem.ownerDocument.defaultView;
-
-        try {
-            util.dump(elem, services.focus.getLastFocusMethod(win) & (0x7000));
-            if (buffer.focusAllowed(win))
-                win.dactylLastFocus = elem;
-            else if (isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement])) {
-                if (win.dactylLastFocus)
-                    dactyl.focus(win.dactylLastFocus);
+            catch (e) {
+                dactyl.reportError(e);
+            }
+            finally {
+                [duringFeed, this.duringFeed] = [this.duringFeed, duringFeed];
+                if (this.feedingKeys)
+                    this.duringFeed = this.duringFeed.concat(duringFeed);
                 else
-                    elem.blur();
+                    for (let event in values(duringFeed))
+                        try {
+                            this.dispatch(event.originalTarget, event, event);
+                        }
+                        catch (e) {
+                            util.reportError(e);
+                        }
+            }
+        },
+
+        keyup: function onKeyUp(event) {
+            // Prevent certain sites from transferring focus to an input box
+            // before we get a chance to process our key bindings on the
+            // "keypress" event.
+
+            if (modes.main == modes.PASS_THROUGH ||
+                modes.main == modes.QUOTE
+                    && modes.getStack(1).main !== modes.PASS_THROUGH
+                    && !events.shouldPass(event) ||
+                !modes.passThrough && events.shouldPass(event))
+                return;
+
+            if (!Events.isInputElement(dactyl.focusedElement))
+                event.stopPropagation();
+        },
+        keydown: function onKeyDown(event) {
+            this.events.keyup.call(this, event);
+        },
+
+        mousedown: function onMouseDown(event) {
+            let elem = event.target;
+            let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem;
+
+            for (; win; win = win != win.parent && win.parent)
+                win.document.dactylFocusAllowed = true;
+        },
+
+        popupshown: function onPopupShown(event) {
+            if (event.originalTarget.localName !== "tooltip" && event.originalTarget.id !== "dactyl-visualbell")
+                modes.add(modes.MENU);
+        },
+
+        popuphidden: function onPopupHidden() {
+            // gContextMenu is set to NULL, when a context menu is closed
+            if (window.gContextMenu == null && !this._activeMenubar)
+                modes.remove(modes.MENU);
+        },
+
+        resize: function onResize(event) {
+            if (window.fullScreen != this._fullscreen) {
+                statusline.statusBar.removeAttribute("moz-collapsed");
+                this._fullscreen = window.fullScreen;
+                dactyl.triggerObserver("fullscreen", this._fullscreen);
+                autocommands.trigger("Fullscreen", { url: this._fullscreen ? "on" : "off", state: this._fullscreen });
             }
         }
-        catch (e) {
-            util.dump(win, String(elem.ownerDocument), String(elem.ownerDocument && elem.ownerDocument.defaultView));
-            util.reportError(e);
-        }
     },
-    */
 
     // argument "event" is deliberately not used, as i don't seem to have
     // access to the real focus target
@@ -1068,166 +1224,6 @@ var Events = Module("events", {
         }
     },
 
-    // this keypress handler gets always called first, even if e.g.
-    // the command-line has focus
-    // TODO: ...help me...please...
-    onKeyPress: function onKeyPress(event) {
-        event.dactylDefaultPrevented = event.getPreventDefault();
-
-        let duringFeed = this.duringFeed || [];
-        this.duringFeed = [];
-        try {
-            if (this.feedingEvent && [!(k in event) || event[k] === v for ([k, v] in Iterator(this.feedingEvent))].every(util.identity)) {
-                for (let [k, v] in Iterator(this.feedingEvent))
-                    if (!(k in event))
-                        event[k] = v;
-                this.feedingEvent = null;
-            }
-
-            let key = events.toString(event);
-            if (!key)
-                 return null;
-
-            if (modes.recording && (!this._input || !mappings.user.hasMap(modes.main, this._input.buffer + key)))
-                events._macroKeys.push(key);
-
-            // feedingKeys needs to be separate from interrupted so
-            // we can differentiate between a recorded 
-            // interrupting whatever it's started and a real 
-            // interrupting our playback.
-            if (events.feedingKeys && !event.isMacro) {
-                if (!event.originalTarget)
-                    util.dumpStack();
-                if (key == "") {
-                    events.feedingKeys = false;
-                    if (modes.replaying) {
-                        modes.replaying = false;
-                        this.timeout(function () { dactyl.echomsg("Canceled playback of macro '" + this._lastMacro + "'"); }, 100);
-                    }
-                }
-                else
-                    duringFeed.push(event);
-
-                return Events.kill(event);
-            }
-
-            if (!this.processor) {
-                let mode = modes.getStack(0);
-                if (event.dactylMode)
-                    mode = Modes.StackElement(event.dactylMode);
-
-                let ignore = false;
-                let overrideMode = null;
-
-                // menus have their own command handlers
-                if (modes.extended & modes.MENU)
-                    overrideMode = modes.MENU;
-
-                if (modes.main == modes.PASS_THROUGH)
-                    ignore = !Events.isEscape(key) && key != "";
-                else if (modes.main == modes.QUOTE) {
-                    if (modes.getStack(1).main == modes.PASS_THROUGH) {
-                        mode.params.mainMode = modes.getStack(2).main;
-                        ignore = Events.isEscape(key);
-                    }
-                    else if (events.shouldPass(event))
-                        mode.params.mainMode = modes.getStack(1).main;
-                    else
-                        ignore = true;
-
-                    if (ignore && !Events.isEscape(key))
-                        modes.pop();
-                }
-                else if (!event.isMacro && !event.noremap && events.shouldPass(event))
-                    ignore = true;
-
-                if (ignore)
-                    return null;
-
-                if (key == "")
-                    util.interrupted = true;
-
-                if (config.ignoreKeys[key] & mode.main)
-                    return null;
-
-                if (overrideMode)
-                    var keyModes = array([overrideMode]);
-                else
-                    keyModes = array([mode.params.keyModes, mode.main, mode.main.allBases]).flatten().compact();
-
-                let hives = mappings.hives.slice(event.noremap ? -1 : 0);
-
-                this.processor = ProcessorStack(mode, hives, keyModes);
-            }
-
-            let processor = this.processor;
-            this.processor = null;
-            if (!processor.process(event))
-                this.processor = processor;
-
-        }
-        catch (e) {
-            dactyl.reportError(e);
-        }
-        finally {
-            [duringFeed, this.duringFeed] = [this.duringFeed, duringFeed];
-            if (this.feedingKeys)
-                this.duringFeed = this.duringFeed.concat(duringFeed);
-            else
-                for (let event in values(duringFeed))
-                    try {
-                        this.dispatch(event.originalTarget, event, event);
-                    }
-                    catch (e) {
-                        util.reportError(e);
-                    }
-        }
-    },
-
-    onKeyUpOrDown: function onKeyUpOrDown(event) {
-        // Prevent certain sites from transferring focus to an input box
-        // before we get a chance to process our key bindings on the
-        // "keypress" event.
-
-        if (modes.main == modes.PASS_THROUGH ||
-            modes.main == modes.QUOTE
-                && modes.getStack(1).main !== modes.PASS_THROUGH
-                && !events.shouldPass(event) ||
-            !modes.passThrough && events.shouldPass(event))
-            return;
-
-        if (!Events.isInputElement(dactyl.focusedElement))
-            event.stopPropagation();
-    },
-
-    onMouseDown: function onMouseDown(event) {
-        let elem = event.target;
-        let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem;
-
-        for (; win; win = win != win.parent && win.parent)
-            win.document.dactylFocusAllowed = true;
-    },
-
-    onPopupShown: function onPopupShown(event) {
-        if (event.originalTarget.localName !== "tooltip" && event.originalTarget.id !== "dactyl-visualbell")
-            modes.add(modes.MENU);
-    },
-
-    onPopupHidden: function onPopupHidden() {
-        // gContextMenu is set to NULL, when a context menu is closed
-        if (window.gContextMenu == null && !this._activeMenubar)
-            modes.remove(modes.MENU);
-    },
-
-    onResize: function onResize(event) {
-        if (window.fullScreen != this._fullscreen) {
-            statusline.statusBar.removeAttribute("moz-collapsed");
-            this._fullscreen = window.fullScreen;
-            dactyl.triggerObserver("fullscreen", this._fullscreen);
-            autocommands.trigger("Fullscreen", { url: this._fullscreen ? "on" : "off", state: this._fullscreen });
-        }
-    },
-
     onSelectionChange: function onSelectionChange(event) {
         let controller = document.commandDispatcher.getControllerForCommand("cmd_copy");
         let couldCopy = controller && controller.isCommandEnabled("cmd_copy");

From 8ae02f49520c4c8df1e36577a4506dffa6a39462 Mon Sep 17 00:00:00 2001
From: Kris Maglione 
Date: Sun, 23 Jan 2011 21:55:49 -0500
Subject: [PATCH 06/36] Cleanup the hints.js rat's nest a bit.

--HG--
branch : key-processing
---
 common/content/hints.js | 870 +++++++++++++++++++---------------------
 common/modules/base.jsm |   4 +-
 2 files changed, 424 insertions(+), 450 deletions(-)

diff --git a/common/content/hints.js b/common/content/hints.js
index cdece8bc..6951d778 100644
--- a/common/content/hints.js
+++ b/common/content/hints.js
@@ -9,177 +9,102 @@
 /** @scope modules */
 /** @instance hints */
 
-var Hints = Module("hints", {
-    init: function init() {
-        const self = this;
+var HintSession = Class("HintSession", {
+    init: function init(mode, opts) {
+        opts = opts || {};
 
-        this._hintMode = null;
-        this._submode = "";             // used for extended mode, can be "o", "t", "y", etc.
-        this._hintString = "";          // the typed string part of the hint is in this string
-        this._hintNumber = 0;           // only the numerical part of the hint
-        this._usedTabKey = false;       // when we used  to select an element
-        this.prevInput = "";            // record previous user input type, "text" || "number"
-        this._extendedhintCount = null; // for the count argument of Mode#action (extended hint only)
+        // Hack.
+        if (!opts.window && modes.main == modes.OUTPUT_MULTILINE)
+            opts.window = commandline.widgets.multilineOutput.contentWindow;
 
-        this._pageHints = [];
-        this._validHints = []; // store the indices of the "hints" array with valid elements
+        this.mode = hints.modes[mode];
+        dactyl.assert(this.mode);
 
-        this._activeTimeout = null; // needed for hinttimeout > 0
+        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.hintNumber = 0;
+        this.hintString = opts.filter || "";
+        this.pageHints = [];
+        this.prevInput = "";
+        this.usedTabKey = false;
+        this.validHints = []; // store the indices of the "hints" array with valid elements
 
-        // keep track of the documents which we generated the hints for
-        // this._docs = { doc: document, start: start_index in hints[], end: end_index in hints[] }
-        this._docs = [];
+        commandline.input(UTF8(this.mode.prompt) + ": ", null, this);
+        modes.extended = modes.HINTS;
 
-        this._resizeTimer = Timer(100, 500, function () {
-            if (self._top && (modes.extended & modes.HINTS)) {
-                self._removeHints(0, true);
-                self._generate(self._top);
-                self._showHints();
-            }
-        });
-        let appContent = document.getElementById("appcontent");
-        if (appContent)
-            events.addSessionListener(appContent, "scroll", this._resizeTimer.closure.tell, false);
+        this.top = opts.window || content;
+        this.top.addEventListener("resize", hints.resizeTimer.closure.tell, true);
 
-        const Mode = Hints.Mode;
-        Mode.defaultValue("tags", function () function () options["hinttags"]);
-        Mode.prototype.__defineGetter__("xpath", function ()
-            options.get("extendedhinttags").getKey(this.name, this.tags()));
+        this.generate();
 
-        this._hintModes = {};
-        this.addMode(";", "Focus hint",                           buffer.closure.focusElement);
-        this.addMode("?", "Show information for hint",            function (elem) buffer.showElementInfo(elem));
-        this.addMode("s", "Save hint",                            function (elem) buffer.saveLink(elem, false));
-        this.addMode("f", "Focus frame",                          function (elem) dactyl.focus(elem.ownerDocument.defaultView));
-        this.addMode("F", "Focus frame or pseudo-frame",          buffer.closure.focusElement, null, isScrollable);
-        this.addMode("o", "Follow hint",                          function (elem) buffer.followLink(elem, dactyl.CURRENT_TAB));
-        this.addMode("t", "Follow hint in a new tab",             function (elem) buffer.followLink(elem, dactyl.NEW_TAB));
-        this.addMode("b", "Follow hint in a background tab",      function (elem) buffer.followLink(elem, dactyl.NEW_BACKGROUND_TAB));
-        this.addMode("w", "Follow hint in a new window",          function (elem) buffer.followLink(elem, dactyl.NEW_WINDOW));
-        this.addMode("O", "Generate an ‘:open URL’ prompt",       function (elem, loc) commandline.open(":", "open " + loc, modes.EX));
-        this.addMode("T", "Generate a ‘:tabopen URL’ prompt",     function (elem, loc) commandline.open(":", "tabopen " + loc, modes.EX));
-        this.addMode("W", "Generate a ‘:winopen URL’ prompt",     function (elem, loc) commandline.open(":", "winopen " + loc, modes.EX));
-        this.addMode("a", "Add a bookmark",                       function (elem) bookmarks.addSearchKeyword(elem));
-        this.addMode("S", "Add a search keyword",                 function (elem) bookmarks.addSearchKeyword(elem));
-        this.addMode("v", "View hint source",                     function (elem, loc) buffer.viewSource(loc, false));
-        this.addMode("V", "View hint source in external editor",  function (elem, loc) buffer.viewSource(loc, true));
-        this.addMode("y", "Yank hint location",                   function (elem, loc) dactyl.clipboardWrite(loc, true));
-        this.addMode("Y", "Yank hint description",                function (elem) dactyl.clipboardWrite(elem.textContent || "", true));
-        this.addMode("c", "Open context menu",                    function (elem) buffer.openContextMenu(elem));
-        this.addMode("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));
+        this.show();
 
-        function isScrollable(elem) isinstance(elem, [HTMLFrameElement, HTMLIFrameElement]) ||
-            Buffer.isScrollable(elem, 0, true) || Buffer.isScrollable(elem, 0, false);
+        if (this.validHints.length == 0) {
+            dactyl.beep();
+            modes.pop();
+        }
+        else if (this.validHints.length == 1 && !this.continue)
+            this.process(false);
+        else // Ticket #185
+            this.checkUnique();
+    },
+
+    get extended() modes.HINTS,
+
+    checkUnique: function _checkUnique() {
+        if (this.hintNumber == 0)
+            return;
+        dactyl.assert(this.hintNumber <= this.validHints.length);
+
+        // if we write a numeric part like 3, but we have 45 hints, only follow
+        // the hint after a timeout, as the user might have wanted to follow link 34
+        if (this.hintNumber > 0 && this.hintNumber * this.hintKeys.length <= this.validHints.length) {
+            let timeout = options["hinttimeout"];
+            if (timeout > 0)
+                this.activeTimeout = this.timeout(function () {
+                    this.process(true);
+                }, timeout);
+        }
+        else // we have a unique hint
+            this.process(true);
     },
 
     /**
      * Clear any timeout which might be active after pressing a number
      */
     clearTimeout: function () {
-        if (this._activeTimeout)
-            this._activeTimeout.cancel();
-        this._activeTimeout = null;
+        if (this.activeTimeout)
+            this.activeTimeout.cancel();
+        this.activeTimeout = null;
     },
 
     /**
-     * Reset hints, so that they can be cleanly used again.
+     * Returns the hint string for a given number based on the values of
+     * the 'hintkeys' option.
+     *
+     * @param {number} n The number to transform.
+     * @returns {string}
      */
-    _reset: function _reset(slight) {
-        if (!slight) {
-            this.__reset();
-            this.prevInput = "";
-            this.escNumbers = false;
-            this._usedTabKey = false;
-            this._hintNumber = 0;
-            this._hintString = "";
-            statusline.updateInputBuffer("");
-            commandline.widgets.command = "";
+    getHintString: function getHintString(n) {
+        let res = [], len = this.hintKeys.length;
+        do {
+            res.push(this.hintKeys[n % len]);
+            n = Math.floor(n / len);
         }
-        this._pageHints = [];
-        this._validHints = [];
-        this._docs = [];
-        this.clearTimeout();
-    },
-    __reset: function __reset() {
-        if (!this._usedTabKey)
-            this._hintNumber = 0;
-        if (this._continue && this._validHints.length <= 1) {
-            this._hintString = "";
-            commandline.widgets.command = this._hintString;
-            this._showHints();
-        }
-        this._updateStatusline();
+        while (n > 0);
+        return res.reverse().join("");
     },
 
     /**
-     * Display the current status to the user.
+     * Returns true if the given key string represents a
+     * pseudo-hint-number.
+     *
+     * @param {string} key The key to test.
+     * @returns {boolean} Whether the key represents a hint number.
      */
-    _updateStatusline: function _updateStatusline() {
-        statusline.updateInputBuffer((hints.escNumbers ? options["mapleader"] : "") +
-                                     (this._hintNumber ? this.getHintString(this._hintNumber) : ""));
-    },
-
-    /**
-     * Get a hint for "input", "textarea" and "select".
-     *
-     * Tries to use