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