]> git.donarmstrong.com Git - dactyl.git/blob - common/content/editor.js
Import 1.0b7.1 supporting Firefox up to 8.*
[dactyl.git] / common / content / editor.js
1 // Copyright (c) 2008-2011 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", {
15     get isCaret() modes.getStack(1).main == modes.CARET,
16     get isTextEdit() modes.getStack(1).main == modes.TEXT_EDIT,
17
18     unselectText: function (toEnd) {
19         try {
20             Editor.getEditor(null).selection[toEnd ? "collapseToEnd" : "collapseToStart"]();
21         }
22         catch (e) {}
23     },
24
25     selectedText: function () String(Editor.getEditor(null).selection),
26
27     pasteClipboard: function (clipboard, toStart) {
28         let elem = dactyl.focusedElement;
29         if (elem.inputField)
30             elem = elem.inputField;
31
32         if (elem.setSelectionRange) {
33             let text = dactyl.clipboardRead(clipboard);
34             if (!text)
35                 return;
36             if (isinstance(elem, [HTMLInputElement, XULTextBoxElement]))
37                 text = text.replace(/\n+/g, "");
38
39             // This is a hacky fix - but it works.
40             // <s-insert> in the bottom of a long textarea bounces up
41             let top = elem.scrollTop;
42             let left = elem.scrollLeft;
43
44             let start = elem.selectionStart; // caret position
45             let end = elem.selectionEnd;
46             let value = elem.value.substring(0, start) + text + elem.value.substring(end);
47             elem.value = value;
48
49             if (/^(search|text)$/.test(elem.type))
50                 Editor.getEditor(elem).rootElement.firstChild.textContent = value;
51
52             elem.selectionStart = Math.min(start + (toStart ? 0 : text.length), elem.value.length);
53             elem.selectionEnd = elem.selectionStart;
54
55             elem.scrollTop = top;
56             elem.scrollLeft = left;
57
58             events.dispatch(elem, events.create(elem.ownerDocument, "input"));
59         }
60     },
61
62     // count is optional, defaults to 1
63     executeCommand: function (cmd, count) {
64         let editor = Editor.getEditor(null);
65         let controller = Editor.getController();
66         dactyl.assert(callable(cmd) ||
67                           controller &&
68                           controller.supportsCommand(cmd) &&
69                           controller.isCommandEnabled(cmd));
70
71         // XXX: better as a precondition
72         if (count == null)
73           count = 1;
74
75         let didCommand = false;
76         while (count--) {
77             // some commands need this try/catch workaround, because a cmd_charPrevious triggered
78             // at the beginning of the textarea, would hang the doCommand()
79             // good thing is, we need this code anyway for proper beeping
80             try {
81                 if (callable(cmd))
82                     cmd(editor, controller);
83                 else
84                     controller.doCommand(cmd);
85                 didCommand = true;
86             }
87             catch (e) {
88                 util.reportError(e);
89                 dactyl.assert(didCommand);
90                 break;
91             }
92         }
93     },
94
95     // cmd = y, d, c
96     // motion = b, 0, gg, G, etc.
97     selectMotion: function selectMotion(cmd, motion, count) {
98         // XXX: better as a precondition
99         if (count == null)
100             count = 1;
101
102         if (cmd == motion) {
103             motion = "j";
104             count--;
105         }
106
107         if (modes.main != modes.VISUAL)
108             modes.push(modes.VISUAL);
109
110         switch (motion) {
111         case "j":
112             this.executeCommand("cmd_beginLine", 1);
113             this.executeCommand("cmd_selectLineNext", count + 1);
114             break;
115         case "k":
116             this.executeCommand("cmd_beginLine", 1);
117             this.executeCommand("cmd_lineNext", 1);
118             this.executeCommand("cmd_selectLinePrevious", count + 1);
119             break;
120         case "h":
121             this.executeCommand("cmd_selectCharPrevious", count);
122             break;
123         case "l":
124             this.executeCommand("cmd_selectCharNext", count);
125             break;
126         case "e":
127         case "w":
128             this.executeCommand("cmd_selectWordNext", count);
129             break;
130         case "b":
131             this.executeCommand("cmd_selectWordPrevious", count);
132             break;
133         case "0":
134         case "^":
135             this.executeCommand("cmd_selectBeginLine", 1);
136             break;
137         case "$":
138             this.executeCommand("cmd_selectEndLine", 1);
139             break;
140         case "gg":
141             this.executeCommand("cmd_endLine", 1);
142             this.executeCommand("cmd_selectTop", 1);
143             this.executeCommand("cmd_selectBeginLine", 1);
144             break;
145         case "G":
146             this.executeCommand("cmd_beginLine", 1);
147             this.executeCommand("cmd_selectBottom", 1);
148             this.executeCommand("cmd_selectEndLine", 1);
149             break;
150
151         default:
152             dactyl.beep();
153             return;
154         }
155     },
156
157     // This function will move/select up to given "pos"
158     // Simple setSelectionRange() would be better, but we want to maintain the correct
159     // order of selectionStart/End (a Gecko bug always makes selectionStart <= selectionEnd)
160     // Use only for small movements!
161     moveToPosition: function (pos, forward, select) {
162         if (!select) {
163             Editor.getEditor().setSelectionRange(pos, pos);
164             return;
165         }
166
167         if (forward) {
168             if (pos <= Editor.getEditor().selectionEnd || pos > Editor.getEditor().value.length)
169                 return;
170
171             do { // TODO: test code for endless loops
172                 this.executeCommand("cmd_selectCharNext", 1);
173             }
174             while (Editor.getEditor().selectionEnd != pos);
175         }
176         else {
177             if (pos >= Editor.getEditor().selectionStart || pos < 0)
178                 return;
179
180             do { // TODO: test code for endless loops
181                 this.executeCommand("cmd_selectCharPrevious", 1);
182             }
183             while (Editor.getEditor().selectionStart != pos);
184         }
185     },
186
187     findChar: function (key, count, backward) {
188
189         let editor = Editor.getEditor();
190         if (!editor)
191             return -1;
192
193         // XXX
194         if (count == null)
195             count = 1;
196
197         let code = events.fromString(key)[0].charCode;
198         util.assert(code);
199         let char = String.fromCharCode(code);
200
201         let text = editor.value;
202         let caret = editor.selectionEnd;
203         if (backward) {
204             let end = text.lastIndexOf("\n", caret);
205             while (caret > end && caret >= 0 && count--)
206                 caret = text.lastIndexOf(char, caret - 1);
207         }
208         else {
209             let end = text.indexOf("\n", caret);
210             if (end == -1)
211                 end = text.length;
212
213             while (caret < end && caret >= 0 && count--)
214                 caret = text.indexOf(char, caret + 1);
215         }
216
217         if (count > 0)
218             caret = -1;
219         if (caret == -1)
220             dactyl.beep();
221         return caret;
222     },
223
224     /**
225      * Edits the given file in the external editor as specified by the
226      * 'editor' option.
227      *
228      * @param {object|File|string} args An object specifying the file, line,
229      *     and column to edit. If a non-object is specified, it is treated as
230      *     the file parameter of the object.
231      * @param {boolean} blocking If true, this function does not return
232      *     until the editor exits.
233      */
234     editFileExternally: function (args, blocking) {
235         if (!isObject(args) || args instanceof File)
236             args = { file: args };
237         args.file = args.file.path || args.file;
238
239         let args = options.get("editor").format(args);
240
241         dactyl.assert(args.length >= 1, _("option.notSet", "editor"));
242
243         io.run(args.shift(), args, blocking);
244     },
245
246     // TODO: clean up with 2 functions for textboxes and currentEditor?
247     editFieldExternally: function editFieldExternally(forceEditing) {
248         if (!options["editor"])
249             return;
250
251         let textBox = config.isComposeWindow ? null : dactyl.focusedElement;
252         let line, column;
253
254         if (!forceEditing && textBox && textBox.type == "password") {
255             commandline.input(_("editor.prompt.editPassword") + " ",
256                 function (resp) {
257                     if (resp && resp.match(/^y(es)?$/i))
258                         editor.editFieldExternally(true);
259                 });
260                 return;
261         }
262
263         if (textBox) {
264             var text = textBox.value;
265             let pre = text.substr(0, textBox.selectionStart);
266             line = 1 + pre.replace(/[^\n]/g, "").length;
267             column = 1 + pre.replace(/[^]*\n/, "").length;
268         }
269         else {
270             var editor_ = window.GetCurrentEditor ? GetCurrentEditor()
271                                                   : Editor.getEditor(document.commandDispatcher.focusedWindow);
272             dactyl.assert(editor_);
273             text = Array.map(editor_.rootElement.childNodes, function (e) util.domToString(e, true)).join("");
274         }
275
276         let origGroup = textBox && textBox.getAttributeNS(NS, "highlight") || "";
277         let cleanup = util.yieldable(function cleanup(error) {
278             if (timer)
279                 timer.cancel();
280
281             let blink = ["EditorBlink1", "EditorBlink2"];
282             if (error) {
283                 dactyl.reportError(error, true);
284                 blink[1] = "EditorError";
285             }
286             else
287                 dactyl.trapErrors(update, null, true);
288
289             if (tmpfile && tmpfile.exists())
290                 tmpfile.remove(false);
291
292             if (textBox) {
293                 dactyl.focus(textBox);
294                 for (let group in values(blink.concat(blink, ""))) {
295                     highlight.highlightNode(textBox, origGroup + " " + group);
296                     yield 100;
297                 }
298             }
299         });
300
301         function update(force) {
302             if (force !== true && tmpfile.lastModifiedTime <= lastUpdate)
303                 return;
304             lastUpdate = Date.now();
305
306             let val = tmpfile.read();
307             if (textBox) {
308                 textBox.value = val;
309
310                 textBox.setAttributeNS(NS, "modifiable", true);
311                 util.computedStyle(textBox).MozUserInput;
312                 events.dispatch(textBox, events.create(textBox.ownerDocument, "input", {}));
313                 textBox.removeAttributeNS(NS, "modifiable");
314             }
315             else {
316                 while (editor_.rootElement.firstChild)
317                     editor_.rootElement.removeChild(editor_.rootElement.firstChild);
318                 editor_.rootElement.innerHTML = val;
319             }
320         }
321
322         try {
323             var tmpfile = io.createTempFile();
324             if (!tmpfile)
325                 throw Error(_("io.cantCreateTempFile"));
326
327             if (textBox) {
328                 highlight.highlightNode(textBox, origGroup + " EditorEditing");
329                 textBox.blur();
330             }
331
332             if (!tmpfile.write(text))
333                 throw Error(_("io.cantEncode"));
334
335             var lastUpdate = Date.now();
336
337             var timer = services.Timer(update, 100, services.Timer.TYPE_REPEATING_SLACK);
338             this.editFileExternally({ file: tmpfile.path, line: line, column: column }, cleanup);
339         }
340         catch (e) {
341             cleanup(e);
342         }
343     },
344
345     /**
346      * Expands an abbreviation in the currently active textbox.
347      *
348      * @param {string} mode The mode filter.
349      * @see Abbreviation#expand
350      */
351     expandAbbreviation: function (mode) {
352         let elem = dactyl.focusedElement;
353         if (!(elem && elem.value))
354             return;
355
356         let text   = elem.value;
357         let start  = elem.selectionStart;
358         let end    = elem.selectionEnd;
359         let abbrev = abbreviations.match(mode, text.substring(0, start).replace(/.*\s/g, ""));
360         if (abbrev) {
361             let len = abbrev.lhs.length;
362             let rhs = abbrev.expand(elem);
363             elem.value = text.substring(0, start - len) + rhs + text.substring(start);
364             elem.selectionStart = start - len + rhs.length;
365             elem.selectionEnd   = end   - len + rhs.length;
366         }
367     },
368 }, {
369     extendRange: function extendRange(range, forward, re, sameWord) {
370         function advance(positive) {
371             let idx = range.endOffset;
372             while (idx < text.length && re.test(text[idx++]) == positive)
373                 range.setEnd(range.endContainer, idx);
374         }
375         function retreat(positive) {
376             let idx = range.startOffset;
377             while (idx > 0 && re.test(text[--idx]) == positive)
378                 range.setStart(range.startContainer, idx);
379         }
380
381         let nodeRange = range.cloneRange();
382         nodeRange.selectNodeContents(range.startContainer);
383         let text = String(nodeRange);
384
385         if (forward) {
386             advance(true);
387             if (!sameWord)
388                 advance(false);
389         }
390         else {
391             if (!sameWord)
392                 retreat(false);
393             retreat(true);
394         }
395         return range;
396     },
397
398     getEditor: function (elem) {
399         if (arguments.length === 0) {
400             dactyl.assert(dactyl.focusedElement);
401             return dactyl.focusedElement;
402         }
403
404         if (!elem)
405             elem = dactyl.focusedElement || document.commandDispatcher.focusedWindow;
406         dactyl.assert(elem);
407
408         try {
409             if (elem instanceof Element)
410                 return elem.QueryInterface(Ci.nsIDOMNSEditableElement).editor;
411             return elem.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
412                        .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIEditingSession)
413                        .getEditorForWindow(elem);
414         }
415         catch (e) {
416             return null;
417         }
418     },
419
420     getController: function () {
421         let ed = dactyl.focusedElement;
422         if (!ed || !ed.controllers)
423             return null;
424
425         return ed.controllers.getControllerForCommand("cmd_beginLine");
426     }
427 }, {
428     mappings: function () {
429
430         // add mappings for commands like h,j,k,l,etc. in CARET, VISUAL and TEXT_EDIT mode
431         function addMovementMap(keys, description, hasCount, caretModeMethod, caretModeArg, textEditCommand, visualTextEditCommand) {
432             let extraInfo = {};
433             if (hasCount)
434                 extraInfo.count = true;
435
436             function caretExecute(arg, again) {
437                 function fixSelection() {
438                     sel.removeAllRanges();
439                     sel.addRange(RangeFind.endpoint(
440                         RangeFind.nodeRange(buffer.focusedFrame.document.documentElement),
441                         true));
442                 }
443
444                 let controller = buffer.selectionController;
445                 let sel = controller.getSelection(controller.SELECTION_NORMAL);
446                 if (!sel.rangeCount) // Hack.
447                     fixSelection();
448
449                 try {
450                     controller[caretModeMethod](caretModeArg, arg);
451                 }
452                 catch (e) {
453                     dactyl.assert(again && e.result === Cr.NS_ERROR_FAILURE);
454                     fixSelection();
455                     caretExecute(arg, false);
456                 }
457             }
458
459             mappings.add([modes.CARET], keys, description,
460                 function ({ count }) {
461                     if (!count)
462                        count = 1;
463
464                     while (count--)
465                         caretExecute(false, true);
466                 },
467                 extraInfo);
468
469             mappings.add([modes.VISUAL], keys, description,
470                 function ({ count }) {
471                     if (!count)
472                         count = 1;
473
474                     let editor_ = Editor.getEditor(null);
475                     let controller = buffer.selectionController;
476                     while (count-- && modes.main == modes.VISUAL) {
477                         if (editor.isTextEdit) {
478                             if (callable(visualTextEditCommand))
479                                 visualTextEditCommand(editor_);
480                             else
481                                 editor.executeCommand(visualTextEditCommand);
482                         }
483                         else
484                             caretExecute(true, true);
485                     }
486                 },
487                 extraInfo);
488
489             mappings.add([modes.TEXT_EDIT], keys, description,
490                 function ({ count }) {
491                     if (!count)
492                         count = 1;
493
494                     editor.executeCommand(textEditCommand, count);
495                 },
496                 extraInfo);
497         }
498
499         // add mappings for commands like i,a,s,c,etc. in TEXT_EDIT mode
500         function addBeginInsertModeMap(keys, commands, description) {
501             mappings.add([modes.TEXT_EDIT], keys, description || "",
502                 function () {
503                     commands.forEach(function (cmd)
504                         editor.executeCommand(cmd, 1));
505                     modes.push(modes.INSERT);
506                 });
507         }
508
509         function selectPreviousLine() {
510             editor.executeCommand("cmd_selectLinePrevious");
511             if ((modes.extended & modes.LINE) && !editor.selectedText())
512                 editor.executeCommand("cmd_selectLinePrevious");
513         }
514
515         function selectNextLine() {
516             editor.executeCommand("cmd_selectLineNext");
517             if ((modes.extended & modes.LINE) && !editor.selectedText())
518                 editor.executeCommand("cmd_selectLineNext");
519         }
520
521         function updateRange(editor, forward, re, modify) {
522             let range = Editor.extendRange(editor.selection.getRangeAt(0),
523                                            forward, re, false);
524             modify(range);
525             editor.selection.removeAllRanges();
526             editor.selection.addRange(range);
527         }
528         function move(forward, re)
529             function _move(editor) {
530                 updateRange(editor, forward, re, function (range) { range.collapse(!forward); });
531             }
532         function select(forward, re)
533             function _select(editor) {
534                 updateRange(editor, forward, re, function (range) {});
535             }
536         function beginLine(editor_) {
537             editor.executeCommand("cmd_beginLine");
538             move(true, /\S/)(editor_);
539         }
540
541         //             COUNT  CARET                   TEXT_EDIT            VISUAL_TEXT_EDIT
542         addMovementMap(["k", "<Up>"],                 "Move up one line",
543                        true,  "lineMove", false,      "cmd_linePrevious", selectPreviousLine);
544         addMovementMap(["j", "<Down>", "<Return>"],   "Move down one line",
545                        true,  "lineMove", true,       "cmd_lineNext",     selectNextLine);
546         addMovementMap(["h", "<Left>", "<BS>"],       "Move left one character",
547                        true,  "characterMove", false, "cmd_charPrevious", "cmd_selectCharPrevious");
548         addMovementMap(["l", "<Right>", "<Space>"],   "Move right one character",
549                        true,  "characterMove", true,  "cmd_charNext",     "cmd_selectCharNext");
550         addMovementMap(["b", "<C-Left>"],             "Move left one word",
551                        true,  "wordMove", false,      "cmd_wordPrevious", "cmd_selectWordPrevious");
552         addMovementMap(["w", "<C-Right>"],            "Move right one word",
553                        true,  "wordMove", true,       "cmd_wordNext",     "cmd_selectWordNext");
554         addMovementMap(["B"],                         "Move left to the previous white space",
555                        true,  "wordMove", false,      move(false, /\S/),  select(false, /\S/));
556         addMovementMap(["W"],                         "Move right to just beyond the next white space",
557                        true,  "wordMove", true,       move(true,  /\S/),  select(true,  /\S/));
558         addMovementMap(["e"],                         "Move to the end of the current word",
559                        true,  "wordMove", true,       move(true,  /\W/),  select(true,  /\W/));
560         addMovementMap(["E"],                         "Move right to the next white space",
561                        true,  "wordMove", true,       move(true,  /\s/),  select(true,  /\s/));
562         addMovementMap(["<C-f>", "<PageDown>"],       "Move down one page",
563                        true,  "pageMove", true,       "cmd_movePageDown", "cmd_selectNextPage");
564         addMovementMap(["<C-b>", "<PageUp>"],         "Move up one page",
565                        true,  "pageMove", false,      "cmd_movePageUp",   "cmd_selectPreviousPage");
566         addMovementMap(["gg", "<C-Home>"],            "Move to the start of text",
567                        false, "completeMove", false,  "cmd_moveTop",      "cmd_selectTop");
568         addMovementMap(["G", "<C-End>"],              "Move to the end of text",
569                        false, "completeMove", true,   "cmd_moveBottom",   "cmd_selectBottom");
570         addMovementMap(["0", "<Home>"],               "Move to the beginning of the line",
571                        false, "intraLineMove", false, "cmd_beginLine",    "cmd_selectBeginLine");
572         addMovementMap(["^"],                         "Move to the first non-whitespace character of the line",
573                        false, "intraLineMove", false, beginLine,          "cmd_selectBeginLine");
574         addMovementMap(["$", "<End>"],                "Move to the end of the current line",
575                        false, "intraLineMove", true,  "cmd_endLine" ,     "cmd_selectEndLine");
576
577         addBeginInsertModeMap(["i", "<Insert>"], [], "Insert text before the cursor");
578         addBeginInsertModeMap(["a"],             ["cmd_charNext"], "Append text after the cursor");
579         addBeginInsertModeMap(["I"],             ["cmd_beginLine"], "Insert text at the beginning of the line");
580         addBeginInsertModeMap(["A"],             ["cmd_endLine"], "Append text at the end of the line");
581         addBeginInsertModeMap(["s"],             ["cmd_deleteCharForward"], "Delete the character in front of the cursor and start insert");
582         addBeginInsertModeMap(["S"],             ["cmd_deleteToEndOfLine", "cmd_deleteToBeginningOfLine"], "Delete the current line and start insert");
583         addBeginInsertModeMap(["C"],             ["cmd_deleteToEndOfLine"], "Delete from the cursor to the end of the line and start insert");
584
585         function addMotionMap(key, desc, cmd, mode) {
586             mappings.add([modes.TEXT_EDIT], [key],
587                 desc,
588                 function ({ count,  motion }) {
589                     editor.selectMotion(key, motion, Math.max(count, 1));
590                     if (callable(cmd))
591                         cmd.call(events, Editor.getEditor(null));
592                     else {
593                         editor.executeCommand(cmd, 1);
594                         modes.pop(modes.TEXT_EDIT);
595                     }
596                     if (mode)
597                         modes.push(mode);
598                 },
599                 { count: true, motion: true });
600         }
601
602         addMotionMap("d", "Delete motion", "cmd_delete");
603         addMotionMap("c", "Change motion", "cmd_delete", modes.INSERT);
604         addMotionMap("y", "Yank motion",   "cmd_copy");
605
606         mappings.add([modes.INPUT],
607             ["<C-w>"], "Delete previous word",
608             function () { editor.executeCommand("cmd_deleteWordBackward", 1); });
609
610         mappings.add([modes.INPUT],
611             ["<C-u>"], "Delete until beginning of current line",
612             function () {
613                 // Deletes the whole line. What the hell.
614                 // editor.executeCommand("cmd_deleteToBeginningOfLine", 1);
615
616                 editor.executeCommand("cmd_selectBeginLine", 1);
617                 if (Editor.getController().isCommandEnabled("cmd_delete"))
618                     editor.executeCommand("cmd_delete", 1);
619             });
620
621         mappings.add([modes.INPUT],
622             ["<C-k>"], "Delete until end of current line",
623             function () { editor.executeCommand("cmd_deleteToEndOfLine", 1); });
624
625         mappings.add([modes.INPUT],
626             ["<C-a>"], "Move cursor to beginning of current line",
627             function () { editor.executeCommand("cmd_beginLine", 1); });
628
629         mappings.add([modes.INPUT],
630             ["<C-e>"], "Move cursor to end of current line",
631             function () { editor.executeCommand("cmd_endLine", 1); });
632
633         mappings.add([modes.INPUT],
634             ["<C-h>"], "Delete character to the left",
635             function () { events.feedkeys("<BS>", true); });
636
637         mappings.add([modes.INPUT],
638             ["<C-d>"], "Delete character to the right",
639             function () { editor.executeCommand("cmd_deleteCharForward", 1); });
640
641         mappings.add([modes.INPUT],
642             ["<S-Insert>"], "Insert clipboard/selection",
643             function () { editor.pasteClipboard(); });
644
645         mappings.add([modes.INPUT, modes.TEXT_EDIT],
646             ["<C-i>"], "Edit text field with an external editor",
647             function () { editor.editFieldExternally(); });
648
649         mappings.add([modes.INPUT],
650             ["<C-t>"], "Edit text field in Vi mode",
651             function () {
652                 dactyl.assert(dactyl.focusedElement);
653                 dactyl.assert(!editor.isTextEdit);
654                 modes.push(modes.TEXT_EDIT);
655             });
656
657         // Ugh.
658         mappings.add([modes.INPUT, modes.CARET],
659             ["<*-CR>", "<*-BS>", "<*-Del>", "<*-Left>", "<*-Right>", "<*-Up>", "<*-Down>",
660              "<*-Home>", "<*-End>", "<*-PageUp>", "<*-PageDown>",
661              "<M-c>", "<M-v>", "<*-Tab>"],
662             "Handled by " + config.host,
663             function () Events.PASS_THROUGH);
664
665         mappings.add([modes.INSERT],
666             ["<Space>", "<Return>"], "Expand Insert mode abbreviation",
667             function () {
668                 editor.expandAbbreviation(modes.INSERT);
669                 return Events.PASS_THROUGH;
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.findChar(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.findChar(arg, Math.max(count, 1), true);
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.findChar(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.findChar(arg, Math.max(count, 1), true);
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> +"sil! call cursor(0, <column>)" <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: