]> git.donarmstrong.com Git - dactyl.git/blob - common/content/modes.js
Import r6923 from upstream hg supporting Firefox up to 22.0a1
[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-2012 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("CARET", {
61             char: "caret",
62             description: "Active when the caret is visible in the web content",
63             bases: [this.NORMAL]
64         }, {
65
66             get pref()    prefs.get("accessibility.browsewithcaret"),
67             set pref(val) prefs.set("accessibility.browsewithcaret", val),
68
69             enter: function (stack) {
70                 if (stack.pop && !this.pref)
71                     modes.pop();
72                 else if (!stack.pop && !this.pref)
73                     this.pref = true;
74                 if (!stack.pop)
75                     buffer.resetCaret();
76             },
77
78             leave: function (stack) {
79                 if (!stack.push && this.pref)
80                     this.pref = false;
81             }
82         });
83
84         this.addMode("INPUT", {
85             char: "I",
86             description: "The base mode for input modes, including Insert and Command Line",
87             bases: [this.MAIN],
88             insert: true
89         });
90
91         this.addMode("EMBED", {
92             description: "Active when an <embed> or <object> element is focused",
93             bases: [modes.MAIN],
94             insert: true,
95             ownsFocus: true,
96             passthrough: true
97         });
98
99         this.addMode("PASS_THROUGH", {
100             description: "All keys but <C-v> are ignored by " + config.appName,
101             bases: [this.BASE],
102             hidden: true,
103             insert: true,
104             passthrough: true
105         });
106         this.addMode("QUOTE", {
107             description: "The next key sequence is ignored by " + config.appName + ", unless in Pass Through mode",
108             bases: [this.BASE],
109             hidden: true,
110             passthrough: true,
111             display: function ()
112                 (modes.getStack(1).main == modes.PASS_THROUGH
113                     ? (modes.getStack(2).main.display() || modes.getStack(2).main.name)
114                     : "PASS THROUGH") + " (next)"
115         }, {
116             // Fix me.
117             preExecute: function (map) { if (modes.main == modes.QUOTE && map.name !== "<C-v>") modes.pop(); },
118             postExecute: function (map) { if (modes.main == modes.QUOTE && map.name === "<C-v>") modes.pop(); },
119             onKeyPress: function (events) { if (modes.main == modes.QUOTE) modes.pop(); }
120         });
121         this.addMode("IGNORE", { hidden: true }, {
122             onKeyPress: function (events_) {
123                 if (events.isCancelKey(DOM.Event.stringify(events_[0])))
124                     return true;
125                 return false;
126             },
127             bases: [],
128             passthrough: true
129         });
130
131         this.addMode("MENU", {
132             description: "Active when a menu or other pop-up is open",
133             input: true,
134             passthrough: true,
135             ownsInput: false
136         }, {
137             leave: function leave(stack) {
138                 util.timeout(function () {
139                     if (stack.pop && !modes.main.input && Events.isInputElement(dactyl.focusedElement))
140                         modes.push(modes.INSERT);
141                 });
142             }
143         });
144
145         this.addMode("LINE", {
146             extended: true, hidden: true
147         });
148
149         this.push(this.NORMAL, 0, {
150             enter: function (stack, prev) {
151                 if (prefs.get("accessibility.browsewithcaret"))
152                     prefs.set("accessibility.browsewithcaret", false);
153
154                 statusline.updateStatus();
155                 if (!stack.fromFocus && prev.main.ownsFocus)
156                     dactyl.focusContent(true);
157                 if (prev.main == modes.NORMAL) {
158                     dactyl.focusContent(true);
159                     for (let frame in values(buffer.allFrames())) {
160                         // clear any selection made
161                         let selection = frame.getSelection();
162                         if (selection && !selection.isCollapsed)
163                             selection.collapseToStart();
164                     }
165                 }
166
167             }
168         });
169     },
170
171     cleanup: function cleanup() {
172         modes.reset();
173     },
174
175     signals: {
176         "io.source": function ioSource(context, file, modTime) {
177             cache.flushEntry("modes.dtd", modTime);
178         }
179     },
180
181     _getModeMessage: function _getModeMessage() {
182         // when recording a macro
183         let macromode = "";
184         if (this.recording)
185             macromode = "recording " + this.recording + " ";
186         else if (this.replaying)
187             macromode = "replaying";
188
189         if (!options.get("showmode").getKey(this.main.allBases, false))
190             return macromode;
191
192         let modeName = this._modeMap[this._main].display();
193         if (!modeName)
194             return macromode;
195
196         if (macromode)
197             macromode = " " + macromode;
198         return "-- " + modeName + " --" + macromode;
199     },
200
201     NONE: 0,
202
203     __iterator__: function __iterator__() array.iterValues(this.all),
204
205     get all() this._modes.slice(),
206
207     get mainModes() (mode for ([k, mode] in Iterator(modes._modeMap)) if (!mode.extended && mode.name == k)),
208
209     get mainMode() this._modeMap[this._main],
210
211     get passThrough() !!(this.main & (this.PASS_THROUGH|this.QUOTE)) ^ (this.getStack(1).main === this.PASS_THROUGH),
212
213     get topOfStack() this._modeStack[this._modeStack.length - 1],
214
215     addMode: function addMode(name, options, params) {
216         let mode = Modes.Mode(name, options, params);
217
218         this[name] = mode;
219         if (mode.char)
220             this.modeChars[mode.char] = (this.modeChars[mode.char] || []).concat(mode);
221         this._modeMap[name] = mode;
222         this._modeMap[mode] = mode;
223
224         this._modes.push(mode);
225         if (!mode.extended)
226             this._mainModes.push(mode);
227
228         dactyl.triggerObserver("modes.add", mode);
229     },
230
231     removeMode: function removeMode(mode) {
232         this.remove(mode);
233         if (this[mode.name] == mode)
234             delete this[mode.name];
235         if (this._modeMap[mode.name] == mode)
236             delete this._modeMap[mode.name];
237         if (this._modeMap[mode.mode] == mode)
238             delete this._modeMap[mode.mode];
239
240         this._mainModes = this._mainModes.filter(function (m) m != mode);
241     },
242
243     dumpStack: function dumpStack() {
244         util.dump("Mode stack:");
245         for (let [i, mode] in array.iterItems(this._modeStack))
246             util.dump("    " + i + ": " + mode);
247     },
248
249     getMode: function getMode(name) this._modeMap[name],
250
251     getStack: function getStack(idx) this._modeStack[this._modeStack.length - idx - 1] || this._modeStack[0],
252
253     get stack() this._modeStack.slice(),
254
255     getCharModes: function getCharModes(chr) (this.modeChars[chr] || []).slice(),
256
257     have: function have(mode) this._modeStack.some(function (m) isinstance(m.main, mode)),
258
259     matchModes: function matchModes(obj)
260         this._modes.filter(function (mode) Object.keys(obj)
261                                                  .every(function (k) obj[k] == (mode[k] || false))),
262
263     // show the current mode string in the command line
264     show: function show() {
265         if (!loaded.modes)
266             return;
267
268         let msg = this._getModeMessage();
269
270         if (msg || loaded.commandline)
271             commandline.widgets.mode = msg || null;
272     },
273
274     remove: function remove(mode, covert) {
275         if (covert && this.topOfStack.main != mode) {
276             util.assert(mode != this.NORMAL);
277             for (let m; m = array.nth(this.modeStack, function (m) m.main == mode, 0);)
278                 this._modeStack.splice(this._modeStack.indexOf(m));
279         }
280         else if (this.stack.some(function (m) m.main == mode)) {
281             this.pop(mode);
282             this.pop();
283         }
284     },
285
286     delayed: [],
287     delay: function delay(callback, self) { this.delayed.push([callback, self]); },
288
289     save: function save(id, obj, prop, test) {
290         if (!(id in this.boundProperties))
291             for (let elem in array.iterValues(this._modeStack))
292                 elem.saved[id] = { obj: obj, prop: prop, value: obj[prop], test: test };
293         this.boundProperties[id] = { obj: util.weakReference(obj), prop: prop, test: test };
294     },
295
296     inSet: false,
297
298     set: function set(mainMode, extendedMode, params, stack) {
299         var delayed, oldExtended, oldMain, prev, push;
300
301         if (this.inSet) {
302             dactyl.reportError(Error(_("mode.recursiveSet")), true);
303             return;
304         }
305
306         params = params || Object.create(this.getMode(mainMode || this.main).params);
307
308         if (!stack && mainMode != null && this._modeStack.length > 1)
309             this.reset();
310
311         this.withSavedValues(["inSet"], function set() {
312             this.inSet = true;
313
314             oldMain = this._main, oldExtended = this._extended;
315
316             if (extendedMode != null)
317                 this._extended = extendedMode;
318             if (mainMode != null) {
319                 this._main = mainMode;
320                 if (!extendedMode)
321                     this._extended = this.NONE;
322             }
323
324             if (stack && stack.pop && stack.pop.params.leave)
325                 dactyl.trapErrors("leave", stack.pop.params,
326                                   stack, this.topOfStack);
327
328             push = mainMode != null && !(stack && stack.pop) &&
329                 Modes.StackElement(this._main, this._extended, params, {});
330
331             if (push && this.topOfStack) {
332                 if (this.topOfStack.params.leave)
333                     dactyl.trapErrors("leave", this.topOfStack.params,
334                                       { push: push }, push);
335
336                 for (let [id, { obj, prop, test }] in Iterator(this.boundProperties)) {
337                     if (!obj.get())
338                         delete this.boundProperties[id];
339                     else
340                         this.topOfStack.saved[id] = { obj: obj.get(), prop: prop, value: obj.get()[prop], test: test };
341                 }
342             }
343
344             delayed = this.delayed;
345             this.delayed = [];
346
347             prev = stack && stack.pop || this.topOfStack;
348             if (push)
349                 this._modeStack.push(push);
350         });
351
352         if (stack && stack.pop)
353             for (let { obj, prop, value, test } in values(this.topOfStack.saved))
354                 if (!test || !test(stack, prev))
355                     dactyl.trapErrors(function () { obj[prop] = value });
356
357         this.show();
358
359         if (this.topOfStack.params.enter && prev)
360             dactyl.trapErrors("enter", this.topOfStack.params,
361                               push ? { push: push } : stack || {},
362                               prev);
363
364         delayed.forEach(function ([fn, self]) dactyl.trapErrors(fn, self));
365
366         dactyl.triggerObserver("modes.change", [oldMain, oldExtended], [this._main, this._extended], stack);
367         this.show();
368     },
369
370     onCaretChange: function onPrefChange(value) {
371         if (!value && modes.main == modes.CARET)
372             modes.pop();
373         if (value && modes.main == modes.NORMAL)
374             modes.push(modes.CARET);
375     },
376
377     push: function push(mainMode, extendedMode, params) {
378         if (this.main == this.IGNORE)
379             this.pop();
380
381         this.set(mainMode, extendedMode, params, { push: this.topOfStack });
382     },
383
384     pop: function pop(mode, args) {
385         while (this._modeStack.length > 1 && this.main != mode) {
386             let a = this._modeStack.pop();
387             this.set(this.topOfStack.main, this.topOfStack.extended, this.topOfStack.params,
388                      update({ pop: a }, args));
389
390             if (mode == null)
391                 return;
392         }
393     },
394
395     replace: function replace(mode, oldMode, args) {
396         while (oldMode && this._modeStack.length > 1 && this.main != oldMode)
397             this.pop();
398
399         if (this._modeStack.length > 1)
400             this.set(mode, null, null,
401                      update({ push: this.topOfStack, pop: this._modeStack.pop() },
402                             args || {}));
403         this.push(mode);
404     },
405
406     reset: function reset() {
407         if (this._modeStack.length == 1 && this.topOfStack.params.enter)
408             this.topOfStack.params.enter({}, this.topOfStack);
409         while (this._modeStack.length > 1)
410             this.pop();
411     },
412
413     get recording() this._recording,
414     set recording(value) { this._recording = value; this.show(); },
415
416     get replaying() this._replaying,
417     set replaying(value) { this._replaying = value; this.show(); },
418
419     get main() this._main,
420     set main(value) { this.set(value); },
421
422     get extended() this._extended,
423     set extended(value) { this.set(null, value); }
424 }, {
425     Mode: Class("Mode", {
426         init: function init(name, options, params) {
427             if (options.bases)
428                 util.assert(options.bases.every(function (m) m instanceof this, this.constructor),
429                            _("mode.invalidBases"), false);
430
431             this.update({
432                 id: 1 << Modes.Mode._id++,
433                 description: name,
434                 name: name,
435                 params: params || {}
436             }, options);
437         },
438
439         description: Messages.Localized(""),
440
441         displayName: Class.Memoize(function () this.name.split("_").map(util.capitalize).join(" ")),
442
443         isinstance: function isinstance(obj)
444             this.allBases.indexOf(obj) >= 0 || callable(obj) && this instanceof obj,
445
446         allBases: Class.Memoize(function () {
447             let seen = {}, res = [], queue = [this].concat(this.bases);
448             for (let mode in array.iterValues(queue))
449                 if (!Set.add(seen, mode)) {
450                     res.push(mode);
451                     queue.push.apply(queue, mode.bases);
452                 }
453             return res;
454         }),
455
456         get bases() this.input ? [modes.INPUT] : [modes.MAIN],
457
458         get count() !this.insert,
459
460         _display: Class.Memoize(function _display() this.name.replace("_", " ", "g")),
461
462         display: function display() this._display,
463
464         extended: false,
465
466         hidden: false,
467
468         input: Class.Memoize(function input() this.insert || this.bases.length && this.bases.some(function (b) b.input)),
469
470         insert: Class.Memoize(function insert() this.bases.length && this.bases.some(function (b) b.insert)),
471
472         ownsFocus: Class.Memoize(function ownsFocus() this.bases.length && this.bases.some(function (b) b.ownsFocus)),
473
474         passEvent: function passEvent(event) this.input && event.charCode && !(event.ctrlKey || event.altKey || event.metaKey),
475
476         passUnknown: Class.Memoize(function () options.get("passunknown").getKey(this.name)),
477
478         get mask() this,
479
480         get toStringParams() [this.name],
481
482         valueOf: function valueOf() this.id
483     }, {
484         _id: 0
485     }),
486     StackElement: (function () {
487         const StackElement = Struct("main", "extended", "params", "saved");
488         StackElement.className = "Modes.StackElement";
489         StackElement.defaultValue("params", function () this.main.params);
490
491         update(StackElement.prototype, {
492             get toStringParams() !loaded.modes ? this.main.name : [
493                 this.main.name,
494                 ["(", modes.all.filter(function (m) this.extended & m, this)
495                            .map(function (m) m.name).join("|"),
496                  ")"].join("")
497             ]
498         });
499         return StackElement;
500     })(),
501     cacheId: 0,
502     boundProperty: function BoundProperty(desc) {
503         let id = this.cacheId++;
504         let value;
505
506         desc = desc || {};
507         return Class.Property(update({
508             configurable: true,
509             enumerable: true,
510             init: function bound_init(prop) update(this, {
511                 get: function bound_get() {
512                     if (desc.get)
513                         var val = desc.get.call(this, value);
514                     return val === undefined ? value : val;
515                 },
516                 set: function bound_set(val) {
517                     modes.save(id, this, prop, desc.test);
518                     if (desc.set)
519                         value = desc.set.call(this, val);
520                     value = !desc.set || value === undefined ? val : value;
521                 }
522             })
523         }, desc));
524     }
525 }, {
526     cache: function initCache() {
527         function makeTree() {
528             let list = modes.all.filter(function (m) m.name !== m.description);
529
530             let tree = {};
531
532             for (let mode in values(list))
533                 tree[mode.name] = {};
534
535             for (let mode in values(list))
536                 for (let base in values(mode.bases))
537                     tree[base.name][mode.name] = tree[mode.name];
538
539             let roots = iter([m.name, tree[m.name]]
540                              for (m in values(list))
541                              if (!m.bases.length)).toObject();
542
543             function rec(obj) {
544                 let res = ["ul", { "dactyl:highlight": "Dense" }];
545                 Object.keys(obj).sort().forEach(function (name) {
546                     let mode = modes.getMode(name);
547                     res.push(["li", {},
548                                 ["em", {}, mode.displayName],
549                                 ": ", mode.description,
550                                 rec(obj[name])]);
551                 });
552
553                 if (res.length > 2)
554                     return res;
555                 return [];
556             }
557
558             return rec(roots);
559         }
560
561         cache.register("modes.dtd", function ()
562             util.makeDTD(iter({ "modes.tree": makeTree() },
563                               config.dtd)));
564     },
565     mappings: function initMappings() {
566         mappings.add([modes.BASE, modes.NORMAL],
567             ["<Esc>", "<C-[>"],
568             "Return to Normal mode",
569             function () { modes.reset(); });
570
571         mappings.add([modes.INPUT, modes.COMMAND, modes.OPERATOR, modes.PASS_THROUGH, modes.QUOTE],
572             ["<Esc>", "<C-[>"],
573             "Return to the previous mode",
574             function () { modes.pop(null, { fromEscape: true }); });
575
576         mappings.add([modes.AUTOCOMPLETE, modes.MENU], ["<C-c>"],
577             "Leave Autocomplete or Menu mode",
578             function () { modes.pop(); });
579
580         mappings.add([modes.MENU], ["<Esc>"],
581             "Close the current popup",
582             function () {
583                 if (events.popups.active.length)
584                     return Events.PASS_THROUGH;
585                 modes.pop();
586             });
587
588         mappings.add([modes.MENU], ["<C-[>"],
589             "Close the current popup",
590             function () { events.feedkeys("<Esc>"); });
591     },
592     options: function initOptions() {
593         let opts = {
594             completer: function completer(context, extra) {
595                 if (extra.value && context.filter[0] == "!")
596                     context.advance(1);
597                 return completer.superapply(this, arguments);
598             },
599
600             getKey: function getKey(val, default_) {
601                 if (isArray(val))
602                     return (array.nth(this.value, function (v) val.some(function (m) m.name === v.mode), 0)
603                                 || { result: default_ }).result;
604
605                 return Set.has(this.valueMap, val) ? this.valueMap[val] : default_;
606             },
607
608             setter: function (vals) {
609                 modes.all.forEach(function (m) { delete m.passUnknown });
610
611                 vals = vals.map(function (v) update(new String(v.toLowerCase()), {
612                     mode: v.replace(/^!/, "").toUpperCase(),
613                     result: v[0] !== "!"
614                 }));
615
616                 this.valueMap = values(vals).map(function (v) [v.mode, v.result]).toObject();
617                 return vals;
618             },
619
620             validator: function validator(vals) vals.map(function (v) v.replace(/^!/, "")).every(Set.has(this.values)),
621
622             get values() array.toObject([[m.name.toLowerCase(), m.description] for (m in values(modes._modes)) if (!m.hidden)])
623         };
624
625         options.add(["passunknown", "pu"],
626             "Pass through unknown keys in these modes",
627             "stringlist", "!text_edit,!visual,base",
628             opts);
629
630         options.add(["showmode", "smd"],
631             "Show the current mode in the command line when it matches this expression",
632             "stringlist", "caret,output_multiline,!normal,base,operator",
633             opts);
634     },
635     prefs: function initPrefs() {
636         prefs.watch("accessibility.browsewithcaret",
637                     function () { modes.onCaretChange.apply(modes, arguments) });
638     }
639 });
640
641 // vim: set fdm=marker sw=4 ts=4 et: