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