// Supported keybindings: // // Cursor movement: // h, j, k, l // e, E, w, W, b, B // Ctrl-f, Ctrl-b // Ctrl-n, Ctrl-p // $, ^, 0 // G // ge, gE // gg // f, F, t, T // Ctrl-o, Ctrl-i TODO (FIXME - Ctrl-O wont work in Chrome) // /, ?, n, N TODO (does not work) // #, * TODO // // Entering insert mode: // i, I, a, A, o, O // s // ce, cb // cc // S, C TODO // cf, cF, ct, cT // // Deleting text: // x, X // J // dd, D // de, db // df, dF, dt, dT // // Yanking and pasting: // yy, Y // p, P // p' TODO - test // y' TODO - test // m TODO - test // // Changing text in place: // ~ // r // // Visual mode: // v, V TODO // // Misc: // . TODO // (function() { var sdir = "f"; var buf = ""; var mark = {}; var repeatCount = 0; function isLine(cm, line) { return line >= 0 && line < cm.lineCount(); } function emptyBuffer() { buf = ""; } function pushInBuffer(str) { buf += str; } function pushRepeatCountDigit(digit) {return function(cm) {repeatCount = (repeatCount * 10) + digit}; } function getCountOrOne() { var i = repeatCount; return i || 1; } function clearCount() { repeatCount = 0; } function iterTimes(func) { for (var i = 0, c = getCountOrOne(); i < c; ++i) func(i, i == c - 1); clearCount(); } function countTimes(func) { if (typeof func == "string") func = CodeMirror.commands[func]; return function(cm) { iterTimes(function (i, last) { func(cm, i, last); }); }; } function iterObj(o, f) { for (var prop in o) if (o.hasOwnProperty(prop)) f(prop, o[prop]); } function iterList(l, f) { for (var i = 0; i < l.length; ++i) f(l[i]); } function toLetter(ch) { // T -> t, Shift-T -> T, '*' -> *, "Space" -> " " if (ch.slice(0, 6) == "Shift-") { return ch.slice(0, 1); } else { if (ch == "Space") return " "; if (ch.length == 3 && ch[0] == "'" && ch[2] == "'") return ch[1]; return ch.toLowerCase(); } } var SPECIAL_SYMBOLS = "~`!@#$%^&*()_-+=[{}]\\|/?.,<>:;\"\'1234567890"; function toCombo(ch) { // t -> T, T -> Shift-T, * -> '*', " " -> "Space" if (ch == " ") return "Space"; var specialIdx = SPECIAL_SYMBOLS.indexOf(ch); if (specialIdx != -1) return "'" + ch + "'"; if (ch.toLowerCase() == ch) return ch.toUpperCase(); return "Shift-" + ch.toUpperCase(); } var word = [/\w/, /[^\w\s]/], bigWord = [/\S/]; // Finds a word on the given line, and continue searching the next line if it can't find one. function findWord(cm, lineNum, pos, dir, regexps) { var line = cm.getLine(lineNum); while (true) { var stop = (dir > 0) ? line.length : -1; var wordStart = stop, wordEnd = stop; // Find bounds of next word. for (; pos != stop; pos += dir) { for (var i = 0; i < regexps.length; ++i) { if (regexps[i].test(line.charAt(pos))) { wordStart = pos; // Advance to end of word. for (; pos != stop && regexps[i].test(line.charAt(pos)); pos += dir) {} wordEnd = (dir > 0) ? pos : pos + 1; return { from: Math.min(wordStart, wordEnd), to: Math.max(wordStart, wordEnd), line: lineNum}; } } } // Advance to next/prev line. lineNum += dir; if (!isLine(cm, lineNum)) return null; line = cm.getLine(lineNum); pos = (dir > 0) ? 0 : line.length; } } /** * @param {boolean} cm CodeMirror object. * @param {regexp} regexps Regular expressions for word characters. * @param {number} dir Direction, +/- 1. * @param {number} times Number of times to advance word. * @param {string} where Go to "start" or "end" of word, 'e' vs 'w'. * @param {boolean} yank Whether we are finding words to yank. If true, * do not go to the next line to look for the last word. This is to * prevent deleting new line on 'dw' at the end of a line. */ function moveToWord(cm, regexps, dir, times, where, yank) { var cur = cm.getCursor(); if (yank) { where = 'start'; } for (var i = 0; i < times; i++) { var startCh = cur.ch, startLine = cur.line, word; while (true) { // Search and advance. word = findWord(cm, cur.line, cur.ch, dir, regexps); if (word) { if (yank && times == 1 && dir == 1 && cur.line != word.line) { // Stop at end of line of last word. Don't want to delete line return // for dw if the last deleted word is at the end of a line. cur.ch = cm.getLine(cur.line).length; break; } else { // Move to the word we just found. If by moving to the word we end up // in the same spot, then move an extra character and search again. cur.line = word.line; if (dir > 0 && where == 'end') { // 'e' if (startCh != word.to - 1 || startLine != word.line) { cur.ch = word.to - 1; break; } else { cur.ch = word.to; } } else if (dir > 0 && where == 'start') { // 'w' if (startCh != word.from || startLine != word.line) { cur.ch = word.from; break; } else { cur.ch = word.to; } } else if (dir < 0 && where == 'end') { // 'ge' if (startCh != word.to || startLine != word.line) { cur.ch = word.to; break; } else { cur.ch = word.from - 1; } } else if (dir < 0 && where == 'start') { // 'b' if (startCh != word.from || startLine != word.line) { cur.ch = word.from; break; } else { cur.ch = word.from - 1; } } } } else { // No more words to be found. Move to end of document. for (; isLine(cm, cur.line + dir); cur.line += dir) {} cur.ch = (dir > 0) ? cm.getLine(cur.line).length : 0; break; } } } if (where == 'end' && yank) { // Include the last character of the word for actions. cur.ch++; } return cur; } function joinLineNext(cm) { var cur = cm.getCursor(), ch = cur.ch, line = cm.getLine(cur.line); CodeMirror.commands.goLineEnd(cm); if (cur.line != cm.lineCount()) { CodeMirror.commands.goLineEnd(cm); cm.replaceSelection(" ", "end"); CodeMirror.commands.delCharRight(cm); } } function delTillMark(cm, cHar) { var i = mark[cHar]; if (i === undefined) { // console.log("Mark not set"); // TODO - show in status bar return; } var l = cm.getCursor().line, start = i > l ? l : i, end = i > l ? i : l; cm.setCursor(start); for (var c = start; c <= end; c++) { pushInBuffer("\n" + cm.getLine(start)); cm.removeLine(start); } } function yankTillMark(cm, cHar) { var i = mark[cHar]; if (i === undefined) { // console.log("Mark not set"); // TODO - show in status bar return; } var l = cm.getCursor().line, start = i > l ? l : i, end = i > l ? i : l; for (var c = start; c <= end; c++) { pushInBuffer("\n" + cm.getLine(c)); } cm.setCursor(start); } function goLineStartText(cm) { // Go to the start of the line where the text begins, or the end for whitespace-only lines var cur = cm.getCursor(), firstNonWS = cm.getLine(cur.line).search(/\S/); cm.setCursor(cur.line, firstNonWS == -1 ? line.length : firstNonWS, true); } function charIdxInLine(cm, cHar, motion_options) { // Search for cHar in line. // motion_options: {forward, inclusive} // If inclusive = true, include it too. // If forward = true, search forward, else search backwards. // If char is not found on this line, do nothing var cur = cm.getCursor(), line = cm.getLine(cur.line), idx; var ch = toLetter(cHar), mo = motion_options; if (mo.forward) { idx = line.indexOf(ch, cur.ch + 1); if (idx != -1 && mo.inclusive) idx += 1; } else { idx = line.lastIndexOf(ch, cur.ch); if (idx != -1 && !mo.inclusive) idx += 1; } return idx; } function moveTillChar(cm, cHar, motion_options) { // Move to cHar in line, as found by charIdxInLine. var idx = charIdxInLine(cm, cHar, motion_options), cur = cm.getCursor(); if (idx != -1) cm.setCursor({line: cur.line, ch: idx}); } function delTillChar(cm, cHar, motion_options) { // delete text in this line, untill cHar is met, // as found by charIdxInLine. // If char is not found on this line, do nothing var idx = charIdxInLine(cm, cHar, motion_options); var cur = cm.getCursor(); if (idx !== -1) { if (motion_options.forward) { cm.replaceRange("", {line: cur.line, ch: cur.ch}, {line: cur.line, ch: idx}); } else { cm.replaceRange("", {line: cur.line, ch: idx}, {line: cur.line, ch: cur.ch}); } } } function enterInsertMode(cm) { // enter insert mode: switch mode and cursor clearCount(); cm.setOption("keyMap", "vim-insert"); } function dialog(cm, text, shortText, f) { if (cm.openDialog) cm.openDialog(text, f); else f(prompt(shortText, "")); } function showAlert(cm, text) { var esc = text.replace(/[<&]/, function(ch) { return ch == "<" ? "<" : "&"; }); if (cm.openDialog) cm.openDialog(esc + " "); else alert(text); } // main keymap var map = CodeMirror.keyMap.vim = { // Pipe (|); TODO: should be *screen* chars, so need a util function to turn tabs into spaces? "'|'": function(cm) { cm.setCursor(cm.getCursor().line, getCountOrOne() - 1, true); clearCount(); }, "A": function(cm) { cm.setCursor(cm.getCursor().line, cm.getCursor().ch+1, true); enterInsertMode(cm); }, "Shift-A": function(cm) { CodeMirror.commands.goLineEnd(cm); enterInsertMode(cm);}, "I": function(cm) { enterInsertMode(cm);}, "Shift-I": function(cm) { goLineStartText(cm); enterInsertMode(cm);}, "O": function(cm) { CodeMirror.commands.goLineEnd(cm); CodeMirror.commands.newlineAndIndent(cm); enterInsertMode(cm); }, "Shift-O": function(cm) { CodeMirror.commands.goLineStart(cm); cm.replaceSelection("\n", "start"); cm.indentLine(cm.getCursor().line); enterInsertMode(cm); }, "G": function(cm) { cm.setOption("keyMap", "vim-prefix-g");}, "Shift-D": function(cm) { var cursor = cm.getCursor(); var lineN = cursor.line; var line = cm.getLine(lineN); cm.setLine(lineN, line.slice(0, cursor.ch)); emptyBuffer(); pushInBuffer(line.slice(cursor.ch)); if (repeatCount > 1) { // we've already done it once --repeatCount; // the lines dissapear (ie, cursor stays on the same lineN), // so only incremenet once ++lineN; iterTimes(function() { pushInBuffer(cm.getLine(lineN)); cm.removeLine(lineN); }); } }, "S": function (cm) { countTimes(function (_cm) { CodeMirror.commands.delCharRight(_cm); })(cm); enterInsertMode(cm); }, "M": function(cm) {cm.setOption("keyMap", "vim-prefix-m"); mark = {};}, "Y": function(cm) {cm.setOption("keyMap", "vim-prefix-y"); emptyBuffer();}, "Shift-Y": function(cm) { emptyBuffer(); iterTimes(function(i) { pushInBuffer("\n" + cm.getLine(cm.getCursor().line + i)); }); }, "/": function(cm) {var f = CodeMirror.commands.find; f && f(cm); sdir = "f";}, "'?'": function(cm) { var f = CodeMirror.commands.find; if (f) { f(cm); CodeMirror.commands.findPrev(cm); sdir = "r"; } }, "N": function(cm) { var fn = CodeMirror.commands.findNext; if (fn) sdir != "r" ? fn(cm) : CodeMirror.commands.findPrev(cm); }, "Shift-N": function(cm) { var fn = CodeMirror.commands.findNext; if (fn) sdir != "r" ? CodeMirror.commands.findPrev(cm) : fn.findNext(cm); }, "Shift-G": function(cm) { (repeatCount == 0) ? cm.setCursor(cm.lineCount()) : cm.setCursor(repeatCount - 1); clearCount(); CodeMirror.commands.goLineStart(cm); }, "':'": function(cm) { var exModeDialog = ': '; dialog(cm, exModeDialog, ':', function(command) { if (command.match(/^\d+$/)) { cm.setCursor(command - 1, cm.getCursor().ch); } else { showAlert(cm, "Bad command: " + command); } }); }, nofallthrough: true, style: "fat-cursor" }; // standard mode switching iterList(["d", "t", "T", "f", "F", "c", "r"], function (ch) { CodeMirror.keyMap.vim[toCombo(ch)] = function (cm) { cm.setOption("keyMap", "vim-prefix-" + ch); emptyBuffer(); }; }); // main num keymap // Add bindings that are influenced by number keys iterObj({ "X": function(cm) {CodeMirror.commands.delCharRight(cm);}, "P": function(cm) { var cur = cm.getCursor().line; if (buf!= "") { if (buf[0] == "\n") CodeMirror.commands.goLineEnd(cm); cm.replaceRange(buf, cm.getCursor()); } }, "Shift-X": function(cm) {CodeMirror.commands.delCharLeft(cm);}, "Shift-J": function(cm) {joinLineNext(cm);}, "Shift-P": function(cm) { var cur = cm.getCursor().line; if (buf!= "") { CodeMirror.commands.goLineUp(cm); CodeMirror.commands.goLineEnd(cm); cm.replaceSelection(buf, "end"); } cm.setCursor(cur+1); }, "'~'": function(cm) { var cur = cm.getCursor(), cHar = cm.getRange({line: cur.line, ch: cur.ch}, {line: cur.line, ch: cur.ch+1}); cHar = cHar != cHar.toLowerCase() ? cHar.toLowerCase() : cHar.toUpperCase(); cm.replaceRange(cHar, {line: cur.line, ch: cur.ch}, {line: cur.line, ch: cur.ch+1}); cm.setCursor(cur.line, cur.ch+1); }, "Ctrl-B": function(cm) {CodeMirror.commands.goPageUp(cm);}, "Ctrl-F": function(cm) {CodeMirror.commands.goPageDown(cm);}, "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", "U": "undo", "Ctrl-R": "redo" }, function(key, cmd) { map[key] = countTimes(cmd); }); // empty key maps iterList([ "vim-prefix-d'", "vim-prefix-y'", "vim-prefix-df", "vim-prefix-dF", "vim-prefix-dt", "vim-prefix-dT", "vim-prefix-c", "vim-prefix-cf", "vim-prefix-cF", "vim-prefix-ct", "vim-prefix-cT", "vim-prefix-", "vim-prefix-f", "vim-prefix-F", "vim-prefix-t", "vim-prefix-T", "vim-prefix-r", "vim-prefix-m" ], function (prefix) { CodeMirror.keyMap[prefix] = { auto: "vim", nofallthrough: true, style: "fat-cursor" }; }); CodeMirror.keyMap["vim-prefix-g"] = { "E": countTimes(function(cm) { cm.setCursor(moveToWord(cm, word, -1, 1, "end"));}), "Shift-E": countTimes(function(cm) { cm.setCursor(moveToWord(cm, bigWord, -1, 1, "end"));}), "G": function (cm) { cm.setCursor({line: repeatCount - 1, ch: cm.getCursor().ch}); clearCount(); }, auto: "vim", nofallthrough: true, style: "fat-cursor" }; CodeMirror.keyMap["vim-prefix-d"] = { "D": countTimes(function(cm) { pushInBuffer("\n" + cm.getLine(cm.getCursor().line)); cm.removeLine(cm.getCursor().line); cm.setOption("keyMap", "vim"); }), "'": function(cm) { cm.setOption("keyMap", "vim-prefix-d'"); emptyBuffer(); }, "B": function(cm) { var cur = cm.getCursor(); var line = cm.getLine(cur.line); var index = line.lastIndexOf(" ", cur.ch); pushInBuffer(line.substring(index, cur.ch)); cm.replaceRange("", {line: cur.line, ch: index}, cur); cm.setOption("keyMap", "vim"); }, nofallthrough: true, style: "fat-cursor" }; CodeMirror.keyMap["vim-prefix-c"] = { "B": function (cm) { countTimes("delWordLeft")(cm); enterInsertMode(cm); }, "C": function (cm) { iterTimes(function (i, last) { CodeMirror.commands.deleteLine(cm); if (i) { CodeMirror.commands.delCharRight(cm); if (last) CodeMirror.commands.deleteLine(cm); } }); enterInsertMode(cm); }, nofallthrough: true, style: "fat-cursor" }; iterList(["vim-prefix-d", "vim-prefix-c", "vim-prefix-"], function (prefix) { iterList(["f", "F", "T", "t"], function (ch) { CodeMirror.keyMap[prefix][toCombo(ch)] = function (cm) { cm.setOption("keyMap", prefix + ch); emptyBuffer(); }; }); }); var MOTION_OPTIONS = { "t": {inclusive: false, forward: true}, "f": {inclusive: true, forward: true}, "T": {inclusive: false, forward: false}, "F": {inclusive: true, forward: false} }; function setupPrefixBindingForKey(m) { CodeMirror.keyMap["vim-prefix-m"][m] = function(cm) { mark[m] = cm.getCursor().line; }; CodeMirror.keyMap["vim-prefix-d'"][m] = function(cm) { delTillMark(cm, m); }; CodeMirror.keyMap["vim-prefix-y'"][m] = function(cm) { yankTillMark(cm, m); }; CodeMirror.keyMap["vim-prefix-r"][m] = function (cm) { var cur = cm.getCursor(); cm.replaceRange(toLetter(m), {line: cur.line, ch: cur.ch}, {line: cur.line, ch: cur.ch + 1}); CodeMirror.commands.goColumnLeft(cm); }; // all commands, related to motions till char in line iterObj(MOTION_OPTIONS, function (ch, options) { CodeMirror.keyMap["vim-prefix-" + ch][m] = function(cm) { moveTillChar(cm, m, options); }; CodeMirror.keyMap["vim-prefix-d" + ch][m] = function(cm) { delTillChar(cm, m, options); }; CodeMirror.keyMap["vim-prefix-c" + ch][m] = function(cm) { delTillChar(cm, m, options); enterInsertMode(cm); }; }); } for (var i = 65; i < 65 + 26; i++) { // uppercase alphabet char codes var ch = String.fromCharCode(i); setupPrefixBindingForKey(toCombo(ch)); setupPrefixBindingForKey(toCombo(ch.toLowerCase())); } for (var i = 0; i < SPECIAL_SYMBOLS.length; ++i) { setupPrefixBindingForKey(toCombo(SPECIAL_SYMBOLS.charAt(i))); } setupPrefixBindingForKey("Space"); CodeMirror.keyMap["vim-prefix-y"] = { "Y": countTimes(function(cm, i, last) { pushInBuffer("\n" + cm.getLine(cm.getCursor().line + i)); cm.setOption("keyMap", "vim"); }), "'": function(cm) {cm.setOption("keyMap", "vim-prefix-y'"); emptyBuffer();}, nofallthrough: true, style: "fat-cursor" }; CodeMirror.keyMap["vim-insert"] = { // TODO: override navigation keys so that Esc will cancel automatic indentation from o, O, i_ "Esc": function(cm) { cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1, true); cm.setOption("keyMap", "vim"); }, "Ctrl-N": "autocomplete", "Ctrl-P": "autocomplete", fallthrough: ["default"] }; function findMatchedSymbol(cm, cur, symb) { var line = cur.line; var symb = symb ? symb : cm.getLine(line)[cur.ch]; // Are we at the opening or closing char var forwards = ['(', '[', '{'].indexOf(symb) != -1; var reverseSymb = (function(sym) { switch (sym) { case '(' : return ')'; case '[' : return ']'; case '{' : return '}'; case ')' : return '('; case ']' : return '['; case '}' : return '{'; default : return null; } })(symb); // Couldn't find a matching symbol, abort if (reverseSymb == null) return cur; // Tracking our imbalance in open/closing symbols. An opening symbol wii be // the first thing we pick up if moving forward, this isn't true moving backwards var disBal = forwards ? 0 : 1; while (true) { if (line == cur.line) { // First pass, do some special stuff var currLine = forwards ? cm.getLine(line).substr(cur.ch).split('') : cm.getLine(line).substr(0,cur.ch).split('').reverse(); } else { var currLine = forwards ? cm.getLine(line).split('') : cm.getLine(line).split('').reverse(); } for (var index = 0; index < currLine.length; index++) { if (currLine[index] == symb) disBal++; else if (currLine[index] == reverseSymb) disBal--; if (disBal == 0) { if (forwards && cur.line == line) return {line: line, ch: index + cur.ch}; else if (forwards) return {line: line, ch: index}; else return {line: line, ch: currLine.length - index - 1 }; } } if (forwards) line++; else line--; } } function selectCompanionObject(cm, revSymb, inclusive) { var cur = cm.getCursor(); var end = findMatchedSymbol(cm, cur, revSymb); var start = findMatchedSymbol(cm, end); start.ch += inclusive ? 1 : 0; end.ch += inclusive ? 0 : 1; return {start: start, end: end}; } // takes in a symbol and a cursor and tries to simulate text objects that have // identical opening and closing symbols // TODO support across multiple lines function findBeginningAndEnd(cm, symb, inclusive) { var cur = cm.getCursor(); var line = cm.getLine(cur.line); var chars = line.split(''); var start = undefined; var end = undefined; var firstIndex = chars.indexOf(symb); // the decision tree is to always look backwards for the beginning first, // but if the cursor is in front of the first instance of the symb, // then move the cursor forward if (cur.ch < firstIndex) { cur.ch = firstIndex; cm.setCursor(cur.line, firstIndex+1); } // otherwise if the cursor is currently on the closing symbol else if (firstIndex < cur.ch && chars[cur.ch] == symb) { end = cur.ch; // assign end to the current cursor --cur.ch; // make sure to look backwards } // if we're currently on the symbol, we've got a start if (chars[cur.ch] == symb && end == null) start = cur.ch + 1; // assign start to ahead of the cursor else { // go backwards to find the start for (var i = cur.ch; i > -1 && start == null; i--) if (chars[i] == symb) start = i + 1; } // look forwards for the end symbol if (start != null && end == null) { for (var i = start, len = chars.length; i < len && end == null; i++) { if (chars[i] == symb) end = i; } } // nothing found // FIXME still enters insert mode if (start == null || end == null) return { start: cur, end: cur }; // include the symbols if (inclusive) { --start; ++end; } return { start: {line: cur.line, ch: start}, end: {line: cur.line, ch: end} }; } function offsetCursor(cm, line, ch) { var cur = cm.getCursor(); return {line: cur.line + line, ch: cur.ch + ch}; } // These are the motion commands we use for navigation and selection with // certain other commands. All should return a cursor object. var motions = { "J": function(cm, times) { return offsetCursor(cm, times, 0); }, "Down": function(cm, times) { return offsetCursor(cm, times, 0); }, "K": function(cm, times) { return offsetCursor(cm, -times, 0); }, "Up": function(cm, times) { return offsetCursor(cm, -times, 0); }, "L": function(cm, times) { return offsetCursor(cm, 0, times); }, "Right": function(cm, times) { return offsetCursor(cm, 0, times); }, "Space": function(cm, times) { return offsetCursor(cm, 0, times); }, "H": function(cm, times) { return offsetCursor(cm, 0, -times); }, "Left": function(cm, times) { return offsetCursor(cm, 0, -times); }, "Backspace": function(cm, times) { return offsetCursor(cm, 0, -times); }, "B": function(cm, times, yank) { return moveToWord(cm, word, -1, times, 'start', yank); }, "Shift-B": function(cm, times, yank) { return moveToWord(cm, bigWord, -1, times, 'start', yank); }, "E": function(cm, times, yank) { return moveToWord(cm, word, 1, times, 'end', yank); }, "Shift-E": function(cm, times, yank) { return moveToWord(cm, bigWord, 1, times, 'end', yank); }, "W": function(cm, times, yank) { return moveToWord(cm, word, 1, times, 'start', yank); }, "Shift-W": function(cm, times, yank) { return moveToWord(cm, bigWord, 1, times, 'start', yank); }, "'^'": function(cm, times) { var cur = cm.getCursor(), line = cm.getLine(cur.line).split(''); for (var i = 0; i < line.length; i++) { if (line[i].match(/[^\s]/)) return {line: cur.line, ch: index}; } return cur; }, "'$'": function(cm) { var cur = cm.getCursor(), ch = cm.getLine(cur.line).length; return {line: cur.line, ch: ch}; }, "'%'": function(cm) { return findMatchedSymbol(cm, cm.getCursor()); }, "Esc" : function(cm) { cm.setOption("keyMap", "vim"); repeatCount = 0; return cm.getCursor(); } }; // Map our movement actions each operator and non-operational movement iterObj(motions, function(key, motion) { CodeMirror.keyMap['vim-prefix-d'][key] = function(cm) { // Get our selected range var start = cm.getCursor(); var end = motion(cm, repeatCount ? repeatCount : 1, true); // Set swap var if range is of negative length if ((start.line > end.line) || (start.line == end.line && start.ch > end.ch)) var swap = true; // Take action, switching start and end if swap var is set pushInBuffer(cm.getRange(swap ? end : start, swap ? start : end)); cm.replaceRange("", swap ? end : start, swap ? start : end); // And clean up repeatCount = 0; cm.setOption("keyMap", "vim"); }; CodeMirror.keyMap['vim-prefix-c'][key] = function(cm) { var start = cm.getCursor(); var end = motion(cm, repeatCount ? repeatCount : 1, true); if ((start.line > end.line) || (start.line == end.line && start.ch > end.ch)) var swap = true; pushInBuffer(cm.getRange(swap ? end : start, swap ? start : end)); cm.replaceRange("", swap ? end : start, swap ? start : end); repeatCount = 0; cm.setOption('keyMap', 'vim-insert'); }; CodeMirror.keyMap['vim-prefix-y'][key] = function(cm) { var start = cm.getCursor(); var end = motion(cm, repeatCount ? repeatCount : 1, true); if ((start.line > end.line) || (start.line == end.line && start.ch > end.ch)) var swap = true; pushInBuffer(cm.getRange(swap ? end : start, swap ? start : end)); repeatCount = 0; cm.setOption("keyMap", "vim"); }; CodeMirror.keyMap['vim'][key] = function(cm) { var cur = motion(cm, repeatCount ? repeatCount : 1); cm.setCursor(cur.line, cur.ch); repeatCount = 0; }; }); function addCountBindings(keyMapName) { // Add bindings for number keys keyMap = CodeMirror.keyMap[keyMapName]; keyMap["0"] = function(cm) { if (repeatCount > 0) { pushRepeatCountDigit(0)(cm); } else { CodeMirror.commands.goLineStart(cm); } }; for (var i = 1; i < 10; ++i) { keyMap[i] = pushRepeatCountDigit(i); } } addCountBindings('vim'); addCountBindings('vim-prefix-d'); addCountBindings('vim-prefix-y'); addCountBindings('vim-prefix-c'); // Create our keymaps for each operator and make xa and xi where x is an operator // change to the corrosponding keymap var operators = ['d', 'y', 'c']; iterList(operators, function(key, index, array) { CodeMirror.keyMap['vim-prefix-'+key+'a'] = { auto: 'vim', nofallthrough: true, style: "fat-cursor" }; CodeMirror.keyMap['vim-prefix-'+key+'i'] = { auto: 'vim', nofallthrough: true, style: "fat-cursor" }; CodeMirror.keyMap['vim-prefix-'+key]['A'] = function(cm) { repeatCount = 0; cm.setOption('keyMap', 'vim-prefix-' + key + 'a'); }; CodeMirror.keyMap['vim-prefix-'+key]['I'] = function(cm) { repeatCount = 0; cm.setOption('keyMap', 'vim-prefix-' + key + 'i'); }; }); function regexLastIndexOf(string, pattern, startIndex) { for (var i = startIndex == null ? string.length : startIndex; i >= 0; --i) if (pattern.test(string.charAt(i))) return i; return -1; } // Create our text object functions. They work similar to motions but they // return a start cursor as well var textObjectList = ['W', 'Shift-[', 'Shift-9', '[', "'", "Shift-'"]; var textObjects = { 'W': function(cm, inclusive) { var cur = cm.getCursor(); var line = cm.getLine(cur.line); var line_to_char = new String(line.substring(0, cur.ch)); var start = regexLastIndexOf(line_to_char, /[^a-zA-Z0-9]/) + 1; var end = motions["E"](cm, 1) ; end.ch += inclusive ? 1 : 0 ; return {start: {line: cur.line, ch: start}, end: end }; }, 'Shift-[': function(cm, inclusive) { return selectCompanionObject(cm, '}', inclusive); }, 'Shift-9': function(cm, inclusive) { return selectCompanionObject(cm, ')', inclusive); }, '[': function(cm, inclusive) { return selectCompanionObject(cm, ']', inclusive); }, "'": function(cm, inclusive) { return findBeginningAndEnd(cm, "'", inclusive); }, "Shift-'": function(cm, inclusive) { return findBeginningAndEnd(cm, '"', inclusive); } }; // One function to handle all operation upon text objects. Kinda funky but it works // better than rewriting this code six times function textObjectManipulation(cm, object, remove, insert, inclusive) { // Object is the text object, delete object if remove is true, enter insert // mode if insert is true, inclusive is the difference between a and i var tmp = textObjects[object](cm, inclusive); var start = tmp.start; var end = tmp.end; if ((start.line > end.line) || (start.line == end.line && start.ch > end.ch)) var swap = true ; pushInBuffer(cm.getRange(swap ? end : start, swap ? start : end)); if (remove) cm.replaceRange("", swap ? end : start, swap ? start : end); if (insert) cm.setOption('keyMap', 'vim-insert'); } // And finally build the keymaps up from the text objects for (var i = 0; i < textObjectList.length; ++i) { var object = textObjectList[i]; (function(object) { CodeMirror.keyMap['vim-prefix-di'][object] = function(cm) { textObjectManipulation(cm, object, true, false, false); }; CodeMirror.keyMap['vim-prefix-da'][object] = function(cm) { textObjectManipulation(cm, object, true, false, true); }; CodeMirror.keyMap['vim-prefix-yi'][object] = function(cm) { textObjectManipulation(cm, object, false, false, false); }; CodeMirror.keyMap['vim-prefix-ya'][object] = function(cm) { textObjectManipulation(cm, object, false, false, true); }; CodeMirror.keyMap['vim-prefix-ci'][object] = function(cm) { textObjectManipulation(cm, object, true, true, false); }; CodeMirror.keyMap['vim-prefix-ca'][object] = function(cm) { textObjectManipulation(cm, object, true, true, true); }; })(object) } })();