]> git.donarmstrong.com Git - dactyl.git/blob - common/content/modes.js
55fa19fe6965ccb3e19200e68f7dfda52a9b0087
[dactyl.git] / common / content / modes.js
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>
4 //
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
7 "use strict";
8
9 /** @scope modules */
10
11 var Modes = Module("modes", {
12     init: function init() {
13         this.modeChars = {};
14         this._main = 1;     // NORMAL
15         this._extended = 0; // NONE
16
17         this._lastShown = null;
18
19         this._passNextKey = false;
20         this._passAllKeys = false;
21         this._recording = false;
22         this._replaying = false; // playing a macro
23
24         this._modeStack = update([], {
25             pop: function pop() {
26                 if (this.length <= 1)
27                     throw Error("Trying to pop last element in mode stack");
28                 return pop.superapply(this, arguments);
29             }
30         });
31
32         this._modes = [];
33         this._mainModes = [];
34         this._modeMap = {};
35
36         this.boundProperties = {};
37
38         this.addMode("BASE", {
39             char: "b",
40             description: "The base mode for all other modes",
41             bases: [],
42             count: false
43         });
44         this.addMode("MAIN", {
45             char: "m",
46             description: "The base mode for most other modes",
47             bases: [this.BASE],
48             count: false
49         });
50         this.addMode("COMMAND", {
51             char: "C",
52             description: "The base mode for most modes which accept commands rather than input"
53         });
54
55         this.addMode("NORMAL", {
56             char: "n",
57             description: "Active when nothing is focused",
58             bases: [this.COMMAND]
59         });
60         this.addMode("VISUAL", {
61             char: "v",
62             description: "Active when text is selected",
63             display: function () "VISUAL" + (this._extended & modes.LINE ? " LINE" : ""),
64             bases: [this.COMMAND],
65             ownsFocus: true,
66             passUnknown: false
67         }, {
68             leave: function (stack, newMode) {
69                 if (newMode.main == modes.CARET) {
70                     let selection = content.getSelection();
71                     if (selection && !selection.isCollapsed)
72                         selection.collapseToStart();
73                 }
74                 else if (stack.pop)
75                     editor.unselectText();
76             }
77         });
78         this.addMode("CARET", {
79             description: "Active when the caret is visible in the web content",
80             bases: [this.COMMAND]
81         }, {
82
83             get pref()    prefs.get("accessibility.browsewithcaret"),
84             set pref(val) prefs.set("accessibility.browsewithcaret", val),
85
86             enter: function (stack) {
87                 if (stack.pop && !this.pref)
88                     modes.pop();
89                 else if (!stack.pop && !this.pref)
90                     this.pref = true;
91             },
92
93             leave: function (stack) {
94                 if (!stack.push && this.pref)
95                     this.pref = false;
96             }
97         });
98         this.addMode("TEXT_EDIT", {
99             char: "t",
100             description: "Vim-like editing of input elements",
101             bases: [this.COMMAND],
102             input: true,
103             ownsFocus: true,
104             passUnknown: false
105         });
106         this.addMode("OUTPUT_MULTILINE", {
107             description: "Active when the multi-line output buffer is open",
108             bases: [this.COMMAND],
109         });
110
111         this.addMode("INPUT", {
112             char: "I",
113             description: "The base mode for input modes, including Insert and Command Line",
114             bases: [this.MAIN],
115             insert: true
116         });
117         this.addMode("INSERT", {
118             char: "i",
119             description: "Active when an input element is focused",
120             insert: true,
121             ownsFocus: true
122         });
123         this.addMode("AUTOCOMPLETE", {
124             description: "Active when an input autocomplete pop-up is active",
125             display: function () "AUTOCOMPLETE (insert)",
126             bases: [this.INSERT]
127         });
128
129         this.addMode("EMBED", {
130             description: "Active when an <embed> or <object> element is focused",
131             insert: true,
132             ownsFocus: true,
133             passthrough: true
134         });
135
136         this.addMode("PASS_THROUGH", {
137             description: "All keys but <C-v> are ignored by " + config.appName,
138             bases: [this.BASE],
139             hidden: true,
140             insert: true,
141             passthrough: true
142         });
143         this.addMode("QUOTE", {
144             description: "The next key sequence is ignored by " + config.appName + ", unless in Pass Through mode",
145             bases: [this.BASE],
146             hidden: true,
147             passthrough: true,
148             display: function ()
149                 (modes.getStack(1).main == modes.PASS_THROUGH
150                     ? (modes.getStack(2).main.display() || modes.getStack(2).main.name)
151                     : "PASS THROUGH") + " (next)"
152         }, {
153             // Fix me.
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(); }
157         });
158         this.addMode("IGNORE", { hidden: true }, {
159             onKeyPress: function (events) Events.KILL,
160             bases: [],
161             passthrough: true
162         });
163
164         this.addMode("MENU", {
165             description: "Active when a menu or other pop-up is open",
166             input: true,
167             passthrough: true,
168             ownsInput: false
169         }, {
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);
174                 });
175             }
176         });
177
178         this.addMode("LINE", {
179             extended: true, hidden: true
180         });
181
182         this.push(this.NORMAL, 0, {
183             enter: function (stack, prev) {
184                 if (prefs.get("accessibility.browsewithcaret"))
185                     prefs.set("accessibility.browsewithcaret", false);
186
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();
197                     }
198                 }
199
200             }
201         });
202     },
203     cleanup: function cleanup() {
204         modes.reset();
205     },
206
207     _getModeMessage: function _getModeMessage() {
208         // when recording a macro
209         let macromode = "";
210         if (this.recording)
211             macromode = "recording";
212         else if (this.replaying)
213             macromode = "replaying";
214
215         let val = this._modeMap[this._main].display();
216         if (val)
217             return "-- " + val + " --" + macromode;;
218         return macromode;
219     },
220
221     NONE: 0,
222
223     __iterator__: function __iterator__() array.iterValues(this.all),
224
225     get all() this._modes.slice(),
226
227     get mainModes() (mode for ([k, mode] in Iterator(modes._modeMap)) if (!mode.extended && mode.name == k)),
228
229     get mainMode() this._modeMap[this._main],
230
231     get passThrough() !!(this.main & (this.PASS_THROUGH|this.QUOTE)) ^ (this.getStack(1).main === this.PASS_THROUGH),
232
233     get topOfStack() this._modeStack[this._modeStack.length - 1],
234
235     addMode: function addMode(name, options, params) {
236         let mode = Modes.Mode(name, options, params);
237
238         this[name] = mode;
239         if (mode.char)
240             this.modeChars[mode.char] = (this.modeChars[mode.char] || []).concat(mode);
241         this._modeMap[name] = mode;
242         this._modeMap[mode] = mode;
243
244         this._modes.push(mode);
245         if (!mode.extended)
246             this._mainModes.push(mode);
247
248         dactyl.triggerObserver("mode-add", mode);
249     },
250
251     dumpStack: function dumpStack() {
252         util.dump("Mode stack:");
253         for (let [i, mode] in array.iterItems(this._modeStack))
254             util.dump("    " + i + ": " + mode);
255     },
256
257     getMode: function getMode(name) this._modeMap[name],
258
259     getStack: function getStack(idx) this._modeStack[this._modeStack.length - idx - 1] || this._modeStack[0],
260
261     get stack() this._modeStack.slice(),
262
263     getCharModes: function getCharModes(chr) (this.modeChars[chr] || []).slice(),
264
265     have: function have(mode) this._modeStack.some(function (m) isinstance(m.main, mode)),
266
267     matchModes: function matchModes(obj)
268         this._modes.filter(function (mode) Object.keys(obj)
269                                                  .every(function (k) obj[k] == (mode[k] || false))),
270
271     // show the current mode string in the command line
272     show: function show() {
273         let msg = null;
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;
278     },
279
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));
285         }
286         else if (this.stack.some(function (m) m.main == mode)) {
287             this.pop(mode);
288             this.pop();
289         }
290     },
291
292     delayed: [],
293     delay: function delay(callback, self) { this.delayed.push([callback, self]); },
294
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 };
300     },
301
302     inSet: false,
303
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;
307
308         if (this.inSet) {
309             dactyl.reportError(Error(_("mode.recursiveSet")), true);
310             return;
311         }
312
313         params = params || this.getMode(mainMode || this.main).params;
314
315         if (!stack && mainMode != null && this._modeStack.length > 1)
316             this.reset();
317
318         this.withSavedValues(["inSet"], function set() {
319             this.inSet = true;
320
321             oldMain = this._main, oldExtended = this._extended;
322
323             if (extendedMode != null)
324                 this._extended = extendedMode;
325             if (mainMode != null) {
326                 this._main = mainMode;
327                 if (!extendedMode)
328                     this._extended = this.NONE;
329             }
330
331             if (stack && stack.pop && stack.pop.params.leave)
332                 dactyl.trapErrors("leave", stack.pop.params,
333                                   stack, this.topOfStack);
334
335             push = mainMode != null && !(stack && stack.pop) &&
336                 Modes.StackElement(this._main, this._extended, params, {});
337
338             if (push && this.topOfStack) {
339                 if (this.topOfStack.params.leave)
340                     dactyl.trapErrors("leave", this.topOfStack.params,
341                                       { push: push }, push);
342
343                 for (let [id, { obj, prop, test }] in Iterator(this.boundProperties)) {
344                     if (!obj.get())
345                         delete this.boundProperties[id];
346                     else
347                         this.topOfStack.saved[id] = { obj: obj.get(), prop: prop, value: obj.get()[prop], test: test };
348                 }
349             }
350
351             delayed = this.delayed;
352             this.delayed = [];
353
354             prev = stack && stack.pop || this.topOfStack;
355             if (push)
356                 this._modeStack.push(push);
357
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 });
362
363             this.show();
364         });
365
366         delayed.forEach(function ([fn, self]) dactyl.trapErrors(fn, self));
367
368         if (this.topOfStack.params.enter && prev)
369             dactyl.trapErrors("enter", this.topOfStack.params,
370                               push ? { push: push } : stack || {},
371                               prev);
372
373         dactyl.triggerObserver("modeChange", [oldMain, oldExtended], [this._main, this._extended], stack);
374         this.show();
375     },
376
377     onCaretChange: function onPrefChange(value) {
378         if (!value && modes.main == modes.CARET)
379             modes.pop();
380         if (value && modes.main == modes.NORMAL)
381             modes.push(modes.CARET);
382     },
383
384     push: function push(mainMode, extendedMode, params) {
385         this.set(mainMode, extendedMode, params, { push: this.topOfStack });
386     },
387
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 || {}));
393
394             if (mode == null)
395                 return;
396         }
397     },
398
399     replace: function replace(mode, oldMode) {
400         while (oldMode && this._modeStack.length > 1 && this.main != oldMode)
401             this.pop();
402
403         if (this._modeStack.length > 1)
404             this.set(mode, null, null, { push: this.topOfStack, pop: this._modeStack.pop() });
405         this.push(mode);
406     },
407
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)
412             this.pop();
413     },
414
415     get recording() this._recording,
416     set recording(value) { this._recording = value; this.show(); },
417
418     get replaying() this._replaying,
419     set replaying(value) { this._replaying = value; this.show(); },
420
421     get main() this._main,
422     set main(value) { this.set(value); },
423
424     get extended() this._extended,
425     set extended(value) { this.set(null, value); }
426 }, {
427     Mode: Class("Mode", {
428         init: function init(name, options, params) {
429             if (options.bases)
430                 util.assert(options.bases.every(function (m) m instanceof this, this.constructor),
431                             "Invalid bases", true);
432
433             update(this, {
434                 id: 1 << Modes.Mode._id++,
435                 name: name,
436                 params: params || {}
437             }, options);
438         },
439
440         isinstance: function isinstance(obj)
441             this === obj || this.allBases.indexOf(obj) >= 0 || callable(obj) && this instanceof obj,
442
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)) {
447                     res.push(mode);
448                     queue.push.apply(queue, mode.bases);
449                 }
450             return res;
451         }),
452
453         get bases() this.input ? [modes.INPUT] : [modes.MAIN],
454
455         get count() !this.insert,
456
457         get description() this._display,
458
459         _display: Class.memoize(function _display() this.name.replace("_", " ", "g")),
460
461         display: function display() this._display,
462
463         extended: false,
464
465         hidden: false,
466
467         input: Class.memoize(function input() this.insert || this.bases.length && this.bases.some(function (b) b.input)),
468
469         insert: Class.memoize(function insert() this.bases.length && this.bases.some(function (b) b.insert)),
470
471         ownsFocus: Class.memoize(function ownsFocus() this.bases.length && this.bases.some(function (b) b.ownsFocus)),
472
473         get passUnknown() this.input,
474
475         get mask() this,
476
477         get toStringParams() [this.name],
478
479         valueOf: function valueOf() this.id
480     }, {
481         _id: 0
482     }),
483     StackElement: (function () {
484         const StackElement = Struct("main", "extended", "params", "saved");
485         StackElement.className = "Modes.StackElement";
486         StackElement.defaultValue("params", function () this.main.params);
487
488         update(StackElement.prototype, {
489             get toStringParams() !loaded.modes ? this.main.name : [
490                 this.main.name,
491                 <>({ modes.all.filter(function (m) this.extended & m, this).map(function (m) m.name).join("|") })</>
492             ]
493         });
494         return StackElement;
495     })(),
496     cacheId: 0,
497     boundProperty: function BoundProperty(desc) {
498         let id = this.cacheId++;
499         let value;
500
501         desc = desc || {};
502         return Class.Property(update({
503             configurable: true,
504             enumerable: true,
505             init: function bound_init(prop) update(this, {
506                 get: function bound_get() {
507                     if (desc.get)
508                         var val = desc.get.call(this, value);
509                     return val === undefined ? value : val;
510                 },
511                 set: function bound_set(val) {
512                     modes.save(id, this, prop, desc.test);
513                     if (desc.set)
514                         value = desc.set.call(this, val);
515                     value = !desc.set || value === undefined ? val : value;
516                 }
517             })
518         }, desc));
519     }
520 }, {
521     mappings: function initMappings() {
522         mappings.add([modes.BASE, modes.NORMAL],
523             ["<Esc>", "<C-[>"],
524             "Return to NORMAL mode",
525             function () { modes.reset(); });
526
527         mappings.add([modes.INPUT, modes.COMMAND, modes.PASS_THROUGH, modes.QUOTE],
528             ["<Esc>", "<C-[>"],
529             "Return to the previous mode",
530             function () { modes.pop(); });
531
532         mappings.add([modes.MENU], ["<Esc>"],
533             "Close the current popup",
534             function () {
535                 modes.pop();
536                 return Events.PASS_THROUGH;
537             });
538
539         mappings.add([modes.MENU], ["<C-[>"],
540             "Close the current popup",
541             function () { events.feedkeys("<Esc>"); });
542     },
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" });
548     },
549     prefs: function initPrefs() {
550         prefs.watch("accessibility.browsewithcaret", function () modes.onCaretChange.apply(modes, arguments));
551     }
552 });
553
554 // vim: set fdm=marker sw=4 ts=4 et: