1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
11 var Modes = Module("modes", {
12 init: function init() {
14 this._main = 1; // NORMAL
15 this._extended = 0; // NONE
17 this._lastShown = null;
19 this._passNextKey = false;
20 this._passAllKeys = false;
21 this._recording = false;
22 this._replaying = false; // playing a macro
24 this._modeStack = update([], {
27 throw Error("Trying to pop last element in mode stack");
28 return pop.superapply(this, arguments);
36 this.boundProperties = {};
38 this.addMode("BASE", {
40 description: "The base mode for all other modes",
44 this.addMode("MAIN", {
46 description: "The base mode for most other modes",
50 this.addMode("COMMAND", {
52 description: "The base mode for most modes which accept commands rather than input"
55 this.addMode("NORMAL", {
57 description: "Active when nothing is focused",
60 this.addMode("VISUAL", {
62 description: "Active when text is selected",
63 display: function () "VISUAL" + (this._extended & modes.LINE ? " LINE" : ""),
64 bases: [this.COMMAND],
67 leave: function (stack, newMode) {
68 if (newMode.main == modes.CARET) {
69 let selection = content.getSelection();
70 if (selection && !selection.isCollapsed)
71 selection.collapseToStart();
74 editor.unselectText();
77 this.addMode("CARET", {
78 description: "Active when the caret is visible in the web content",
82 get pref() prefs.get("accessibility.browsewithcaret"),
83 set pref(val) prefs.set("accessibility.browsewithcaret", val),
85 enter: function (stack) {
86 if (stack.pop && !this.pref)
88 else if (!stack.pop && !this.pref)
92 leave: function (stack) {
93 if (!stack.push && this.pref)
97 this.addMode("TEXT_EDIT", {
99 description: "Vim-like editing of input elements",
100 bases: [this.COMMAND],
103 onKeyPress: function (eventList) {
104 const KILL = false, PASS = true;
107 if (eventList[0].charCode || /^<(?:.-)*(?:BS|Del|C-h|C-w|C-u|C-k)>$/.test(events.toString(eventList[0]))) {
114 this.addMode("OUTPUT_MULTILINE", {
115 description: "Active when the multi-line output buffer is open",
119 this.addMode("INPUT", {
121 description: "The base mode for input modes, including Insert and Command Line",
125 this.addMode("INSERT", {
127 description: "Active when an input element is focused",
131 this.addMode("AUTOCOMPLETE", {
132 description: "Active when an input autocomplete pop-up is active",
133 display: function () "AUTOCOMPLETE (insert)",
137 this.addMode("EMBED", {
138 description: "Active when an <embed> or <object> element is focused",
144 this.addMode("PASS_THROUGH", {
145 description: "All keys but <C-v> are ignored by " + config.appName,
151 this.addMode("QUOTE", {
152 description: "The next key sequence is ignored by " + config.appName + ", unless in Pass Through mode",
157 (modes.getStack(1).main == modes.PASS_THROUGH
158 ? (modes.getStack(2).main.display() || modes.getStack(2).main.name)
159 : "PASS THROUGH") + " (next)"
162 preExecute: function (map) { if (modes.main == modes.QUOTE && map.name !== "<C-v>") modes.pop(); },
163 postExecute: function (map) { if (modes.main == modes.QUOTE && map.name === "<C-v>") modes.pop(); },
164 onKeyPress: function (events) { if (modes.main == modes.QUOTE) modes.pop(); }
166 this.addMode("IGNORE", { hidden: true }, {
167 onKeyPress: function (events) Events.KILL,
172 this.addMode("MENU", {
173 description: "Active when a menu or other pop-up is open",
178 leave: function leave(stack) {
179 util.timeout(function () {
180 if (stack.pop && !modes.main.input && Events.isInputElement(dactyl.focusedElement))
181 modes.push(modes.INSERT);
186 this.addMode("LINE", {
187 extended: true, hidden: true
190 this.push(this.NORMAL, 0, {
191 enter: function (stack, prev) {
192 if (prefs.get("accessibility.browsewithcaret"))
193 prefs.set("accessibility.browsewithcaret", false);
195 statusline.updateStatus();
196 if (!stack.fromFocus && prev.main.ownsFocus)
197 dactyl.focusContent(true);
198 if (prev.main == modes.NORMAL) {
199 dactyl.focusContent(true);
200 for (let frame in values(buffer.allFrames())) {
201 // clear any selection made
202 let selection = frame.getSelection();
203 if (selection && !selection.isCollapsed)
204 selection.collapseToStart();
211 function makeTree() {
212 let list = modes.all.filter(function (m) m.name !== m.description);
216 for (let mode in values(list))
217 tree[mode.name] = {};
219 for (let mode in values(list))
220 for (let base in values(mode.bases))
221 tree[base.name][mode.name] = tree[mode.name];
223 let roots = iter([m.name, tree[m.name]] for (m in values(list)) if (!m.bases.length)).toObject();
225 default xml namespace = NS;
227 XML.ignoreWhitespace = XML.prettyPrinting = false;
229 let res = <ul dactyl:highlight="Dense" xmlns:dactyl={NS}/>;
230 Object.keys(obj).sort().forEach(function (name) {
231 let mode = modes.getMode(name);
232 res.* += <li><em>{mode.displayName}</em>: {mode.description}{
245 util.timeout(function () {
246 // Waits for the add-on to become available, if necessary.
250 services["dactyl:"].pages["modes.dtd"] = services["dactyl:"].pages["modes.dtd"]();
253 services["dactyl:"].pages["modes.dtd"] = function () [null,
254 util.makeDTD(iter({ "modes.tree": makeTree() },
257 cleanup: function cleanup() {
261 _getModeMessage: function _getModeMessage() {
262 // when recording a macro
265 macromode = "recording";
266 else if (this.replaying)
267 macromode = "replaying";
269 let val = this._modeMap[this._main].display();
271 return "-- " + val + " --" + macromode;
277 __iterator__: function __iterator__() array.iterValues(this.all),
279 get all() this._modes.slice(),
281 get mainModes() (mode for ([k, mode] in Iterator(modes._modeMap)) if (!mode.extended && mode.name == k)),
283 get mainMode() this._modeMap[this._main],
285 get passThrough() !!(this.main & (this.PASS_THROUGH|this.QUOTE)) ^ (this.getStack(1).main === this.PASS_THROUGH),
287 get topOfStack() this._modeStack[this._modeStack.length - 1],
289 addMode: function addMode(name, options, params) {
290 let mode = Modes.Mode(name, options, params);
294 this.modeChars[mode.char] = (this.modeChars[mode.char] || []).concat(mode);
295 this._modeMap[name] = mode;
296 this._modeMap[mode] = mode;
298 this._modes.push(mode);
300 this._mainModes.push(mode);
302 dactyl.triggerObserver("modes.add", mode);
305 dumpStack: function dumpStack() {
306 util.dump("Mode stack:");
307 for (let [i, mode] in array.iterItems(this._modeStack))
308 util.dump(" " + i + ": " + mode);
311 getMode: function getMode(name) this._modeMap[name],
313 getStack: function getStack(idx) this._modeStack[this._modeStack.length - idx - 1] || this._modeStack[0],
315 get stack() this._modeStack.slice(),
317 getCharModes: function getCharModes(chr) (this.modeChars[chr] || []).slice(),
319 have: function have(mode) this._modeStack.some(function (m) isinstance(m.main, mode)),
321 matchModes: function matchModes(obj)
322 this._modes.filter(function (mode) Object.keys(obj)
323 .every(function (k) obj[k] == (mode[k] || false))),
325 // show the current mode string in the command line
326 show: function show() {
331 if (options.get("showmode").getKey(this.main.allBases, false))
332 msg = this._getModeMessage();
334 if (msg || loaded.commandline)
335 commandline.widgets.mode = msg || null;
338 remove: function remove(mode, covert) {
339 if (covert && this.topOfStack.main != mode) {
340 util.assert(mode != this.NORMAL);
341 for (let m; m = array.nth(this.modeStack, function (m) m.main == mode, 0);)
342 this._modeStack.splice(this._modeStack.indexOf(m));
344 else if (this.stack.some(function (m) m.main == mode)) {
351 delay: function delay(callback, self) { this.delayed.push([callback, self]); },
353 save: function save(id, obj, prop, test) {
354 if (!(id in this.boundProperties))
355 for (let elem in array.iterValues(this._modeStack))
356 elem.saved[id] = { obj: obj, prop: prop, value: obj[prop], test: test };
357 this.boundProperties[id] = { obj: Cu.getWeakReference(obj), prop: prop, test: test };
362 // helper function to set both modes in one go
363 set: function set(mainMode, extendedMode, params, stack) {
364 var delayed, oldExtended, oldMain, prev, push;
367 dactyl.reportError(Error(_("mode.recursiveSet")), true);
371 params = params || this.getMode(mainMode || this.main).params;
373 if (!stack && mainMode != null && this._modeStack.length > 1)
376 this.withSavedValues(["inSet"], function set() {
379 oldMain = this._main, oldExtended = this._extended;
381 if (extendedMode != null)
382 this._extended = extendedMode;
383 if (mainMode != null) {
384 this._main = mainMode;
386 this._extended = this.NONE;
389 if (stack && stack.pop && stack.pop.params.leave)
390 dactyl.trapErrors("leave", stack.pop.params,
391 stack, this.topOfStack);
393 push = mainMode != null && !(stack && stack.pop) &&
394 Modes.StackElement(this._main, this._extended, params, {});
396 if (push && this.topOfStack) {
397 if (this.topOfStack.params.leave)
398 dactyl.trapErrors("leave", this.topOfStack.params,
399 { push: push }, push);
401 for (let [id, { obj, prop, test }] in Iterator(this.boundProperties)) {
403 delete this.boundProperties[id];
405 this.topOfStack.saved[id] = { obj: obj.get(), prop: prop, value: obj.get()[prop], test: test };
409 delayed = this.delayed;
412 prev = stack && stack.pop || this.topOfStack;
414 this._modeStack.push(push);
417 if (stack && stack.pop)
418 for (let { obj, prop, value, test } in values(this.topOfStack.saved))
419 if (!test || !test(stack, prev))
420 dactyl.trapErrors(function () { obj[prop] = value });
424 if (this.topOfStack.params.enter && prev)
425 dactyl.trapErrors("enter", this.topOfStack.params,
426 push ? { push: push } : stack || {},
429 delayed.forEach(function ([fn, self]) dactyl.trapErrors(fn, self));
431 dactyl.triggerObserver("modes.change", [oldMain, oldExtended], [this._main, this._extended], stack);
435 onCaretChange: function onPrefChange(value) {
436 if (!value && modes.main == modes.CARET)
438 if (value && modes.main == modes.NORMAL)
439 modes.push(modes.CARET);
442 push: function push(mainMode, extendedMode, params) {
443 this.set(mainMode, extendedMode, params, { push: this.topOfStack });
446 pop: function pop(mode, args) {
447 while (this._modeStack.length > 1 && this.main != mode) {
448 let a = this._modeStack.pop();
449 this.set(this.topOfStack.main, this.topOfStack.extended, this.topOfStack.params,
458 replace: function replace(mode, oldMode, args) {
459 while (oldMode && this._modeStack.length > 1 && this.main != oldMode)
462 if (this._modeStack.length > 1)
463 this.set(mode, null, null,
464 update({ push: this.topOfStack, pop: this._modeStack.pop() },
469 reset: function reset() {
470 if (this._modeStack.length == 1 && this.topOfStack.params.enter)
471 this.topOfStack.params.enter({}, this.topOfStack);
472 while (this._modeStack.length > 1)
476 get recording() this._recording,
477 set recording(value) { this._recording = value; this.show(); },
479 get replaying() this._replaying,
480 set replaying(value) { this._replaying = value; this.show(); },
482 get main() this._main,
483 set main(value) { this.set(value); },
485 get extended() this._extended,
486 set extended(value) { this.set(null, value); }
488 Mode: Class("Mode", {
489 init: function init(name, options, params) {
491 util.assert(options.bases.every(function (m) m instanceof this, this.constructor),
492 _("mode.invalidBases"), true);
495 id: 1 << Modes.Mode._id++,
502 description: Messages.Localized(""),
504 displayName: Class.memoize(function () this.name.split("_").map(util.capitalize).join(" ")),
506 isinstance: function isinstance(obj)
507 this.allBases.indexOf(obj) >= 0 || callable(obj) && this instanceof obj,
509 allBases: Class.memoize(function () {
510 let seen = {}, res = [], queue = [this].concat(this.bases);
511 for (let mode in array.iterValues(queue))
512 if (!Set.add(seen, mode)) {
514 queue.push.apply(queue, mode.bases);
519 get bases() this.input ? [modes.INPUT] : [modes.MAIN],
521 get count() !this.insert,
523 _display: Class.memoize(function _display() this.name.replace("_", " ", "g")),
525 display: function display() this._display,
531 input: Class.memoize(function input() this.insert || this.bases.length && this.bases.some(function (b) b.input)),
533 insert: Class.memoize(function insert() this.bases.length && this.bases.some(function (b) b.insert)),
535 ownsFocus: Class.memoize(function ownsFocus() this.bases.length && this.bases.some(function (b) b.ownsFocus)),
537 passEvent: function passEvent(event) this.input && event.charCode && !(event.ctrlKey || event.altKey || event.metaKey),
539 passUnknown: Class.memoize(function () options.get("passunknown").getKey(this.name)),
543 get toStringParams() [this.name],
545 valueOf: function valueOf() this.id
549 StackElement: (function () {
550 const StackElement = Struct("main", "extended", "params", "saved");
551 StackElement.className = "Modes.StackElement";
552 StackElement.defaultValue("params", function () this.main.params);
554 update(StackElement.prototype, {
555 get toStringParams() !loaded.modes ? this.main.name : [
557 <>({ modes.all.filter(function (m) this.extended & m, this).map(function (m) m.name).join("|") })</>
563 boundProperty: function BoundProperty(desc) {
564 let id = this.cacheId++;
568 return Class.Property(update({
571 init: function bound_init(prop) update(this, {
572 get: function bound_get() {
574 var val = desc.get.call(this, value);
575 return val === undefined ? value : val;
577 set: function bound_set(val) {
578 modes.save(id, this, prop, desc.test);
580 value = desc.set.call(this, val);
581 value = !desc.set || value === undefined ? val : value;
587 mappings: function initMappings() {
588 mappings.add([modes.BASE, modes.NORMAL],
590 "Return to Normal mode",
591 function () { modes.reset(); });
593 mappings.add([modes.INPUT, modes.COMMAND, modes.PASS_THROUGH, modes.QUOTE],
595 "Return to the previous mode",
596 function () { modes.pop(); });
598 mappings.add([modes.MENU], ["<C-c>"],
600 function () { modes.pop(); });
602 mappings.add([modes.MENU], ["<Esc>"],
603 "Close the current popup",
604 function () { return Events.PASS_THROUGH; });
606 mappings.add([modes.MENU], ["<C-[>"],
607 "Close the current popup",
608 function () { events.feedkeys("<Esc>"); });
610 options: function initOptions() {
612 completer: function completer(context, extra) {
613 if (extra.value && context.filter[0] == "!")
615 return completer.superapply(this, arguments);
618 getKey: function getKey(val, default_) {
620 return (array.nth(this.value, function (v) val.some(function (m) m.name === v.mode), 0)
621 || { result: default_ }).result;
623 return Set.has(this.valueMap, val) ? this.valueMap[val] : default_;
626 setter: function (vals) {
627 modes.all.forEach(function (m) { delete m.passUnknown });
629 vals = vals.map(function (v) update(new String(v.toLowerCase()), {
630 mode: v.replace(/^!/, "").toUpperCase(),
634 this.valueMap = values(vals).map(function (v) [v.mode, v.result]).toObject();
638 validator: function validator(vals) vals.map(function (v) v.replace(/^!/, "")).every(Set.has(this.values)),
640 get values() array.toObject([[m.name.toLowerCase(), m.description] for (m in values(modes._modes)) if (!m.hidden)])
643 options.add(["passunknown", "pu"],
644 "Pass through unknown keys in these modes",
645 "stringlist", "!text_edit,base",
648 options.add(["showmode", "smd"],
649 "Show the current mode in the command line when it matches this expression",
650 "stringlist", "caret,output_multiline,!normal,base",
653 prefs: function initPrefs() {
654 prefs.watch("accessibility.browsewithcaret", function () modes.onCaretChange.apply(modes, arguments));
658 // vim: set fdm=marker sw=4 ts=4 et: