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],
68 leave: function (stack, newMode) {
69 if (newMode.main == modes.CARET) {
70 let selection = content.getSelection();
71 if (selection && !selection.isCollapsed)
72 selection.collapseToStart();
75 editor.unselectText();
78 this.addMode("CARET", {
79 description: "Active when the caret is visible in the web content",
83 get pref() prefs.get("accessibility.browsewithcaret"),
84 set pref(val) prefs.set("accessibility.browsewithcaret", val),
86 enter: function (stack) {
87 if (stack.pop && !this.pref)
89 else if (!stack.pop && !this.pref)
93 leave: function (stack) {
94 if (!stack.push && this.pref)
98 this.addMode("TEXT_EDIT", {
100 description: "Vim-like editing of input elements",
101 bases: [this.COMMAND],
106 this.addMode("OUTPUT_MULTILINE", {
107 description: "Active when the multi-line output buffer is open",
108 bases: [this.COMMAND],
111 this.addMode("INPUT", {
113 description: "The base mode for input modes, including Insert and Command Line",
117 this.addMode("INSERT", {
119 description: "Active when an input element is focused",
123 this.addMode("AUTOCOMPLETE", {
124 description: "Active when an input autocomplete pop-up is active",
125 display: function () "AUTOCOMPLETE (insert)",
129 this.addMode("EMBED", {
130 description: "Active when an <embed> or <object> element is focused",
136 this.addMode("PASS_THROUGH", {
137 description: "All keys but <C-v> are ignored by " + config.appName,
143 this.addMode("QUOTE", {
144 description: "The next key sequence is ignored by " + config.appName + ", unless in Pass Through mode",
149 (modes.getStack(1).main == modes.PASS_THROUGH
150 ? (modes.getStack(2).main.display() || modes.getStack(2).main.name)
151 : "PASS THROUGH") + " (next)"
154 preExecute: function (map) { if (modes.main == modes.QUOTE && map.name !== "<C-v>") modes.pop(); },
155 postExecute: function (map) { if (modes.main == modes.QUOTE && map.name === "<C-v>") modes.pop(); },
156 onKeyPress: function (events) { if (modes.main == modes.QUOTE) modes.pop(); }
158 this.addMode("IGNORE", { hidden: true }, {
159 onKeyPress: function (events) Events.KILL,
164 this.addMode("MENU", {
165 description: "Active when a menu or other pop-up is open",
170 leave: function leave(stack) {
171 util.timeout(function () {
172 if (stack.pop && !modes.main.input && Events.isInputElement(dactyl.focusedElement))
173 modes.push(modes.INSERT);
178 this.addMode("LINE", {
179 extended: true, hidden: true
182 this.push(this.NORMAL, 0, {
183 enter: function (stack, prev) {
184 if (prefs.get("accessibility.browsewithcaret"))
185 prefs.set("accessibility.browsewithcaret", false);
187 statusline.updateStatus();
188 if (!stack.fromFocus && prev.main.ownsFocus)
189 dactyl.focusContent(true);
190 if (prev.main == modes.NORMAL) {
191 dactyl.focusContent(true);
192 for (let frame in values(buffer.allFrames())) {
193 // clear any selection made
194 let selection = frame.getSelection();
195 if (selection && !selection.isCollapsed)
196 selection.collapseToStart();
203 cleanup: function cleanup() {
207 _getModeMessage: function _getModeMessage() {
208 // when recording a macro
211 macromode = "recording";
212 else if (this.replaying)
213 macromode = "replaying";
215 let val = this._modeMap[this._main].display();
217 return "-- " + val + " --" + macromode;;
223 __iterator__: function __iterator__() array.iterValues(this.all),
225 get all() this._modes.slice(),
227 get mainModes() (mode for ([k, mode] in Iterator(modes._modeMap)) if (!mode.extended && mode.name == k)),
229 get mainMode() this._modeMap[this._main],
231 get passThrough() !!(this.main & (this.PASS_THROUGH|this.QUOTE)) ^ (this.getStack(1).main === this.PASS_THROUGH),
233 get topOfStack() this._modeStack[this._modeStack.length - 1],
235 addMode: function addMode(name, options, params) {
236 let mode = Modes.Mode(name, options, params);
240 this.modeChars[mode.char] = (this.modeChars[mode.char] || []).concat(mode);
241 this._modeMap[name] = mode;
242 this._modeMap[mode] = mode;
244 this._modes.push(mode);
246 this._mainModes.push(mode);
248 dactyl.triggerObserver("mode-add", mode);
251 dumpStack: function dumpStack() {
252 util.dump("Mode stack:");
253 for (let [i, mode] in array.iterItems(this._modeStack))
254 util.dump(" " + i + ": " + mode);
257 getMode: function getMode(name) this._modeMap[name],
259 getStack: function getStack(idx) this._modeStack[this._modeStack.length - idx - 1] || this._modeStack[0],
261 get stack() this._modeStack.slice(),
263 getCharModes: function getCharModes(chr) (this.modeChars[chr] || []).slice(),
265 have: function have(mode) this._modeStack.some(function (m) isinstance(m.main, mode)),
267 matchModes: function matchModes(obj)
268 this._modes.filter(function (mode) Object.keys(obj)
269 .every(function (k) obj[k] == (mode[k] || false))),
271 // show the current mode string in the command line
272 show: function show() {
274 if (options.get("showmode").getKey(this.main.name, true))
275 msg = this._getModeMessage();
276 if (msg || loaded.commandline)
277 commandline.widgets.mode = msg || null;
280 remove: function remove(mode, covert) {
281 if (covert && this.topOfStack.main != mode) {
282 util.assert(mode != this.NORMAL);
283 for (let m; m = array.nth(this.modeStack, function (m) m.main == mode, 0);)
284 this._modeStack.splice(this._modeStack.indexOf(m));
286 else if (this.stack.some(function (m) m.main == mode)) {
293 delay: function delay(callback, self) { this.delayed.push([callback, self]); },
295 save: function save(id, obj, prop, test) {
296 if (!(id in this.boundProperties))
297 for (let elem in array.iterValues(this._modeStack))
298 elem.saved[id] = { obj: obj, prop: prop, value: obj[prop], test: test };
299 this.boundProperties[id] = { obj: Cu.getWeakReference(obj), prop: prop, test: test };
304 // helper function to set both modes in one go
305 set: function set(mainMode, extendedMode, params, stack) {
306 var delayed, oldExtended, oldMain, prev, push;
309 dactyl.reportError(Error(_("mode.recursiveSet")), true);
313 params = params || this.getMode(mainMode || this.main).params;
315 if (!stack && mainMode != null && this._modeStack.length > 1)
318 this.withSavedValues(["inSet"], function set() {
321 oldMain = this._main, oldExtended = this._extended;
323 if (extendedMode != null)
324 this._extended = extendedMode;
325 if (mainMode != null) {
326 this._main = mainMode;
328 this._extended = this.NONE;
331 if (stack && stack.pop && stack.pop.params.leave)
332 dactyl.trapErrors("leave", stack.pop.params,
333 stack, this.topOfStack);
335 push = mainMode != null && !(stack && stack.pop) &&
336 Modes.StackElement(this._main, this._extended, params, {});
338 if (push && this.topOfStack) {
339 if (this.topOfStack.params.leave)
340 dactyl.trapErrors("leave", this.topOfStack.params,
341 { push: push }, push);
343 for (let [id, { obj, prop, test }] in Iterator(this.boundProperties)) {
345 delete this.boundProperties[id];
347 this.topOfStack.saved[id] = { obj: obj.get(), prop: prop, value: obj.get()[prop], test: test };
351 delayed = this.delayed;
354 prev = stack && stack.pop || this.topOfStack;
356 this._modeStack.push(push);
358 if (stack && stack.pop)
359 for (let { obj, prop, value, test } in values(this.topOfStack.saved))
360 if (!test || !test(stack, prev))
361 dactyl.trapErrors(function () { obj[prop] = value });
366 delayed.forEach(function ([fn, self]) dactyl.trapErrors(fn, self));
368 if (this.topOfStack.params.enter && prev)
369 dactyl.trapErrors("enter", this.topOfStack.params,
370 push ? { push: push } : stack || {},
373 dactyl.triggerObserver("modeChange", [oldMain, oldExtended], [this._main, this._extended], stack);
377 onCaretChange: function onPrefChange(value) {
378 if (!value && modes.main == modes.CARET)
380 if (value && modes.main == modes.NORMAL)
381 modes.push(modes.CARET);
384 push: function push(mainMode, extendedMode, params) {
385 this.set(mainMode, extendedMode, params, { push: this.topOfStack });
388 pop: function pop(mode, args) {
389 while (this._modeStack.length > 1 && this.main != mode) {
390 let a = this._modeStack.pop();
391 this.set(this.topOfStack.main, this.topOfStack.extended, this.topOfStack.params,
392 update({ pop: a }, args || {}));
399 replace: function replace(mode, oldMode) {
400 while (oldMode && this._modeStack.length > 1 && this.main != oldMode)
403 if (this._modeStack.length > 1)
404 this.set(mode, null, null, { push: this.topOfStack, pop: this._modeStack.pop() });
408 reset: function reset() {
409 if (this._modeStack.length == 1 && this.topOfStack.params.enter)
410 this.topOfStack.params.enter({}, this.topOfStack);
411 while (this._modeStack.length > 1)
415 get recording() this._recording,
416 set recording(value) { this._recording = value; this.show(); },
418 get replaying() this._replaying,
419 set replaying(value) { this._replaying = value; this.show(); },
421 get main() this._main,
422 set main(value) { this.set(value); },
424 get extended() this._extended,
425 set extended(value) { this.set(null, value); }
427 Mode: Class("Mode", {
428 init: function init(name, options, params) {
430 util.assert(options.bases.every(function (m) m instanceof this, this.constructor),
431 "Invalid bases", true);
434 id: 1 << Modes.Mode._id++,
440 isinstance: function isinstance(obj)
441 this === obj || this.allBases.indexOf(obj) >= 0 || callable(obj) && this instanceof obj,
443 allBases: Class.memoize(function () {
444 let seen = {}, res = [], queue = this.bases;
445 for (let mode in array.iterValues(queue))
446 if (!set.add(seen, mode)) {
448 queue.push.apply(queue, mode.bases);
453 get bases() this.input ? [modes.INPUT] : [modes.MAIN],
455 get count() !this.insert,
457 get description() this._display,
459 _display: Class.memoize(function _display() this.name.replace("_", " ", "g")),
461 display: function display() this._display,
467 input: Class.memoize(function input() this.insert || this.bases.length && this.bases.some(function (b) b.input)),
469 insert: Class.memoize(function insert() this.bases.length && this.bases.some(function (b) b.insert)),
471 ownsFocus: Class.memoize(function ownsFocus() this.bases.length && this.bases.some(function (b) b.ownsFocus)),
473 get passUnknown() this.input,
477 get toStringParams() [this.name],
479 valueOf: function valueOf() this.id
483 StackElement: (function () {
484 const StackElement = Struct("main", "extended", "params", "saved");
485 StackElement.className = "Modes.StackElement";
486 StackElement.defaultValue("params", function () this.main.params);
488 update(StackElement.prototype, {
489 get toStringParams() !loaded.modes ? this.main.name : [
491 <>({ modes.all.filter(function (m) this.extended & m, this).map(function (m) m.name).join("|") })</>
497 boundProperty: function BoundProperty(desc) {
498 let id = this.cacheId++;
502 return Class.Property(update({
505 init: function bound_init(prop) update(this, {
506 get: function bound_get() {
508 var val = desc.get.call(this, value);
509 return val === undefined ? value : val;
511 set: function bound_set(val) {
512 modes.save(id, this, prop, desc.test);
514 value = desc.set.call(this, val);
515 value = !desc.set || value === undefined ? val : value;
521 mappings: function initMappings() {
522 mappings.add([modes.BASE, modes.NORMAL],
524 "Return to NORMAL mode",
525 function () { modes.reset(); });
527 mappings.add([modes.INPUT, modes.COMMAND, modes.PASS_THROUGH, modes.QUOTE],
529 "Return to the previous mode",
530 function () { modes.pop(); });
532 mappings.add([modes.MENU], ["<Esc>"],
533 "Close the current popup",
536 return Events.PASS_THROUGH;
539 mappings.add([modes.MENU], ["<C-[>"],
540 "Close the current popup",
541 function () { events.feedkeys("<Esc>"); });
543 options: function initOptions() {
544 options.add(["showmode", "smd"],
545 "Show the current mode in the command line when it matches this expression",
546 "regexplist", "!^normal$",
547 { regexpFlags: "i" });
549 prefs: function initPrefs() {
550 prefs.watch("accessibility.browsewithcaret", function () modes.onCaretChange.apply(modes, arguments));
554 // vim: set fdm=marker sw=4 ts=4 et: