]> git.donarmstrong.com Git - dactyl.git/blob - common/content/editor.js
d98fa950da6b4f8ad99cc7431beaee07762cd034
[dactyl.git] / common / content / editor.js
1 // Copyright (c) 2008-2013 Kris Maglione <maglione.k at Gmail>
2 // Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
3 //
4 // This work is licensed for reuse under an MIT license. Details are
5 // given in the LICENSE.txt file included with this file.
6 "use strict";
7
8 /** @scope modules */
9
10 // command names taken from:
11 // http://developer.mozilla.org/en/docs/Editor_Embedding_Guide
12
13 /** @instance editor */
14 var Editor = Module("editor", XPCOM(Ci.nsIEditActionListener, ModuleBase), {
15     init: function init(elem) {
16         if (elem)
17             this.element = elem;
18         else
19             this.__defineGetter__("element", function () {
20                 let elem = dactyl.focusedElement;
21                 if (elem)
22                     return elem.inputField || elem;
23
24                 let win = document.commandDispatcher.focusedWindow;
25                 return DOM(win).isEditable && win || null;
26             });
27     },
28
29     get registers() storage.newMap("registers", { privateData: true, store: true }),
30     get registerRing() storage.newArray("register-ring", { privateData: true, store: true }),
31
32     skipSave: false,
33
34     // Fixme: Move off this object.
35     currentRegister: null,
36
37     /**
38      * Temporarily set the default register for the span of the next
39      * mapping.
40      */
41     pushRegister: function pushRegister(arg) {
42         let restore = this.currentRegister;
43         this.currentRegister = arg;
44         mappings.afterCommands(2, function () {
45             this.currentRegister = restore;
46         }, this);
47     },
48
49     defaultRegister: "*",
50
51     selectionRegisters: {
52         "*": "selection",
53         "+": "global"
54     },
55
56     /**
57      * Get the value of the register *name*.
58      *
59      * @param {string|number} name The name of the register to get.
60      * @returns {string|null}
61      * @see #setRegister
62      */
63     getRegister: function getRegister(name) {
64         if (name == null)
65             name = editor.currentRegister || editor.defaultRegister;
66
67         if (name == '"')
68             name = 0;
69         if (name == "_")
70             var res = null;
71         else if (Set.has(this.selectionRegisters, name))
72             res = { text: dactyl.clipboardRead(this.selectionRegisters[name]) || "" };
73         else if (!/^[0-9]$/.test(name))
74             res = this.registers.get(name);
75         else
76             res = this.registerRing.get(name);
77
78         return res != null ? res.text : res;
79     },
80
81     /**
82      * Sets the value of register *name* to value. The following
83      * registers have special semantics:
84      *
85      *   *   - Tied to the PRIMARY selection value on X11 systems.
86      *   +   - Tied to the primary global clipboard.
87      *   _   - The null register. Never has any value.
88      *   "   - Equivalent to 0.
89      *   0-9 - These act as a kill ring. Setting any of them pushes the
90      *         values of higher numbered registers up one slot.
91      *
92      * @param {string|number} name The name of the register to set.
93      * @param {string|Range|Selection|Node} value The value to save to
94      *      the register.
95      */
96     setRegister: function setRegister(name, value, verbose) {
97         if (name == null)
98             name = editor.currentRegister || editor.defaultRegister;
99
100         if (isinstance(value, [Ci.nsIDOMRange, Ci.nsIDOMNode, Ci.nsISelection]))
101             value = DOM.stringify(value);
102         value = { text: value, isLine: modes.extended & modes.LINE, timestamp: Date.now() * 1000 };
103
104         if (name == '"')
105             name = 0;
106         if (name == "_")
107             ;
108         else if (Set.has(this.selectionRegisters, name))
109             dactyl.clipboardWrite(value.text, verbose, this.selectionRegisters[name]);
110         else if (!/^[0-9]$/.test(name))
111             this.registers.set(name, value);
112         else {
113             this.registerRing.insert(value, name);
114             this.registerRing.truncate(10);
115         }
116     },
117
118     get isCaret() modes.getStack(1).main == modes.CARET,
119     get isTextEdit() modes.getStack(1).main == modes.TEXT_EDIT,
120
121     get editor() DOM(this.element).editor,
122
123     getController: function getController(cmd) {
124         let controllers = this.element && this.element.controllers;
125         dactyl.assert(controllers);
126
127         return controllers.getControllerForCommand(cmd || "cmd_beginLine");
128     },
129
130     get selection() this.editor && this.editor.selection || null,
131     get selectionController() this.editor && this.editor.selectionController || null,
132
133     deselect: function () {
134         if (this.selection && this.selection.focusNode)
135             this.selection.collapse(this.selection.focusNode,
136                                     this.selection.focusOffset);
137     },
138
139     get selectedRange() {
140         if (!this.selection)
141             return null;
142
143         if (!this.selection.rangeCount) {
144             let range = RangeFind.nodeContents(this.editor.rootElement.ownerDocument);
145             range.collapse(true);
146             this.selectedRange = range;
147         }
148         return this.selection.getRangeAt(0);
149     },
150     set selectedRange(range) {
151         this.selection.removeAllRanges();
152         if (range != null)
153             this.selection.addRange(range);
154     },
155
156     get selectedText() String(this.selection),
157
158     get preserveSelection() this.editor && !this.editor.shouldTxnSetSelection,
159     set preserveSelection(val) {
160         if (this.editor)
161             this.editor.setShouldTxnSetSelection(!val);
162     },
163
164     copy: function copy(range, name) {
165         range = range || this.selection;
166
167         if (!range.collapsed)
168             this.setRegister(name, range);
169     },
170
171     cut: function cut(range, name, noStrip) {
172         if (range)
173             this.selectedRange = range;
174
175         if (!this.selection.isCollapsed)
176             this.setRegister(name, this.selection);
177
178         this.editor.deleteSelection(0, this.editor[noStrip ? "eNoStrip" : "eStrip"]);
179     },
180
181     paste: function paste(name) {
182         let text = this.getRegister(name);
183         dactyl.assert(text && this.editor instanceof Ci.nsIPlaintextEditor);
184
185         this.editor.insertText(text);
186     },
187
188     // count is optional, defaults to 1
189     executeCommand: function executeCommand(cmd, count) {
190         if (!callable(cmd)) {
191             var controller = this.getController(cmd);
192             util.assert(controller &&
193                         controller.supportsCommand(cmd) &&
194                         controller.isCommandEnabled(cmd));
195             cmd = bind("doCommand", controller, cmd);
196         }
197
198         // XXX: better as a precondition
199         if (count == null)
200             count = 1;
201
202         let didCommand = false;
203         while (count--) {
204             // some commands need this try/catch workaround, because a cmd_charPrevious triggered
205             // at the beginning of the textarea, would hang the doCommand()
206             // good thing is, we need this code anyway for proper beeping
207
208             // What huh? --Kris
209             try {
210                 cmd(this.editor, controller);
211                 didCommand = true;
212             }
213             catch (e) {
214                 util.reportError(e);
215                 dactyl.assert(didCommand);
216                 break;
217             }
218         }
219     },
220
221     moveToPosition: function (pos, select) {
222         if (isObject(pos))
223             var { startContainer, startOffset } = pos;
224         else
225             [startOffset, startOffset] = [this.selection.focusNode, pos];
226         this.selection[select ? "extend" : "collapse"](startContainer, startOffset);
227     },
228
229     mungeRange: function mungeRange(range, munger, selectEnd) {
230         let { editor } = this;
231         editor.beginPlaceHolderTransaction(null);
232
233         let [container, offset] = ["startContainer", "startOffset"];
234         if (selectEnd)
235             [container, offset] = ["endContainer", "endOffset"];
236
237         try {
238             // :(
239             let idx = range[offset];
240             let parent = range[container].parentNode;
241             let parentIdx = Array.indexOf(parent.childNodes,
242                                           range[container]);
243
244             let delta = 0;
245             for (let node in Editor.TextsIterator(range)) {
246                 let text = node.textContent;
247                 let start = 0, end = text.length;
248                 if (node == range.startContainer)
249                     start = range.startOffset;
250                 if (node == range.endContainer)
251                     end = range.endOffset;
252
253                 if (start == 0 && end == text.length)
254                     text = munger(text);
255                 else
256                     text = text.slice(0, start)
257                          + munger(text.slice(start, end))
258                          + text.slice(end);
259
260                 if (text == node.textContent)
261                     continue;
262
263                 if (selectEnd)
264                     delta = text.length - node.textContent.length;
265
266                 if (editor instanceof Ci.nsIPlaintextEditor) {
267                     this.selectedRange = RangeFind.nodeContents(node);
268                     editor.insertText(text);
269                 }
270                 else
271                     node.textContent = text;
272             }
273             let node = parent.childNodes[parentIdx];
274             if (node instanceof Text)
275                 idx = Math.constrain(idx + delta, 0, node.textContent.length);
276             this.selection.collapse(node, idx);
277         }
278         finally {
279             editor.endPlaceHolderTransaction();
280         }
281     },
282
283     findChar: function findChar(key, count, backward, offset) {
284         count  = count || 1; // XXX ?
285         offset = (offset || 0) - !!backward;
286
287         // Grab the charcode of the key spec. Using the key name
288         // directly will break keys like <
289         let code = DOM.Event.parse(key)[0].charCode;
290         let char = String.fromCharCode(code);
291         util.assert(code);
292
293         let range = this.selectedRange.cloneRange();
294         let collapse = DOM(this.element).whiteSpace == "normal";
295
296         // Find the *count*th occurance of *char* before a non-collapsed
297         // \n, ignoring the character at the caret.
298         let i = 0;
299         function test(c) (collapse || c != "\n") && !!(!i++ || c != char || --count)
300
301         Editor.extendRange(range, !backward, { test: test }, true);
302         dactyl.assert(count == 0);
303         range.collapse(backward);
304
305         // Skip to any requested offset.
306         count = Math.abs(offset);
307         Editor.extendRange(range, offset > 0,
308                            { test: c => !!count-- },
309                            true);
310         range.collapse(offset < 0);
311
312         return range;
313     },
314
315     findNumber: function findNumber(range) {
316         if (!range)
317             range = this.selectedRange.cloneRange();
318
319         // Find digit (or \n).
320         Editor.extendRange(range, true, /[^\n\d]/, true);
321         range.collapse(false);
322         // Select entire number.
323         Editor.extendRange(range, true, /\d/, true);
324         Editor.extendRange(range, false, /\d/, true);
325
326         // Sanity check.
327         dactyl.assert(/^\d+$/.test(range));
328
329         if (false) // Skip for now.
330         if (range.startContainer instanceof Text && range.startOffset > 2) {
331             if (range.startContainer.textContent.substr(range.startOffset - 2, 2) == "0x")
332                 range.setStart(range.startContainer, range.startOffset - 2);
333         }
334
335         // Grab the sign, if it's there.
336         Editor.extendRange(range, false, /[+-]/, true);
337
338         return range;
339     },
340
341     modifyNumber: function modifyNumber(delta, range) {
342         range = this.findNumber(range);
343         let number = parseInt(range) + delta;
344         if (/^[+-]?0x/.test(range))
345             number = number.toString(16).replace(/^[+-]?/, "$&0x");
346         else if (/^[+-]?0\d/.test(range))
347             number = number.toString(8).replace(/^[+-]?/, "$&0");
348
349         this.selectedRange = range;
350         this.editor.insertText(String(number));
351         this.selection.modify("move", "backward", "character");
352     },
353
354     /**
355      * Edits the given file in the external editor as specified by the
356      * 'editor' option.
357      *
358      * @param {object|File|string} args An object specifying the file, line,
359      *     and column to edit. If a non-object is specified, it is treated as
360      *     the file parameter of the object.
361      * @param {boolean} blocking If true, this function does not return
362      *     until the editor exits.
363      */
364     editFileExternally: function (args, blocking) {
365         if (!isObject(args) || args instanceof File)
366             args = { file: args };
367         args.file = args.file.path || args.file;
368
369         let args = options.get("editor").format(args);
370
371         dactyl.assert(args.length >= 1, _("option.notSet", "editor"));
372
373         io.run(args.shift(), args, blocking);
374     },
375
376     // TODO: clean up with 2 functions for textboxes and currentEditor?
377     editFieldExternally: function editFieldExternally(forceEditing) {
378         if (!options["editor"])
379             return;
380
381         let textBox = config.isComposeWindow ? null : dactyl.focusedElement;
382         if (!DOM(textBox).isInput)
383             textBox = null;
384
385         let line, column;
386         let keepFocus = modes.stack.some(m => isinstance(m.main, modes.COMMAND_LINE));
387
388         if (!forceEditing && textBox && textBox.type == "password") {
389             commandline.input(_("editor.prompt.editPassword") + " ",
390                 function (resp) {
391                     if (resp && resp.match(/^y(es)?$/i))
392                         editor.editFieldExternally(true);
393                 });
394                 return;
395         }
396
397         if (textBox) {
398             var text = textBox.value;
399             var pre = text.substr(0, textBox.selectionStart);
400         }
401         else {
402             var editor_ = window.GetCurrentEditor ? GetCurrentEditor()
403                                                   : Editor.getEditor(document.commandDispatcher.focusedWindow);
404             dactyl.assert(editor_);
405             text = Array.map(editor_.rootElement.childNodes,
406                              e => DOM.stringify(e, true))
407                         .join("");
408
409             if (!editor_.selection.rangeCount)
410                 var sel = "";
411             else {
412                 let range = RangeFind.nodeContents(editor_.rootElement);
413                 let end = editor_.selection.getRangeAt(0);
414                 range.setEnd(end.startContainer, end.startOffset);
415                 pre = DOM.stringify(range, true);
416                 if (range.startContainer instanceof Text)
417                     pre = pre.replace(/^(?:<[^>"]+>)+/, "");
418                 if (range.endContainer instanceof Text)
419                     pre = pre.replace(/(?:<\/[^>"]+>)+$/, "");
420             }
421         }
422
423         line = 1 + pre.replace(/[^\n]/g, "").length;
424         column = 1 + pre.replace(/[^]*\n/, "").length;
425
426         let origGroup = DOM(textBox).highlight.toString();
427         let cleanup = util.yieldable(function cleanup(error) {
428             if (timer)
429                 timer.cancel();
430
431             let blink = ["EditorBlink1", "EditorBlink2"];
432             if (error) {
433                 dactyl.reportError(error, true);
434                 blink[1] = "EditorError";
435             }
436             else
437                 dactyl.trapErrors(update, null, true);
438
439             if (tmpfile && tmpfile.exists())
440                 tmpfile.remove(false);
441
442             if (textBox) {
443                 DOM(textBox).highlight.remove("EditorEditing");
444                 if (!keepFocus)
445                     dactyl.focus(textBox);
446                 for (let group in values(blink.concat(blink, ""))) {
447                     highlight.highlightNode(textBox, origGroup + " " + group);
448                     yield 100;
449                 }
450             }
451         });
452
453         function update(force) {
454             if (force !== true && tmpfile.lastModifiedTime <= lastUpdate)
455                 return;
456             lastUpdate = Date.now();
457
458             let val = tmpfile.read();
459             if (textBox) {
460                 textBox.value = val;
461
462                 if (true) {
463                     let elem = DOM(textBox);
464                     elem.attrNS(NS, "modifiable", true)
465                         .style.MozUserInput;
466                     elem.input().attrNS(NS, "modifiable", null);
467                 }
468             }
469             else {
470                 while (editor_.rootElement.firstChild)
471                     editor_.rootElement.removeChild(editor_.rootElement.firstChild);
472                 editor_.rootElement.innerHTML = val;
473             }
474         }
475
476         try {
477             var tmpfile = io.createTempFile("txt", "." + buffer.uri.host);
478             if (!tmpfile)
479                 throw Error(_("io.cantCreateTempFile"));
480
481             if (textBox) {
482                 if (!keepFocus)
483                     textBox.blur();
484                 DOM(textBox).highlight.add("EditorEditing");
485             }
486
487             if (!tmpfile.write(text))
488                 throw Error(_("io.cantEncode"));
489
490             var lastUpdate = Date.now();
491
492             var timer = services.Timer(update, 100, services.Timer.TYPE_REPEATING_SLACK);
493             this.editFileExternally({ file: tmpfile.path, line: line, column: column }, cleanup);
494         }
495         catch (e) {
496             cleanup(e);
497         }
498     },
499
500     /**
501      * Expands an abbreviation in the currently active textbox.
502      *
503      * @param {string} mode The mode filter.
504      * @see Abbreviation#expand
505      */
506     expandAbbreviation: function (mode) {
507         if (!this.selection)
508             return;
509
510         let range = this.selectedRange.cloneRange();
511         if (!range.collapsed)
512             return;
513
514         Editor.extendRange(range, false, /\S/, true);
515         let abbrev = abbreviations.match(mode, String(range));
516         if (abbrev) {
517             range.setStart(range.startContainer, range.endOffset - abbrev.lhs.length);
518             this.selectedRange = range;
519             this.editor.insertText(abbrev.expand(this.element));
520         }
521     },
522
523     // nsIEditActionListener:
524     WillDeleteNode: util.wrapCallback(function WillDeleteNode(node) {
525         if (!editor.skipSave && node.textContent)
526             this.setRegister(0, node);
527     }),
528     WillDeleteSelection: util.wrapCallback(function WillDeleteSelection(selection) {
529         if (!editor.skipSave && !selection.isCollapsed)
530             this.setRegister(0, selection);
531     }),
532     WillDeleteText: util.wrapCallback(function WillDeleteText(node, start, length) {
533         if (!editor.skipSave && length)
534             this.setRegister(0, node.textContent.substr(start, length));
535     })
536 }, {
537     TextsIterator: Class("TextsIterator", {
538         init: function init(range, context, after) {
539             this.after = after;
540             this.start = context || range[after ? "endContainer" : "startContainer"];
541             if (after)
542                 this.context = this.start;
543             this.range = range;
544         },
545
546         __iterator__: function __iterator__() {
547             while (this.nextNode())
548                 yield this.context;
549         },
550
551         prevNode: function prevNode() {
552             if (!this.context)
553                 return this.context = this.start;
554
555             var node = this.context;
556             if (!this.after)
557                 node = node.previousSibling;
558
559             if (!node)
560                 node = this.context.parentNode;
561             else
562                 while (node.lastChild)
563                     node = node.lastChild;
564
565             if (!node || !RangeFind.containsNode(this.range, node, true))
566                 return null;
567             this.after = false;
568             return this.context = node;
569         },
570
571         nextNode: function nextNode() {
572             if (!this.context)
573                 return this.context = this.start;
574
575             if (!this.after)
576                 var node = this.context.firstChild;
577
578             if (!node) {
579                 node = this.context;
580                 while (node.parentNode && node != this.range.endContainer
581                         && !node.nextSibling)
582                     node = node.parentNode;
583
584                 node = node.nextSibling;
585             }
586
587             if (!node || !RangeFind.containsNode(this.range, node, true))
588                 return null;
589             this.after = false;
590             return this.context = node;
591         },
592
593         getPrev: function getPrev() {
594             return this.filter("prevNode");
595         },
596
597         getNext: function getNext() {
598             return this.filter("nextNode");
599         },
600
601         filter: function filter(meth) {
602             let node;
603             while (node = this[meth]())
604                 if (node instanceof Ci.nsIDOMText &&
605                         DOM(node).isVisible &&
606                         DOM(node).style.MozUserSelect != "none")
607                     return node;
608         }
609     }),
610
611     extendRange: function extendRange(range, forward, re, sameWord, root, end) {
612         function advance(positive) {
613             while (true) {
614                 while (idx == text.length && (node = iterator.getNext())) {
615                     if (node == iterator.start)
616                         idx = range[offset];
617
618                     start = text.length;
619                     text += node.textContent;
620                     range[set](node, idx - start);
621                 }
622
623                 if (idx >= text.length || re.test(text[idx]) != positive)
624                     break;
625                 range[set](range[container], ++idx - start);
626             }
627         }
628         function retreat(positive) {
629             while (true) {
630                 while (idx == 0 && (node = iterator.getPrev())) {
631                     let str = node.textContent;
632                     if (node == iterator.start)
633                         idx = range[offset];
634                     else
635                         idx = str.length;
636
637                     text = str + text;
638                     range[set](node, idx);
639                 }
640                 if (idx == 0 || re.test(text[idx - 1]) != positive)
641                     break;
642                 range[set](range[container], --idx);
643             }
644         }
645
646         if (end == null)
647             end = forward ? "end" : "start";
648         let [container, offset, set] = [end + "Container", end + "Offset",
649                                         "set" + util.capitalize(end)];
650
651         if (!root)
652             for (root = range[container];
653                  root.parentNode instanceof Element && !DOM(root).isEditable;
654                  root = root.parentNode)
655                 ;
656         if (root instanceof Ci.nsIDOMNSEditableElement)
657             root = root.editor;
658         if (root instanceof Ci.nsIEditor)
659             root = root.rootElement;
660
661         let node = range[container];
662         let iterator = Editor.TextsIterator(RangeFind.nodeContents(root),
663                                             node, !forward);
664
665         let text = "";
666         let idx  = 0;
667         let start = 0;
668
669         if (forward) {
670             advance(true);
671             if (!sameWord)
672                 advance(false);
673         }
674         else {
675             if (!sameWord)
676                 retreat(false);
677             retreat(true);
678         }
679         return range;
680     },
681
682     getEditor: function (elem) {
683         if (arguments.length === 0) {
684             dactyl.assert(dactyl.focusedElement);
685             return dactyl.focusedElement;
686         }
687
688         if (!elem)
689             elem = dactyl.focusedElement || document.commandDispatcher.focusedWindow;
690         dactyl.assert(elem);
691
692         return DOM(elem).editor;
693     }
694 }, {
695     modes: function initModes() {
696         modes.addMode("OPERATOR", {
697             char: "o",
698             description: "Mappings which move the cursor",
699             bases: []
700         });
701         modes.addMode("VISUAL", {
702             char: "v",
703             description: "Active when text is selected",
704             display: function () "VISUAL" + (this._extended & modes.LINE ? " LINE" : ""),
705             bases: [modes.COMMAND],
706             ownsFocus: true
707         }, {
708             enter: function (stack) {
709                 if (editor.selectionController)
710                     editor.selectionController.setCaretVisibilityDuringSelection(true);
711             },
712             leave: function (stack, newMode) {
713                 if (newMode.main == modes.CARET) {
714                     let selection = content.getSelection();
715                     if (selection && !selection.isCollapsed)
716                         selection.collapseToStart();
717                 }
718                 else if (stack.pop)
719                     editor.deselect();
720             }
721         });
722         modes.addMode("TEXT_EDIT", {
723             char: "t",
724             description: "Vim-like editing of input elements",
725             bases: [modes.COMMAND],
726             ownsFocus: true
727         }, {
728             onKeyPress: function (eventList) {
729                 const KILL = false, PASS = true;
730
731                 // Hack, really.
732                 if (eventList[0].charCode || /^<(?:.-)*(?:BS|Del|C-h|C-w|C-u|C-k)>$/.test(DOM.Event.stringify(eventList[0]))) {
733                     dactyl.beep();
734                     return KILL;
735                 }
736                 return PASS;
737             }
738         });
739
740         modes.addMode("INSERT", {
741             char: "i",
742             description: "Active when an input element is focused",
743             insert: true,
744             ownsFocus: true
745         });
746         modes.addMode("AUTOCOMPLETE", {
747             description: "Active when an input autocomplete pop-up is active",
748             display: function () "AUTOCOMPLETE (insert)",
749             bases: [modes.INSERT]
750         });
751     },
752     commands: function initCommands() {
753         commands.add(["reg[isters]"],
754             "List the contents of known registers",
755             function (args) {
756                 completion.listCompleter("register", args[0]);
757             },
758             { argCount: "*" });
759     },
760     completion: function initCompletion() {
761         completion.register = function complete_register(context) {
762             context = context.fork("registers");
763             context.keys = { text: util.identity, description: editor.closure.getRegister };
764
765             context.match = function (r) !this.filter || ~this.filter.indexOf(r);
766
767             context.fork("clipboard", 0, this, function (ctxt) {
768                 ctxt.match = context.match;
769                 ctxt.title = ["Clipboard Registers"];
770                 ctxt.completions = Object.keys(editor.selectionRegisters);
771             });
772             context.fork("kill-ring", 0, this, function (ctxt) {
773                 ctxt.match = context.match;
774                 ctxt.title = ["Kill Ring Registers"];
775                 ctxt.completions = Array.slice("0123456789");
776             });
777             context.fork("user", 0, this, function (ctxt) {
778                 ctxt.match = context.match;
779                 ctxt.title = ["User Defined Registers"];
780                 ctxt.completions = editor.registers.keys();
781             });
782         };
783     },
784     mappings: function initMappings() {
785
786         Map.types["editor"] = {
787             preExecute: function preExecute(args) {
788                 if (editor.editor && !this.editor) {
789                     this.editor = editor.editor;
790                     if (!this.noTransaction)
791                         this.editor.beginTransaction();
792                 }
793                 editor.inEditMap = true;
794             },
795             postExecute: function preExecute(args) {
796                 editor.inEditMap = false;
797                 if (this.editor) {
798                     if (!this.noTransaction)
799                         this.editor.endTransaction();
800                     this.editor = null;
801                 }
802             },
803         };
804         Map.types["operator"] = {
805             preExecute: function preExecute(args) {
806                 editor.inEditMap = true;
807             },
808             postExecute: function preExecute(args) {
809                 editor.inEditMap = true;
810                 if (modes.main == modes.OPERATOR)
811                     modes.pop();
812             }
813         };
814
815         // add mappings for commands like h,j,k,l,etc. in CARET, VISUAL and TEXT_EDIT mode
816         function addMovementMap(keys, description, hasCount, caretModeMethod, caretModeArg, textEditCommand, visualTextEditCommand) {
817             let extraInfo = {
818                 count: !!hasCount,
819                 type: "operator"
820             };
821
822             function caretExecute(arg) {
823                 let win = document.commandDispatcher.focusedWindow;
824                 let controller = util.selectionController(win);
825                 let sel = controller.getSelection(controller.SELECTION_NORMAL);
826
827                 let buffer = Buffer(win);
828                 if (!sel.rangeCount) // Hack.
829                     buffer.resetCaret();
830
831                 if (caretModeMethod == "pageMove") { // Grr.
832                     buffer.scrollVertical("pages", caretModeArg ? 1 : -1);
833                     buffer.resetCaret();
834                 }
835                 else
836                     controller[caretModeMethod](caretModeArg, arg);
837             }
838
839             mappings.add([modes.VISUAL], keys, description,
840                 function ({ count }) {
841                     count = count || 1;
842
843                     let caret = !dactyl.focusedElement;
844                     let controller = buffer.selectionController;
845
846                     while (count-- && modes.main == modes.VISUAL) {
847                         if (caret)
848                             caretExecute(true, true);
849                         else {
850                             if (callable(visualTextEditCommand))
851                                 visualTextEditCommand(editor.editor);
852                             else
853                                 editor.executeCommand(visualTextEditCommand);
854                         }
855                     }
856                 },
857                 extraInfo);
858
859             mappings.add([modes.CARET, modes.TEXT_EDIT, modes.OPERATOR], keys, description,
860                 function ({ count }) {
861                     count = count || 1;
862
863                     if (editor.editor)
864                         editor.executeCommand(textEditCommand, count);
865                     else {
866                         while (count--)
867                             caretExecute(false);
868                     }
869                 },
870                 extraInfo);
871         }
872
873         // add mappings for commands like i,a,s,c,etc. in TEXT_EDIT mode
874         function addBeginInsertModeMap(keys, commands, description) {
875             mappings.add([modes.TEXT_EDIT], keys, description || "",
876                 function () {
877                     commands.forEach(function (cmd) { editor.executeCommand(cmd, 1); });
878                     modes.push(modes.INSERT);
879                 },
880                 { type: "editor" });
881         }
882
883         function selectPreviousLine() {
884             editor.executeCommand("cmd_selectLinePrevious");
885             if ((modes.extended & modes.LINE) && !editor.selectedText)
886                 editor.executeCommand("cmd_selectLinePrevious");
887         }
888
889         function selectNextLine() {
890             editor.executeCommand("cmd_selectLineNext");
891             if ((modes.extended & modes.LINE) && !editor.selectedText)
892                 editor.executeCommand("cmd_selectLineNext");
893         }
894
895         function updateRange(editor, forward, re, modify, sameWord) {
896             let sel   = editor.selection;
897             let range = sel.getRangeAt(0);
898
899             let end = range.endContainer == sel.focusNode && range.endOffset == sel.focusOffset;
900             if (range.collapsed)
901                 end = forward;
902
903             Editor.extendRange(range, forward, re, sameWord,
904                                editor.rootElement, end ? "end" : "start");
905             modify(range);
906             editor.selectionController.repaintSelection(editor.selectionController.SELECTION_NORMAL);
907         }
908
909         function clear(forward, re)
910             function _clear(editor) {
911                 updateRange(editor, forward, re, function (range) {});
912                 dactyl.assert(!editor.selection.isCollapsed);
913                 editor.selection.deleteFromDocument();
914                 let parent = DOM(editor.rootElement.parentNode);
915                 if (parent.isInput)
916                     parent.input();
917             }
918
919         function move(forward, re, sameWord)
920             function _move(editor) {
921                 updateRange(editor, forward, re,
922                             function (range) { range.collapse(!forward); },
923                             sameWord);
924             }
925         function select(forward, re)
926             function _select(editor) {
927                 updateRange(editor, forward, re,
928                             function (range) {});
929             }
930         function beginLine(editor_) {
931             editor.executeCommand("cmd_beginLine");
932             move(true, /\s/, true)(editor_);
933         }
934
935         //             COUNT  CARET                   TEXT_EDIT            VISUAL_TEXT_EDIT
936         addMovementMap(["k", "<Up>"],                 "Move up one line",
937                        true,  "lineMove", false,      "cmd_linePrevious", selectPreviousLine);
938         addMovementMap(["j", "<Down>", "<Return>"],   "Move down one line",
939                        true,  "lineMove", true,       "cmd_lineNext",     selectNextLine);
940         addMovementMap(["h", "<Left>", "<BS>"],       "Move left one character",
941                        true,  "characterMove", false, "cmd_charPrevious", "cmd_selectCharPrevious");
942         addMovementMap(["l", "<Right>", "<Space>"],   "Move right one character",
943                        true,  "characterMove", true,  "cmd_charNext",     "cmd_selectCharNext");
944         addMovementMap(["b", "<C-Left>"],             "Move left one word",
945                        true,  "wordMove", false,      move(false,  /\w/), select(false, /\w/));
946         addMovementMap(["w", "<C-Right>"],            "Move right one word",
947                        true,  "wordMove", true,       move(true,  /\w/),  select(true, /\w/));
948         addMovementMap(["B"],                         "Move left to the previous white space",
949                        true,  "wordMove", false,      move(false, /\S/),  select(false, /\S/));
950         addMovementMap(["W"],                         "Move right to just beyond the next white space",
951                        true,  "wordMove", true,       move(true,  /\S/),  select(true,  /\S/));
952         addMovementMap(["e"],                         "Move to the end of the current word",
953                        true,  "wordMove", true,       move(true,  /\W/),  select(true,  /\W/));
954         addMovementMap(["E"],                         "Move right to the next white space",
955                        true,  "wordMove", true,       move(true,  /\s/),  select(true,  /\s/));
956         addMovementMap(["<C-f>", "<PageDown>"],       "Move down one page",
957                        true,  "pageMove", true,       "cmd_movePageDown", "cmd_selectNextPage");
958         addMovementMap(["<C-b>", "<PageUp>"],         "Move up one page",
959                        true,  "pageMove", false,      "cmd_movePageUp",   "cmd_selectPreviousPage");
960         addMovementMap(["gg", "<C-Home>"],            "Move to the start of text",
961                        false, "completeMove", false,  "cmd_moveTop",      "cmd_selectTop");
962         addMovementMap(["G", "<C-End>"],              "Move to the end of text",
963                        false, "completeMove", true,   "cmd_moveBottom",   "cmd_selectBottom");
964         addMovementMap(["0", "<Home>"],               "Move to the beginning of the line",
965                        false, "intraLineMove", false, "cmd_beginLine",    "cmd_selectBeginLine");
966         addMovementMap(["^"],                         "Move to the first non-whitespace character of the line",
967                        false, "intraLineMove", false, beginLine,          "cmd_selectBeginLine");
968         addMovementMap(["$", "<End>"],                "Move to the end of the current line",
969                        false, "intraLineMove", true,  "cmd_endLine" ,     "cmd_selectEndLine");
970
971         addBeginInsertModeMap(["i", "<Insert>"], [], "Insert text before the cursor");
972         addBeginInsertModeMap(["a"],             ["cmd_charNext"], "Append text after the cursor");
973         addBeginInsertModeMap(["I"],             ["cmd_beginLine"], "Insert text at the beginning of the line");
974         addBeginInsertModeMap(["A"],             ["cmd_endLine"], "Append text at the end of the line");
975         addBeginInsertModeMap(["s"],             ["cmd_deleteCharForward"], "Delete the character in front of the cursor and start insert");
976         addBeginInsertModeMap(["S"],             ["cmd_deleteToEndOfLine", "cmd_deleteToBeginningOfLine"], "Delete the current line and start insert");
977         addBeginInsertModeMap(["C"],             ["cmd_deleteToEndOfLine"], "Delete from the cursor to the end of the line and start insert");
978
979         function addMotionMap(key, desc, select, cmd, mode, caretOk) {
980             function doTxn(range, editor) {
981                 try {
982                     editor.editor.beginTransaction();
983                     cmd(editor, range, editor.editor);
984                 }
985                 finally {
986                     editor.editor.endTransaction();
987                 }
988             }
989
990             mappings.add([modes.TEXT_EDIT], key,
991                 desc,
992                 function ({ command, count, motion }) {
993                     let start = editor.selectedRange.cloneRange();
994
995                     mappings.pushCommand();
996                     modes.push(modes.OPERATOR, null, {
997                         forCommand: command,
998
999                         count: count,
1000
1001                         leave: function leave(stack) {
1002                             try {
1003                                 if (stack.push || stack.fromEscape)
1004                                     return;
1005
1006                                 editor.withSavedValues(["inEditMap"], function () {
1007                                     this.inEditMap = true;
1008
1009                                     let range = RangeFind.union(start, editor.selectedRange);
1010                                     editor.selectedRange = select ? range : start;
1011                                     doTxn(range, editor);
1012                                 });
1013
1014                                 editor.currentRegister = null;
1015                                 modes.delay(function () {
1016                                     if (mode)
1017                                         modes.push(mode);
1018                                 });
1019                             }
1020                             finally {
1021                                 if (!stack.push)
1022                                     mappings.popCommand();
1023                             }
1024                         }
1025                     });
1026                 },
1027                 { count: true, type: "motion" });
1028
1029             mappings.add([modes.VISUAL], key,
1030                 desc,
1031                 function ({ count,  motion }) {
1032                     dactyl.assert(caretOk || editor.isTextEdit);
1033                     if (editor.isTextEdit)
1034                         doTxn(editor.selectedRange, editor);
1035                     else
1036                         cmd(editor, buffer.selection.getRangeAt(0));
1037                 },
1038                 { count: true, type: "motion" });
1039         }
1040
1041         addMotionMap(["d", "x"], "Delete text", true,  function (editor) { editor.cut(); });
1042         addMotionMap(["c"],      "Change text", true,  function (editor) { editor.cut(null, null, true); }, modes.INSERT);
1043         addMotionMap(["y"],      "Yank text",   false, function (editor, range) { editor.copy(range); }, null, true);
1044
1045         addMotionMap(["gu"], "Lowercase text", false,
1046              function (editor, range) {
1047                  editor.mungeRange(range, String.toLocaleLowerCase);
1048              });
1049
1050         addMotionMap(["gU"], "Uppercase text", false,
1051             function (editor, range) {
1052                 editor.mungeRange(range, String.toLocaleUpperCase);
1053             });
1054
1055         mappings.add([modes.OPERATOR],
1056             ["c", "d", "y"], "Select the entire line",
1057             function ({ command, count }) {
1058                 dactyl.assert(command == modes.getStack(0).params.forCommand);
1059
1060                 let sel = editor.selection;
1061                 sel.modify("move", "backward", "lineboundary");
1062                 sel.modify("extend", "forward", "lineboundary");
1063
1064                 if (command != "c")
1065                     sel.modify("extend", "forward", "character");
1066             },
1067             { count: true, type: "operator" });
1068
1069         let bind = function bind(names, description, action, params)
1070             mappings.add([modes.INPUT], names, description,
1071                          action, update({ type: "editor" }, params));
1072
1073         bind(["<C-w>"], "Delete previous word",
1074              function () {
1075                  if (editor.editor)
1076                      clear(false, /\w/)(editor.editor);
1077                  else
1078                      editor.executeCommand("cmd_deleteWordBackward", 1);
1079              });
1080
1081         bind(["<C-u>"], "Delete until beginning of current line",
1082              function () {
1083                  // Deletes the whole line. What the hell.
1084                  // editor.executeCommand("cmd_deleteToBeginningOfLine", 1);
1085
1086                  editor.executeCommand("cmd_selectBeginLine", 1);
1087                  if (editor.selection && editor.selection.isCollapsed) {
1088                      editor.executeCommand("cmd_deleteCharBackward", 1);
1089                      editor.executeCommand("cmd_selectBeginLine", 1);
1090                  }
1091
1092                  if (editor.getController("cmd_delete").isCommandEnabled("cmd_delete"))
1093                      editor.executeCommand("cmd_delete", 1);
1094              });
1095
1096         bind(["<C-k>"], "Delete until end of current line",
1097              function () { editor.executeCommand("cmd_deleteToEndOfLine", 1); });
1098
1099         bind(["<C-a>"], "Move cursor to beginning of current line",
1100              function () { editor.executeCommand("cmd_beginLine", 1); });
1101
1102         bind(["<C-e>"], "Move cursor to end of current line",
1103              function () { editor.executeCommand("cmd_endLine", 1); });
1104
1105         bind(["<C-h>"], "Delete character to the left",
1106              function () { events.feedkeys("<BS>", true); });
1107
1108         bind(["<C-d>"], "Delete character to the right",
1109              function () { editor.executeCommand("cmd_deleteCharForward", 1); });
1110
1111         bind(["<S-Insert>"], "Insert clipboard/selection",
1112              function () { editor.paste(); });
1113
1114         bind(["<C-i>"], "Edit text field with an external editor",
1115              function () { editor.editFieldExternally(); });
1116
1117         bind(["<C-t>"], "Edit text field in Text Edit mode",
1118              function () {
1119                  dactyl.assert(!editor.isTextEdit && editor.editor);
1120                  dactyl.assert(dactyl.focusedElement ||
1121                                // Sites like Google like to use a
1122                                // hidden, editable window for keyboard
1123                                // focus and use their own WYSIWYG editor
1124                                // implementations for the visible area,
1125                                // which we can't handle.
1126                                let (f = document.commandDispatcher.focusedWindow.frameElement)
1127                                     f && Hints.isVisible(f, true));
1128
1129                  modes.push(modes.TEXT_EDIT);
1130              });
1131
1132         // Ugh.
1133         mappings.add([modes.INPUT, modes.CARET],
1134             ["<*-CR>", "<*-BS>", "<*-Del>", "<*-Left>", "<*-Right>", "<*-Up>", "<*-Down>",
1135              "<*-Home>", "<*-End>", "<*-PageUp>", "<*-PageDown>",
1136              "<M-c>", "<M-v>", "<*-Tab>"],
1137             "Handled by " + config.host,
1138             () => Events.PASS_THROUGH);
1139
1140         mappings.add([modes.INSERT],
1141             ["<Space>", "<Return>"], "Expand Insert mode abbreviation",
1142             function () {
1143                 editor.expandAbbreviation(modes.INSERT);
1144                 return Events.PASS_THROUGH;
1145             });
1146
1147         mappings.add([modes.INSERT],
1148             ["<C-]>", "<C-5>"], "Expand Insert mode abbreviation",
1149             function () { editor.expandAbbreviation(modes.INSERT); });
1150
1151         let bind = function bind(names, description, action, params)
1152             mappings.add([modes.TEXT_EDIT], names, description,
1153                          action, update({ type: "editor" }, params));
1154
1155         bind(["<C-a>"], "Increment the next number",
1156              function ({ count }) { editor.modifyNumber(count || 1); },
1157              { count: true });
1158
1159         bind(["<C-x>"], "Decrement the next number",
1160              function ({ count }) { editor.modifyNumber(-(count || 1)); },
1161              { count: true });
1162
1163         // text edit mode
1164         bind(["u"], "Undo changes",
1165              function ({ count }) {
1166                  editor.editor.undo(Math.max(count, 1));
1167                  editor.deselect();
1168              },
1169              { count: true, noTransaction: true });
1170
1171         bind(["<C-r>"], "Redo undone changes",
1172              function ({ count }) {
1173                  editor.editor.redo(Math.max(count, 1));
1174                  editor.deselect();
1175              },
1176              { count: true, noTransaction: true });
1177
1178         bind(["D"], "Delete characters from the cursor to the end of the line",
1179              function () { editor.executeCommand("cmd_deleteToEndOfLine"); });
1180
1181         bind(["o"], "Open line below current",
1182              function () {
1183                  editor.executeCommand("cmd_endLine", 1);
1184                  modes.push(modes.INSERT);
1185                  events.feedkeys("<Return>");
1186              });
1187
1188         bind(["O"], "Open line above current",
1189              function () {
1190                  editor.executeCommand("cmd_beginLine", 1);
1191                  modes.push(modes.INSERT);
1192                  events.feedkeys("<Return>");
1193                  editor.executeCommand("cmd_linePrevious", 1);
1194              });
1195
1196         bind(["X"], "Delete character to the left",
1197              function (args) { editor.executeCommand("cmd_deleteCharBackward", Math.max(args.count, 1)); },
1198             { count: true });
1199
1200         bind(["x"], "Delete character to the right",
1201              function (args) { editor.executeCommand("cmd_deleteCharForward", Math.max(args.count, 1)); },
1202             { count: true });
1203
1204         // visual mode
1205         mappings.add([modes.CARET, modes.TEXT_EDIT],
1206             ["v"], "Start Visual mode",
1207             function () { modes.push(modes.VISUAL); });
1208
1209         mappings.add([modes.VISUAL],
1210             ["v", "V"], "End Visual mode",
1211             function () { modes.pop(); });
1212
1213         bind(["V"], "Start Visual Line mode",
1214              function () {
1215                  modes.push(modes.VISUAL, modes.LINE);
1216                  editor.executeCommand("cmd_beginLine", 1);
1217                  editor.executeCommand("cmd_selectLineNext", 1);
1218              });
1219
1220         mappings.add([modes.VISUAL],
1221             ["s"], "Change selected text",
1222             function () {
1223                 dactyl.assert(editor.isTextEdit);
1224                 editor.executeCommand("cmd_cut");
1225                 modes.push(modes.INSERT);
1226             });
1227
1228         mappings.add([modes.VISUAL],
1229             ["o"], "Move cursor to the other end of the selection",
1230             function () {
1231                 if (editor.isTextEdit)
1232                     var selection = editor.selection;
1233                 else
1234                     selection = buffer.focusedFrame.getSelection();
1235
1236                 util.assert(selection.focusNode);
1237                 let { focusOffset, anchorOffset, focusNode, anchorNode } = selection;
1238                 selection.collapse(focusNode, focusOffset);
1239                 selection.extend(anchorNode, anchorOffset);
1240             });
1241
1242         bind(["p"], "Paste clipboard contents",
1243              function ({ count }) {
1244                 dactyl.assert(!editor.isCaret);
1245                 editor.executeCommand(modules.bind("paste", editor, null),
1246                                       count || 1);
1247             },
1248             { count: true });
1249
1250         mappings.add([modes.COMMAND],
1251             ['"'], "Bind a register to the next command",
1252             function ({ arg }) {
1253                 editor.pushRegister(arg);
1254             },
1255             { arg: true });
1256
1257         mappings.add([modes.INPUT],
1258             ["<C-'>", '<C-">'], "Bind a register to the next command",
1259             function ({ arg }) {
1260                 editor.pushRegister(arg);
1261             },
1262             { arg: true });
1263
1264         let bind = function bind(names, description, action, params)
1265             mappings.add([modes.TEXT_EDIT, modes.OPERATOR, modes.VISUAL],
1266                          names, description,
1267                          action, update({ type: "editor" }, params));
1268
1269         // finding characters
1270         function offset(backward, before, pos) {
1271             if (!backward && modes.main != modes.TEXT_EDIT)
1272                 return before ? 0 : 1;
1273             if (before)
1274                 return backward ? +1 : -1;
1275             return 0;
1276         }
1277
1278         bind(["f"], "Find a character on the current line, forwards",
1279              function ({ arg, count }) {
1280                  editor.moveToPosition(editor.findChar(arg, Math.max(count, 1), false,
1281                                                        offset(false, false)),
1282                                        modes.main == modes.VISUAL);
1283              },
1284              { arg: true, count: true, type: "operator" });
1285
1286         bind(["F"], "Find a character on the current line, backwards",
1287              function ({ arg, count }) {
1288                  editor.moveToPosition(editor.findChar(arg, Math.max(count, 1), true,
1289                                                        offset(true, false)),
1290                                        modes.main == modes.VISUAL);
1291              },
1292              { arg: true, count: true, type: "operator" });
1293
1294         bind(["t"], "Find a character on the current line, forwards, and move to the character before it",
1295              function ({ arg, count }) {
1296                  editor.moveToPosition(editor.findChar(arg, Math.max(count, 1), false,
1297                                                        offset(false, true)),
1298                                        modes.main == modes.VISUAL);
1299              },
1300              { arg: true, count: true, type: "operator" });
1301
1302         bind(["T"], "Find a character on the current line, backwards, and move to the character after it",
1303              function ({ arg, count }) {
1304                  editor.moveToPosition(editor.findChar(arg, Math.max(count, 1), true,
1305                                                        offset(true, true)),
1306                                        modes.main == modes.VISUAL);
1307              },
1308              { arg: true, count: true, type: "operator" });
1309
1310         // text edit and visual mode
1311         mappings.add([modes.TEXT_EDIT, modes.VISUAL],
1312             ["~"], "Switch case of the character under the cursor and move the cursor to the right",
1313             function ({ count }) {
1314                 function munger(range)
1315                     String(range).replace(/./g, function (c) {
1316                         let lc = c.toLocaleLowerCase();
1317                         return c == lc ? c.toLocaleUpperCase() : lc;
1318                     });
1319
1320                 var range = editor.selectedRange;
1321                 if (range.collapsed) {
1322                     count = count || 1;
1323                     Editor.extendRange(range, true, { test: c => !!count-- }, true);
1324                 }
1325                 editor.mungeRange(range, munger, count != null);
1326
1327                 modes.pop(modes.TEXT_EDIT);
1328             },
1329             { count: true });
1330
1331         let bind = function bind(...args) mappings.add.apply(mappings, [[modes.AUTOCOMPLETE]].concat(args));
1332
1333         bind(["<Esc>"], "Return to Insert mode",
1334              () => Events.PASS_THROUGH);
1335
1336         bind(["<C-[>"], "Return to Insert mode",
1337              function () { events.feedkeys("<Esc>", { skipmap: true }); });
1338
1339         bind(["<Up>"], "Select the previous autocomplete result",
1340              () => Events.PASS_THROUGH);
1341
1342         bind(["<C-p>"], "Select the previous autocomplete result",
1343              function () { events.feedkeys("<Up>", { skipmap: true }); });
1344
1345         bind(["<Down>"], "Select the next autocomplete result",
1346              () => Events.PASS_THROUGH);
1347
1348         bind(["<C-n>"], "Select the next autocomplete result",
1349              function () { events.feedkeys("<Down>", { skipmap: true }); });
1350     },
1351     options: function initOptions() {
1352         options.add(["editor"],
1353             "The external text editor",
1354             "string", 'gvim -f +<line> +"sil! call cursor(0, <column>)" <file>', {
1355                 format: function (obj, value) {
1356                     let args = commands.parseArgs(value || this.value,
1357                                                   { argCount: "*", allowUnknownOptions: true })
1358                                        .map(util.compileMacro)
1359                                        .filter(fmt => fmt.valid(obj))
1360                                        .map(fmt => fmt(obj));
1361
1362                     if (obj["file"] && !this.has("file"))
1363                         args.push(obj["file"]);
1364                     return args;
1365                 },
1366                 has: function (key) Set.has(util.compileMacro(this.value).seen, key),
1367                 validator: function (value) {
1368                     this.format({}, value);
1369                     return Object.keys(util.compileMacro(value).seen)
1370                                  .every(k => ["column", "file", "line"].indexOf(k) >= 0);
1371                 }
1372             });
1373
1374         options.add(["insertmode", "im"],
1375             "Enter Insert mode rather than Text Edit mode when focusing text areas",
1376             "boolean", true);
1377
1378         options.add(["spelllang", "spl"],
1379             "The language used by the spell checker",
1380             "string", config.locale,
1381             {
1382                 initValue: function () {},
1383                 getter: function getter() {
1384                     try {
1385                         return services.spell.dictionary || "";
1386                     }
1387                     catch (e) {
1388                         return "";
1389                     }
1390                 },
1391                 setter: function setter(val) { services.spell.dictionary = val; },
1392                 completer: function completer(context) {
1393                     let res = {};
1394                     services.spell.getDictionaryList(res, {});
1395                     context.completions = res.value;
1396                     context.keys = { text: util.identity, description: util.identity };
1397                 }
1398             });
1399     },
1400     sanitizer: function initSanitizer() {
1401         sanitizer.addItem("registers", {
1402             description: "Register values",
1403             persistent: true,
1404             action: function (timespan, host) {
1405                 if (!host) {
1406                     for (let [k, v] in editor.registers)
1407                         if (timespan.contains(v.timestamp))
1408                             editor.registers.remove(k);
1409                     editor.registerRing.truncate(0);
1410                 }
1411             }
1412         });
1413     }
1414 });
1415
1416 // vim: set fdm=marker sw=4 sts=4 ts=8 et: