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-2014 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 = Modes.ModeStack([]);
30 this.boundProperties = {};
32 this.addMode("BASE", {
34 description: "The base mode for all other modes",
38 this.addMode("MAIN", {
40 description: "The base mode for most other modes",
44 this.addMode("COMMAND", {
46 description: "The base mode for most modes which accept commands rather than input"
49 this.addMode("NORMAL", {
51 description: "Active when nothing is focused",
54 this.addMode("CARET", {
56 description: "Active when the caret is visible in the web content",
60 get pref() prefs.get("accessibility.browsewithcaret"),
61 set pref(val) prefs.set("accessibility.browsewithcaret", val),
63 enter: function (stack) {
64 if (stack.pop && !this.pref)
66 else if (!stack.pop && !this.pref)
72 leave: function (stack) {
73 if (!stack.push && this.pref)
78 this.addMode("INPUT", {
80 description: "The base mode for input modes, including Insert and Command Line",
85 this.addMode("EMBED", {
86 description: "Active when an <embed> or <object> element is focused",
93 this.addMode("PASS_THROUGH", {
94 description: "All keys but <C-v> are ignored by " + config.appName,
100 this.addMode("QUOTE", {
101 description: "The next key sequence is ignored by " + config.appName + ", unless in Pass Through mode",
106 (modes.getStack(1).main == modes.PASS_THROUGH
107 ? (modes.getStack(2).main.display() || modes.getStack(2).main.name)
108 : "PASS THROUGH") + " (next)"
111 preExecute: function (map) { if (modes.main == modes.QUOTE && map.name !== "<C-v>") modes.pop(); },
112 postExecute: function (map) { if (modes.main == modes.QUOTE && map.name === "<C-v>") modes.pop(); },
113 onKeyPress: function (events) { if (modes.main == modes.QUOTE) modes.pop(); }
115 this.addMode("IGNORE", { hidden: true }, {
116 onKeyPress: function (events_) {
117 if (events.isCancelKey(DOM.Event.stringify(events_[0])))
125 this.addMode("MENU", {
126 description: "Active when a menu or other pop-up is open",
131 leave: function leave(stack) {
132 util.timeout(function () {
133 if (stack.pop && !modes.main.input && Events.isInputElement(dactyl.focusedElement))
134 modes.push(modes.INSERT);
139 this.addMode("LINE", {
140 extended: true, hidden: true
143 this.push(this.NORMAL, 0, {
144 enter: function (stack, prev) {
145 if (prefs.get("accessibility.browsewithcaret"))
146 prefs.set("accessibility.browsewithcaret", false);
148 statusline.updateStatus();
149 if (!stack.fromFocus && prev.main.ownsFocus)
150 dactyl.focusContent(true);
151 if (prev.main == modes.NORMAL) {
152 dactyl.focusContent(true);
153 for (let frame in values(buffer.allFrames())) {
154 // clear any selection made
155 let selection = frame.getSelection();
156 if (selection && !selection.isCollapsed)
157 selection.collapseToStart();
165 cleanup: function cleanup() {
170 "io.source": function ioSource(context, file, modTime) {
171 cache.flushEntry("modes.dtd", modTime);
175 _getModeMessage: function _getModeMessage() {
176 // when recording a macro
179 macromode = "recording " + this.recording + " ";
180 else if (this.replaying)
181 macromode = "replaying";
183 if (!options.get("showmode").getKey(this.main.allBases, false))
186 let modeName = this._modeMap[this._main].display();
191 macromode = " " + macromode;
192 return "-- " + modeName + " --" + macromode;
197 __iterator__: function __iterator__() array.iterValues(this.all),
199 get all() this._modes.slice(),
201 get mainModes() (mode for ([k, mode] in Iterator(modes._modeMap)) if (!mode.extended && mode.name == k)),
203 get mainMode() this._modeMap[this._main],
205 get passThrough() !!(this.main & (this.PASS_THROUGH|this.QUOTE)) ^ (this.getStack(1).main === this.PASS_THROUGH),
207 get topOfStack() this._modeStack[this._modeStack.length - 1],
209 addMode: function addMode(name, options, params) {
210 let mode = Modes.Mode(name, options, params);
214 this.modeChars[mode.char] = (this.modeChars[mode.char] || []).concat(mode);
215 this._modeMap[name] = mode;
216 this._modeMap[mode] = mode;
218 this._modes.push(mode);
220 this._mainModes.push(mode);
222 dactyl.triggerObserver("modes.add", mode);
225 removeMode: function removeMode(mode) {
227 if (this[mode.name] == mode)
228 delete this[mode.name];
229 if (this._modeMap[mode.name] == mode)
230 delete this._modeMap[mode.name];
231 if (this._modeMap[mode.mode] == mode)
232 delete this._modeMap[mode.mode];
234 this._mainModes = this._mainModes.filter(m => m != mode);
237 dumpStack: function dumpStack() {
238 util.dump("Mode stack:");
239 for (let [i, mode] in array.iterItems(this._modeStack))
240 util.dump(" " + i + ": " + mode);
243 getMode: function getMode(name) this._modeMap[name],
245 getStack: function getStack(idx) this._modeStack[this._modeStack.length - idx - 1] || this._modeStack[0],
247 get stack() this._modeStack.slice(),
249 getCharModes: function getCharModes(chr) (this.modeChars[chr] || []).slice(),
251 have: function have(mode) this._modeStack.some(m => isinstance(m.main, mode)),
253 matchModes: function matchModes(obj)
254 this._modes.filter(mode => Object.keys(obj)
255 .every(k => obj[k] == (mode[k] || false))),
257 // show the current mode string in the command line
258 show: function show() {
259 if (!loaded.has("modes"))
262 let msg = this._getModeMessage();
264 if (msg || loaded.has("commandline"))
265 commandline.widgets.mode = msg || null;
268 remove: function remove(mode, covert) {
269 if (covert && this.topOfStack.main != mode) {
270 util.assert(mode != this.NORMAL);
272 this._modeStack = Modes.ModeStack(
273 this._modeStack.filter(m => m.main != mode));
275 else if (this.stack.some(m => m.main == mode)) {
282 delay: function delay(callback, self) { this.delayed.push([callback, self]); },
284 save: function save(id, obj, prop, test) {
285 if (!(id in this.boundProperties))
286 for (let elem in array.iterValues(this._modeStack))
287 elem.saved[id] = { obj: obj, prop: prop, value: obj[prop], test: test };
288 this.boundProperties[id] = { obj: util.weakReference(obj), prop: prop, test: test };
293 set: function set(mainMode, extendedMode, params, stack) {
294 var delayed, oldExtended, oldMain, prev, push;
297 dactyl.reportError(Error(_("mode.recursiveSet")), true);
301 params = params || Object.create(this.getMode(mainMode || this.main).params);
303 if (!stack && mainMode != null && this._modeStack.length > 1)
306 this.withSavedValues(["inSet"], function set() {
309 oldMain = this._main, oldExtended = this._extended;
311 if (extendedMode != null)
312 this._extended = extendedMode;
313 if (mainMode != null) {
314 this._main = mainMode;
316 this._extended = this.NONE;
319 if (stack && stack.pop && stack.pop.params.leave)
320 dactyl.trapErrors("leave", stack.pop.params,
321 stack, this.topOfStack);
323 push = mainMode != null && !(stack && stack.pop) &&
324 Modes.StackElement(this._main, this._extended, params, {});
326 if (push && this.topOfStack) {
327 if (this.topOfStack.params.leave)
328 dactyl.trapErrors("leave", this.topOfStack.params,
329 { push: push }, push);
331 for (let [id, { obj, prop, test }] in Iterator(this.boundProperties)) {
334 delete this.boundProperties[id];
336 this.topOfStack.saved[id] = { obj: obj, prop: prop, value: obj[prop], test: test };
340 delayed = this.delayed;
343 prev = stack && stack.pop || this.topOfStack;
345 this._modeStack.push(push);
348 if (stack && stack.pop)
349 for (let { obj, prop, value, test } in values(this.topOfStack.saved))
350 if (!test || !test(stack, prev))
351 dactyl.trapErrors(function () { obj[prop] = value });
355 if (this.topOfStack.params.enter && prev)
356 dactyl.trapErrors("enter", this.topOfStack.params,
357 push ? { push: push } : stack || {},
360 delayed.forEach(([fn, self]) => {
361 dactyl.trapErrors(fn, self);
364 dactyl.triggerObserver("modes.change", [oldMain, oldExtended], [this._main, this._extended], stack);
368 onCaretChange: function onPrefChange(value) {
369 if (!value && modes.main == modes.CARET)
371 if (value && modes.main == modes.NORMAL)
372 modes.push(modes.CARET);
375 push: function push(mainMode, extendedMode, params) {
376 if (this.main == this.IGNORE)
379 this.set(mainMode, extendedMode, params, { push: this.topOfStack });
382 pop: function pop(mode, args) {
383 while (this._modeStack.length > 1 && this.main != mode) {
384 let a = this._modeStack.pop();
385 this.set(this.topOfStack.main, this.topOfStack.extended, this.topOfStack.params,
386 update({ pop: a }, args));
393 replace: function replace(mode, oldMode, args) {
394 while (oldMode && this._modeStack.length > 1 && this.main != oldMode)
397 if (this._modeStack.length > 1)
398 this.set(mode, null, null,
399 update({ push: this.topOfStack, pop: this._modeStack.pop() },
404 reset: function reset() {
405 if (this._modeStack.length == 1 && this.topOfStack.params.enter)
406 this.topOfStack.params.enter({}, this.topOfStack);
407 while (this._modeStack.length > 1)
411 get recording() this._recording,
412 set recording(value) { this._recording = value; this.show(); },
414 get replaying() this._replaying,
415 set replaying(value) { this._replaying = value; this.show(); },
417 get main() this._main,
418 set main(value) { this.set(value); },
420 get extended() this._extended,
421 set extended(value) { this.set(null, value); }
423 Mode: Class("Mode", {
424 init: function init(name, options, params) {
426 util.assert(options.bases.every(m => m instanceof this.constructor),
427 _("mode.invalidBases"), false);
430 id: 1 << Modes.Mode._id++,
437 description: Messages.Localized(""),
439 displayName: Class.Memoize(function () this.name.split("_").map(util.capitalize).join(" ")),
441 isinstance: function isinstance(obj)
442 this.allBases.indexOf(obj) >= 0 || callable(obj) && this instanceof obj,
444 allBases: Class.Memoize(function () {
445 let seen = RealSet(),
447 queue = [this].concat(this.bases);
448 for (let mode in array.iterValues(queue))
449 if (!seen.add(mode)) {
451 queue.push.apply(queue, mode.bases);
456 get bases() this.input ? [modes.INPUT] : [modes.MAIN],
458 get count() !this.insert,
460 _display: Class.Memoize(function _display() this.name.replace("_", " ", "g")),
462 display: function display() this._display,
468 input: Class.Memoize(function input() this.insert || this.bases.length && this.bases.some(b => b.input)),
470 insert: Class.Memoize(function insert() this.bases.length && this.bases.some(b => b.insert)),
472 ownsFocus: Class.Memoize(function ownsFocus() this.bases.length && this.bases.some(b => b.ownsFocus)),
474 passEvent: function passEvent(event) this.input && event.charCode && !(event.ctrlKey || event.altKey || event.metaKey),
476 passUnknown: Class.Memoize(function () options.get("passunknown").getKey(this.name)),
480 get toStringParams() [this.name],
482 valueOf: function valueOf() this.id
486 ModeStack: function ModeStack(array)
488 pop: function pop() {
489 if (this.length <= 1)
490 throw Error("Trying to pop last element in mode stack");
491 return pop.superapply(this, arguments);
494 StackElement: (function () {
495 const StackElement = Struct("main", "extended", "params", "saved");
496 StackElement.className = "Modes.StackElement";
497 StackElement.defaultValue("params", function () this.main.params);
499 update(StackElement.prototype, {
500 get toStringParams() !loaded.has("modes") ? [this.main.name] : [
502 ["(", modes.all.filter(m => this.extended & m)
511 boundProperty: function BoundProperty(desc={}) {
512 let id = this.cacheId++;
515 return Class.Property(update({
518 init: function bound_init(prop) update(this, {
519 get: function bound_get() {
521 var val = desc.get.call(this, value);
522 return val === undefined ? value : val;
524 set: function bound_set(val) {
525 modes.save(id, this, prop, desc.test);
527 value = desc.set.call(this, val);
528 value = !desc.set || value === undefined ? val : value;
534 cache: function initCache() {
535 function makeTree() {
536 let list = modes.all.filter(m => m.name !== m.description);
540 for (let mode in values(list))
541 tree[mode.name] = {};
543 for (let mode in values(list))
544 for (let base in values(mode.bases))
545 tree[base.name][mode.name] = tree[mode.name];
547 let roots = iter([m.name, tree[m.name]]
548 for (m in values(list))
549 if (!m.bases.length)).toObject();
552 let res = ["ul", { "dactyl:highlight": "Dense" }];
553 Object.keys(obj).sort().forEach(function (name) {
554 let mode = modes.getMode(name);
556 ["em", {}, mode.displayName],
557 ": ", mode.description,
569 cache.register("modes.dtd",
570 () => util.makeDTD(iter({ "modes.tree": makeTree() },
574 mappings: function initMappings() {
575 mappings.add([modes.BASE, modes.NORMAL],
577 "Return to Normal mode",
578 function () { modes.reset(); });
580 mappings.add([modes.INPUT, modes.COMMAND, modes.OPERATOR, modes.PASS_THROUGH, modes.QUOTE],
582 "Return to the previous mode",
583 function () { modes.pop(null, { fromEscape: true }); });
585 mappings.add([modes.AUTOCOMPLETE, modes.MENU], ["<C-c>"],
586 "Leave Autocomplete or Menu mode",
587 function () { modes.pop(); });
589 mappings.add([modes.MENU], ["<Esc>"],
590 "Close the current popup",
592 if (events.popups.active.length)
593 return Events.PASS_THROUGH;
597 mappings.add([modes.MENU], ["<C-[>"],
598 "Close the current popup",
599 function () { events.feedkeys("<Esc>"); });
601 options: function initOptions() {
603 completer: function completer(context, extra) {
604 if (extra.value && context.filter[0] == "!")
606 return completer.superapply(this, arguments);
609 getKey: function getKey(val, default_) {
611 return (this.value.find(v => val.some(m => m.name === v.mode))
612 || { result: default_ }).result;
614 return hasOwnProperty(this.valueMap, val) ? this.valueMap[val] : default_;
617 setter: function (vals) {
618 modes.all.forEach(function (m) { delete m.passUnknown; });
620 vals = vals.map(v => update(new String(v.toLowerCase()),
622 mode: v.replace(/^!/, "").toUpperCase(),
626 this.valueMap = values(vals).map(v => [v.mode, v.result])
631 validator: function validator(vals) vals.map(v => v.replace(/^!/, ""))
632 .every(k => hasOwnProperty(this.values, k)),
634 get values() array.toObject([[m.name.toLowerCase(), m.description]
635 for (m in values(modes._modes)) if (!m.hidden)])
638 options.add(["passunknown", "pu"],
639 "Pass through unknown keys in these modes",
640 "stringlist", "!text_edit,!visual,base",
643 options.add(["showmode", "smd"],
644 "Show the current mode in the command line when it matches this expression",
645 "stringlist", "caret,output_multiline,!normal,base,operator",
648 prefs: function initPrefs() {
649 prefs.watch("accessibility.browsewithcaret",
650 function () { modes.onCaretChange.apply(modes, arguments); });
654 // vim: set fdm=marker sw=4 sts=4 ts=8 et: