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