1 // Copyright (c) 2008-2014 Kris Maglione <maglione.k at Gmail>
2 // Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
4 // This work is licensed for reuse under an MIT license. Details are
5 // given in the LICENSE.txt file included with this file.
10 // command names taken from:
11 // http://developer.mozilla.org/en/docs/Editor_Embedding_Guide
13 /** @instance editor */
14 var Editor = Module("editor", XPCOM(Ci.nsIEditActionListener, ModuleBase), {
15 init: function init(elem) {
19 this.__defineGetter__("element", function () {
20 let elem = dactyl.focusedElement;
22 return elem.inputField || elem;
24 let win = document.commandDispatcher.focusedWindow;
25 return DOM(win).isEditable && win || null;
29 get registers() storage.newMap("registers", { privateData: true, store: true }),
30 get registerRing() storage.newArray("register-ring", { privateData: true, store: true }),
34 // Fixme: Move off this object.
35 currentRegister: null,
38 * Temporarily set the default register for the span of the next
41 pushRegister: function pushRegister(arg) {
42 let restore = this.currentRegister;
43 this.currentRegister = arg;
44 mappings.afterCommands(2, function () {
45 this.currentRegister = restore;
49 defaultRegister: "*+",
57 * Get the value of the register *name*.
59 * @param {string|number} name The name of the register to get.
60 * @returns {string|null}
63 getRegister: function getRegister(name) {
65 name = editor.currentRegister || editor.defaultRegister;
67 name = String(name)[0];
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);
77 res = this.registerRing.get(name);
79 return res != null ? res.text : res;
83 * Sets the value of register *name* to value. The following
84 * registers have special semantics:
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.
93 * @param {string|number} name The name of the register to set.
94 * @param {string|Range|Selection|Node} value The value to save to
97 setRegister: function setRegister(name, value, verbose) {
99 name = editor.currentRegister || editor.defaultRegister;
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 };
105 for (let n of String(name)) {
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);
115 this.registerRing.insert(value, n);
116 this.registerRing.truncate(10);
121 get isCaret() modes.getStack(1).main == modes.CARET,
122 get isTextEdit() modes.getStack(1).main == modes.TEXT_EDIT,
124 get editor() DOM(this.element).editor,
126 getController: function getController(cmd) {
127 let controllers = this.element && this.element.controllers;
128 dactyl.assert(controllers);
130 return controllers.getControllerForCommand(cmd || "cmd_beginLine");
133 get selection() this.editor && this.editor.selection || null,
134 get selectionController() this.editor && this.editor.selectionController || null,
136 deselect: function () {
137 if (this.selection && this.selection.focusNode)
138 this.selection.collapse(this.selection.focusNode,
139 this.selection.focusOffset);
142 get selectedRange() {
146 if (!this.selection.rangeCount) {
147 let range = RangeFind.nodeContents(this.editor.rootElement.ownerDocument);
148 range.collapse(true);
149 this.selectedRange = range;
151 return this.selection.getRangeAt(0);
153 set selectedRange(range) {
154 this.selection.removeAllRanges();
156 this.selection.addRange(range);
159 get selectedText() String(this.selection),
161 get preserveSelection() this.editor && !this.editor.shouldTxnSetSelection,
162 set preserveSelection(val) {
164 this.editor.setShouldTxnSetSelection(!val);
167 copy: function copy(range, name) {
168 range = range || this.selection;
170 if (!range.collapsed)
171 this.setRegister(name, range);
174 cut: function cut(range, name, noStrip) {
176 this.selectedRange = range;
178 if (!this.selection.isCollapsed)
179 this.setRegister(name, this.selection);
181 this.editor.deleteSelection(0, this.editor[noStrip ? "eNoStrip" : "eStrip"]);
184 paste: function paste(name) {
185 let text = this.getRegister(name);
186 dactyl.assert(text && this.editor instanceof Ci.nsIPlaintextEditor);
188 this.editor.insertText(text);
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);
201 // XXX: better as a precondition
205 let didCommand = false;
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
213 cmd(this.editor, controller);
218 dactyl.assert(didCommand);
224 moveToPosition: function (pos, select) {
226 var { startContainer, startOffset } = pos;
228 [startOffset, startOffset] = [this.selection.focusNode, pos];
229 this.selection[select ? "extend" : "collapse"](startContainer, startOffset);
232 mungeRange: function mungeRange(range, munger, selectEnd) {
233 let { editor } = this;
234 editor.beginPlaceHolderTransaction(null);
236 let [container, offset] = ["startContainer", "startOffset"];
238 [container, offset] = ["endContainer", "endOffset"];
242 let idx = range[offset];
243 let parent = range[container].parentNode;
244 let parentIdx = Array.indexOf(parent.childNodes,
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;
256 if (start == 0 && end == text.length)
259 text = text.slice(0, start)
260 + munger(text.slice(start, end))
263 if (text == node.textContent)
267 delta = text.length - node.textContent.length;
269 if (editor instanceof Ci.nsIPlaintextEditor) {
270 this.selectedRange = RangeFind.nodeContents(node);
271 editor.insertText(text);
274 node.textContent = text;
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);
282 editor.endPlaceHolderTransaction();
286 findChar: function findChar(key, count, backward, offset) {
287 count = count || 1; // XXX ?
288 offset = (offset || 0) - !!backward;
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);
296 let range = this.selectedRange.cloneRange();
297 let collapse = DOM(this.element).whiteSpace == "normal";
299 // Find the *count*th occurance of *char* before a non-collapsed
300 // \n, ignoring the character at the caret.
302 function test(c) (collapse || c != "\n") && !!(!i++ || c != char || --count)
304 Editor.extendRange(range, !backward, { test: test }, true);
305 dactyl.assert(count == 0);
306 range.collapse(backward);
308 // Skip to any requested offset.
309 count = Math.abs(offset);
310 Editor.extendRange(range, offset > 0,
311 { test: c => !!count-- },
313 range.collapse(offset < 0);
318 findNumber: function findNumber(range) {
320 range = this.selectedRange.cloneRange();
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);
330 dactyl.assert(/^\d+$/.test(range));
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);
338 // Grab the sign, if it's there.
339 Editor.extendRange(range, false, /[+-]/, true);
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");
352 this.selectedRange = range;
353 this.editor.insertText(String(number));
354 this.selection.modify("move", "backward", "character");
358 * Edits the given file in the external editor as specified by the
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.
367 editFileExternally: function (args, blocking) {
368 if (!isObject(args) || args instanceof File)
369 args = { file: args };
370 args.file = args.file.path || args.file;
372 let args = options.get("editor").format(args);
374 dactyl.assert(args.length >= 1, _("option.notSet", "editor"));
376 io.run(args.shift(), args, blocking);
379 // TODO: clean up with 2 functions for textboxes and currentEditor?
380 editFieldExternally: function editFieldExternally(forceEditing) {
381 if (!options["editor"])
384 let textBox = config.isComposeWindow ? null : dactyl.focusedElement;
385 if (!DOM(textBox).isInput)
389 let keepFocus = modes.stack.some(m => isinstance(m.main, modes.COMMAND_LINE));
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);
401 var text = textBox.value;
402 var pre = text.substr(0, textBox.selectionStart);
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))
412 if (!editor_.selection.rangeCount)
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(/(?:<\/[^>"]+>)+$/, "");
426 line = 1 + pre.replace(/[^\n]/g, "").length;
427 column = 1 + pre.replace(/[^]*\n/, "").length;
429 let origGroup = DOM(textBox).highlight.toString();
430 let cleanup = promises.task(function cleanup(error) {
434 let blink = ["EditorBlink1", "EditorBlink2"];
436 dactyl.reportError(error, true);
437 blink[1] = "EditorError";
440 dactyl.trapErrors(update, null, true);
442 if (tmpfile && tmpfile.exists())
443 tmpfile.remove(false);
446 DOM(textBox).highlight.remove("EditorEditing");
448 dactyl.focus(textBox);
450 for (let group in values(blink.concat(blink, ""))) {
451 highlight.highlightNode(textBox, origGroup + " " + group);
453 yield promises.sleep(100);
458 function update(force) {
459 if (force !== true && tmpfile.lastModifiedTime <= lastUpdate)
461 lastUpdate = Date.now();
463 let val = tmpfile.read();
468 let elem = DOM(textBox);
469 elem.attrNS(NS, "modifiable", true)
471 elem.input().attrNS(NS, "modifiable", null);
475 while (editor_.rootElement.firstChild)
476 editor_.rootElement.removeChild(editor_.rootElement.firstChild);
477 editor_.rootElement.innerHTML = val;
482 var tmpfile = io.createTempFile("txt", "." + buffer.uri.host);
484 throw Error(_("io.cantCreateTempFile"));
489 DOM(textBox).highlight.add("EditorEditing");
492 if (!tmpfile.write(text))
493 throw Error(_("io.cantEncode"));
495 var lastUpdate = Date.now();
497 var timer = services.Timer(update, 100, services.Timer.TYPE_REPEATING_SLACK);
498 this.editFileExternally({ file: tmpfile.path, line: line, column: column }, cleanup);
506 * Expands an abbreviation in the currently active textbox.
508 * @param {string} mode The mode filter.
509 * @see Abbreviation#expand
511 expandAbbreviation: function (mode) {
515 let range = this.selectedRange.cloneRange();
516 if (!range.collapsed)
519 Editor.extendRange(range, false, /\S/, true);
520 let abbrev = abbreviations.match(mode, String(range));
522 range.setStart(range.startContainer, range.endOffset - abbrev.lhs.length);
523 this.selectedRange = range;
524 this.editor.insertText(abbrev.expand(this.element));
528 // nsIEditActionListener:
529 WillDeleteNode: util.wrapCallback(function WillDeleteNode(node) {
530 if (!editor.skipSave && node.textContent)
531 this.setRegister(0, node);
533 WillDeleteSelection: util.wrapCallback(function WillDeleteSelection(selection) {
534 if (!editor.skipSave && !selection.isCollapsed)
535 this.setRegister(0, selection);
537 WillDeleteText: util.wrapCallback(function WillDeleteText(node, start, length) {
538 if (!editor.skipSave && length)
539 this.setRegister(0, node.textContent.substr(start, length));
542 TextsIterator: Class("TextsIterator", {
543 init: function init(range, context, after) {
545 this.start = context || range[after ? "endContainer" : "startContainer"];
547 this.context = this.start;
551 __iterator__: function __iterator__() {
552 while (this.nextNode())
556 prevNode: function prevNode() {
558 return this.context = this.start;
560 var node = this.context;
562 node = node.previousSibling;
565 node = this.context.parentNode;
567 while (node.lastChild)
568 node = node.lastChild;
570 if (!node || !RangeFind.containsNode(this.range, node, true))
573 return this.context = node;
576 nextNode: function nextNode() {
578 return this.context = this.start;
581 var node = this.context.firstChild;
585 while (node.parentNode && node != this.range.endContainer
586 && !node.nextSibling)
587 node = node.parentNode;
589 node = node.nextSibling;
592 if (!node || !RangeFind.containsNode(this.range, node, true))
595 return this.context = node;
598 getPrev: function getPrev() {
599 return this.filter("prevNode");
602 getNext: function getNext() {
603 return this.filter("nextNode");
606 filter: function filter(meth) {
608 while (node = this[meth]())
609 if (node instanceof Ci.nsIDOMText &&
610 DOM(node).isVisible &&
611 DOM(node).style.MozUserSelect != "none")
616 extendRange: function extendRange(range, forward, re, sameWord, root, end) {
617 function advance(positive) {
619 while (idx == text.length && (node = iterator.getNext())) {
620 if (node == iterator.start)
624 text += node.textContent;
625 range[set](node, idx - start);
628 if (idx >= text.length || re.test(text[idx]) != positive)
630 range[set](range[container], ++idx - start);
633 function retreat(positive) {
635 while (idx == 0 && (node = iterator.getPrev())) {
636 let str = node.textContent;
637 if (node == iterator.start)
643 range[set](node, idx);
645 if (idx == 0 || re.test(text[idx - 1]) != positive)
647 range[set](range[container], --idx);
652 end = forward ? "end" : "start";
653 let [container, offset, set] = [end + "Container", end + "Offset",
654 "set" + util.capitalize(end)];
657 for (root = range[container];
658 root.parentNode instanceof Element && !DOM(root).isEditable;
659 root = root.parentNode)
661 if (root instanceof Ci.nsIDOMNSEditableElement)
663 if (root instanceof Ci.nsIEditor)
664 root = root.rootElement;
666 let node = range[container];
667 let iterator = Editor.TextsIterator(RangeFind.nodeContents(root),
687 getEditor: function (elem) {
688 if (arguments.length === 0) {
689 dactyl.assert(dactyl.focusedElement);
690 return dactyl.focusedElement;
694 elem = dactyl.focusedElement || document.commandDispatcher.focusedWindow;
697 return DOM(elem).editor;
700 modes: function initModes() {
701 modes.addMode("OPERATOR", {
703 description: "Mappings which move the cursor",
706 modes.addMode("VISUAL", {
708 description: "Active when text is selected",
709 display: function () "VISUAL" + (this._extended & modes.LINE ? " LINE" : ""),
710 bases: [modes.COMMAND],
713 enter: function (stack) {
714 if (editor.selectionController)
715 editor.selectionController.setCaretVisibilityDuringSelection(true);
717 leave: function (stack, newMode) {
718 if (newMode.main == modes.CARET) {
719 let selection = content.getSelection();
720 if (selection && !selection.isCollapsed)
721 selection.collapseToStart();
727 modes.addMode("TEXT_EDIT", {
729 description: "Vim-like editing of input elements",
730 bases: [modes.COMMAND],
733 onKeyPress: function (eventList) {
734 const KILL = false, PASS = true;
737 if (eventList[0].charCode || /^<(?:.-)*(?:BS|Del|C-h|C-w|C-u|C-k)>$/.test(DOM.Event.stringify(eventList[0]))) {
745 modes.addMode("INSERT", {
747 description: "Active when an input element is focused",
751 modes.addMode("AUTOCOMPLETE", {
752 description: "Active when an input autocomplete pop-up is active",
753 display: function () "AUTOCOMPLETE (insert)",
754 bases: [modes.INSERT]
757 commands: function initCommands() {
758 commands.add(["reg[isters]"],
759 "List the contents of known registers",
761 completion.listCompleter("register", args[0]);
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 };
770 context.match = function (r) !this.filter || this.filter.contains(r);
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);
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");
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();
789 mappings: function initMappings() {
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();
798 editor.inEditMap = true;
800 postExecute: function preExecute(args) {
801 editor.inEditMap = false;
803 if (!this.noTransaction)
804 this.editor.endTransaction();
809 Map.types["operator"] = {
810 preExecute: function preExecute(args) {
811 editor.inEditMap = true;
813 postExecute: function preExecute(args) {
814 editor.inEditMap = true;
815 if (modes.main == modes.OPERATOR)
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) {
827 function caretExecute(arg) {
828 let win = document.commandDispatcher.focusedWindow;
829 let controller = util.selectionController(win);
830 let sel = controller.getSelection(controller.SELECTION_NORMAL);
832 let buffer = Buffer(win);
833 if (!sel.rangeCount) // Hack.
836 if (caretModeMethod == "pageMove") { // Grr.
837 buffer.scrollVertical("pages", caretModeArg ? 1 : -1);
841 controller[caretModeMethod](caretModeArg, arg);
844 mappings.add([modes.VISUAL], keys, description,
845 function ({ count }) {
848 let caret = !dactyl.focusedElement;
849 let controller = buffer.selectionController;
851 while (count-- && modes.main == modes.VISUAL) {
853 caretExecute(true, true);
855 if (callable(visualTextEditCommand))
856 visualTextEditCommand(editor.editor);
858 editor.executeCommand(visualTextEditCommand);
864 mappings.add([modes.CARET, modes.TEXT_EDIT, modes.OPERATOR], keys, description,
865 function ({ count }) {
869 editor.executeCommand(textEditCommand, count);
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 || "",
882 commands.forEach(function (cmd) { editor.executeCommand(cmd, 1); });
883 modes.push(modes.INSERT);
888 function selectPreviousLine() {
889 editor.executeCommand("cmd_selectLinePrevious");
890 if ((modes.extended & modes.LINE) && !editor.selectedText)
891 editor.executeCommand("cmd_selectLinePrevious");
894 function selectNextLine() {
895 editor.executeCommand("cmd_selectLineNext");
896 if ((modes.extended & modes.LINE) && !editor.selectedText)
897 editor.executeCommand("cmd_selectLineNext");
900 function updateRange(editor, forward, re, modify, sameWord) {
901 let sel = editor.selection;
902 let range = sel.getRangeAt(0);
904 let end = range.endContainer == sel.focusNode && range.endOffset == sel.focusOffset;
908 Editor.extendRange(range, forward, re, sameWord,
909 editor.rootElement, end ? "end" : "start");
911 editor.selectionController.repaintSelection(editor.selectionController.SELECTION_NORMAL);
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);
924 function move(forward, re, sameWord)
925 function _move(editor) {
926 updateRange(editor, forward, re,
927 function (range) { range.collapse(!forward); },
930 function select(forward, re)
931 function _select(editor) {
932 updateRange(editor, forward, re,
933 function (range) {});
935 function beginLine(editor_) {
936 editor.executeCommand("cmd_beginLine");
937 move(true, /\s/, true)(editor_);
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");
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");
984 function addMotionMap(key, desc, select, cmd, mode, caretOk) {
985 function doTxn(range, editor) {
987 editor.editor.beginTransaction();
988 cmd(editor, range, editor.editor);
991 editor.editor.endTransaction();
995 mappings.add([modes.TEXT_EDIT], key,
997 function ({ command, count, motion }) {
998 let start = editor.selectedRange.cloneRange();
1000 mappings.pushCommand();
1001 modes.push(modes.OPERATOR, null, {
1002 forCommand: command,
1006 leave: function leave(stack) {
1008 if (stack.push || stack.fromEscape)
1011 editor.withSavedValues(["inEditMap"], function () {
1012 this.inEditMap = true;
1014 let range = RangeFind.union(start, editor.selectedRange);
1015 editor.selectedRange = select ? range : start;
1016 doTxn(range, editor);
1019 editor.currentRegister = null;
1020 modes.delay(function () {
1027 mappings.popCommand();
1032 { count: true, type: "motion" });
1034 mappings.add([modes.VISUAL], key,
1036 function ({ count, motion }) {
1037 dactyl.assert(caretOk || editor.isTextEdit);
1038 if (editor.isTextEdit)
1039 doTxn(editor.selectedRange, editor);
1041 cmd(editor, buffer.selection.getRangeAt(0));
1043 { count: true, type: "motion" });
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);
1050 addMotionMap(["gu"], "Lowercase text", false,
1051 function (editor, range) {
1052 editor.mungeRange(range, String.toLocaleLowerCase);
1055 addMotionMap(["gU"], "Uppercase text", false,
1056 function (editor, range) {
1057 editor.mungeRange(range, String.toLocaleUpperCase);
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);
1065 let sel = editor.selection;
1066 sel.modify("move", "backward", "lineboundary");
1067 sel.modify("extend", "forward", "lineboundary");
1070 sel.modify("extend", "forward", "character");
1072 { count: true, type: "operator" });
1074 let bind = function bind(names, description, action, params)
1075 mappings.add([modes.INPUT], names, description,
1076 action, update({ type: "editor" }, params));
1078 bind(["<C-w>"], "Delete previous word",
1081 clear(false, /\w/)(editor.editor);
1083 editor.executeCommand("cmd_deleteWordBackward", 1);
1086 bind(["<C-u>"], "Delete until beginning of current line",
1088 // Deletes the whole line. What the hell.
1089 // editor.executeCommand("cmd_deleteToBeginningOfLine", 1);
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);
1097 if (editor.getController("cmd_delete").isCommandEnabled("cmd_delete"))
1098 editor.executeCommand("cmd_delete", 1);
1101 bind(["<C-k>"], "Delete until end of current line",
1102 function () { editor.executeCommand("cmd_deleteToEndOfLine", 1); });
1104 bind(["<C-a>"], "Move cursor to beginning of current line",
1105 function () { editor.executeCommand("cmd_beginLine", 1); });
1107 bind(["<C-e>"], "Move cursor to end of current line",
1108 function () { editor.executeCommand("cmd_endLine", 1); });
1110 bind(["<C-h>"], "Delete character to the left",
1111 function () { events.feedkeys("<BS>", true); });
1113 bind(["<C-d>"], "Delete character to the right",
1114 function () { editor.executeCommand("cmd_deleteCharForward", 1); });
1116 bind(["<S-Insert>"], "Insert clipboard/selection",
1117 function () { editor.paste(); });
1119 bind(["<C-i>"], "Edit text field with an external editor",
1120 function () { editor.editFieldExternally(); });
1122 bind(["<C-t>"], "Edit text field in Text Edit mode",
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));
1134 modes.push(modes.TEXT_EDIT);
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);
1145 mappings.add([modes.INSERT],
1146 ["<Space>", "<Return>"], "Expand Insert mode abbreviation",
1148 editor.expandAbbreviation(modes.INSERT);
1149 return Events.PASS_THROUGH;
1152 mappings.add([modes.INSERT],
1153 ["<C-]>", "<C-5>"], "Expand Insert mode abbreviation",
1154 function () { editor.expandAbbreviation(modes.INSERT); });
1156 let bind = function bind(names, description, action, params)
1157 mappings.add([modes.TEXT_EDIT], names, description,
1158 action, update({ type: "editor" }, params));
1160 bind(["<C-a>"], "Increment the next number",
1161 function ({ count }) { editor.modifyNumber(count || 1); },
1164 bind(["<C-x>"], "Decrement the next number",
1165 function ({ count }) { editor.modifyNumber(-(count || 1)); },
1169 bind(["u"], "Undo changes",
1170 function ({ count }) {
1171 editor.editor.undo(Math.max(count, 1));
1174 { count: true, noTransaction: true });
1176 bind(["<C-r>"], "Redo undone changes",
1177 function ({ count }) {
1178 editor.editor.redo(Math.max(count, 1));
1181 { count: true, noTransaction: true });
1183 bind(["D"], "Delete characters from the cursor to the end of the line",
1184 function () { editor.executeCommand("cmd_deleteToEndOfLine"); });
1186 bind(["o"], "Open line below current",
1188 editor.executeCommand("cmd_endLine", 1);
1189 modes.push(modes.INSERT);
1190 events.feedkeys("<Return>");
1193 bind(["O"], "Open line above current",
1195 editor.executeCommand("cmd_beginLine", 1);
1196 modes.push(modes.INSERT);
1197 events.feedkeys("<Return>");
1198 editor.executeCommand("cmd_linePrevious", 1);
1201 bind(["X"], "Delete character to the left",
1202 function (args) { editor.executeCommand("cmd_deleteCharBackward", Math.max(args.count, 1)); },
1205 bind(["x"], "Delete character to the right",
1206 function (args) { editor.executeCommand("cmd_deleteCharForward", Math.max(args.count, 1)); },
1210 mappings.add([modes.CARET, modes.TEXT_EDIT],
1211 ["v"], "Start Visual mode",
1212 function () { modes.push(modes.VISUAL); });
1214 mappings.add([modes.VISUAL],
1215 ["v", "V"], "End Visual mode",
1216 function () { modes.pop(); });
1218 bind(["V"], "Start Visual Line mode",
1220 modes.push(modes.VISUAL, modes.LINE);
1221 editor.executeCommand("cmd_beginLine", 1);
1222 editor.executeCommand("cmd_selectLineNext", 1);
1225 mappings.add([modes.VISUAL],
1226 ["s"], "Change selected text",
1228 dactyl.assert(editor.isTextEdit);
1229 editor.executeCommand("cmd_cut");
1230 modes.push(modes.INSERT);
1233 mappings.add([modes.VISUAL],
1234 ["o"], "Move cursor to the other end of the selection",
1236 if (editor.isTextEdit)
1237 var selection = editor.selection;
1239 selection = buffer.focusedFrame.getSelection();
1241 util.assert(selection.focusNode);
1242 let { focusOffset, anchorOffset, focusNode, anchorNode } = selection;
1243 selection.collapse(focusNode, focusOffset);
1244 selection.extend(anchorNode, anchorOffset);
1247 bind(["p"], "Paste clipboard contents",
1248 function ({ count }) {
1249 dactyl.assert(!editor.isCaret);
1250 editor.executeCommand(modules.bind("paste", editor, null),
1255 mappings.add([modes.COMMAND],
1256 ['"'], "Bind a register to the next command",
1257 function ({ arg }) {
1258 editor.pushRegister(arg);
1262 mappings.add([modes.INPUT],
1263 ["<C-'>", '<C-">'], "Bind a register to the next command",
1264 function ({ arg }) {
1265 editor.pushRegister(arg);
1269 let bind = function bind(names, description, action, params)
1270 mappings.add([modes.TEXT_EDIT, modes.OPERATOR, modes.VISUAL],
1272 action, update({ type: "editor" }, params));
1274 // finding characters
1275 function offset(backward, before, pos) {
1276 if (!backward && modes.main != modes.TEXT_EDIT)
1277 return before ? 0 : 1;
1279 return backward ? +1 : -1;
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);
1289 { arg: true, count: true, type: "operator" });
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);
1297 { arg: true, count: true, type: "operator" });
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);
1305 { arg: true, count: true, type: "operator" });
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);
1313 { arg: true, count: true, type: "operator" });
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;
1325 var range = editor.selectedRange;
1326 if (range.collapsed) {
1328 Editor.extendRange(range, true, { test: c => !!count-- }, true);
1330 editor.mungeRange(range, munger, count != null);
1332 modes.pop(modes.TEXT_EDIT);
1336 let bind = function bind(...args) mappings.add.apply(mappings, [[modes.AUTOCOMPLETE]].concat(args));
1338 bind(["<Esc>"], "Return to Insert mode",
1339 () => Events.PASS_THROUGH);
1341 bind(["<C-[>"], "Return to Insert mode",
1342 function () { events.feedkeys("<Esc>", { skipmap: true }); });
1344 bind(["<Up>"], "Select the previous autocomplete result",
1345 () => Events.PASS_THROUGH);
1347 bind(["<C-p>"], "Select the previous autocomplete result",
1348 function () { events.feedkeys("<Up>", { skipmap: true }); });
1350 bind(["<Down>"], "Select the next autocomplete result",
1351 () => Events.PASS_THROUGH);
1353 bind(["<C-n>"], "Select the next autocomplete result",
1354 function () { events.feedkeys("<Down>", { skipmap: true }); });
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));
1367 if (obj["file"] && !this.has("file"))
1368 args.push(obj["file"]);
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));
1380 options.add(["insertmode", "im"],
1381 "Enter Insert mode rather than Text Edit mode when focusing text areas",
1384 options.add(["spelllang", "spl"],
1385 "The language used by the spell checker",
1386 "string", config.locale,
1388 initValue: function () {},
1389 getter: function getter() {
1391 return services.spell.dictionary || "";
1397 setter: function setter(val) { services.spell.dictionary = val; },
1398 completer: function completer(context) {
1400 services.spell.getDictionaryList(res, {});
1401 context.completions = res.value;
1402 context.keys = { text: util.identity, description: util.identity };
1406 sanitizer: function initSanitizer() {
1407 sanitizer.addItem("registers", {
1408 description: "Register values",
1410 action: function (timespan, 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);
1422 // vim: set fdm=marker sw=4 sts=4 ts=8 et: