]> git.donarmstrong.com Git - dactyl.git/blob - common/content/editor.js
e1df65370adea22d02da9706ae7bccabcc88d41c
[dactyl.git] / common / content / editor.js
1 // Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 //
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
5 "use strict";
6
7 /** @scope modules */
8
9 // command names taken from:
10 // http://developer.mozilla.org/en/docs/Editor_Embedding_Guide
11
12 /** @instance editor */
13 var Editor = Module("editor", {
14     get isCaret() modes.getStack(1).main == modes.CARET,
15     get isTextEdit() modes.getStack(1).main == modes.TEXT_EDIT,
16
17     unselectText: function (toEnd) {
18         try {
19             Editor.getEditor(null).selection[toEnd ? "collapseToEnd" : "collapseToStart"]();
20         }
21         catch (e) {}
22     },
23
24     selectedText: function () String(Editor.getEditor(null).selection),
25
26     pasteClipboard: function (clipboard, toStart) {
27         // TODO: I don't think this is needed anymore? --djk
28         if (util.OS.isWindows) {
29             this.executeCommand("cmd_paste");
30             return;
31         }
32
33         let elem = dactyl.focusedElement;
34         if (elem.inputField)
35             elem = elem.inputField;
36
37         if (elem.setSelectionRange) {
38             let text = dactyl.clipboardRead(clipboard);
39             if (!text)
40                 return;
41             if (isinstance(elem, [HTMLInputElement, XULTextBoxElement]))
42                 text = text.replace(/\n+/g, "");
43
44             // This is a hacky fix - but it works.
45             // <s-insert> in the bottom of a long textarea bounces up
46             let top = elem.scrollTop;
47             let left = elem.scrollLeft;
48
49             let start = elem.selectionStart; // caret position
50             let end = elem.selectionEnd;
51             let value = elem.value.substring(0, start) + text + elem.value.substring(end);
52             elem.value = value;
53
54             if (/^(search|text)$/.test(elem.type))
55                 Editor.getEditor(elem).rootElement.firstChild.textContent = value;
56
57             elem.selectionStart = Math.min(start + (toStart ? 0 : text.length), elem.value.length);
58             elem.selectionEnd = elem.selectionStart;
59
60             elem.scrollTop = top;
61             elem.scrollLeft = left;
62
63             events.dispatch(elem, events.create(elem.ownerDocument, "input"));
64         }
65     },
66
67     // count is optional, defaults to 1
68     executeCommand: function (cmd, count) {
69         let editor = Editor.getEditor(null);
70         let controller = Editor.getController();
71         dactyl.assert(callable(cmd) ||
72                           controller &&
73                           controller.supportsCommand(cmd) &&
74                           controller.isCommandEnabled(cmd));
75
76         // XXX: better as a precondition
77         if (count == null)
78           count = 1;
79
80         let didCommand = false;
81         while (count--) {
82             // some commands need this try/catch workaround, because a cmd_charPrevious triggered
83             // at the beginning of the textarea, would hang the doCommand()
84             // good thing is, we need this code anyway for proper beeping
85             try {
86                 if (callable(cmd))
87                     cmd(editor, controller);
88                 else
89                     controller.doCommand(cmd);
90                 didCommand = true;
91             }
92             catch (e) {
93                 util.reportError(e);
94                 dactyl.assert(didCommand);
95                 break;
96             }
97         }
98     },
99
100     // cmd = y, d, c
101     // motion = b, 0, gg, G, etc.
102     selectMotion: function selectMotion(cmd, motion, count) {
103         // XXX: better as a precondition
104         if (count == null)
105             count = 1;
106
107         if (cmd == motion) {
108             motion = "j";
109             count--;
110         }
111
112         if (modes.main != modes.VISUAL)
113             modes.push(modes.VISUAL);
114
115         switch (motion) {
116         case "j":
117             this.executeCommand("cmd_beginLine", 1);
118             this.executeCommand("cmd_selectLineNext", count + 1);
119             break;
120         case "k":
121             this.executeCommand("cmd_beginLine", 1);
122             this.executeCommand("cmd_lineNext", 1);
123             this.executeCommand("cmd_selectLinePrevious", count + 1);
124             break;
125         case "h":
126             this.executeCommand("cmd_selectCharPrevious", count);
127             break;
128         case "l":
129             this.executeCommand("cmd_selectCharNext", count);
130             break;
131         case "e":
132         case "w":
133             this.executeCommand("cmd_selectWordNext", count);
134             break;
135         case "b":
136             this.executeCommand("cmd_selectWordPrevious", count);
137             break;
138         case "0":
139         case "^":
140             this.executeCommand("cmd_selectBeginLine", 1);
141             break;
142         case "$":
143             this.executeCommand("cmd_selectEndLine", 1);
144             break;
145         case "gg":
146             this.executeCommand("cmd_endLine", 1);
147             this.executeCommand("cmd_selectTop", 1);
148             this.executeCommand("cmd_selectBeginLine", 1);
149             break;
150         case "G":
151             this.executeCommand("cmd_beginLine", 1);
152             this.executeCommand("cmd_selectBottom", 1);
153             this.executeCommand("cmd_selectEndLine", 1);
154             break;
155
156         default:
157             dactyl.beep();
158             return;
159         }
160     },
161
162     // This function will move/select up to given "pos"
163     // Simple setSelectionRange() would be better, but we want to maintain the correct
164     // order of selectionStart/End (a Gecko bug always makes selectionStart <= selectionEnd)
165     // Use only for small movements!
166     moveToPosition: function (pos, forward, select) {
167         if (!select) {
168             Editor.getEditor().setSelectionRange(pos, pos);
169             return;
170         }
171
172         if (forward) {
173             if (pos <= Editor.getEditor().selectionEnd || pos > Editor.getEditor().value.length)
174                 return;
175
176             do { // TODO: test code for endless loops
177                 this.executeCommand("cmd_selectCharNext", 1);
178             }
179             while (Editor.getEditor().selectionEnd != pos);
180         }
181         else {
182             if (pos >= Editor.getEditor().selectionStart || pos < 0)
183                 return;
184
185             do { // TODO: test code for endless loops
186                 this.executeCommand("cmd_selectCharPrevious", 1);
187             }
188             while (Editor.getEditor().selectionStart != pos);
189         }
190     },
191
192     // returns the position of char
193     findCharForward: function (ch, count) {
194         if (!Editor.getEditor())
195             return -1;
196
197         let text = Editor.getEditor().value;
198         // XXX
199         if (count == null)
200             count = 1;
201
202         for (let i = Editor.getEditor().selectionEnd + 1; i < text.length; i++) {
203             if (text[i] == "\n")
204                 break;
205             if (text[i] == ch)
206                 count--;
207             if (count == 0)
208                 return i + 1; // always position the cursor after the char
209         }
210
211         dactyl.beep();
212         return -1;
213     },
214
215     // returns the position of char
216     findCharBackward: function (ch, count) {
217         if (!Editor.getEditor())
218             return -1;
219
220         let text = Editor.getEditor().value;
221         // XXX
222         if (count == null)
223             count = 1;
224
225         for (let i = Editor.getEditor().selectionStart - 1; i >= 0; i--) {
226             if (text[i] == "\n")
227                 break;
228             if (text[i] == ch)
229                 count--;
230             if (count == 0)
231                 return i;
232         }
233
234         dactyl.beep();
235         return -1;
236     },
237
238     /**
239      * Edits the given file in the external editor as specified by the
240      * 'editor' option.
241      *
242      * @param {object|File|string} args An object specifying the file,
243      *  line, and column to edit. If a non-object is specified, it is
244      *  treated as the file parameter of the object.
245      * @param {boolean} blocking If true, this function does not return
246      *  until the editor exits.
247      */
248     editFileExternally: function (args, blocking) {
249         if (!isObject(args) || args instanceof File)
250             args = { file: args };
251         args.file = args.file.path || args.file;
252
253         let args = options.get("editor").format(args);
254
255         dactyl.assert(args.length >= 1, _("editor.noEditor"));
256
257         io.run(args.shift(), args, blocking);
258     },
259
260     // TODO: clean up with 2 functions for textboxes and currentEditor?
261     editFieldExternally: function editFieldExternally(forceEditing) {
262         if (!options["editor"])
263             return;
264
265         let textBox = config.isComposeWindow ? null : dactyl.focusedElement;
266         let line, column;
267
268         if (!forceEditing && textBox && textBox.type == "password") {
269             commandline.input("Editing a password field externally will reveal the password. Would you like to continue? (yes/[no]): ",
270                 function (resp) {
271                     if (resp && resp.match(/^y(es)?$/i))
272                         editor.editFieldExternally(true);
273                 });
274                 return;
275         }
276
277         if (textBox) {
278             var text = textBox.value;
279             let pre = text.substr(0, textBox.selectionStart);
280             line = 1 + pre.replace(/[^\n]/g, "").length;
281             column = 1 + pre.replace(/[^]*\n/, "").length;
282         }
283         else {
284             var editor = window.GetCurrentEditor ? GetCurrentEditor()
285                                                  : Editor.getEditor(document.commandDispatcher.focusedWindow);
286             dactyl.assert(editor);
287             text = Array.map(editor.rootElement.childNodes, function (e) util.domToString(e, true)).join("");
288         }
289
290         let origGroup = textBox && textBox.getAttributeNS(NS, "highlight") || "";
291         let cleanup = util.yieldable(function cleanup(error) {
292             if (timer)
293                 timer.cancel();
294
295             let blink = ["EditorBlink1", "EditorBlink2"];
296             if (error) {
297                 dactyl.reportError(error, true);
298                 blink[1] = "EditorError";
299             }
300             else
301                 dactyl.trapErrors(update, null, true);
302
303             if (tmpfile && tmpfile.exists())
304                 tmpfile.remove(false);
305
306             if (textBox) {
307                 dactyl.focus(textBox);
308                 for (let group in values(blink.concat(blink, ""))) {
309                     highlight.highlightNode(textBox, origGroup + " " + group);
310                     yield 100;
311                 }
312             }
313         });
314
315         function update(force) {
316             if (force !== true && tmpfile.lastModifiedTime <= lastUpdate)
317                 return;
318             lastUpdate = Date.now();
319
320             let val = tmpfile.read();
321             if (textBox)
322                 textBox.value = val;
323             else {
324                 while (editor.rootElement.firstChild)
325                     editor.rootElement.removeChild(editor.rootElement.firstChild);
326                 editor.rootElement.innerHTML = val;
327             }
328         }
329
330         try {
331             var tmpfile = io.createTempFile();
332             if (!tmpfile)
333                 throw Error("Couldn't create temporary file");
334
335             if (textBox) {
336                 highlight.highlightNode(textBox, origGroup + " EditorEditing");
337                 textBox.blur();
338             }
339
340             if (!tmpfile.write(text))
341                 throw Error("Input contains characters not valid in the current " +
342                             "file encoding");
343
344             var lastUpdate = Date.now();
345
346             var timer = services.Timer(update, 100, services.Timer.TYPE_REPEATING_SLACK);
347             this.editFileExternally({ file: tmpfile.path, line: line, column: column }, cleanup);
348         }
349         catch (e) {
350             cleanup(e);
351         }
352     },
353
354     /**
355      * Expands an abbreviation in the currently active textbox.
356      *
357      * @param {string} mode The mode filter.
358      * @see Abbreviation#expand
359      */
360     expandAbbreviation: function (mode) {
361         let elem = dactyl.focusedElement;
362         if (!(elem && elem.value))
363             return;
364
365         let text   = elem.value;
366         let start  = elem.selectionStart;
367         let end    = elem.selectionEnd;
368         let abbrev = abbreviations.match(mode, text.substring(0, start).replace(/.*\s/g, ""));
369         if (abbrev) {
370             let len = abbrev.lhs.length;
371             let rhs = abbrev.expand(elem);
372             elem.value = text.substring(0, start - len) + rhs + text.substring(start);
373             elem.selectionStart = start - len + rhs.length;
374             elem.selectionEnd   = end   - len + rhs.length;
375         }
376     },
377 }, {
378     extendRange: function extendRange(range, forward, re, sameWord) {
379         function advance(positive) {
380             let idx = range.endOffset;
381             while (idx < text.length && re.test(text[idx++]) == positive)
382                 range.setEnd(range.endContainer, idx);
383         }
384         function retreat(positive) {
385             let idx = range.startOffset;
386             while (idx > 0 && re.test(text[--idx]) == positive)
387                 range.setStart(range.startContainer, idx);
388         }
389
390         let nodeRange = range.cloneRange();
391         nodeRange.selectNodeContents(range.startContainer);
392         let text = String(nodeRange);
393
394         if (forward) {
395             advance(true);
396             if (!sameWord)
397                 advance(false);
398         }
399         else {
400             if (!sameWord)
401                 retreat(false);
402             retreat(true);
403         }
404         return range;
405     },
406
407     getEditor: function (elem) {
408         if (arguments.length === 0) {
409             dactyl.assert(dactyl.focusedElement);
410             return dactyl.focusedElement;
411         }
412
413         if (!elem)
414             elem = dactyl.focusedElement || document.commandDispatcher.focusedWindow;
415         dactyl.assert(elem);
416
417         if (elem instanceof Element)
418             return elem.QueryInterface(Ci.nsIDOMNSEditableElement).editor;
419         try {
420             return elem.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
421                        .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIEditingSession)
422                        .getEditorForWindow(elem);
423         }
424         catch (e) {
425             return null;
426         }
427     },
428
429     getController: function () {
430         let ed = dactyl.focusedElement;
431         if (!ed || !ed.controllers)
432             return null;
433
434         return ed.controllers.getControllerForCommand("cmd_beginLine");
435     }
436 }, {
437     mappings: function () {
438
439         // add mappings for commands like h,j,k,l,etc. in CARET, VISUAL and TEXT_EDIT mode
440         function addMovementMap(keys, description, hasCount, caretModeMethod, caretModeArg, textEditCommand, visualTextEditCommand) {
441             let extraInfo = {};
442             if (hasCount)
443                 extraInfo.count = true;
444
445             function caretExecute(arg, again) {
446                 function fixSelection() {
447                     sel.removeAllRanges();
448                     sel.addRange(RangeFind.endpoint(
449                         RangeFind.nodeRange(buffer.focusedFrame.document.documentElement),
450                         true));
451                 }
452
453                 let controller = buffer.selectionController;
454                 let sel = controller.getSelection(controller.SELECTION_NORMAL);
455                 if (!sel.rangeCount) // Hack.
456                     fixSelection();
457
458                 try {
459                     controller[caretModeMethod](caretModeArg, arg);
460                 }
461                 catch (e) {
462                     dactyl.assert(again && e.result === Cr.NS_ERROR_FAILURE);
463                     fixSelection();
464                     caretExecute(arg, false);
465                 }
466             }
467
468             mappings.add([modes.CARET], keys, description,
469                 function ({ count }) {
470                     if (!count)
471                        count = 1;
472
473                     while (count--)
474                         caretExecute(false, true);
475                 },
476                 extraInfo);
477
478             mappings.add([modes.VISUAL], keys, description,
479                 function ({ count }) {
480                     if (!count)
481                         count = 1;
482
483                     let editor_ = Editor.getEditor(null);
484                     let controller = buffer.selectionController;
485                     while (count-- && modes.main == modes.VISUAL) {
486                         if (editor.isTextEdit) {
487                             if (callable(visualTextEditCommand))
488                                 visualTextEditCommand(editor_);
489                             else
490                                 editor.executeCommand(visualTextEditCommand);
491                         }
492                         else
493                             caretExecute(true, true);
494                     }
495                 },
496                 extraInfo);
497
498             mappings.add([modes.TEXT_EDIT], keys, description,
499                 function ({ count }) {
500                     if (!count)
501                         count = 1;
502
503                     editor.executeCommand(textEditCommand, count);
504                 },
505                 extraInfo);
506         }
507
508         // add mappings for commands like i,a,s,c,etc. in TEXT_EDIT mode
509         function addBeginInsertModeMap(keys, commands, description) {
510             mappings.add([modes.TEXT_EDIT], keys, description || "",
511                 function () {
512                     commands.forEach(function (cmd)
513                         editor.executeCommand(cmd, 1));
514                     modes.push(modes.INSERT);
515                 });
516         }
517
518         function selectPreviousLine() {
519             editor.executeCommand("cmd_selectLinePrevious");
520             if ((modes.extended & modes.LINE) && !editor.selectedText())
521                 editor.executeCommand("cmd_selectLinePrevious");
522         }
523
524         function selectNextLine() {
525             editor.executeCommand("cmd_selectLineNext");
526             if ((modes.extended & modes.LINE) && !editor.selectedText())
527                 editor.executeCommand("cmd_selectLineNext");
528         }
529
530         function updateRange(editor, forward, re, modify) {
531             let range = Editor.extendRange(editor.selection.getRangeAt(0),
532                                            forward, re, false);
533             modify(range);
534             editor.selection.removeAllRanges();
535             editor.selection.addRange(range);
536         }
537         function move(forward, re)
538             function _move(editor) {
539                 updateRange(editor, forward, re, function (range) { range.collapse(!forward); });
540             }
541         function select(forward, re)
542             function _select(editor) {
543                 updateRange(editor, forward, re, function (range) {});
544             }
545         function beginLine(editor_) {
546             editor.executeCommand("cmd_beginLine");
547             move(true, /\S/)(editor_);
548         }
549
550         //             COUNT  CARET                   TEXT_EDIT            VISUAL_TEXT_EDIT
551         addMovementMap(["k", "<Up>"],                 "Move up one line",
552                        true,  "lineMove", false,      "cmd_linePrevious", selectPreviousLine);
553         addMovementMap(["j", "<Down>", "<Return>"],   "Move down one line",
554                        true,  "lineMove", true,       "cmd_lineNext",     selectNextLine);
555         addMovementMap(["h", "<Left>", "<BS>"],       "Move left one character",
556                        true,  "characterMove", false, "cmd_charPrevious", "cmd_selectCharPrevious");
557         addMovementMap(["l", "<Right>", "<Space>"],   "Move right one character",
558                        true,  "characterMove", true,  "cmd_charNext",     "cmd_selectCharNext");
559         addMovementMap(["b", "<C-Left>"],             "Move left one word",
560                        true,  "wordMove", false,      "cmd_wordPrevious", "cmd_selectWordPrevious");
561         addMovementMap(["w", "<C-Right>"],            "Move right one word",
562                        true,  "wordMove", true,       "cmd_wordNext",     "cmd_selectWordNext");
563         addMovementMap(["B"],                         "Move left to the previous white space",
564                        true,  "wordMove", false,      move(false, /\S/),  select(false, /\S/));
565         addMovementMap(["W"],                         "Move right to just beyond the next white space",
566                        true,  "wordMove", true,       move(true,  /\S/),  select(true,  /\S/));
567         addMovementMap(["e"],                         "Move to the end of the current word",
568                        true,  "wordMove", true,       move(true,  /\W/),  select(true,  /\W/));
569         addMovementMap(["E"],                         "Move right to the next white space",
570                        true,  "wordMove", true,       move(true,  /\s/),  select(true,  /\s/));
571         addMovementMap(["<C-f>", "<PageDown>"],       "Move down one page",
572                        true,  "pageMove", true,       "cmd_movePageDown", "cmd_selectNextPage");
573         addMovementMap(["<C-b>", "<PageUp>"],         "Move up one page",
574                        true,  "pageMove", false,      "cmd_movePageUp",   "cmd_selectPreviousPage");
575         addMovementMap(["gg", "<C-Home>"],            "Move to the start of text",
576                        false, "completeMove", false,  "cmd_moveTop",      "cmd_selectTop");
577         addMovementMap(["G", "<C-End>"],              "Move to the end of text",
578                        false, "completeMove", true,   "cmd_moveBottom",   "cmd_selectBottom");
579         addMovementMap(["0", "<Home>"],               "Move to the beginning of the line",
580                        false, "intraLineMove", false, "cmd_beginLine",    "cmd_selectBeginLine");
581         addMovementMap(["^"],                         "Move to the first non-whitespace character of the line",
582                        false, "intraLineMove", false, beginLine,          "cmd_selectBeginLine");
583         addMovementMap(["$", "<End>"],                "Move to the end of the current line",
584                        false, "intraLineMove", true,  "cmd_endLine" ,     "cmd_selectEndLine");
585
586         addBeginInsertModeMap(["i", "<Insert>"], [], "Insert text before the cursor");
587         addBeginInsertModeMap(["a"],             ["cmd_charNext"], "Append text after the cursor");
588         addBeginInsertModeMap(["I"],             ["cmd_beginLine"], "Insert text at the beginning of the line");
589         addBeginInsertModeMap(["A"],             ["cmd_endLine"], "Append text at the end of the line");
590         addBeginInsertModeMap(["s"],             ["cmd_deleteCharForward"], "Delete the character in front of the cursor and start insert");
591         addBeginInsertModeMap(["S"],             ["cmd_deleteToEndOfLine", "cmd_deleteToBeginningOfLine"], "Delete the current line and start insert");
592         addBeginInsertModeMap(["C"],             ["cmd_deleteToEndOfLine"], "Delete from the cursor to the end of the line and start insert");
593
594         function addMotionMap(key, desc, cmd, mode) {
595             mappings.add([modes.TEXT_EDIT], [key],
596                 desc,
597                 function ({ count,  motion }) {
598                     editor.selectMotion(key, motion, Math.max(count, 1));
599                     if (callable(cmd))
600                         cmd.call(events, Editor.getEditor(null));
601                     else {
602                         editor.executeCommand(cmd, 1);
603                         modes.pop(modes.TEXT_EDIT);
604                     }
605                     if (mode)
606                         modes.push(mode);
607                 },
608                 { count: true, motion: true });
609         }
610
611         addMotionMap("d", "Delete motion", "cmd_delete");
612         addMotionMap("c", "Change motion", "cmd_delete", modes.INSERT);
613         addMotionMap("y", "Yank motion",   "cmd_copy");
614
615         mappings.add([modes.INPUT],
616             ["<C-w>"], "Delete previous word",
617             function () { editor.executeCommand("cmd_deleteWordBackward", 1); });
618
619         mappings.add([modes.INPUT],
620             ["<C-u>"], "Delete until beginning of current line",
621             function () {
622                 // Deletes the whole line. What the hell.
623                 // editor.executeCommand("cmd_deleteToBeginningOfLine", 1);
624
625                 editor.executeCommand("cmd_selectBeginLine", 1);
626                 if (Editor.getController().isCommandEnabled("cmd_delete"))
627                     editor.executeCommand("cmd_delete", 1);
628             });
629
630         mappings.add([modes.INPUT],
631             ["<C-k>"], "Delete until end of current line",
632             function () { editor.executeCommand("cmd_deleteToEndOfLine", 1); });
633
634         mappings.add([modes.INPUT],
635             ["<C-a>"], "Move cursor to beginning of current line",
636             function () { editor.executeCommand("cmd_beginLine", 1); });
637
638         mappings.add([modes.INPUT],
639             ["<C-e>"], "Move cursor to end of current line",
640             function () { editor.executeCommand("cmd_endLine", 1); });
641
642         mappings.add([modes.INPUT],
643             ["<C-h>"], "Delete character to the left",
644             function () { events.feedkeys("<BS>", true); });
645
646         mappings.add([modes.INPUT],
647             ["<C-d>"], "Delete character to the right",
648             function () { editor.executeCommand("cmd_deleteCharForward", 1); });
649
650         mappings.add([modes.INPUT],
651             ["<S-Insert>"], "Insert clipboard/selection",
652             function () { editor.pasteClipboard(); });
653
654         mappings.add([modes.INPUT, modes.TEXT_EDIT],
655             ["<C-i>"], "Edit text field with an external editor",
656             function () { editor.editFieldExternally(); });
657
658         mappings.add([modes.INPUT],
659             ["<C-t>"], "Edit text field in Vi mode",
660             function () {
661                 dactyl.assert(!editor.isTextEdit);
662                 modes.push(modes.TEXT_EDIT);
663             });
664
665         mappings.add([modes.INSERT],
666             ["<Space>", "<Return>"], "Expand insert mode abbreviation",
667             function () {
668                 editor.expandAbbreviation(modes.INSERT);
669                 return Events.PASS;
670             });
671
672         mappings.add([modes.INSERT],
673             ["<C-]>", "<C-5>"], "Expand insert mode abbreviation",
674             function () { editor.expandAbbreviation(modes.INSERT); });
675
676         // text edit mode
677         mappings.add([modes.TEXT_EDIT],
678             ["u"], "Undo changes",
679             function (args) {
680                 editor.executeCommand("cmd_undo", Math.max(args.count, 1));
681                 editor.unselectText();
682             },
683             { count: true });
684
685         mappings.add([modes.TEXT_EDIT],
686             ["<C-r>"], "Redo undone changes",
687             function (args) {
688                 editor.executeCommand("cmd_redo", Math.max(args.count, 1));
689                 editor.unselectText();
690             },
691             { count: true });
692
693         mappings.add([modes.TEXT_EDIT],
694             ["D"], "Delete the characters under the cursor until the end of the line",
695             function () { editor.executeCommand("cmd_deleteToEndOfLine"); });
696
697         mappings.add([modes.TEXT_EDIT],
698             ["o"], "Open line below current",
699             function () {
700                 editor.executeCommand("cmd_endLine", 1);
701                 modes.push(modes.INSERT);
702                 events.feedkeys("<Return>");
703             });
704
705         mappings.add([modes.TEXT_EDIT],
706             ["O"], "Open line above current",
707             function () {
708                 editor.executeCommand("cmd_beginLine", 1);
709                 modes.push(modes.INSERT);
710                 events.feedkeys("<Return>");
711                 editor.executeCommand("cmd_linePrevious", 1);
712             });
713
714         mappings.add([modes.TEXT_EDIT],
715             ["X"], "Delete character to the left",
716             function (args) { editor.executeCommand("cmd_deleteCharBackward", Math.max(args.count, 1)); },
717             { count: true });
718
719         mappings.add([modes.TEXT_EDIT],
720             ["x"], "Delete character to the right",
721             function (args) { editor.executeCommand("cmd_deleteCharForward", Math.max(args.count, 1)); },
722             { count: true });
723
724         // visual mode
725         mappings.add([modes.CARET, modes.TEXT_EDIT],
726             ["v"], "Start visual mode",
727             function () { modes.push(modes.VISUAL); });
728
729         mappings.add([modes.VISUAL],
730             ["v", "V"], "End visual mode",
731             function () { modes.pop(); });
732
733         mappings.add([modes.TEXT_EDIT],
734             ["V"], "Start visual line mode",
735             function () {
736                 modes.push(modes.VISUAL, modes.LINE);
737                 editor.executeCommand("cmd_beginLine", 1);
738                 editor.executeCommand("cmd_selectLineNext", 1);
739             });
740
741         mappings.add([modes.VISUAL],
742             ["c", "s"], "Change selected text",
743             function () {
744                 dactyl.assert(editor.isTextEdit);
745                 editor.executeCommand("cmd_cut");
746                 modes.push(modes.INSERT);
747             });
748
749         mappings.add([modes.VISUAL],
750             ["d", "x"], "Delete selected text",
751             function () {
752                 dactyl.assert(editor.isTextEdit);
753                 editor.executeCommand("cmd_cut");
754             });
755
756         mappings.add([modes.VISUAL],
757             ["y"], "Yank selected text",
758             function () {
759                 if (editor.isTextEdit) {
760                     editor.executeCommand("cmd_copy");
761                     modes.pop();
762                 }
763                 else
764                     dactyl.clipboardWrite(buffer.currentWord, true);
765             });
766
767         mappings.add([modes.VISUAL, modes.TEXT_EDIT],
768             ["p"], "Paste clipboard contents",
769             function ({ count }) {
770                 dactyl.assert(!editor.isCaret);
771                 editor.executeCommand("cmd_paste", count || 1);
772                 modes.pop(modes.TEXT_EDIT);
773             },
774             { count: true });
775
776         // finding characters
777         mappings.add([modes.TEXT_EDIT, modes.VISUAL],
778             ["f"], "Move to a character on the current line after the cursor",
779             function ({ arg, count }) {
780                 let pos = editor.findCharForward(arg, Math.max(count, 1));
781                 if (pos >= 0)
782                     editor.moveToPosition(pos, true, modes.main == modes.VISUAL);
783             },
784             { arg: true, count: true });
785
786         mappings.add([modes.TEXT_EDIT, modes.VISUAL],
787             ["F"], "Move to a character on the current line before the cursor",
788             function ({ arg, count }) {
789                 let pos = editor.findCharBackward(arg, Math.max(count, 1));
790                 if (pos >= 0)
791                     editor.moveToPosition(pos, false, modes.main == modes.VISUAL);
792             },
793             { arg: true, count: true });
794
795         mappings.add([modes.TEXT_EDIT, modes.VISUAL],
796             ["t"], "Move before a character on the current line",
797             function ({ arg, count }) {
798                 let pos = editor.findCharForward(arg, Math.max(count, 1));
799                 if (pos >= 0)
800                     editor.moveToPosition(pos - 1, true, modes.main == modes.VISUAL);
801             },
802             { arg: true, count: true });
803
804         mappings.add([modes.TEXT_EDIT, modes.VISUAL],
805             ["T"], "Move before a character on the current line, backwards",
806             function ({ arg, count }) {
807                 let pos = editor.findCharBackward(arg, Math.max(count, 1));
808                 if (pos >= 0)
809                     editor.moveToPosition(pos + 1, false, modes.main == modes.VISUAL);
810             },
811             { arg: true, count: true });
812
813         // text edit and visual mode
814         mappings.add([modes.TEXT_EDIT, modes.VISUAL],
815             ["~"], "Switch case of the character under the cursor and move the cursor to the right",
816             function ({ count }) {
817                 if (modes.main == modes.VISUAL)
818                     count = Editor.getEditor().selectionEnd - Editor.getEditor().selectionStart;
819                 count = Math.max(count, 1);
820
821                 // FIXME: do this in one pass?
822                 while (count-- > 0) {
823                     let text = Editor.getEditor().value;
824                     let pos = Editor.getEditor().selectionStart;
825                     dactyl.assert(pos < text.length);
826
827                     let chr = text[pos];
828                     Editor.getEditor().value = text.substring(0, pos) +
829                         (chr == chr.toLocaleLowerCase() ? chr.toLocaleUpperCase() : chr.toLocaleLowerCase()) +
830                         text.substring(pos + 1);
831                     editor.moveToPosition(pos + 1, true, false);
832                 }
833                 modes.pop(modes.TEXT_EDIT);
834             },
835             { count: true });
836
837         function bind() mappings.add.apply(mappings,
838                                            [[modes.AUTOCOMPLETE]].concat(Array.slice(arguments)))
839
840         bind(["<Esc>"], "Return to INSERT mode",
841              function () Events.PASS_THROUGH);
842
843         bind(["<C-[>"], "Return to INSERT mode",
844              function () { events.feedkeys("<Esc>", { skipmap: true }); });
845
846         bind(["<Up>"], "Select the previous autocomplete result",
847              function () Events.PASS_THROUGH);
848
849         bind(["<C-p>"], "Select the previous autocomplete result",
850              function () { events.feedkeys("<Up>", { skipmap: true }); });
851
852         bind(["<Down>"], "Select the next autocomplete result",
853              function () Events.PASS_THROUGH);
854
855         bind(["<C-n>"], "Select the next autocomplete result",
856              function () { events.feedkeys("<Down>", { skipmap: true }); });
857     },
858
859     options: function () {
860         options.add(["editor"],
861             "The external text editor",
862             "string", "gvim -f +<line> <file>", {
863                 format: function (obj, value) {
864                     let args = commands.parseArgs(value || this.value, { argCount: "*", allowUnknownOptions: true })
865                                        .map(util.compileMacro).filter(function (fmt) fmt.valid(obj))
866                                        .map(function (fmt) fmt(obj));
867                     if (obj["file"] && !this.has("file"))
868                         args.push(obj["file"]);
869                     return args;
870                 },
871                 has: function (key) set.has(util.compileMacro(this.value).seen, key),
872                 validator: function (value) {
873                     this.format({}, value);
874                     return Object.keys(util.compileMacro(value).seen).every(function (k) ["column", "file", "line"].indexOf(k) >= 0);
875                 }
876             });
877
878         options.add(["insertmode", "im"],
879             "Enter Insert mode rather than Text Edit mode when focusing text areas",
880             "boolean", true);
881     }
882 });
883
884 // vim: set fdm=marker sw=4 ts=4 et: