diff --git a/demo/kitchen-sink/demo.js b/demo/kitchen-sink/demo.js
index c7beb3d0..efc2e6f8 100644
--- a/demo/kitchen-sink/demo.js
+++ b/demo/kitchen-sink/demo.js
@@ -254,6 +254,7 @@ commands.addCommand({
var keybindings = {
ace: null, // Null = use "default" keymapping
vim: require("ace/keyboard/vim").handler,
+ vim2: require("ace/keyboard/vim2").handler,
emacs: "ace/keyboard/emacs",
// This is a way to define simple keyboard remappings
custom: new HashHandler({
diff --git a/kitchen-sink.html b/kitchen-sink.html
index 65fe2a2a..b2ab3577 100644
--- a/kitchen-sink.html
+++ b/kitchen-sink.html
@@ -12,8 +12,8 @@
-->
-
-
+
@@ -96,6 +96,7 @@
diff --git a/lib/ace/keyboard/vim2.js b/lib/ace/keyboard/vim2.js
new file mode 100644
index 00000000..27f18c6e
--- /dev/null
+++ b/lib/ace/keyboard/vim2.js
@@ -0,0 +1,4841 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+/**
+ * Supported keybindings:
+ *
+ * Motion:
+ * h, j, k, l
+ * gj, gk
+ * e, E, w, W, b, B, ge, gE
+ * f, F, t, T
+ * $, ^, 0, -, +, _
+ * gg, G
+ * %
+ * ', `
+ *
+ * Operator:
+ * d, y, c
+ * dd, yy, cc
+ * g~, g~g~
+ * >, <, >>, <<
+ *
+ * Operator-Motion:
+ * x, X, D, Y, C, ~
+ *
+ * Action:
+ * a, i, s, A, I, S, o, O
+ * zz, z., z, zt, zb, z-
+ * J
+ * u, Ctrl-r
+ * m
+ * r
+ *
+ * Modes:
+ * ESC - leave insert mode, visual mode, and clear input state.
+ * Ctrl-[, Ctrl-c - same as ESC.
+ *
+ * Registers: unnamed, -, a-z, A-Z, 0-9
+ * (Does not respect the special case for number registers when delete
+ * operator is made with these commands: %, (, ), , /, ?, n, N, {, } )
+ * TODO: Implement the remaining registers.
+ * Marks: a-z, A-Z, and 0-9
+ * TODO: Implement the remaining special marks. They have more complex
+ * behavior.
+ *
+ * Events:
+ * 'vim-mode-change' - raised on the editor anytime the current mode changes,
+ * Event object: {mode: "visual", subMode: "linewise"}
+ *
+ * Code structure:
+ * 1. Default keymap
+ * 2. Variable declarations and short basic helpers
+ * 3. Instance (External API) implementation
+ * 4. Internal state tracking objects (input state, counter) implementation
+ * and instanstiation
+ * 5. Key handler (the main command dispatcher) implementation
+ * 6. Motion, operator, and action implementations
+ * 7. Helper functions for the key handler, motions, operators, and actions
+ * 8. Set up Vim to work as a keymap for CodeMirror.
+ */
+
+define(function(require, exports, module) {
+ 'use strict';
+
+ var defaultKeymap = [
+ // Key to key mapping. This goes first to make it possible to override
+ // existing mappings.
+ { keys: '', type: 'keyToKey', toKeys: 'h' },
+ { keys: '', type: 'keyToKey', toKeys: 'l' },
+ { keys: '', type: 'keyToKey', toKeys: 'k' },
+ { keys: '', type: 'keyToKey', toKeys: 'j' },
+ { keys: '', type: 'keyToKey', toKeys: 'l' },
+ { keys: '', type: 'keyToKey', toKeys: 'h' },
+ { keys: '', type: 'keyToKey', toKeys: 'W' },
+ { keys: '', type: 'keyToKey', toKeys: 'B' },
+ { keys: '', type: 'keyToKey', toKeys: 'w' },
+ { keys: '', type: 'keyToKey', toKeys: 'b' },
+ { keys: '', type: 'keyToKey', toKeys: 'j' },
+ { keys: '', type: 'keyToKey', toKeys: 'k' },
+ { keys: '', type: 'keyToKey', toKeys: '' },
+ { keys: '', type: 'keyToKey', toKeys: '' },
+ { keys: '', type: 'keyToKey', toKeys: '', context: 'insert' },
+ { keys: '', type: 'keyToKey', toKeys: '', context: 'insert' },
+ { keys: 's', type: 'keyToKey', toKeys: 'cl', context: 'normal' },
+ { keys: 's', type: 'keyToKey', toKeys: 'xi', context: 'visual'},
+ { keys: 'S', type: 'keyToKey', toKeys: 'cc', context: 'normal' },
+ { keys: 'S', type: 'keyToKey', toKeys: 'dcc', context: 'visual' },
+ { keys: '', type: 'keyToKey', toKeys: '0' },
+ { keys: '', type: 'keyToKey', toKeys: '$' },
+ { keys: '', type: 'keyToKey', toKeys: '' },
+ { keys: '', type: 'keyToKey', toKeys: '' },
+ { keys: '', type: 'keyToKey', toKeys: 'j^', context: 'normal' },
+ // Motions
+ { keys: 'H', type: 'motion', motion: 'moveToTopLine', motionArgs: { linewise: true, toJumplist: true }},
+ { keys: 'M', type: 'motion', motion: 'moveToMiddleLine', motionArgs: { linewise: true, toJumplist: true }},
+ { keys: 'L', type: 'motion', motion: 'moveToBottomLine', motionArgs: { linewise: true, toJumplist: true }},
+ { keys: 'h', type: 'motion', motion: 'moveByCharacters', motionArgs: { forward: false }},
+ { keys: 'l', type: 'motion', motion: 'moveByCharacters', motionArgs: { forward: true }},
+ { keys: 'j', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, linewise: true }},
+ { keys: 'k', type: 'motion', motion: 'moveByLines', motionArgs: { forward: false, linewise: true }},
+ { keys: 'gj', type: 'motion', motion: 'moveByDisplayLines', motionArgs: { forward: true }},
+ { keys: 'gk', type: 'motion', motion: 'moveByDisplayLines', motionArgs: { forward: false }},
+ { keys: 'w', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: false }},
+ { keys: 'W', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: false, bigWord: true }},
+ { keys: 'e', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: true, inclusive: true }},
+ { keys: 'E', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: true, bigWord: true, inclusive: true }},
+ { keys: 'b', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false }},
+ { keys: 'B', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false, bigWord: true }},
+ { keys: 'ge', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: true, inclusive: true }},
+ { keys: 'gE', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: true, bigWord: true, inclusive: true }},
+ { keys: '{', type: 'motion', motion: 'moveByParagraph', motionArgs: { forward: false, toJumplist: true }},
+ { keys: '}', type: 'motion', motion: 'moveByParagraph', motionArgs: { forward: true, toJumplist: true }},
+ { keys: '', type: 'motion', motion: 'moveByPage', motionArgs: { forward: true }},
+ { keys: '', type: 'motion', motion: 'moveByPage', motionArgs: { forward: false }},
+ { keys: '', type: 'motion', motion: 'moveByScroll', motionArgs: { forward: true, explicitRepeat: true }},
+ { keys: '', type: 'motion', motion: 'moveByScroll', motionArgs: { forward: false, explicitRepeat: true }},
+ { keys: 'gg', type: 'motion', motion: 'moveToLineOrEdgeOfDocument', motionArgs: { forward: false, explicitRepeat: true, linewise: true, toJumplist: true }},
+ { keys: 'G', type: 'motion', motion: 'moveToLineOrEdgeOfDocument', motionArgs: { forward: true, explicitRepeat: true, linewise: true, toJumplist: true }},
+ { keys: '0', type: 'motion', motion: 'moveToStartOfLine' },
+ { keys: '^', type: 'motion', motion: 'moveToFirstNonWhiteSpaceCharacter' },
+ { keys: '+', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, toFirstChar:true }},
+ { keys: '-', type: 'motion', motion: 'moveByLines', motionArgs: { forward: false, toFirstChar:true }},
+ { keys: '_', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, toFirstChar:true, repeatOffset:-1 }},
+ { keys: '$', type: 'motion', motion: 'moveToEol', motionArgs: { inclusive: true }},
+ { keys: '%', type: 'motion', motion: 'moveToMatchedSymbol', motionArgs: { inclusive: true, toJumplist: true }},
+ { keys: 'f', type: 'motion', motion: 'moveToCharacter', motionArgs: { forward: true , inclusive: true }},
+ { keys: 'F', type: 'motion', motion: 'moveToCharacter', motionArgs: { forward: false }},
+ { keys: 't', type: 'motion', motion: 'moveTillCharacter', motionArgs: { forward: true, inclusive: true }},
+ { keys: 'T', type: 'motion', motion: 'moveTillCharacter', motionArgs: { forward: false }},
+ { keys: ';', type: 'motion', motion: 'repeatLastCharacterSearch', motionArgs: { forward: true }},
+ { keys: ',', type: 'motion', motion: 'repeatLastCharacterSearch', motionArgs: { forward: false }},
+ { keys: '\'', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true, linewise: true}},
+ { keys: '`', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true}},
+ { keys: ']`', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true } },
+ { keys: '[`', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false } },
+ { keys: ']\'', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true, linewise: true } },
+ { keys: '[\'', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false, linewise: true } },
+ // the next two aren't motions but must come before more general motion declarations
+ { keys: ']p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: true, isEdit: true, matchIndent: true}},
+ { keys: '[p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: false, isEdit: true, matchIndent: true}},
+ { keys: ']', type: 'motion', motion: 'moveToSymbol', motionArgs: { forward: true, toJumplist: true}},
+ { keys: '[', type: 'motion', motion: 'moveToSymbol', motionArgs: { forward: false, toJumplist: true}},
+ { keys: '|', type: 'motion', motion: 'moveToColumn'},
+ { keys: 'o', type: 'motion', motion: 'moveToOtherHighlightedEnd', context:'visual'},
+ { keys: 'O', type: 'motion', motion: 'moveToOtherHighlightedEnd', motionArgs: {sameLine: true}, context:'visual'},
+ // Operators
+ { keys: 'd', type: 'operator', operator: 'delete' },
+ { keys: 'y', type: 'operator', operator: 'yank' },
+ { keys: 'c', type: 'operator', operator: 'change' },
+ { keys: '>', type: 'operator', operator: 'indent', operatorArgs: { indentRight: true }},
+ { keys: '<', type: 'operator', operator: 'indent', operatorArgs: { indentRight: false }},
+ { keys: 'g~', type: 'operator', operator: 'changeCase' },
+ { keys: 'gu', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: true}, isEdit: true },
+ { keys: 'gU', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: false}, isEdit: true },
+ { keys: 'n', type: 'motion', motion: 'findNext', motionArgs: { forward: true, toJumplist: true }},
+ { keys: 'N', type: 'motion', motion: 'findNext', motionArgs: { forward: false, toJumplist: true }},
+ // Operator-Motion dual commands
+ { keys: 'x', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: true }, operatorMotionArgs: { visualLine: false }},
+ { keys: 'X', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: false }, operatorMotionArgs: { visualLine: true }},
+ { keys: 'D', type: 'operatorMotion', operator: 'delete', motion: 'moveToEol', motionArgs: { inclusive: true }, operatorMotionArgs: { visualLine: true }},
+ { keys: 'Y', type: 'operatorMotion', operator: 'yank', motion: 'moveToEol', motionArgs: { inclusive: true }, operatorMotionArgs: { visualLine: true }},
+ { keys: 'C', type: 'operatorMotion', operator: 'change', motion: 'moveToEol', motionArgs: { inclusive: true }, operatorMotionArgs: { visualLine: true }},
+ { keys: '~', type: 'operatorMotion', operator: 'changeCase', operatorArgs: { shouldMoveCursor: true }, motion: 'moveByCharacters', motionArgs: { forward: true }},
+ { keys: '', type: 'operatorMotion', operator: 'delete', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false }, context: 'insert' },
+ // Actions
+ { keys: '', type: 'action', action: 'jumpListWalk', actionArgs: { forward: true }},
+ { keys: '', type: 'action', action: 'jumpListWalk', actionArgs: { forward: false }},
+ { keys: '', type: 'action', action: 'scroll', actionArgs: { forward: true, linewise: true }},
+ { keys: '', type: 'action', action: 'scroll', actionArgs: { forward: false, linewise: true }},
+ { keys: 'a', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'charAfter' }, context: 'normal' },
+ { keys: 'A', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'eol' }, context: 'normal' },
+ { keys: 'A', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'endOfSelectedArea' }, context: 'visual' },
+ { keys: 'i', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'inplace' }, context: 'normal' },
+ { keys: 'I', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'firstNonBlank' }},
+ { keys: 'o', type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: true }, context: 'normal' },
+ { keys: 'O', type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: false }, context: 'normal' },
+ { keys: 'v', type: 'action', action: 'toggleVisualMode' },
+ { keys: 'V', type: 'action', action: 'toggleVisualMode', actionArgs: { linewise: true }},
+ { keys: '', type: 'action', action: 'toggleVisualMode', actionArgs: { blockwise: true }},
+ { keys: 'gv', type: 'action', action: 'reselectLastSelection' },
+ { keys: 'J', type: 'action', action: 'joinLines', isEdit: true },
+ { keys: 'p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: true, isEdit: true }},
+ { keys: 'P', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: false, isEdit: true }},
+ { keys: 'r', type: 'action', action: 'replace', isEdit: true },
+ { keys: '@', type: 'action', action: 'replayMacro' },
+ { keys: 'q', type: 'action', action: 'enterMacroRecordMode' },
+ // Handle Replace-mode as a special case of insert mode.
+ { keys: 'R', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { replace: true }},
+ { keys: 'u', type: 'action', action: 'undo', context: 'normal' },
+ { keys: 'u', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: true}, context: 'visual', isEdit: true },
+ { keys: 'U', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: false}, context: 'visual', isEdit: true },
+ { keys: '', type: 'action', action: 'redo' },
+ { keys: 'm', type: 'action', action: 'setMark' },
+ { keys: '"', type: 'action', action: 'setRegister' },
+ { keys: 'zz', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }},
+ { keys: 'z.', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }, motion: 'moveToFirstNonWhiteSpaceCharacter' },
+ { keys: 'zt', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'top' }},
+ { keys: 'z', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'top' }, motion: 'moveToFirstNonWhiteSpaceCharacter' },
+ { keys: 'z-', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }},
+ { keys: 'zb', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }, motion: 'moveToFirstNonWhiteSpaceCharacter' },
+ { keys: '.', type: 'action', action: 'repeatLastEdit' },
+ { keys: '', type: 'action', action: 'incrementNumberToken', isEdit: true, actionArgs: {increase: true, backtrack: false}},
+ { keys: '', type: 'action', action: 'incrementNumberToken', isEdit: true, actionArgs: {increase: false, backtrack: false}},
+ // Text object motions
+ { keys: 'a', type: 'motion', motion: 'textObjectManipulation' },
+ { keys: 'i', type: 'motion', motion: 'textObjectManipulation', motionArgs: { textObjectInner: true }},
+ // Search
+ { keys: '/', type: 'search', searchArgs: { forward: true, querySrc: 'prompt', toJumplist: true }},
+ { keys: '?', type: 'search', searchArgs: { forward: false, querySrc: 'prompt', toJumplist: true }},
+ { keys: '*', type: 'search', searchArgs: { forward: true, querySrc: 'wordUnderCursor', wholeWordOnly: true, toJumplist: true }},
+ { keys: '#', type: 'search', searchArgs: { forward: false, querySrc: 'wordUnderCursor', wholeWordOnly: true, toJumplist: true }},
+ { keys: 'g*', type: 'search', searchArgs: { forward: true, querySrc: 'wordUnderCursor', toJumplist: true }},
+ { keys: 'g#', type: 'search', searchArgs: { forward: false, querySrc: 'wordUnderCursor', toJumplist: true }},
+ // Ex command
+ { keys: ':', type: 'ex' }
+ ];
+
+ var Pos = CodeMirror.Pos;
+
+ var modifierCodes = [16, 17, 18, 91];
+ var specialKey = {Enter:'CR',Backspace:'BS',Delete:'Del'};
+ var mac = /Mac/.test(navigator.platform);
+ var Vim = function() { return vimApi; } //{
+ function lookupKey(e) {
+ var keyCode = e.keyCode;
+ if (modifierCodes.indexOf(keyCode) != -1) { return; }
+ var hasModifier = e.ctrlKey || e.metaKey;
+ var key = CodeMirror.keyNames[keyCode];
+ key = specialKey[key] || key;
+ var name = '';
+ if (e.ctrlKey) { name += 'C-'; }
+ if (e.altKey) { name += 'A-'; }
+ if (mac && e.metaKey || (!hasModifier && e.shiftKey) && key.length < 2) {
+ // Shift key bindings can only specified for special characters.
+ return;
+ } else if (e.shiftKey && !/^[A-Za-z]$/.test(key)) {
+ name += 'S-';
+ }
+ if (key.length == 1) { key = key.toLowerCase(); }
+ name += key;
+ if (name.length > 1) { name = '<' + name + '>'; }
+ return name;
+ }
+ // Keys with modifiers are handled using keydown due to limitations of
+ // keypress event.
+ function handleKeyDown(cm, e) {
+ var name = lookupKey(e);
+ if (!name) { return; }
+
+ CodeMirror.signal(cm, 'vim-keypress', name);
+ if (CodeMirror.Vim.handleKey(cm, name, 'user')) {
+ CodeMirror.e_stop(e);
+ }
+ }
+ // Keys without modifiers are handled using keypress to work best with
+ // non-standard keyboard layouts.
+ function handleKeyPress(cm, e) {
+ var code = e.charCode || e.keyCode;
+ if (e.ctrlKey || e.metaKey || e.altKey ||
+ e.shiftKey && code < 32) { return; }
+ var name = String.fromCharCode(code);
+
+ CodeMirror.signal(cm, 'vim-keypress', name);
+ if (CodeMirror.Vim.handleKey(cm, name, 'user')) {
+ CodeMirror.e_stop(e);
+ }
+ }
+
+ function enterVimMode(cm) {
+ cm.setOption('disableInput', true);
+ cm.setOption('showCursorWhenSelecting', false);
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
+ cm.on('cursorActivity', onCursorActivity);
+ maybeInitVimState(cm);
+ CodeMirror.on(cm.getInputField(), 'paste', getOnPasteFn(cm));
+ cm.on('keypress', handleKeyPress);
+ cm.on('keydown', handleKeyDown);
+ CodeMirror.addClass(cm.getWrapperElement(), "cm-fat-cursor");
+ }
+
+ function leaveVimMode(cm) {
+ cm.setOption('disableInput', false);
+ cm.off('cursorActivity', onCursorActivity);
+ CodeMirror.off(cm.getInputField(), 'paste', getOnPasteFn(cm));
+ cm.state.vim = null;
+ cm.off('keypress', handleKeyPress);
+ cm.off('keydown', handleKeyDown);
+ }
+
+ function detachVimMap(cm, next) {
+ if (this == CodeMirror.keyMap.vim)
+ CodeMirror.rmClass(cm.getWrapperElement(), "cm-fat-cursor");
+
+ if (!next || next.attach != attachVimMap)
+ leaveVimMode(cm, false);
+ }
+ function attachVimMap(cm, prev) {
+ if (this == CodeMirror.keyMap.vim)
+ CodeMirror.addClass(cm.getWrapperElement(), "cm-fat-cursor");
+
+ if (!prev || prev.attach != attachVimMap)
+ enterVimMode(cm);
+ }
+
+ // Deprecated, simply setting the keymap works again.
+ CodeMirror.defineOption('vimMode', false, function(cm, val, prev) {
+ if (val && cm.getOption("keyMap") != "vim")
+ cm.setOption("keyMap", "vim");
+ else if (!val && prev != CodeMirror.Init && /^vim/.test(cm.getOption("keyMap")))
+ cm.setOption("keyMap", "default");
+ });
+ function getOnPasteFn(cm) {
+ var vim = cm.state.vim;
+ if (!vim.onPasteFn) {
+ vim.onPasteFn = function() {
+ if (!vim.insertMode) {
+ cm.setCursor(offsetCursor(cm.getCursor(), 0, 1));
+ actions.enterInsertMode(cm, {}, vim);
+ }
+ };
+ }
+ return vim.onPasteFn;
+ }
+
+ var numberRegex = /[\d]/;
+ var wordRegexp = [(/\w/), (/[^\w\s]/)], bigWordRegexp = [(/\S/)];
+ function makeKeyRange(start, size) {
+ var keys = [];
+ for (var i = start; i < start + size; i++) {
+ keys.push(String.fromCharCode(i));
+ }
+ return keys;
+ }
+ var upperCaseAlphabet = makeKeyRange(65, 26);
+ var lowerCaseAlphabet = makeKeyRange(97, 26);
+ var numbers = makeKeyRange(48, 10);
+ var validMarks = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['<', '>']);
+ var validRegisters = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['-', '"', '.', ':', '/']);
+
+ function isLine(cm, line) {
+ return line >= cm.firstLine() && line <= cm.lastLine();
+ }
+ function isLowerCase(k) {
+ return (/^[a-z]$/).test(k);
+ }
+ function isMatchableSymbol(k) {
+ return '()[]{}'.indexOf(k) != -1;
+ }
+ function isNumber(k) {
+ return numberRegex.test(k);
+ }
+ function isUpperCase(k) {
+ return (/^[A-Z]$/).test(k);
+ }
+ function isWhiteSpaceString(k) {
+ return (/^\s*$/).test(k);
+ }
+ function inArray(val, arr) {
+ for (var i = 0; i < arr.length; i++) {
+ if (arr[i] == val) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ var options = {};
+ function defineOption(name, defaultValue, type) {
+ if (defaultValue === undefined) { throw Error('defaultValue is required'); }
+ if (!type) { type = 'string'; }
+ options[name] = {
+ type: type,
+ defaultValue: defaultValue
+ };
+ setOption(name, defaultValue);
+ }
+
+ function setOption(name, value) {
+ var option = options[name];
+ if (!option) {
+ throw Error('Unknown option: ' + name);
+ }
+ if (option.type == 'boolean') {
+ if (value && value !== true) {
+ throw Error('Invalid argument: ' + name + '=' + value);
+ } else if (value !== false) {
+ // Boolean options are set to true if value is not defined.
+ value = true;
+ }
+ }
+ option.value = option.type == 'boolean' ? !!value : value;
+ }
+
+ function getOption(name) {
+ var option = options[name];
+ if (!option) {
+ throw Error('Unknown option: ' + name);
+ }
+ return option.value;
+ }
+
+ var createCircularJumpList = function() {
+ var size = 100;
+ var pointer = -1;
+ var head = 0;
+ var tail = 0;
+ var buffer = new Array(size);
+ function add(cm, oldCur, newCur) {
+ var current = pointer % size;
+ var curMark = buffer[current];
+ function useNextSlot(cursor) {
+ var next = ++pointer % size;
+ var trashMark = buffer[next];
+ if (trashMark) {
+ trashMark.clear();
+ }
+ buffer[next] = cm.setBookmark(cursor);
+ }
+ if (curMark) {
+ var markPos = curMark.find();
+ // avoid recording redundant cursor position
+ if (markPos && !cursorEqual(markPos, oldCur)) {
+ useNextSlot(oldCur);
+ }
+ } else {
+ useNextSlot(oldCur);
+ }
+ useNextSlot(newCur);
+ head = pointer;
+ tail = pointer - size + 1;
+ if (tail < 0) {
+ tail = 0;
+ }
+ }
+ function move(cm, offset) {
+ pointer += offset;
+ if (pointer > head) {
+ pointer = head;
+ } else if (pointer < tail) {
+ pointer = tail;
+ }
+ var mark = buffer[(size + pointer) % size];
+ // skip marks that are temporarily removed from text buffer
+ if (mark && !mark.find()) {
+ var inc = offset > 0 ? 1 : -1;
+ var newCur;
+ var oldCur = cm.getCursor();
+ do {
+ pointer += inc;
+ mark = buffer[(size + pointer) % size];
+ // skip marks that are the same as current position
+ if (mark &&
+ (newCur = mark.find()) &&
+ !cursorEqual(oldCur, newCur)) {
+ break;
+ }
+ } while (pointer < head && pointer > tail);
+ }
+ return mark;
+ }
+ return {
+ cachedCursor: undefined, //used for # and * jumps
+ add: add,
+ move: move
+ };
+ };
+
+ // Returns an object to track the changes associated insert mode. It
+ // clones the object that is passed in, or creates an empty object one if
+ // none is provided.
+ var createInsertModeChanges = function(c) {
+ if (c) {
+ // Copy construction
+ return {
+ changes: c.changes,
+ expectCursorActivityForChange: c.expectCursorActivityForChange
+ };
+ }
+ return {
+ // Change list
+ changes: [],
+ // Set to true on change, false on cursorActivity.
+ expectCursorActivityForChange: false
+ };
+ };
+
+ function MacroModeState() {
+ this.latestRegister = undefined;
+ this.isPlaying = false;
+ this.isRecording = false;
+ this.replaySearchQueries = [];
+ this.onRecordingDone = undefined;
+ this.lastInsertModeChanges = createInsertModeChanges();
+ }
+ MacroModeState.prototype = {
+ exitMacroRecordMode: function() {
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.onRecordingDone) {
+ macroModeState.onRecordingDone(); // close dialog
+ }
+ macroModeState.onRecordingDone = undefined;
+ macroModeState.isRecording = false;
+ },
+ enterMacroRecordMode: function(cm, registerName) {
+ var register =
+ vimGlobalState.registerController.getRegister(registerName);
+ if (register) {
+ register.clear();
+ this.latestRegister = registerName;
+ if (cm.openDialog) {
+ this.onRecordingDone = cm.openDialog(
+ '(recording)['+registerName+']', null, {bottom:true});
+ }
+ this.isRecording = true;
+ }
+ }
+ };
+
+ function maybeInitVimState(cm) {
+ if (!cm.state.vim) {
+ // Store instance state in the CodeMirror object.
+ cm.state.vim = {
+ inputState: new InputState(),
+ // Vim's input state that triggered the last edit, used to repeat
+ // motions and operators with '.'.
+ lastEditInputState: undefined,
+ // Vim's action command before the last edit, used to repeat actions
+ // with '.' and insert mode repeat.
+ lastEditActionCommand: undefined,
+ // When using jk for navigation, if you move from a longer line to a
+ // shorter line, the cursor may clip to the end of the shorter line.
+ // If j is pressed again and cursor goes to the next line, the
+ // cursor should go back to its horizontal position on the longer
+ // line if it can. This is to keep track of the horizontal position.
+ lastHPos: -1,
+ // Doing the same with screen-position for gj/gk
+ lastHSPos: -1,
+ // The last motion command run. Cleared if a non-motion command gets
+ // executed in between.
+ lastMotion: null,
+ marks: {},
+ // Mark for rendering fake cursor for visual mode.
+ fakeCursor: null,
+ insertMode: false,
+ // Repeat count for changes made in insert mode, triggered by key
+ // sequences like 3,i. Only exists when insertMode is true.
+ insertModeRepeat: undefined,
+ visualMode: false,
+ // If we are in visual line mode. No effect if visualMode is false.
+ visualLine: false,
+ visualBlock: false,
+ lastSelection: null,
+ lastPastedText: null
+ };
+ }
+ return cm.state.vim;
+ }
+ var vimGlobalState;
+ function resetVimGlobalState() {
+ vimGlobalState = {
+ // The current search query.
+ searchQuery: null,
+ // Whether we are searching backwards.
+ searchIsReversed: false,
+ // Replace part of the last substituted pattern
+ lastSubstituteReplacePart: undefined,
+ jumpList: createCircularJumpList(),
+ macroModeState: new MacroModeState,
+ // Recording latest f, t, F or T motion command.
+ lastChararacterSearch: {increment:0, forward:true, selectedCharacter:''},
+ registerController: new RegisterController({}),
+ // search history buffer
+ searchHistoryController: new HistoryController({}),
+ // ex Command history buffer
+ exCommandHistoryController : new HistoryController({})
+ };
+ for (var optionName in options) {
+ var option = options[optionName];
+ option.value = option.defaultValue;
+ }
+ }
+
+ var lastInsertModeKeyTimer;
+ var vimApi= {
+ buildKeyMap: function() {
+ // TODO: Convert keymap into dictionary format for fast lookup.
+ },
+ // Testing hook, though it might be useful to expose the register
+ // controller anyways.
+ getRegisterController: function() {
+ return vimGlobalState.registerController;
+ },
+ // Testing hook.
+ resetVimGlobalState_: resetVimGlobalState,
+
+ // Testing hook.
+ getVimGlobalState_: function() {
+ return vimGlobalState;
+ },
+
+ // Testing hook.
+ maybeInitVimState_: maybeInitVimState,
+
+ InsertModeKey: InsertModeKey,
+ map: function(lhs, rhs, ctx) {
+ // Add user defined key bindings.
+ exCommandDispatcher.map(lhs, rhs, ctx);
+ },
+ setOption: setOption,
+ getOption: getOption,
+ defineOption: defineOption,
+ defineEx: function(name, prefix, func){
+ if (name.indexOf(prefix) !== 0) {
+ throw new Error('(Vim.defineEx) "'+prefix+'" is not a prefix of "'+name+'", command not registered');
+ }
+ exCommands[name]=func;
+ exCommandDispatcher.commandMap_[prefix]={name:name, shortName:prefix, type:'api'};
+ },
+ // This is the outermost function called by CodeMirror, after keys have
+ // been mapped to their Vim equivalents.
+ handleKey: function(cm, key, origin) {
+ var vim = maybeInitVimState(cm);
+ function handleMacroRecording() {
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.isRecording) {
+ if (key == 'q') {
+ macroModeState.exitMacroRecordMode();
+ clearInputState(cm);
+ return true;
+ }
+ if (origin != 'mapping') {
+ logKey(macroModeState, key);
+ }
+ }
+ }
+ function handleEsc() {
+ if (key == '') {
+ // Clear input state and get back to normal mode.
+ clearInputState(cm);
+ if (vim.visualMode) {
+ exitVisualMode(cm);
+ } else if (vim.insertMode) {
+ exitInsertMode(cm);
+ }
+ return true;
+ }
+ }
+ function doKeyToKey(keys) {
+ // TODO: prevent infinite recursion.
+ var match;
+ while (keys) {
+ // Pull off one command key, which is either a single character
+ // or a special sequence wrapped in '<' and '>', e.g. ''.
+ match = (/<\w+-.+?>|<\w+>|./).exec(keys);
+ key = match[0];
+ keys = keys.substring(match.index + key.length);
+ CodeMirror.Vim.handleKey(cm, key, 'mapping');
+ }
+ }
+
+ function handleKeyInsertMode() {
+ if (handleEsc()) { return true; }
+ var keys = vim.inputState.keyBuffer = vim.inputState.keyBuffer + key;
+ var keysAreChars = key.length == 1;
+ var match = commandDispatcher.matchCommand(keys, defaultKeymap, vim.inputState, 'insert');
+ // Need to check all key substrings in insert mode.
+ while (keys.length > 1 && match.type != 'full') {
+ var keys = vim.inputState.keyBuffer = keys.slice(1);
+ var thisMatch = commandDispatcher.matchCommand(keys, defaultKeymap, vim.inputState, 'insert');
+ if (thisMatch.type != 'none') { match = thisMatch; }
+ }
+ if (match.type == 'none') { clearInputState(cm); return false; }
+ else if (match.type == 'partial') {
+ if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); }
+ lastInsertModeKeyTimer = window.setTimeout(
+ function() { if (vim.insertMode && vim.inputState.keyBuffer) { clearInputState(cm); } },
+ getOption('insertModeEscKeysTimeout'));
+ return !keysAreChars;
+ }
+
+ if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); }
+ if (keysAreChars) {
+ var here = cm.getCursor();
+ cm.replaceRange('', offsetCursor(here, 0, -(keys.length - 1)), here, '+input');
+ }
+ clearInputState(cm);
+ var command = match.command;
+ if (command.type == 'keyToKey') {
+ doKeyToKey(command.toKeys);
+ } else {
+ commandDispatcher.processCommand(cm, vim, command);
+ }
+ return true;
+ }
+
+ function handleKeyNonInsertMode() {
+ if (handleMacroRecording() || handleEsc()) { return true; };
+
+ var keys = vim.inputState.keyBuffer = vim.inputState.keyBuffer + key;
+ if (/^[1-9]\d*$/.test(keys)) { return true; }
+
+ var keysMatcher = /^(\d*)(.*)$/.exec(keys);
+ if (!keysMatcher) { clearInputState(cm); return false; }
+ var context = vim.visualMode ? 'visual' :
+ 'normal';
+ var match = commandDispatcher.matchCommand(keysMatcher[2] || keysMatcher[1], defaultKeymap, vim.inputState, context);
+ if (match.type == 'none') { clearInputState(cm); return false; }
+ else if (match.type == 'partial') { return true; }
+
+ vim.inputState.keyBuffer = '';
+ var command = match.command;
+ var keysMatcher = /^(\d*)(.*)$/.exec(keys);
+ if (keysMatcher[1] && keysMatcher[1] != '0') {
+ vim.inputState.pushRepeatDigit(keysMatcher[1]);
+ }
+ if (command.type == 'keyToKey') {
+ doKeyToKey(command.toKeys);
+ } else {
+ commandDispatcher.processCommand(cm, vim, command);
+ }
+ return true;
+ }
+
+ return cm.operation(function() {
+ cm.curOp.isVimOp = true;
+ try {
+ if (vim.insertMode) { return handleKeyInsertMode(); }
+ else { return handleKeyNonInsertMode(); }
+ } catch (e) {
+ // clear VIM state in case it's in a bad state.
+ cm.state.vim = undefined;
+ throw e;
+ }
+ });
+ },
+ handleEx: function(cm, input) {
+ exCommandDispatcher.processCommand(cm, input);
+ }
+ };
+
+ // Represents the current input state.
+ function InputState() {
+ this.prefixRepeat = [];
+ this.motionRepeat = [];
+
+ this.operator = null;
+ this.operatorArgs = null;
+ this.motion = null;
+ this.motionArgs = null;
+ this.keyBuffer = []; // For matching multi-key commands.
+ this.registerName = null; // Defaults to the unnamed register.
+ }
+ InputState.prototype.pushRepeatDigit = function(n) {
+ if (!this.operator) {
+ this.prefixRepeat = this.prefixRepeat.concat(n);
+ } else {
+ this.motionRepeat = this.motionRepeat.concat(n);
+ }
+ };
+ InputState.prototype.getRepeat = function() {
+ var repeat = 0;
+ if (this.prefixRepeat.length > 0 || this.motionRepeat.length > 0) {
+ repeat = 1;
+ if (this.prefixRepeat.length > 0) {
+ repeat *= parseInt(this.prefixRepeat.join(''), 10);
+ }
+ if (this.motionRepeat.length > 0) {
+ repeat *= parseInt(this.motionRepeat.join(''), 10);
+ }
+ }
+ return repeat;
+ };
+
+ function clearInputState(cm, reason) {
+ cm.state.vim.inputState = new InputState();
+ CodeMirror.signal(cm, 'vim-command-done', reason);
+ }
+
+ /*
+ * Register stores information about copy and paste registers. Besides
+ * text, a register must store whether it is linewise (i.e., when it is
+ * pasted, should it insert itself into a new line, or should the text be
+ * inserted at the cursor position.)
+ */
+ function Register(text, linewise, blockwise) {
+ this.clear();
+ this.keyBuffer = [text || ''];
+ this.insertModeChanges = [];
+ this.searchQueries = [];
+ this.linewise = !!linewise;
+ this.blockwise = !!blockwise;
+ }
+ Register.prototype = {
+ setText: function(text, linewise, blockwise) {
+ this.keyBuffer = [text || ''];
+ this.linewise = !!linewise;
+ this.blockwise = !!blockwise;
+ },
+ pushText: function(text, linewise) {
+ // if this register has ever been set to linewise, use linewise.
+ if (linewise) {
+ if (!this.linewise) {
+ this.keyBuffer.push('\n');
+ }
+ this.linewise = true;
+ }
+ this.keyBuffer.push(text);
+ },
+ pushInsertModeChanges: function(changes) {
+ this.insertModeChanges.push(createInsertModeChanges(changes));
+ },
+ pushSearchQuery: function(query) {
+ this.searchQueries.push(query);
+ },
+ clear: function() {
+ this.keyBuffer = [];
+ this.insertModeChanges = [];
+ this.searchQueries = [];
+ this.linewise = false;
+ },
+ toString: function() {
+ return this.keyBuffer.join('');
+ }
+ };
+
+ /*
+ * vim registers allow you to keep many independent copy and paste buffers.
+ * See http://usevim.com/2012/04/13/registers/ for an introduction.
+ *
+ * RegisterController keeps the state of all the registers. An initial
+ * state may be passed in. The unnamed register '"' will always be
+ * overridden.
+ */
+ function RegisterController(registers) {
+ this.registers = registers;
+ this.unnamedRegister = registers['"'] = new Register();
+ registers['.'] = new Register();
+ registers[':'] = new Register();
+ registers['/'] = new Register();
+ }
+ RegisterController.prototype = {
+ pushText: function(registerName, operator, text, linewise, blockwise) {
+ if (linewise && text.charAt(0) == '\n') {
+ text = text.slice(1) + '\n';
+ }
+ if (linewise && text.charAt(text.length - 1) !== '\n'){
+ text += '\n';
+ }
+ // Lowercase and uppercase registers refer to the same register.
+ // Uppercase just means append.
+ var register = this.isValidRegister(registerName) ?
+ this.getRegister(registerName) : null;
+ // if no register/an invalid register was specified, things go to the
+ // default registers
+ if (!register) {
+ switch (operator) {
+ case 'yank':
+ // The 0 register contains the text from the most recent yank.
+ this.registers['0'] = new Register(text, linewise, blockwise);
+ break;
+ case 'delete':
+ case 'change':
+ if (text.indexOf('\n') == -1) {
+ // Delete less than 1 line. Update the small delete register.
+ this.registers['-'] = new Register(text, linewise);
+ } else {
+ // Shift down the contents of the numbered registers and put the
+ // deleted text into register 1.
+ this.shiftNumericRegisters_();
+ this.registers['1'] = new Register(text, linewise);
+ }
+ break;
+ }
+ // Make sure the unnamed register is set to what just happened
+ this.unnamedRegister.setText(text, linewise, blockwise);
+ return;
+ }
+
+ // If we've gotten to this point, we've actually specified a register
+ var append = isUpperCase(registerName);
+ if (append) {
+ register.pushText(text, linewise);
+ } else {
+ register.setText(text, linewise, blockwise);
+ }
+ // The unnamed register always has the same value as the last used
+ // register.
+ this.unnamedRegister.setText(register.toString(), linewise);
+ },
+ // Gets the register named @name. If one of @name doesn't already exist,
+ // create it. If @name is invalid, return the unnamedRegister.
+ getRegister: function(name) {
+ if (!this.isValidRegister(name)) {
+ return this.unnamedRegister;
+ }
+ name = name.toLowerCase();
+ if (!this.registers[name]) {
+ this.registers[name] = new Register();
+ }
+ return this.registers[name];
+ },
+ isValidRegister: function(name) {
+ return name && inArray(name, validRegisters);
+ },
+ shiftNumericRegisters_: function() {
+ for (var i = 9; i >= 2; i--) {
+ this.registers[i] = this.getRegister('' + (i - 1));
+ }
+ }
+ };
+ function HistoryController() {
+ this.historyBuffer = [];
+ this.iterator;
+ this.initialPrefix = null;
+ }
+ HistoryController.prototype = {
+ // the input argument here acts a user entered prefix for a small time
+ // until we start autocompletion in which case it is the autocompleted.
+ nextMatch: function (input, up) {
+ var historyBuffer = this.historyBuffer;
+ var dir = up ? -1 : 1;
+ if (this.initialPrefix === null) this.initialPrefix = input;
+ for (var i = this.iterator + dir; up ? i >= 0 : i < historyBuffer.length; i+= dir) {
+ var element = historyBuffer[i];
+ for (var j = 0; j <= element.length; j++) {
+ if (this.initialPrefix == element.substring(0, j)) {
+ this.iterator = i;
+ return element;
+ }
+ }
+ }
+ // should return the user input in case we reach the end of buffer.
+ if (i >= historyBuffer.length) {
+ this.iterator = historyBuffer.length;
+ return this.initialPrefix;
+ }
+ // return the last autocompleted query or exCommand as it is.
+ if (i < 0 ) return input;
+ },
+ pushInput: function(input) {
+ var index = this.historyBuffer.indexOf(input);
+ if (index > -1) this.historyBuffer.splice(index, 1);
+ if (input.length) this.historyBuffer.push(input);
+ },
+ reset: function() {
+ this.initialPrefix = null;
+ this.iterator = this.historyBuffer.length;
+ }
+ };
+ var commandDispatcher = {
+ matchCommand: function(keys, keyMap, inputState, context) {
+ var matches = commandMatches(keys, keyMap, context, inputState);
+ if (!matches.full && !matches.partial) {
+ return {type: 'none'};
+ } else if (!matches.full && matches.partial) {
+ return {type: 'partial'};
+ }
+
+ var bestMatch;
+ for (var i = 0; i < matches.full.length; i++) {
+ var match = matches.full[i];
+ if (!bestMatch) {
+ bestMatch = match;
+ }
+ }
+ if (bestMatch.keys.slice(-11) == '') {
+ inputState.selectedCharacter = lastChar(keys);
+ }
+ return {type: 'full', command: bestMatch};
+ },
+ processCommand: function(cm, vim, command) {
+ vim.inputState.repeatOverride = command.repeatOverride;
+ switch (command.type) {
+ case 'motion':
+ this.processMotion(cm, vim, command);
+ break;
+ case 'operator':
+ this.processOperator(cm, vim, command);
+ break;
+ case 'operatorMotion':
+ this.processOperatorMotion(cm, vim, command);
+ break;
+ case 'action':
+ this.processAction(cm, vim, command);
+ break;
+ case 'search':
+ this.processSearch(cm, vim, command);
+ clearInputState(cm);
+ break;
+ case 'ex':
+ case 'keyToEx':
+ this.processEx(cm, vim, command);
+ clearInputState(cm);
+ break;
+ default:
+ break;
+ }
+ },
+ processMotion: function(cm, vim, command) {
+ vim.inputState.motion = command.motion;
+ vim.inputState.motionArgs = copyArgs(command.motionArgs);
+ this.evalInput(cm, vim);
+ },
+ processOperator: function(cm, vim, command) {
+ var inputState = vim.inputState;
+ if (inputState.operator) {
+ if (inputState.operator == command.operator) {
+ // Typing an operator twice like 'dd' makes the operator operate
+ // linewise
+ inputState.motion = 'expandToLine';
+ inputState.motionArgs = { linewise: true };
+ this.evalInput(cm, vim);
+ return;
+ } else {
+ // 2 different operators in a row doesn't make sense.
+ clearInputState(cm);
+ }
+ }
+ inputState.operator = command.operator;
+ inputState.operatorArgs = copyArgs(command.operatorArgs);
+ if (vim.visualMode) {
+ // Operating on a selection in visual mode. We don't need a motion.
+ this.evalInput(cm, vim);
+ }
+ },
+ processOperatorMotion: function(cm, vim, command) {
+ var visualMode = vim.visualMode;
+ var operatorMotionArgs = copyArgs(command.operatorMotionArgs);
+ if (operatorMotionArgs) {
+ // Operator motions may have special behavior in visual mode.
+ if (visualMode && operatorMotionArgs.visualLine) {
+ vim.visualLine = true;
+ }
+ }
+ this.processOperator(cm, vim, command);
+ if (!visualMode) {
+ this.processMotion(cm, vim, command);
+ }
+ },
+ processAction: function(cm, vim, command) {
+ var inputState = vim.inputState;
+ var repeat = inputState.getRepeat();
+ var repeatIsExplicit = !!repeat;
+ var actionArgs = copyArgs(command.actionArgs) || {};
+ if (inputState.selectedCharacter) {
+ actionArgs.selectedCharacter = inputState.selectedCharacter;
+ }
+ // Actions may or may not have motions and operators. Do these first.
+ if (command.operator) {
+ this.processOperator(cm, vim, command);
+ }
+ if (command.motion) {
+ this.processMotion(cm, vim, command);
+ }
+ if (command.motion || command.operator) {
+ this.evalInput(cm, vim);
+ }
+ actionArgs.repeat = repeat || 1;
+ actionArgs.repeatIsExplicit = repeatIsExplicit;
+ actionArgs.registerName = inputState.registerName;
+ clearInputState(cm);
+ vim.lastMotion = null;
+ if (command.isEdit) {
+ this.recordLastEdit(vim, inputState, command);
+ }
+ actions[command.action](cm, actionArgs, vim);
+ },
+ processSearch: function(cm, vim, command) {
+ if (!cm.getSearchCursor) {
+ // Search depends on SearchCursor.
+ return;
+ }
+ var forward = command.searchArgs.forward;
+ var wholeWordOnly = command.searchArgs.wholeWordOnly;
+ getSearchState(cm).setReversed(!forward);
+ var promptPrefix = (forward) ? '/' : '?';
+ var originalQuery = getSearchState(cm).getQuery();
+ var originalScrollPos = cm.getScrollInfo();
+ function handleQuery(query, ignoreCase, smartCase) {
+ vimGlobalState.searchHistoryController.pushInput(query);
+ vimGlobalState.searchHistoryController.reset();
+ try {
+ updateSearchQuery(cm, query, ignoreCase, smartCase);
+ } catch (e) {
+ showConfirm(cm, 'Invalid regex: ' + query);
+ return;
+ }
+ commandDispatcher.processMotion(cm, vim, {
+ type: 'motion',
+ motion: 'findNext',
+ motionArgs: { forward: true, toJumplist: command.searchArgs.toJumplist }
+ });
+ }
+ function onPromptClose(query) {
+ cm.scrollTo(originalScrollPos.left, originalScrollPos.top);
+ handleQuery(query, true /** ignoreCase */, true /** smartCase */);
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.isRecording) {
+ logSearchQuery(macroModeState, query);
+ }
+ }
+ function onPromptKeyUp(e, query, close) {
+ var keyName = CodeMirror.keyName(e), up;
+ if (keyName == 'Up' || keyName == 'Down') {
+ up = keyName == 'Up' ? true : false;
+ query = vimGlobalState.searchHistoryController.nextMatch(query, up) || '';
+ close(query);
+ } else {
+ if ( keyName != 'Left' && keyName != 'Right' && keyName != 'Ctrl' && keyName != 'Alt' && keyName != 'Shift')
+ vimGlobalState.searchHistoryController.reset();
+ }
+ var parsedQuery;
+ try {
+ parsedQuery = updateSearchQuery(cm, query,
+ true /** ignoreCase */, true /** smartCase */);
+ } catch (e) {
+ // Swallow bad regexes for incremental search.
+ }
+ if (parsedQuery) {
+ cm.scrollIntoView(findNext(cm, !forward, parsedQuery), 30);
+ } else {
+ clearSearchHighlight(cm);
+ cm.scrollTo(originalScrollPos.left, originalScrollPos.top);
+ }
+ }
+ function onPromptKeyDown(e, query, close) {
+ var keyName = CodeMirror.keyName(e);
+ if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[') {
+ vimGlobalState.searchHistoryController.pushInput(query);
+ vimGlobalState.searchHistoryController.reset();
+ updateSearchQuery(cm, originalQuery);
+ clearSearchHighlight(cm);
+ cm.scrollTo(originalScrollPos.left, originalScrollPos.top);
+ CodeMirror.e_stop(e);
+ close();
+ cm.focus();
+ }
+ }
+ switch (command.searchArgs.querySrc) {
+ case 'prompt':
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.isPlaying) {
+ var query = macroModeState.replaySearchQueries.shift();
+ handleQuery(query, true /** ignoreCase */, false /** smartCase */);
+ } else {
+ showPrompt(cm, {
+ onClose: onPromptClose,
+ prefix: promptPrefix,
+ desc: searchPromptDesc,
+ onKeyUp: onPromptKeyUp,
+ onKeyDown: onPromptKeyDown
+ });
+ }
+ break;
+ case 'wordUnderCursor':
+ var word = expandWordUnderCursor(cm, false /** inclusive */,
+ true /** forward */, false /** bigWord */,
+ true /** noSymbol */);
+ var isKeyword = true;
+ if (!word) {
+ word = expandWordUnderCursor(cm, false /** inclusive */,
+ true /** forward */, false /** bigWord */,
+ false /** noSymbol */);
+ isKeyword = false;
+ }
+ if (!word) {
+ return;
+ }
+ var query = cm.getLine(word.start.line).substring(word.start.ch,
+ word.end.ch);
+ if (isKeyword && wholeWordOnly) {
+ query = '\\b' + query + '\\b';
+ } else {
+ query = escapeRegex(query);
+ }
+
+ // cachedCursor is used to save the old position of the cursor
+ // when * or # causes vim to seek for the nearest word and shift
+ // the cursor before entering the motion.
+ vimGlobalState.jumpList.cachedCursor = cm.getCursor();
+ cm.setCursor(word.start);
+
+ handleQuery(query, true /** ignoreCase */, false /** smartCase */);
+ break;
+ }
+ },
+ processEx: function(cm, vim, command) {
+ function onPromptClose(input) {
+ // Give the prompt some time to close so that if processCommand shows
+ // an error, the elements don't overlap.
+ vimGlobalState.exCommandHistoryController.pushInput(input);
+ vimGlobalState.exCommandHistoryController.reset();
+ exCommandDispatcher.processCommand(cm, input);
+ }
+ function onPromptKeyDown(e, input, close) {
+ var keyName = CodeMirror.keyName(e), up;
+ if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[') {
+ vimGlobalState.exCommandHistoryController.pushInput(input);
+ vimGlobalState.exCommandHistoryController.reset();
+ CodeMirror.e_stop(e);
+ close();
+ cm.focus();
+ }
+ if (keyName == 'Up' || keyName == 'Down') {
+ up = keyName == 'Up' ? true : false;
+ input = vimGlobalState.exCommandHistoryController.nextMatch(input, up) || '';
+ close(input);
+ } else {
+ if ( keyName != 'Left' && keyName != 'Right' && keyName != 'Ctrl' && keyName != 'Alt' && keyName != 'Shift')
+ vimGlobalState.exCommandHistoryController.reset();
+ }
+ }
+ if (command.type == 'keyToEx') {
+ // Handle user defined Ex to Ex mappings
+ exCommandDispatcher.processCommand(cm, command.exArgs.input);
+ } else {
+ if (vim.visualMode) {
+ showPrompt(cm, { onClose: onPromptClose, prefix: ':', value: '\'<,\'>',
+ onKeyDown: onPromptKeyDown});
+ } else {
+ showPrompt(cm, { onClose: onPromptClose, prefix: ':',
+ onKeyDown: onPromptKeyDown});
+ }
+ }
+ },
+ evalInput: function(cm, vim) {
+ // If the motion comand is set, execute both the operator and motion.
+ // Otherwise return.
+ var inputState = vim.inputState;
+ var motion = inputState.motion;
+ var motionArgs = inputState.motionArgs || {};
+ var operator = inputState.operator;
+ var operatorArgs = inputState.operatorArgs || {};
+ var registerName = inputState.registerName;
+ var selectionEnd = copyCursor(cm.getCursor('head'));
+ var selectionStart = copyCursor(cm.getCursor('anchor'));
+ // The difference between cur and selection cursors are that cur is
+ // being operated on and ignores that there is a selection.
+ var curStart = copyCursor(selectionEnd);
+ var curOriginal = copyCursor(curStart);
+ var curEnd;
+ var repeat;
+ if (operator) {
+ this.recordLastEdit(vim, inputState);
+ }
+ if (inputState.repeatOverride !== undefined) {
+ // If repeatOverride is specified, that takes precedence over the
+ // input state's repeat. Used by Ex mode and can be user defined.
+ repeat = inputState.repeatOverride;
+ } else {
+ repeat = inputState.getRepeat();
+ }
+ if (repeat > 0 && motionArgs.explicitRepeat) {
+ motionArgs.repeatIsExplicit = true;
+ } else if (motionArgs.noRepeat ||
+ (!motionArgs.explicitRepeat && repeat === 0)) {
+ repeat = 1;
+ motionArgs.repeatIsExplicit = false;
+ }
+ if (inputState.selectedCharacter) {
+ // If there is a character input, stick it in all of the arg arrays.
+ motionArgs.selectedCharacter = operatorArgs.selectedCharacter =
+ inputState.selectedCharacter;
+ }
+ motionArgs.repeat = repeat;
+ clearInputState(cm);
+ if (motion) {
+ var motionResult = motions[motion](cm, motionArgs, vim);
+ vim.lastMotion = motions[motion];
+ if (!motionResult) {
+ return;
+ }
+ if (motionArgs.toJumplist) {
+ var jumpList = vimGlobalState.jumpList;
+ // if the current motion is # or *, use cachedCursor
+ var cachedCursor = jumpList.cachedCursor;
+ if (cachedCursor) {
+ recordJumpPosition(cm, cachedCursor, motionResult);
+ delete jumpList.cachedCursor;
+ } else {
+ recordJumpPosition(cm, curOriginal, motionResult);
+ }
+ }
+ if (motionResult instanceof Array) {
+ curStart = motionResult[0];
+ curEnd = motionResult[1];
+ } else {
+ curEnd = motionResult;
+ }
+ // TODO: Handle null returns from motion commands better.
+ if (!curEnd) {
+ curEnd = Pos(curStart.line, curStart.ch);
+ }
+ if (vim.visualMode) {
+ // Check if the selection crossed over itself. Will need to shift
+ // the start point if that happened.
+ // offset is set to -1 or 1 to shift the curEnd
+ // left or right
+ var offset = 0;
+ if (cursorIsBefore(selectionStart, selectionEnd) &&
+ (cursorEqual(selectionStart, curEnd) ||
+ cursorIsBefore(curEnd, selectionStart))) {
+ // The end of the selection has moved from after the start to
+ // before the start. We will shift the start right by 1.
+ selectionStart.ch += 1;
+ offset = -1;
+ } else if (cursorIsBefore(selectionEnd, selectionStart) &&
+ (cursorEqual(selectionStart, curEnd) ||
+ cursorIsBefore(selectionStart, curEnd))) {
+ // The opposite happened. We will shift the start left by 1.
+ selectionStart.ch -= 1;
+ offset = 1;
+ }
+ // in case of visual Block selectionStart and curEnd
+ // may not be on the same line,
+ // Also, In case of v_o this should not happen.
+ if (!vim.visualBlock && !(motionResult instanceof Array)) {
+ curEnd.ch += offset;
+ }
+ if (vim.lastHPos != Infinity) {
+ vim.lastHPos = curEnd.ch;
+ }
+ selectionEnd = curEnd;
+ selectionStart = (motionResult instanceof Array) ? curStart : selectionStart;
+ if (vim.visualLine) {
+ if (cursorIsBefore(selectionStart, selectionEnd)) {
+ selectionStart.ch = 0;
+
+ var lastLine = cm.lastLine();
+ if (selectionEnd.line > lastLine) {
+ selectionEnd.line = lastLine;
+ }
+ selectionEnd.ch = lineLength(cm, selectionEnd.line);
+ } else {
+ selectionEnd.ch = 0;
+ selectionStart.ch = lineLength(cm, selectionStart.line);
+ }
+ } else if (vim.visualBlock) {
+ // Select a block and
+ // return the diagonally opposite end.
+ selectionStart = selectBlock(cm, selectionEnd);
+ }
+ if (!vim.visualBlock) {
+ cm.setSelection(selectionStart, selectionEnd);
+ }
+ updateMark(cm, vim, '<',
+ cursorIsBefore(selectionStart, selectionEnd) ? selectionStart
+ : selectionEnd);
+ updateMark(cm, vim, '>',
+ cursorIsBefore(selectionStart, selectionEnd) ? selectionEnd
+ : selectionStart);
+ } else if (!operator) {
+ curEnd = clipCursorToContent(cm, curEnd);
+ cm.setCursor(curEnd.line, curEnd.ch);
+ }
+ }
+
+ if (operator) {
+ var inverted = false;
+ vim.lastMotion = null;
+ var lastSelection = vim.lastSelection;
+ operatorArgs.repeat = repeat; // Indent in visual mode needs this.
+ if (vim.visualMode) {
+ curStart = selectionStart;
+ curEnd = selectionEnd;
+ motionArgs.inclusive = true;
+ operatorArgs.shouldMoveCursor = false;
+ }
+ // Swap start and end if motion was backward.
+ if (curEnd && cursorIsBefore(curEnd, curStart)) {
+ var tmp = curStart;
+ curStart = curEnd;
+ curEnd = tmp;
+ inverted = true;
+ } else if (!curEnd) {
+ curEnd = copyCursor(curStart);
+ }
+ if (motionArgs.inclusive && !vim.visualMode) {
+ // Move the selection end one to the right to include the last
+ // character.
+ curEnd.ch++;
+ }
+ if (operatorArgs.selOffset) {
+ // Replaying a visual mode operation
+ curEnd.line = curStart.line + operatorArgs.selOffset.line;
+ if (operatorArgs.selOffset.line) {curEnd.ch = operatorArgs.selOffset.ch; }
+ else { curEnd.ch = curStart.ch + operatorArgs.selOffset.ch; }
+ // In case of blockwise visual
+ if (lastSelection && lastSelection.visualBlock) {
+ var block = lastSelection.visualBlock;
+ var width = block.width;
+ var height = block.height;
+ curEnd = Pos(curStart.line + height, curStart.ch + width);
+ // selectBlock creates a 'proper' rectangular block.
+ // We do not want that in all cases, so we manually set selections.
+ var selections = [];
+ for (var i = curStart.line; i < curEnd.line; i++) {
+ var anchor = Pos(i, curStart.ch);
+ var head = Pos(i, curEnd.ch);
+ var range = {anchor: anchor, head: head};
+ selections.push(range);
+ }
+ cm.setSelections(selections);
+ var blockSelected = true;
+ }
+ } else if (vim.visualMode) {
+ var selOffset = Pos();
+ selOffset.line = curEnd.line - curStart.line;
+ if (selOffset.line) { selOffset.ch = curEnd.ch; }
+ else { selOffset.ch = curEnd.ch - curStart.ch; }
+ operatorArgs.selOffset = selOffset;
+ }
+ var linewise = motionArgs.linewise ||
+ (vim.visualMode && vim.visualLine) ||
+ operatorArgs.linewise;
+ if (linewise) {
+ // Expand selection to entire line.
+ expandSelectionToLine(cm, curStart, curEnd);
+ } else if (motionArgs.forward) {
+ // Clip to trailing newlines only if the motion goes forward.
+ clipToLine(cm, curStart, curEnd);
+ }
+ operatorArgs.registerName = registerName;
+ // Keep track of linewise as it affects how paste and change behave.
+ operatorArgs.linewise = linewise;
+ if (!vim.visualBlock && !blockSelected) {
+ cm.setSelection(curStart, curEnd);
+ }
+ operators[operator](cm, operatorArgs, vim, curStart,
+ curEnd, curOriginal);
+ if (vim.visualMode) {
+ exitVisualMode(cm);
+ }
+ }
+ },
+ recordLastEdit: function(vim, inputState, actionCommand) {
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.isPlaying) { return; }
+ vim.lastEditInputState = inputState;
+ vim.lastEditActionCommand = actionCommand;
+ macroModeState.lastInsertModeChanges.changes = [];
+ macroModeState.lastInsertModeChanges.expectCursorActivityForChange = false;
+ }
+ };
+
+ /**
+ * typedef {Object{line:number,ch:number}} Cursor An object containing the
+ * position of the cursor.
+ */
+ // All of the functions below return Cursor objects.
+ var motions = {
+ moveToTopLine: function(cm, motionArgs) {
+ var line = getUserVisibleLines(cm).top + motionArgs.repeat -1;
+ return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line)));
+ },
+ moveToMiddleLine: function(cm) {
+ var range = getUserVisibleLines(cm);
+ var line = Math.floor((range.top + range.bottom) * 0.5);
+ return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line)));
+ },
+ moveToBottomLine: function(cm, motionArgs) {
+ var line = getUserVisibleLines(cm).bottom - motionArgs.repeat +1;
+ return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line)));
+ },
+ expandToLine: function(cm, motionArgs) {
+ // Expands forward to end of line, and then to next line if repeat is
+ // >1. Does not handle backward motion!
+ var cur = cm.getCursor();
+ return Pos(cur.line + motionArgs.repeat - 1, Infinity);
+ },
+ findNext: function(cm, motionArgs) {
+ var state = getSearchState(cm);
+ var query = state.getQuery();
+ if (!query) {
+ return;
+ }
+ var prev = !motionArgs.forward;
+ // If search is initiated with ? instead of /, negate direction.
+ prev = (state.isReversed()) ? !prev : prev;
+ highlightSearchMatches(cm, query);
+ return findNext(cm, prev/** prev */, query, motionArgs.repeat);
+ },
+ goToMark: function(cm, motionArgs, vim) {
+ var mark = vim.marks[motionArgs.selectedCharacter];
+ if (mark) {
+ var pos = mark.find();
+ return motionArgs.linewise ? { line: pos.line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(pos.line)) } : pos;
+ }
+ return null;
+ },
+ moveToOtherHighlightedEnd: function(cm, motionArgs, vim) {
+ var ranges = cm.listSelections();
+ var curEnd = cm.getCursor('head');
+ var curStart = ranges[0].anchor;
+ var curIndex = cursorEqual(ranges[0].head, curEnd) ? ranges.length-1 : 0;
+ if (motionArgs.sameLine && vim.visualBlock) {
+ curStart = Pos(curEnd.line, ranges[curIndex].anchor.ch);
+ curEnd = Pos(ranges[curIndex].head.line, curEnd.ch);
+ } else {
+ curStart = ranges[curIndex].anchor;
+ }
+ cm.setCursor(curEnd);
+ return ([curEnd, curStart]);
+ },
+ jumpToMark: function(cm, motionArgs, vim) {
+ var best = cm.getCursor();
+ for (var i = 0; i < motionArgs.repeat; i++) {
+ var cursor = best;
+ for (var key in vim.marks) {
+ if (!isLowerCase(key)) {
+ continue;
+ }
+ var mark = vim.marks[key].find();
+ var isWrongDirection = (motionArgs.forward) ?
+ cursorIsBefore(mark, cursor) : cursorIsBefore(cursor, mark);
+
+ if (isWrongDirection) {
+ continue;
+ }
+ if (motionArgs.linewise && (mark.line == cursor.line)) {
+ continue;
+ }
+
+ var equal = cursorEqual(cursor, best);
+ var between = (motionArgs.forward) ?
+ cursorIsBetween(cursor, mark, best) :
+ cursorIsBetween(best, mark, cursor);
+
+ if (equal || between) {
+ best = mark;
+ }
+ }
+ }
+
+ if (motionArgs.linewise) {
+ // Vim places the cursor on the first non-whitespace character of
+ // the line if there is one, else it places the cursor at the end
+ // of the line, regardless of whether a mark was found.
+ best = Pos(best.line, findFirstNonWhiteSpaceCharacter(cm.getLine(best.line)));
+ }
+ return best;
+ },
+ moveByCharacters: function(cm, motionArgs) {
+ var cur = cm.getCursor();
+ var repeat = motionArgs.repeat;
+ var ch = motionArgs.forward ? cur.ch + repeat : cur.ch - repeat;
+ return Pos(cur.line, ch);
+ },
+ moveByLines: function(cm, motionArgs, vim) {
+ var cur = cm.getCursor();
+ var endCh = cur.ch;
+ // Depending what our last motion was, we may want to do different
+ // things. If our last motion was moving vertically, we want to
+ // preserve the HPos from our last horizontal move. If our last motion
+ // was going to the end of a line, moving vertically we should go to
+ // the end of the line, etc.
+ switch (vim.lastMotion) {
+ case this.moveByLines:
+ case this.moveByDisplayLines:
+ case this.moveByScroll:
+ case this.moveToColumn:
+ case this.moveToEol:
+ endCh = vim.lastHPos;
+ break;
+ default:
+ vim.lastHPos = endCh;
+ }
+ var repeat = motionArgs.repeat+(motionArgs.repeatOffset||0);
+ var line = motionArgs.forward ? cur.line + repeat : cur.line - repeat;
+ var first = cm.firstLine();
+ var last = cm.lastLine();
+ // Vim cancels linewise motions that start on an edge and move beyond
+ // that edge. It does not cancel motions that do not start on an edge.
+ if ((line < first && cur.line == first) ||
+ (line > last && cur.line == last)) {
+ return;
+ }
+ if (motionArgs.toFirstChar){
+ endCh=findFirstNonWhiteSpaceCharacter(cm.getLine(line));
+ vim.lastHPos = endCh;
+ }
+ vim.lastHSPos = cm.charCoords(Pos(line, endCh),'div').left;
+ return Pos(line, endCh);
+ },
+ moveByDisplayLines: function(cm, motionArgs, vim) {
+ var cur = cm.getCursor();
+ switch (vim.lastMotion) {
+ case this.moveByDisplayLines:
+ case this.moveByScroll:
+ case this.moveByLines:
+ case this.moveToColumn:
+ case this.moveToEol:
+ break;
+ default:
+ vim.lastHSPos = cm.charCoords(cur,'div').left;
+ }
+ var repeat = motionArgs.repeat;
+ var res=cm.findPosV(cur,(motionArgs.forward ? repeat : -repeat),'line',vim.lastHSPos);
+ if (res.hitSide) {
+ if (motionArgs.forward) {
+ var lastCharCoords = cm.charCoords(res, 'div');
+ var goalCoords = { top: lastCharCoords.top + 8, left: vim.lastHSPos };
+ var res = cm.coordsChar(goalCoords, 'div');
+ } else {
+ var resCoords = cm.charCoords(Pos(cm.firstLine(), 0), 'div');
+ resCoords.left = vim.lastHSPos;
+ res = cm.coordsChar(resCoords, 'div');
+ }
+ }
+ vim.lastHPos = res.ch;
+ return res;
+ },
+ moveByPage: function(cm, motionArgs) {
+ // CodeMirror only exposes functions that move the cursor page down, so
+ // doing this bad hack to move the cursor and move it back. evalInput
+ // will move the cursor to where it should be in the end.
+ var curStart = cm.getCursor();
+ var repeat = motionArgs.repeat;
+ return cm.findPosV(curStart, (motionArgs.forward ? repeat : -repeat), 'page');
+ },
+ moveByParagraph: function(cm, motionArgs) {
+ var line = cm.getCursor().line;
+ var repeat = motionArgs.repeat;
+ var inc = motionArgs.forward ? 1 : -1;
+ for (var i = 0; i < repeat; i++) {
+ if ((!motionArgs.forward && line === cm.firstLine() ) ||
+ (motionArgs.forward && line == cm.lastLine())) {
+ break;
+ }
+ line += inc;
+ while (line !== cm.firstLine() && line != cm.lastLine() && cm.getLine(line)) {
+ line += inc;
+ }
+ }
+ return Pos(line, 0);
+ },
+ moveByScroll: function(cm, motionArgs, vim) {
+ var scrollbox = cm.getScrollInfo();
+ var curEnd = null;
+ var repeat = motionArgs.repeat;
+ if (!repeat) {
+ repeat = scrollbox.clientHeight / (2 * cm.defaultTextHeight());
+ }
+ var orig = cm.charCoords(cm.getCursor(), 'local');
+ motionArgs.repeat = repeat;
+ var curEnd = motions.moveByDisplayLines(cm, motionArgs, vim);
+ if (!curEnd) {
+ return null;
+ }
+ var dest = cm.charCoords(curEnd, 'local');
+ cm.scrollTo(null, scrollbox.top + dest.top - orig.top);
+ return curEnd;
+ },
+ moveByWords: function(cm, motionArgs) {
+ return moveToWord(cm, motionArgs.repeat, !!motionArgs.forward,
+ !!motionArgs.wordEnd, !!motionArgs.bigWord);
+ },
+ moveTillCharacter: function(cm, motionArgs) {
+ var repeat = motionArgs.repeat;
+ var curEnd = moveToCharacter(cm, repeat, motionArgs.forward,
+ motionArgs.selectedCharacter);
+ var increment = motionArgs.forward ? -1 : 1;
+ recordLastCharacterSearch(increment, motionArgs);
+ if (!curEnd) return null;
+ curEnd.ch += increment;
+ return curEnd;
+ },
+ moveToCharacter: function(cm, motionArgs) {
+ var repeat = motionArgs.repeat;
+ recordLastCharacterSearch(0, motionArgs);
+ return moveToCharacter(cm, repeat, motionArgs.forward,
+ motionArgs.selectedCharacter) || cm.getCursor();
+ },
+ moveToSymbol: function(cm, motionArgs) {
+ var repeat = motionArgs.repeat;
+ return findSymbol(cm, repeat, motionArgs.forward,
+ motionArgs.selectedCharacter) || cm.getCursor();
+ },
+ moveToColumn: function(cm, motionArgs, vim) {
+ var repeat = motionArgs.repeat;
+ // repeat is equivalent to which column we want to move to!
+ vim.lastHPos = repeat - 1;
+ vim.lastHSPos = cm.charCoords(cm.getCursor(),'div').left;
+ return moveToColumn(cm, repeat);
+ },
+ moveToEol: function(cm, motionArgs, vim) {
+ var cur = cm.getCursor();
+ vim.lastHPos = Infinity;
+ var retval= Pos(cur.line + motionArgs.repeat - 1, Infinity);
+ var end=cm.clipPos(retval);
+ end.ch--;
+ vim.lastHSPos = cm.charCoords(end,'div').left;
+ return retval;
+ },
+ moveToFirstNonWhiteSpaceCharacter: function(cm) {
+ // Go to the start of the line where the text begins, or the end for
+ // whitespace-only lines
+ var cursor = cm.getCursor();
+ return Pos(cursor.line,
+ findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line)));
+ },
+ moveToMatchedSymbol: function(cm) {
+ var cursor = cm.getCursor();
+ var line = cursor.line;
+ var ch = cursor.ch;
+ var lineText = cm.getLine(line);
+ var symbol;
+ do {
+ symbol = lineText.charAt(ch++);
+ if (symbol && isMatchableSymbol(symbol)) {
+ var style = cm.getTokenTypeAt(Pos(line, ch));
+ if (style !== "string" && style !== "comment") {
+ break;
+ }
+ }
+ } while (symbol);
+ if (symbol) {
+ var matched = cm.findMatchingBracket(Pos(line, ch));
+ return matched.to;
+ } else {
+ return cursor;
+ }
+ },
+ moveToStartOfLine: function(cm) {
+ var cursor = cm.getCursor();
+ return Pos(cursor.line, 0);
+ },
+ moveToLineOrEdgeOfDocument: function(cm, motionArgs) {
+ var lineNum = motionArgs.forward ? cm.lastLine() : cm.firstLine();
+ if (motionArgs.repeatIsExplicit) {
+ lineNum = motionArgs.repeat - cm.getOption('firstLineNumber');
+ }
+ return Pos(lineNum,
+ findFirstNonWhiteSpaceCharacter(cm.getLine(lineNum)));
+ },
+ textObjectManipulation: function(cm, motionArgs) {
+ // TODO: lots of possible exceptions that can be thrown here. Try da(
+ // outside of a () block.
+
+ // TODO: adding <> >< to this map doesn't work, presumably because
+ // they're operators
+ var mirroredPairs = {'(': ')', ')': '(',
+ '{': '}', '}': '{',
+ '[': ']', ']': '['};
+ var selfPaired = {'\'': true, '"': true};
+
+ var character = motionArgs.selectedCharacter;
+ // 'b' refers to '()' block.
+ // 'B' refers to '{}' block.
+ if (character == 'b') {
+ character = '(';
+ } else if (character == 'B') {
+ character = '{';
+ }
+
+ // Inclusive is the difference between a and i
+ // TODO: Instead of using the additional text object map to perform text
+ // object operations, merge the map into the defaultKeyMap and use
+ // motionArgs to define behavior. Define separate entries for 'aw',
+ // 'iw', 'a[', 'i[', etc.
+ var inclusive = !motionArgs.textObjectInner;
+
+ var tmp;
+ if (mirroredPairs[character]) {
+ tmp = selectCompanionObject(cm, character, inclusive);
+ } else if (selfPaired[character]) {
+ tmp = findBeginningAndEnd(cm, character, inclusive);
+ } else if (character === 'W') {
+ tmp = expandWordUnderCursor(cm, inclusive, true /** forward */,
+ true /** bigWord */);
+ } else if (character === 'w') {
+ tmp = expandWordUnderCursor(cm, inclusive, true /** forward */,
+ false /** bigWord */);
+ } else {
+ // No text object defined for this, don't move.
+ return null;
+ }
+
+ if (!cm.state.vim.visualMode) {
+ return [tmp.start, tmp.end];
+ } else {
+ return expandSelection(cm, tmp.start, tmp.end);
+ }
+ },
+
+ repeatLastCharacterSearch: function(cm, motionArgs) {
+ var lastSearch = vimGlobalState.lastChararacterSearch;
+ var repeat = motionArgs.repeat;
+ var forward = motionArgs.forward === lastSearch.forward;
+ var increment = (lastSearch.increment ? 1 : 0) * (forward ? -1 : 1);
+ cm.moveH(-increment, 'char');
+ motionArgs.inclusive = forward ? true : false;
+ var curEnd = moveToCharacter(cm, repeat, forward, lastSearch.selectedCharacter);
+ if (!curEnd) {
+ cm.moveH(increment, 'char');
+ return cm.getCursor();
+ }
+ curEnd.ch += increment;
+ return curEnd;
+ }
+ };
+
+ var operators = {
+ change: function(cm, operatorArgs, vim) {
+ var selections = cm.listSelections();
+ var start = selections[0], end = selections[selections.length-1];
+ var curStart = cursorIsBefore(start.anchor, start.head) ? start.anchor : start.head;
+ var curEnd = cursorIsBefore(end.anchor, end.head) ? end.head : end.anchor;
+ var text = cm.getSelection();
+ var visualBlock = vim.visualBlock;
+ if (vim.lastSelection && !vim.visualMode) {
+ visualBlock = vim.lastSelection.visualBlock ? true : visualBlock;
+ }
+ var lastInsertModeChanges = vimGlobalState.macroModeState.lastInsertModeChanges;
+ lastInsertModeChanges.inVisualBlock = visualBlock;
+ var replacement = new Array(selections.length).join('1').split('1');
+ // save the selectionEnd mark
+ var selectionEnd = vim.marks[">"] && vim.marks[">"].find();
+ if (!selectionEnd) {
+ selectionEnd = cm.getCursor("head");
+ }
+ vimGlobalState.registerController.pushText(
+ operatorArgs.registerName, 'change', text,
+ operatorArgs.linewise);
+ if (operatorArgs.linewise) {
+ // 'C' in visual block extends the block till eol for all lines
+ if (visualBlock){
+ var startLine = curStart.line;
+ while (startLine <= curEnd.line) {
+ var endCh = lineLength(cm, startLine);
+ var head = Pos(startLine, endCh);
+ var anchor = Pos(startLine, curStart.ch);
+ startLine++;
+ cm.replaceRange('', anchor, head);
+ }
+ } else {
+ // Push the next line back down, if there is a next line.
+ replacement = '\n';
+ if (curEnd.line == curStart.line && curEnd.line == cm.lastLine()) {
+ replacement = '';
+ }
+ cm.replaceRange(replacement, curStart, curEnd);
+ cm.indentLine(curStart.line, 'smart');
+ // null ch so setCursor moves to end of line.
+ curStart.ch = null;
+ cm.setCursor(curStart);
+ }
+ } else {
+ // Exclude trailing whitespace if the range is not all whitespace.
+ var text = cm.getRange(curStart, curEnd);
+ if (!isWhiteSpaceString(text)) {
+ var match = (/\s+$/).exec(text);
+ if (match) {
+ curEnd = offsetCursor(curEnd, 0, - match[0].length);
+ }
+ }
+ if (visualBlock) {
+ cm.replaceSelections(replacement);
+ } else {
+ cm.setCursor(curStart);
+ cm.replaceRange('', curStart, curEnd);
+ }
+ }
+ vim.marks['>'] = cm.setBookmark(selectionEnd);
+ actions.enterInsertMode(cm, {}, cm.state.vim);
+ },
+ // delete is a javascript keyword.
+ 'delete': function(cm, operatorArgs, vim) {
+ var selections = cm.listSelections();
+ var start = selections[0], end = selections[selections.length-1];
+ var curStart = cursorIsBefore(start.anchor, start.head) ? start.anchor : start.head;
+ var curEnd = cursorIsBefore(end.anchor, end.head) ? end.head : end.anchor;
+ // Save the '>' mark before cm.replaceRange clears it.
+ var selectionEnd, selectionStart;
+ var blockwise = vim.visualBlock;
+ if (vim.visualMode) {
+ selectionEnd = vim.marks['>'].find();
+ selectionStart = vim.marks['<'].find();
+ } else if (vim.lastSelection) {
+ selectionEnd = vim.lastSelection.curStartMark.find();
+ selectionStart = vim.lastSelection.curEndMark.find();
+ blockwise = vim.lastSelection.visualBlock;
+ }
+ var text = cm.getSelection();
+ vimGlobalState.registerController.pushText(
+ operatorArgs.registerName, 'delete', text,
+ operatorArgs.linewise, blockwise);
+ var replacement = new Array(selections.length).join('1').split('1');
+ // If the ending line is past the last line, inclusive, instead of
+ // including the trailing \n, include the \n before the starting line
+ if (operatorArgs.linewise &&
+ curEnd.line == cm.lastLine() && curStart.line == curEnd.line) {
+ if (curEnd.line == 0) {
+ curStart.ch = 0;
+ }
+ else {
+ var tmp = copyCursor(curEnd);
+ curStart.line--;
+ curStart.ch = lineLength(cm, curStart.line);
+ curEnd = tmp;
+ }
+ cm.replaceRange('', curStart, curEnd);
+ } else {
+ cm.replaceSelections(replacement);
+ }
+ // restore the saved bookmark
+ if (selectionEnd) {
+ var curStartMark = cm.setBookmark(selectionStart);
+ var curEndMark = cm.setBookmark(selectionEnd);
+ if (vim.visualMode) {
+ vim.marks['<'] = curStartMark;
+ vim.marks['>'] = curEndMark;
+ } else {
+ vim.lastSelection.curStartMark = curStartMark;
+ vim.lastSelection.curEndMark = curEndMark;
+ }
+ }
+ if (operatorArgs.linewise) {
+ cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm));
+ } else {
+ cm.setCursor(curStart);
+ }
+ },
+ indent: function(cm, operatorArgs, vim, curStart, curEnd) {
+ var startLine = curStart.line;
+ var endLine = curEnd.line;
+ // In visual mode, n> shifts the selection right n times, instead of
+ // shifting n lines right once.
+ var repeat = (vim.visualMode) ? operatorArgs.repeat : 1;
+ if (operatorArgs.linewise) {
+ // The only way to delete a newline is to delete until the start of
+ // the next line, so in linewise mode evalInput will include the next
+ // line. We don't want this in indent, so we go back a line.
+ endLine--;
+ }
+ for (var i = startLine; i <= endLine; i++) {
+ for (var j = 0; j < repeat; j++) {
+ cm.indentLine(i, operatorArgs.indentRight);
+ }
+ }
+ cm.setCursor(curStart);
+ cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm));
+ },
+ changeCase: function(cm, operatorArgs, _vim, _curStart, _curEnd, _curOriginal) {
+ var selections = cm.getSelections();
+ var ranges = cm.listSelections();
+ var swapped = [];
+ var toLower = operatorArgs.toLower;
+ for (var j = 0; j < selections.length; j++) {
+ var toSwap = selections[j];
+ var text = '';
+ if (toLower === true) {
+ text = toSwap.toLowerCase();
+ } else if (toLower === false) {
+ text = toSwap.toUpperCase();
+ } else {
+ for (var i = 0; i < toSwap.length; i++) {
+ var character = toSwap.charAt(i);
+ text += isUpperCase(character) ? character.toLowerCase() :
+ character.toUpperCase();
+ }
+ }
+ swapped.push(text);
+ }
+ cm.replaceSelections(swapped);
+ var curStart = ranges[0].anchor;
+ var curEnd = ranges[0].head;
+ if (!operatorArgs.shouldMoveCursor) {
+ cm.setCursor(cursorIsBefore(curStart, curEnd) ? curStart : curEnd);
+ }
+ },
+ yank: function(cm, operatorArgs, vim, _curStart, _curEnd, curOriginal) {
+ var text = cm.getSelection();
+ vimGlobalState.registerController.pushText(
+ operatorArgs.registerName, 'yank',
+ text, operatorArgs.linewise, vim.visualBlock);
+ cm.setCursor(curOriginal);
+ }
+ };
+
+ var actions = {
+ jumpListWalk: function(cm, actionArgs, vim) {
+ if (vim.visualMode) {
+ return;
+ }
+ var repeat = actionArgs.repeat;
+ var forward = actionArgs.forward;
+ var jumpList = vimGlobalState.jumpList;
+
+ var mark = jumpList.move(cm, forward ? repeat : -repeat);
+ var markPos = mark ? mark.find() : undefined;
+ markPos = markPos ? markPos : cm.getCursor();
+ cm.setCursor(markPos);
+ },
+ scroll: function(cm, actionArgs, vim) {
+ if (vim.visualMode) {
+ return;
+ }
+ var repeat = actionArgs.repeat || 1;
+ var lineHeight = cm.defaultTextHeight();
+ var top = cm.getScrollInfo().top;
+ var delta = lineHeight * repeat;
+ var newPos = actionArgs.forward ? top + delta : top - delta;
+ var cursor = copyCursor(cm.getCursor());
+ var cursorCoords = cm.charCoords(cursor, 'local');
+ if (actionArgs.forward) {
+ if (newPos > cursorCoords.top) {
+ cursor.line += (newPos - cursorCoords.top) / lineHeight;
+ cursor.line = Math.ceil(cursor.line);
+ cm.setCursor(cursor);
+ cursorCoords = cm.charCoords(cursor, 'local');
+ cm.scrollTo(null, cursorCoords.top);
+ } else {
+ // Cursor stays within bounds. Just reposition the scroll window.
+ cm.scrollTo(null, newPos);
+ }
+ } else {
+ var newBottom = newPos + cm.getScrollInfo().clientHeight;
+ if (newBottom < cursorCoords.bottom) {
+ cursor.line -= (cursorCoords.bottom - newBottom) / lineHeight;
+ cursor.line = Math.floor(cursor.line);
+ cm.setCursor(cursor);
+ cursorCoords = cm.charCoords(cursor, 'local');
+ cm.scrollTo(
+ null, cursorCoords.bottom - cm.getScrollInfo().clientHeight);
+ } else {
+ // Cursor stays within bounds. Just reposition the scroll window.
+ cm.scrollTo(null, newPos);
+ }
+ }
+ },
+ scrollToCursor: function(cm, actionArgs) {
+ var lineNum = cm.getCursor().line;
+ var charCoords = cm.charCoords(Pos(lineNum, 0), 'local');
+ var height = cm.getScrollInfo().clientHeight;
+ var y = charCoords.top;
+ var lineHeight = charCoords.bottom - y;
+ switch (actionArgs.position) {
+ case 'center': y = y - (height / 2) + lineHeight;
+ break;
+ case 'bottom': y = y - height + lineHeight*1.4;
+ break;
+ case 'top': y = y + lineHeight*0.4;
+ break;
+ }
+ cm.scrollTo(null, y);
+ },
+ replayMacro: function(cm, actionArgs, vim) {
+ var registerName = actionArgs.selectedCharacter;
+ var repeat = actionArgs.repeat;
+ var macroModeState = vimGlobalState.macroModeState;
+ if (registerName == '@') {
+ registerName = macroModeState.latestRegister;
+ }
+ while(repeat--){
+ executeMacroRegister(cm, vim, macroModeState, registerName);
+ }
+ },
+ enterMacroRecordMode: function(cm, actionArgs) {
+ var macroModeState = vimGlobalState.macroModeState;
+ var registerName = actionArgs.selectedCharacter;
+ macroModeState.enterMacroRecordMode(cm, registerName);
+ },
+ enterInsertMode: function(cm, actionArgs, vim) {
+ if (cm.getOption('readOnly')) { return; }
+ vim.insertMode = true;
+ vim.insertModeRepeat = actionArgs && actionArgs.repeat || 1;
+ var insertAt = (actionArgs) ? actionArgs.insertAt : null;
+ if (vim.visualMode) {
+ var selections = getSelectedAreaRange(cm, vim);
+ var selectionStart = selections[0];
+ var selectionEnd = selections[1];
+ }
+ if (insertAt == 'eol') {
+ var cursor = cm.getCursor();
+ cursor = Pos(cursor.line, lineLength(cm, cursor.line));
+ cm.setCursor(cursor);
+ } else if (insertAt == 'charAfter') {
+ cm.setCursor(offsetCursor(cm.getCursor(), 0, 1));
+ } else if (insertAt == 'firstNonBlank') {
+ if (vim.visualMode && !vim.visualBlock) {
+ if (selectionEnd.line < selectionStart.line) {
+ cm.setCursor(selectionEnd);
+ } else {
+ selectionStart = Pos(selectionStart.line, 0);
+ cm.setCursor(selectionStart);
+ }
+ cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm));
+ } else if (vim.visualBlock) {
+ selectionEnd = Pos(selectionEnd.line, selectionStart.ch);
+ cm.setCursor(selectionStart);
+ selectBlock(cm, selectionEnd);
+ } else {
+ cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm));
+ }
+ } else if (insertAt == 'endOfSelectedArea') {
+ if (vim.visualBlock) {
+ selectionStart = Pos(selectionStart.line, selectionEnd.ch);
+ cm.setCursor(selectionStart);
+ selectBlock(cm, selectionEnd);
+ } else if (selectionEnd && selectionEnd.line < selectionStart.line) {
+ selectionEnd = Pos(selectionStart.line, 0);
+ cm.setCursor(selectionEnd);
+ }
+ } else if (insertAt == 'inplace') {
+ if (vim.visualMode){
+ return;
+ }
+ }
+ cm.setOption('keyMap', 'vim-insert');
+ cm.setOption('disableInput', false);
+ if (actionArgs && actionArgs.replace) {
+ // Handle Replace-mode as a special case of insert mode.
+ cm.toggleOverwrite(true);
+ cm.setOption('keyMap', 'vim-replace');
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "replace"});
+ } else {
+ cm.setOption('keyMap', 'vim-insert');
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"});
+ }
+ if (!vimGlobalState.macroModeState.isPlaying) {
+ // Only record if not replaying.
+ cm.on('change', onChange);
+ CodeMirror.on(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown);
+ }
+ if (vim.visualMode) {
+ exitVisualMode(cm);
+ }
+ },
+ toggleVisualMode: function(cm, actionArgs, vim) {
+ var repeat = actionArgs.repeat;
+ var curStart = cm.getCursor();
+ var curEnd;
+ var selections = cm.listSelections();
+ // TODO: The repeat should actually select number of characters/lines
+ // equal to the repeat times the size of the previous visual
+ // operation.
+ if (!vim.visualMode) {
+ vim.visualMode = true;
+ vim.visualLine = !!actionArgs.linewise;
+ vim.visualBlock = !!actionArgs.blockwise;
+ if (vim.visualLine) {
+ curStart.ch = 0;
+ curEnd = clipCursorToContent(
+ cm, Pos(curStart.line + repeat - 1, lineLength(cm, curStart.line)),
+ true /** includeLineBreak */);
+ } else {
+ curEnd = clipCursorToContent(
+ cm, Pos(curStart.line, curStart.ch + repeat),
+ true /** includeLineBreak */);
+ }
+ cm.setSelection(curStart, curEnd);
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : ""});
+ } else {
+ curStart = cm.getCursor('anchor');
+ curEnd = cm.getCursor('head');
+ if (vim.visualLine) {
+ if (actionArgs.blockwise) {
+ // This means Ctrl-V pressed in linewise visual
+ vim.visualBlock = true;
+ selectBlock(cm, curEnd);
+ CodeMirror.signal(cm, 'vim-mode-change', {mode: 'visual', subMode: 'blockwise'});
+ } else if (!actionArgs.linewise) {
+ // v pressed in linewise, switch to characterwise visual mode
+ CodeMirror.signal(cm, 'vim-mode-change', {mode: 'visual'});
+ } else {
+ exitVisualMode(cm);
+ }
+ vim.visualLine = false;
+ } else if (vim.visualBlock) {
+ if (actionArgs.linewise) {
+ // Shift-V pressed in blockwise visual mode
+ vim.visualLine = true;
+ curStart = Pos(selections[0].anchor.line, 0);
+ curEnd = Pos(selections[selections.length-1].anchor.line, lineLength(cm, selections[selections.length-1].anchor.line));
+ cm.setSelection(curStart, curEnd);
+ CodeMirror.signal(cm, 'vim-mode-change', {mode: 'visual', subMode: 'linewise'});
+ } else if (!actionArgs.blockwise) {
+ // v pressed in blockwise mode, Switch to characterwise
+ if (curEnd != selections[0].head) {
+ curStart = selections[0].anchor;
+ } else {
+ curStart = selections[selections.length-1].anchor;
+ }
+ cm.setSelection(curStart, curEnd);
+ CodeMirror.signal(cm, 'vim-mode-change', {mode: 'visual'});
+ } else {
+ exitVisualMode(cm);
+ }
+ vim.visualBlock = false;
+ } else if (actionArgs.linewise) {
+ // Shift-V pressed in characterwise visual mode. Switch to linewise
+ // visual mode instead of exiting visual mode.
+ vim.visualLine = true;
+ curStart.ch = cursorIsBefore(curStart, curEnd) ? 0 :
+ lineLength(cm, curStart.line);
+ curEnd.ch = cursorIsBefore(curStart, curEnd) ?
+ lineLength(cm, curEnd.line) : 0;
+ cm.setSelection(curStart, curEnd);
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: "linewise"});
+ } else if (actionArgs.blockwise) {
+ vim.visualBlock = true;
+ selectBlock(cm, curEnd);
+ CodeMirror.signal(cm, 'vim-mode-change', {mode: 'visual', subMode: 'blockwise'});
+ } else {
+ exitVisualMode(cm);
+ }
+ }
+ updateMark(cm, vim, '<', cursorIsBefore(curStart, curEnd) ? curStart
+ : curEnd);
+ updateMark(cm, vim, '>', cursorIsBefore(curStart, curEnd) ? curEnd
+ : curStart);
+ },
+ reselectLastSelection: function(cm, _actionArgs, vim) {
+ var curStart = vim.marks['<'].find();
+ var curEnd = vim.marks['>'].find();
+ var lastSelection = vim.lastSelection;
+ if (lastSelection) {
+ // Set the selections as per last selection
+ var selectionStart = lastSelection.curStartMark.find();
+ var selectionEnd = lastSelection.curEndMark.find();
+ var blockwise = lastSelection.visualBlock;
+ // update last selection
+ updateLastSelection(cm, vim, curStart, curEnd);
+ if (blockwise) {
+ cm.setCursor(selectionStart);
+ selectionStart = selectBlock(cm, selectionEnd);
+ } else {
+ cm.setSelection(selectionStart, selectionEnd);
+ selectionStart = cm.getCursor('anchor');
+ selectionEnd = cm.getCursor('head');
+ }
+ if (vim.visualMode) {
+ updateMark(cm, vim, '<', cursorIsBefore(selectionStart, selectionEnd) ? selectionStart
+ : selectionEnd);
+ updateMark(cm, vim, '>', cursorIsBefore(selectionStart, selectionEnd) ? selectionEnd
+ : selectionStart);
+ }
+ // Last selection is updated now
+ vim.visualMode = true;
+ if (lastSelection.visualLine) {
+ vim.visualLine = true;
+ vim.visualBlock = false;
+ } else if (lastSelection.visualBlock) {
+ vim.visualLine = false;
+ vim.visualBlock = true;
+ } else {
+ vim.visualBlock = vim.visualLine = false;
+ }
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : ""});
+ }
+ },
+ joinLines: function(cm, actionArgs, vim) {
+ var curStart, curEnd;
+ if (vim.visualMode) {
+ curStart = cm.getCursor('anchor');
+ curEnd = cm.getCursor('head');
+ curEnd.ch = lineLength(cm, curEnd.line) - 1;
+ } else {
+ // Repeat is the number of lines to join. Minimum 2 lines.
+ var repeat = Math.max(actionArgs.repeat, 2);
+ curStart = cm.getCursor();
+ curEnd = clipCursorToContent(cm, Pos(curStart.line + repeat - 1,
+ Infinity));
+ }
+ var finalCh = 0;
+ cm.operation(function() {
+ for (var i = curStart.line; i < curEnd.line; i++) {
+ finalCh = lineLength(cm, curStart.line);
+ var tmp = Pos(curStart.line + 1,
+ lineLength(cm, curStart.line + 1));
+ var text = cm.getRange(curStart, tmp);
+ text = text.replace(/\n\s*/g, ' ');
+ cm.replaceRange(text, curStart, tmp);
+ }
+ var curFinalPos = Pos(curStart.line, finalCh);
+ cm.setCursor(curFinalPos);
+ });
+ },
+ newLineAndEnterInsertMode: function(cm, actionArgs, vim) {
+ vim.insertMode = true;
+ var insertAt = copyCursor(cm.getCursor());
+ if (insertAt.line === cm.firstLine() && !actionArgs.after) {
+ // Special case for inserting newline before start of document.
+ cm.replaceRange('\n', Pos(cm.firstLine(), 0));
+ cm.setCursor(cm.firstLine(), 0);
+ } else {
+ insertAt.line = (actionArgs.after) ? insertAt.line :
+ insertAt.line - 1;
+ insertAt.ch = lineLength(cm, insertAt.line);
+ cm.setCursor(insertAt);
+ var newlineFn = CodeMirror.commands.newlineAndIndentContinueComment ||
+ CodeMirror.commands.newlineAndIndent;
+ newlineFn(cm);
+ }
+ this.enterInsertMode(cm, { repeat: actionArgs.repeat }, vim);
+ },
+ paste: function(cm, actionArgs, vim) {
+ var cur = copyCursor(cm.getCursor());
+ var register = vimGlobalState.registerController.getRegister(
+ actionArgs.registerName);
+ var text = register.toString();
+ if (!text) {
+ return;
+ }
+ if (actionArgs.matchIndent) {
+ var tabSize = cm.getOption("tabSize");
+ // length that considers tabs and tabSize
+ var whitespaceLength = function(str) {
+ var tabs = (str.split("\t").length - 1);
+ var spaces = (str.split(" ").length - 1);
+ return tabs * tabSize + spaces * 1;
+ };
+ var currentLine = cm.getLine(cm.getCursor().line);
+ var indent = whitespaceLength(currentLine.match(/^\s*/)[0]);
+ // chomp last newline b/c don't want it to match /^\s*/gm
+ var chompedText = text.replace(/\n$/, '');
+ var wasChomped = text !== chompedText;
+ var firstIndent = whitespaceLength(text.match(/^\s*/)[0]);
+ var text = chompedText.replace(/^\s*/gm, function(wspace) {
+ var newIndent = indent + (whitespaceLength(wspace) - firstIndent);
+ if (newIndent < 0) {
+ return "";
+ }
+ else if (cm.getOption("indentWithTabs")) {
+ var quotient = Math.floor(newIndent / tabSize);
+ return Array(quotient + 1).join('\t');
+ }
+ else {
+ return Array(newIndent + 1).join(' ');
+ }
+ });
+ text += wasChomped ? "\n" : "";
+ }
+ if (actionArgs.repeat > 1) {
+ var text = Array(actionArgs.repeat + 1).join(text);
+ }
+ var linewise = register.linewise;
+ var blockwise = register.blockwise;
+ if (linewise) {
+ if(vim.visualMode) {
+ text = vim.visualLine ? text.slice(0, -1) : '\n' + text.slice(0, text.length - 1) + '\n';
+ } else if (actionArgs.after) {
+ // Move the newline at the end to the start instead, and paste just
+ // before the newline character of the line we are on right now.
+ text = '\n' + text.slice(0, text.length - 1);
+ cur.ch = lineLength(cm, cur.line);
+ } else {
+ cur.ch = 0;
+ }
+ } else {
+ if (blockwise) {
+ text = text.split('\n');
+ for (var i = 0; i < text.length; i++) {
+ text[i] = (text[i] == '') ? ' ' : text[i];
+ }
+ }
+ cur.ch += actionArgs.after ? 1 : 0;
+ }
+ var curPosFinal;
+ var idx;
+ if (vim.visualMode) {
+ // save the pasted text for reselection if the need arises
+ vim.lastPastedText = text;
+ var lastSelectionCurEnd;
+ var selectedArea = getSelectedAreaRange(cm, vim);
+ var selectionStart = selectedArea[0];
+ var selectionEnd = selectedArea[1];
+ var selectedText = cm.getSelection();
+ var selections = cm.listSelections();
+ var emptyStrings = new Array(selections.length).join('1').split('1');
+ // save the curEnd marker before it get cleared due to cm.replaceRange.
+ if (vim.lastSelection) {
+ lastSelectionCurEnd = vim.lastSelection.curEndMark.find();
+ }
+ // push the previously selected text to unnamed register
+ vimGlobalState.registerController.unnamedRegister.setText(selectedText);
+ if (blockwise) {
+ // first delete the selected text
+ cm.replaceSelections(emptyStrings);
+ // Set new selections as per the block length of the yanked text
+ selectionEnd = Pos(selectionStart.line + text.length-1, selectionStart.ch);
+ cm.setCursor(selectionStart);
+ selectBlock(cm, selectionEnd);
+ cm.replaceSelections(text);
+ curPosFinal = selectionStart;
+ } else if (vim.visualBlock) {
+ cm.replaceSelections(emptyStrings);
+ cm.setCursor(selectionStart);
+ cm.replaceRange(text, selectionStart, selectionStart);
+ curPosFinal = selectionStart;
+ } else {
+ cm.replaceRange(text, selectionStart, selectionEnd);
+ curPosFinal = cm.posFromIndex(cm.indexFromPos(selectionStart) + text.length - 1);
+ }
+ // restore the the curEnd marker
+ if(lastSelectionCurEnd) {
+ vim.lastSelection.curEndMark = cm.setBookmark(lastSelectionCurEnd);
+ }
+ if (linewise) {
+ curPosFinal.ch=0;
+ }
+ } else {
+ if (blockwise) {
+ cm.setCursor(cur);
+ for (var i = 0; i < text.length; i++) {
+ var line = cur.line+i;
+ if (line > cm.lastLine()) {
+ cm.replaceRange('\n', Pos(line, 0));
+ }
+ var lastCh = lineLength(cm, line);
+ if (lastCh < cur.ch) {
+ extendLineToColumn(cm, line, cur.ch);
+ }
+ }
+ cm.setCursor(cur);
+ selectBlock(cm, Pos(cur.line + text.length-1, cur.ch));
+ cm.replaceSelections(text);
+ curPosFinal = cur;
+ } else {
+ cm.replaceRange(text, cur);
+ // Now fine tune the cursor to where we want it.
+ if (linewise && actionArgs.after) {
+ curPosFinal = Pos(
+ cur.line + 1,
+ findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line + 1)));
+ } else if (linewise && !actionArgs.after) {
+ curPosFinal = Pos(
+ cur.line,
+ findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line)));
+ } else if (!linewise && actionArgs.after) {
+ idx = cm.indexFromPos(cur);
+ curPosFinal = cm.posFromIndex(idx + text.length - 1);
+ } else {
+ idx = cm.indexFromPos(cur);
+ curPosFinal = cm.posFromIndex(idx + text.length);
+ }
+ }
+ }
+ cm.setCursor(curPosFinal);
+ if (vim.visualMode) {
+ exitVisualMode(cm);
+ }
+ },
+ undo: function(cm, actionArgs) {
+ cm.operation(function() {
+ repeatFn(cm, CodeMirror.commands.undo, actionArgs.repeat)();
+ cm.setCursor(cm.getCursor('anchor'));
+ });
+ },
+ redo: function(cm, actionArgs) {
+ repeatFn(cm, CodeMirror.commands.redo, actionArgs.repeat)();
+ },
+ setRegister: function(_cm, actionArgs, vim) {
+ vim.inputState.registerName = actionArgs.selectedCharacter;
+ },
+ setMark: function(cm, actionArgs, vim) {
+ var markName = actionArgs.selectedCharacter;
+ updateMark(cm, vim, markName, cm.getCursor());
+ },
+ replace: function(cm, actionArgs, vim) {
+ var replaceWith = actionArgs.selectedCharacter;
+ var curStart = cm.getCursor();
+ var replaceTo;
+ var curEnd;
+ var selections = cm.listSelections();
+ if (vim.visualMode) {
+ curStart = cm.getCursor('start');
+ curEnd = cm.getCursor('end');
+ } else {
+ var line = cm.getLine(curStart.line);
+ replaceTo = curStart.ch + actionArgs.repeat;
+ if (replaceTo > line.length) {
+ replaceTo=line.length;
+ }
+ curEnd = Pos(curStart.line, replaceTo);
+ }
+ if (replaceWith=='\n') {
+ if (!vim.visualMode) cm.replaceRange('', curStart, curEnd);
+ // special case, where vim help says to replace by just one line-break
+ (CodeMirror.commands.newlineAndIndentContinueComment || CodeMirror.commands.newlineAndIndent)(cm);
+ } else {
+ var replaceWithStr = cm.getRange(curStart, curEnd);
+ //replace all characters in range by selected, but keep linebreaks
+ replaceWithStr = replaceWithStr.replace(/[^\n]/g, replaceWith);
+ if (vim.visualBlock) {
+ // Tabs are split in visua block before replacing
+ var spaces = new Array(cm.getOption("tabSize")+1).join(' ');
+ replaceWithStr = cm.getSelection();
+ replaceWithStr = replaceWithStr.replace(/\t/g, spaces).replace(/[^\n]/g, replaceWith).split('\n');
+ cm.replaceSelections(replaceWithStr);
+ } else {
+ cm.replaceRange(replaceWithStr, curStart, curEnd);
+ }
+ if (vim.visualMode) {
+ curStart = cursorIsBefore(selections[0].anchor, selections[0].head) ?
+ selections[0].anchor : selections[0].head;
+ cm.setCursor(curStart);
+ exitVisualMode(cm);
+ } else {
+ cm.setCursor(offsetCursor(curEnd, 0, -1));
+ }
+ }
+ },
+ incrementNumberToken: function(cm, actionArgs) {
+ var cur = cm.getCursor();
+ var lineStr = cm.getLine(cur.line);
+ var re = /-?\d+/g;
+ var match;
+ var start;
+ var end;
+ var numberStr;
+ var token;
+ while ((match = re.exec(lineStr)) !== null) {
+ token = match[0];
+ start = match.index;
+ end = start + token.length;
+ if (cur.ch < end)break;
+ }
+ if (!actionArgs.backtrack && (end <= cur.ch))return;
+ if (token) {
+ var increment = actionArgs.increase ? 1 : -1;
+ var number = parseInt(token) + (increment * actionArgs.repeat);
+ var from = Pos(cur.line, start);
+ var to = Pos(cur.line, end);
+ numberStr = number.toString();
+ cm.replaceRange(numberStr, from, to);
+ } else {
+ return;
+ }
+ cm.setCursor(Pos(cur.line, start + numberStr.length - 1));
+ },
+ repeatLastEdit: function(cm, actionArgs, vim) {
+ var lastEditInputState = vim.lastEditInputState;
+ if (!lastEditInputState) { return; }
+ var repeat = actionArgs.repeat;
+ if (repeat && actionArgs.repeatIsExplicit) {
+ vim.lastEditInputState.repeatOverride = repeat;
+ } else {
+ repeat = vim.lastEditInputState.repeatOverride || repeat;
+ }
+ repeatLastEdit(cm, vim, repeat, false /** repeatForInsert */);
+ },
+ exitInsertMode: exitInsertMode
+ };
+
+ /*
+ * Below are miscellaneous utility functions used by vim.js
+ */
+
+ /**
+ * Clips cursor to ensure that line is within the buffer's range
+ * If includeLineBreak is true, then allow cur.ch == lineLength.
+ */
+ function clipCursorToContent(cm, cur, includeLineBreak) {
+ var line = Math.min(Math.max(cm.firstLine(), cur.line), cm.lastLine() );
+ var maxCh = lineLength(cm, line) - 1;
+ maxCh = (includeLineBreak) ? maxCh + 1 : maxCh;
+ var ch = Math.min(Math.max(0, cur.ch), maxCh);
+ return Pos(line, ch);
+ }
+ function copyArgs(args) {
+ var ret = {};
+ for (var prop in args) {
+ if (args.hasOwnProperty(prop)) {
+ ret[prop] = args[prop];
+ }
+ }
+ return ret;
+ }
+ function offsetCursor(cur, offsetLine, offsetCh) {
+ return Pos(cur.line + offsetLine, cur.ch + offsetCh);
+ }
+ function commandMatches(keys, keyMap, context, inputState) {
+ // Partial matches are not applied. They inform the key handler
+ // that the current key sequence is a subsequence of a valid key
+ // sequence, so that the key buffer is not cleared.
+ var match, partial = [], full = [];
+ for (var i = 0; i < keyMap.length; i++) {
+ var command = keyMap[i];
+ if (context == 'insert' && command.context != 'insert' ||
+ command.context && command.context != context ||
+ inputState.operator && command.type == 'action' ||
+ !(match = commandMatch(keys, command.keys))) { continue; }
+ if (match == 'partial') { partial.push(command); }
+ if (match == 'full') { full.push(command); }
+ }
+ return {
+ partial: partial.length && partial,
+ full: full.length && full
+ };
+ }
+ function commandMatch(pressed, mapped) {
+ if (mapped.slice(-11) == '') {
+ // Last character matches anything.
+ var prefixLen = mapped.length - 11;
+ var pressedPrefix = pressed.slice(0, prefixLen);
+ var mappedPrefix = mapped.slice(0, prefixLen);
+ return pressedPrefix == mappedPrefix && pressed.length > prefixLen ? 'full' :
+ mappedPrefix.indexOf(pressedPrefix) == 0 ? 'partial' : false;
+ } else {
+ return pressed == mapped ? 'full' :
+ mapped.indexOf(pressed) == 0 ? 'partial' : false;
+ }
+ }
+ function lastChar(keys) {
+ var match = /^.*(<[\w\-]+>)$/.exec(keys);
+ var selectedCharacter = match ? match[1] : keys.slice(-1);
+ if (selectedCharacter.length > 1){
+ switch(selectedCharacter){
+ case '':
+ selectedCharacter='\n';
+ break;
+ case '':
+ selectedCharacter=' ';
+ break;
+ default:
+ break;
+ }
+ }
+ return selectedCharacter;
+ }
+ function repeatFn(cm, fn, repeat) {
+ return function() {
+ for (var i = 0; i < repeat; i++) {
+ fn(cm);
+ }
+ };
+ }
+ function copyCursor(cur) {
+ return Pos(cur.line, cur.ch);
+ }
+ function cursorEqual(cur1, cur2) {
+ return cur1.ch == cur2.ch && cur1.line == cur2.line;
+ }
+ function cursorIsBefore(cur1, cur2) {
+ if (cur1.line < cur2.line) {
+ return true;
+ }
+ if (cur1.line == cur2.line && cur1.ch < cur2.ch) {
+ return true;
+ }
+ return false;
+ }
+ function cursorMin(cur1, cur2) {
+ return cursorIsBefore(cur1, cur2) ? cur1 : cur2;
+ }
+ function cursorMax(cur1, cur2) {
+ return cursorIsBefore(cur1, cur2) ? cur2 : cur1;
+ }
+ function cursorIsBetween(cur1, cur2, cur3) {
+ // returns true if cur2 is between cur1 and cur3.
+ var cur1before2 = cursorIsBefore(cur1, cur2);
+ var cur2before3 = cursorIsBefore(cur2, cur3);
+ return cur1before2 && cur2before3;
+ }
+ function lineLength(cm, lineNum) {
+ return cm.getLine(lineNum).length;
+ }
+ function reverse(s){
+ return s.split('').reverse().join('');
+ }
+ function trim(s) {
+ if (s.trim) {
+ return s.trim();
+ }
+ return s.replace(/^\s+|\s+$/g, '');
+ }
+ function escapeRegex(s) {
+ return s.replace(/([.?*+$\[\]\/\\(){}|\-])/g, '\\$1');
+ }
+ function extendLineToColumn(cm, lineNum, column) {
+ var endCh = lineLength(cm, lineNum);
+ var spaces = new Array(column-endCh+1).join(' ');
+ cm.setCursor(Pos(lineNum, endCh));
+ cm.replaceRange(spaces, cm.getCursor());
+ }
+ // This functions selects a rectangular block
+ // of text with selectionEnd as any of its corner
+ // Height of block:
+ // Difference in selectionEnd.line and first/last selection.line
+ // Width of the block:
+ // Distance between selectionEnd.ch and any(first considered here) selection.ch
+ function selectBlock(cm, selectionEnd) {
+ var selections = [], ranges = cm.listSelections();
+ var head = copyCursor(cm.clipPos(selectionEnd));
+ var isClipped = !cursorEqual(selectionEnd, head);
+ var curHead = cm.getCursor('head');
+ var primIndex = getIndex(ranges, curHead);
+ var wasClipped = cursorEqual(ranges[primIndex].head, ranges[primIndex].anchor);
+ var max = ranges.length - 1;
+ var index = max - primIndex > primIndex ? max : 0;
+ var base = ranges[index].anchor;
+
+ var firstLine = Math.min(base.line, head.line);
+ var lastLine = Math.max(base.line, head.line);
+ var baseCh = base.ch, headCh = head.ch;
+
+ var dir = ranges[index].head.ch - baseCh;
+ var newDir = headCh - baseCh;
+ if (dir > 0 && newDir <= 0) {
+ baseCh++;
+ if (!isClipped) { headCh--; }
+ } else if (dir < 0 && newDir >= 0) {
+ baseCh--;
+ if (!wasClipped) { headCh++; }
+ } else if (dir < 0 && newDir == -1) {
+ baseCh--;
+ headCh++;
+ }
+ for (var line = firstLine; line <= lastLine; line++) {
+ var range = {anchor: new Pos(line, baseCh), head: new Pos(line, headCh)};
+ selections.push(range);
+ }
+ primIndex = head.line == lastLine ? selections.length - 1 : 0;
+ cm.setSelections(selections, primIndex);
+ selectionEnd.ch = headCh;
+ base.ch = baseCh;
+ return base;
+ }
+ // getIndex returns the index of the cursor in the selections.
+ function getIndex(ranges, cursor, end) {
+ for (var i = 0; i < ranges.length; i++) {
+ var atAnchor = end != 'head' && cursorEqual(ranges[i].anchor, cursor);
+ var atHead = end != 'anchor' && cursorEqual(ranges[i].head, cursor);
+ if (atAnchor || atHead) {
+ return i;
+ }
+ }
+ return -1;
+ }
+ function getSelectedAreaRange(cm, vim) {
+ var lastSelection = vim.lastSelection;
+ var getCurrentSelectedAreaRange = function() {
+ var selections = cm.listSelections();
+ var start = selections[0];
+ var end = selections[selections.length-1];
+ var selectionStart = cursorIsBefore(start.anchor, start.head) ? start.anchor : start.head;
+ var selectionEnd = cursorIsBefore(end.anchor, end.head) ? end.head : end.anchor;
+ return [selectionStart, selectionEnd];
+ };
+ var getLastSelectedAreaRange = function() {
+ var selectionStart = cm.getCursor();
+ var selectionEnd = cm.getCursor();
+ var block = lastSelection.visualBlock;
+ if (block) {
+ var width = block.width;
+ var height = block.height;
+ selectionEnd = Pos(selectionStart.line + height, selectionStart.ch + width);
+ var selections = [];
+ // selectBlock creates a 'proper' rectangular block.
+ // We do not want that in all cases, so we manually set selections.
+ for (var i = selectionStart.line; i < selectionEnd.line; i++) {
+ var anchor = Pos(i, selectionStart.ch);
+ var head = Pos(i, selectionEnd.ch);
+ var range = {anchor: anchor, head: head};
+ selections.push(range);
+ }
+ cm.setSelections(selections);
+ } else {
+ var start = lastSelection.curStartMark.find();
+ var end = lastSelection.curEndMark.find();
+ var line = end.line - start.line;
+ var ch = end.ch - start.ch;
+ selectionEnd = {line: selectionEnd.line + line, ch: line ? selectionEnd.ch : ch + selectionEnd.ch};
+ if (lastSelection.visualLine) {
+ selectionStart = Pos(selectionStart.line, 0);
+ selectionEnd = Pos(selectionEnd.line, lineLength(cm, selectionEnd.line));
+ }
+ cm.setSelection(selectionStart, selectionEnd);
+ }
+ return [selectionStart, selectionEnd];
+ };
+ if (!vim.visualMode) {
+ // In case of replaying the action.
+ return getLastSelectedAreaRange();
+ } else {
+ return getCurrentSelectedAreaRange();
+ }
+ }
+ function updateLastSelection(cm, vim, selectionStart, selectionEnd) {
+ if (!selectionStart || !selectionEnd) {
+ selectionStart = vim.marks['<'].find() || cm.getCursor('anchor');
+ selectionEnd = vim.marks['>'].find() || cm.getCursor('head');
+ }
+ // To accommodate the effect of lastPastedText in the last selection
+ if (vim.lastPastedText) {
+ selectionEnd = cm.posFromIndex(cm.indexFromPos(selectionStart) + vim.lastPastedText.length);
+ vim.lastPastedText = null;
+ }
+ var ranges = cm.listSelections();
+ // This check ensures to set the cursor
+ // position where we left off in previous selection
+ var swap = getIndex(ranges, selectionStart, 'head') > -1;
+ if (vim.visualBlock) {
+ var height = Math.abs(selectionStart.line - selectionEnd.line)+1;
+ var width = Math.abs(selectionStart.ch - selectionEnd.ch);
+ var block = {height: height, width: width};
+ }
+ // can't use selection state here because yank has already reset its cursor
+ // Also, Bookmarks make the visual selections robust to edit operations
+ vim.lastSelection = {'curStartMark': cm.setBookmark(swap ? selectionEnd : selectionStart),
+ 'curEndMark': cm.setBookmark(swap ? selectionStart : selectionEnd),
+ 'visualMode': vim.visualMode,
+ 'visualLine': vim.visualLine,
+ 'visualBlock': block};
+ }
+ function expandSelection(cm, start, end) {
+ var head = cm.getCursor('head');
+ var anchor = cm.getCursor('anchor');
+ var tmp;
+ if (cursorIsBefore(end, start)) {
+ tmp = end;
+ end = start;
+ start = tmp;
+ }
+ if (cursorIsBefore(head, anchor)) {
+ head = cursorMin(start, head);
+ anchor = cursorMax(anchor, end);
+ } else {
+ anchor = cursorMin(start, anchor);
+ head = cursorMax(head, end);
+ }
+ return [anchor, head];
+ }
+ function getHead(cm) {
+ var cur = cm.getCursor('head');
+ if (cm.getSelection().length == 1) {
+ // Small corner case when only 1 character is selected. The "real"
+ // head is the left of head and anchor.
+ cur = cursorMin(cur, cm.getCursor('anchor'));
+ }
+ return cur;
+ }
+
+ function exitVisualMode(cm) {
+ var vim = cm.state.vim;
+ var selectionStart = cm.getCursor('anchor');
+ var selectionEnd = cm.getCursor('head');
+ // hack to place the cursor at the right place
+ // in case of visual block
+ if (vim.visualBlock && (cursorIsBefore(selectionStart, selectionEnd))) {
+ selectionEnd.ch--;
+ }
+ updateLastSelection(cm, vim);
+ vim.visualMode = false;
+ vim.visualLine = false;
+ vim.visualBlock = false;
+ if (!cursorEqual(selectionStart, selectionEnd)) {
+ cm.setCursor(clipCursorToContent(cm, selectionEnd));
+ }
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
+ if (vim.fakeCursor) {
+ vim.fakeCursor.clear();
+ }
+ }
+
+ // Remove any trailing newlines from the selection. For
+ // example, with the caret at the start of the last word on the line,
+ // 'dw' should word, but not the newline, while 'w' should advance the
+ // caret to the first character of the next line.
+ function clipToLine(cm, curStart, curEnd) {
+ var selection = cm.getRange(curStart, curEnd);
+ // Only clip if the selection ends with trailing newline + whitespace
+ if (/\n\s*$/.test(selection)) {
+ var lines = selection.split('\n');
+ // We know this is all whitepsace.
+ lines.pop();
+
+ // Cases:
+ // 1. Last word is an empty line - do not clip the trailing '\n'
+ // 2. Last word is not an empty line - clip the trailing '\n'
+ var line;
+ // Find the line containing the last word, and clip all whitespace up
+ // to it.
+ for (var line = lines.pop(); lines.length > 0 && line && isWhiteSpaceString(line); line = lines.pop()) {
+ curEnd.line--;
+ curEnd.ch = 0;
+ }
+ // If the last word is not an empty line, clip an additional newline
+ if (line) {
+ curEnd.line--;
+ curEnd.ch = lineLength(cm, curEnd.line);
+ } else {
+ curEnd.ch = 0;
+ }
+ }
+ }
+
+ // Expand the selection to line ends.
+ function expandSelectionToLine(_cm, curStart, curEnd) {
+ curStart.ch = 0;
+ curEnd.ch = 0;
+ curEnd.line++;
+ }
+
+ function findFirstNonWhiteSpaceCharacter(text) {
+ if (!text) {
+ return 0;
+ }
+ var firstNonWS = text.search(/\S/);
+ return firstNonWS == -1 ? text.length : firstNonWS;
+ }
+
+ function expandWordUnderCursor(cm, inclusive, _forward, bigWord, noSymbol) {
+ var cur = getHead(cm);
+ var line = cm.getLine(cur.line);
+ var idx = cur.ch;
+
+ // Seek to first word or non-whitespace character, depending on if
+ // noSymbol is true.
+ var textAfterIdx = line.substring(idx);
+ var firstMatchedChar;
+ if (noSymbol) {
+ firstMatchedChar = textAfterIdx.search(/\w/);
+ } else {
+ firstMatchedChar = textAfterIdx.search(/\S/);
+ }
+ if (firstMatchedChar == -1) {
+ return null;
+ }
+ idx += firstMatchedChar;
+ textAfterIdx = line.substring(idx);
+ var textBeforeIdx = line.substring(0, idx);
+
+ var matchRegex;
+ // Greedy matchers for the "word" we are trying to expand.
+ if (bigWord) {
+ matchRegex = /^\S+/;
+ } else {
+ if ((/\w/).test(line.charAt(idx))) {
+ matchRegex = /^\w+/;
+ } else {
+ matchRegex = /^[^\w\s]+/;
+ }
+ }
+
+ var wordAfterRegex = matchRegex.exec(textAfterIdx);
+ var wordStart = idx;
+ var wordEnd = idx + wordAfterRegex[0].length;
+ // TODO: Find a better way to do this. It will be slow on very long lines.
+ var revTextBeforeIdx = reverse(textBeforeIdx);
+ var wordBeforeRegex = matchRegex.exec(revTextBeforeIdx);
+ if (wordBeforeRegex) {
+ wordStart -= wordBeforeRegex[0].length;
+ }
+
+ if (inclusive) {
+ // If present, trim all whitespace after word.
+ // Otherwise, trim all whitespace before word.
+ var textAfterWordEnd = line.substring(wordEnd);
+ var whitespacesAfterWord = textAfterWordEnd.match(/^\s*/)[0].length;
+ if (whitespacesAfterWord > 0) {
+ wordEnd += whitespacesAfterWord;
+ } else {
+ var revTrim = revTextBeforeIdx.length - wordStart;
+ var textBeforeWordStart = revTextBeforeIdx.substring(revTrim);
+ var whitespacesBeforeWord = textBeforeWordStart.match(/^\s*/)[0].length;
+ wordStart -= whitespacesBeforeWord;
+ }
+ }
+
+ return { start: Pos(cur.line, wordStart),
+ end: Pos(cur.line, wordEnd) };
+ }
+
+ function recordJumpPosition(cm, oldCur, newCur) {
+ if (!cursorEqual(oldCur, newCur)) {
+ vimGlobalState.jumpList.add(cm, oldCur, newCur);
+ }
+ }
+
+ function recordLastCharacterSearch(increment, args) {
+ vimGlobalState.lastChararacterSearch.increment = increment;
+ vimGlobalState.lastChararacterSearch.forward = args.forward;
+ vimGlobalState.lastChararacterSearch.selectedCharacter = args.selectedCharacter;
+ }
+
+ var symbolToMode = {
+ '(': 'bracket', ')': 'bracket', '{': 'bracket', '}': 'bracket',
+ '[': 'section', ']': 'section',
+ '*': 'comment', '/': 'comment',
+ 'm': 'method', 'M': 'method',
+ '#': 'preprocess'
+ };
+ var findSymbolModes = {
+ bracket: {
+ isComplete: function(state) {
+ if (state.nextCh === state.symb) {
+ state.depth++;
+ if (state.depth >= 1)return true;
+ } else if (state.nextCh === state.reverseSymb) {
+ state.depth--;
+ }
+ return false;
+ }
+ },
+ section: {
+ init: function(state) {
+ state.curMoveThrough = true;
+ state.symb = (state.forward ? ']' : '[') === state.symb ? '{' : '}';
+ },
+ isComplete: function(state) {
+ return state.index === 0 && state.nextCh === state.symb;
+ }
+ },
+ comment: {
+ isComplete: function(state) {
+ var found = state.lastCh === '*' && state.nextCh === '/';
+ state.lastCh = state.nextCh;
+ return found;
+ }
+ },
+ // TODO: The original Vim implementation only operates on level 1 and 2.
+ // The current implementation doesn't check for code block level and
+ // therefore it operates on any levels.
+ method: {
+ init: function(state) {
+ state.symb = (state.symb === 'm' ? '{' : '}');
+ state.reverseSymb = state.symb === '{' ? '}' : '{';
+ },
+ isComplete: function(state) {
+ if (state.nextCh === state.symb)return true;
+ return false;
+ }
+ },
+ preprocess: {
+ init: function(state) {
+ state.index = 0;
+ },
+ isComplete: function(state) {
+ if (state.nextCh === '#') {
+ var token = state.lineText.match(/#(\w+)/)[1];
+ if (token === 'endif') {
+ if (state.forward && state.depth === 0) {
+ return true;
+ }
+ state.depth++;
+ } else if (token === 'if') {
+ if (!state.forward && state.depth === 0) {
+ return true;
+ }
+ state.depth--;
+ }
+ if (token === 'else' && state.depth === 0)return true;
+ }
+ return false;
+ }
+ }
+ };
+ function findSymbol(cm, repeat, forward, symb) {
+ var cur = copyCursor(cm.getCursor());
+ var increment = forward ? 1 : -1;
+ var endLine = forward ? cm.lineCount() : -1;
+ var curCh = cur.ch;
+ var line = cur.line;
+ var lineText = cm.getLine(line);
+ var state = {
+ lineText: lineText,
+ nextCh: lineText.charAt(curCh),
+ lastCh: null,
+ index: curCh,
+ symb: symb,
+ reverseSymb: (forward ? { ')': '(', '}': '{' } : { '(': ')', '{': '}' })[symb],
+ forward: forward,
+ depth: 0,
+ curMoveThrough: false
+ };
+ var mode = symbolToMode[symb];
+ if (!mode)return cur;
+ var init = findSymbolModes[mode].init;
+ var isComplete = findSymbolModes[mode].isComplete;
+ if (init) { init(state); }
+ while (line !== endLine && repeat) {
+ state.index += increment;
+ state.nextCh = state.lineText.charAt(state.index);
+ if (!state.nextCh) {
+ line += increment;
+ state.lineText = cm.getLine(line) || '';
+ if (increment > 0) {
+ state.index = 0;
+ } else {
+ var lineLen = state.lineText.length;
+ state.index = (lineLen > 0) ? (lineLen-1) : 0;
+ }
+ state.nextCh = state.lineText.charAt(state.index);
+ }
+ if (isComplete(state)) {
+ cur.line = line;
+ cur.ch = state.index;
+ repeat--;
+ }
+ }
+ if (state.nextCh || state.curMoveThrough) {
+ return Pos(line, state.index);
+ }
+ return cur;
+ }
+
+ /*
+ * Returns the boundaries of the next word. If the cursor in the middle of
+ * the word, then returns the boundaries of the current word, starting at
+ * the cursor. If the cursor is at the start/end of a word, and we are going
+ * forward/backward, respectively, find the boundaries of the next word.
+ *
+ * @param {CodeMirror} cm CodeMirror object.
+ * @param {Cursor} cur The cursor position.
+ * @param {boolean} forward True to search forward. False to search
+ * backward.
+ * @param {boolean} bigWord True if punctuation count as part of the word.
+ * False if only [a-zA-Z0-9] characters count as part of the word.
+ * @param {boolean} emptyLineIsWord True if empty lines should be treated
+ * as words.
+ * @return {Object{from:number, to:number, line: number}} The boundaries of
+ * the word, or null if there are no more words.
+ */
+ function findWord(cm, cur, forward, bigWord, emptyLineIsWord) {
+ var lineNum = cur.line;
+ var pos = cur.ch;
+ var line = cm.getLine(lineNum);
+ var dir = forward ? 1 : -1;
+ var regexps = bigWord ? bigWordRegexp : wordRegexp;
+
+ if (emptyLineIsWord && line == '') {
+ lineNum += dir;
+ line = cm.getLine(lineNum);
+ if (!isLine(cm, lineNum)) {
+ return null;
+ }
+ pos = (forward) ? 0 : line.length;
+ }
+
+ while (true) {
+ if (emptyLineIsWord && line == '') {
+ return { from: 0, to: 0, line: lineNum };
+ }
+ var stop = (dir > 0) ? line.length : -1;
+ var wordStart = stop, wordEnd = stop;
+ // Find bounds of next word.
+ while (pos != stop) {
+ var foundWord = false;
+ for (var i = 0; i < regexps.length && !foundWord; ++i) {
+ if (regexps[i].test(line.charAt(pos))) {
+ wordStart = pos;
+ // Advance to end of word.
+ while (pos != stop && regexps[i].test(line.charAt(pos))) {
+ pos += dir;
+ }
+ wordEnd = pos;
+ foundWord = wordStart != wordEnd;
+ if (wordStart == cur.ch && lineNum == cur.line &&
+ wordEnd == wordStart + dir) {
+ // We started at the end of a word. Find the next one.
+ continue;
+ } else {
+ return {
+ from: Math.min(wordStart, wordEnd + 1),
+ to: Math.max(wordStart, wordEnd),
+ line: lineNum };
+ }
+ }
+ }
+ if (!foundWord) {
+ pos += dir;
+ }
+ }
+ // Advance to next/prev line.
+ lineNum += dir;
+ if (!isLine(cm, lineNum)) {
+ return null;
+ }
+ line = cm.getLine(lineNum);
+ pos = (dir > 0) ? 0 : line.length;
+ }
+ // Should never get here.
+ throw new Error('The impossible happened.');
+ }
+
+ /**
+ * @param {CodeMirror} cm CodeMirror object.
+ * @param {int} repeat Number of words to move past.
+ * @param {boolean} forward True to search forward. False to search
+ * backward.
+ * @param {boolean} wordEnd True to move to end of word. False to move to
+ * beginning of word.
+ * @param {boolean} bigWord True if punctuation count as part of the word.
+ * False if only alphabet characters count as part of the word.
+ * @return {Cursor} The position the cursor should move to.
+ */
+ function moveToWord(cm, repeat, forward, wordEnd, bigWord) {
+ var cur = cm.getCursor();
+ var curStart = copyCursor(cur);
+ var words = [];
+ if (forward && !wordEnd || !forward && wordEnd) {
+ repeat++;
+ }
+ // For 'e', empty lines are not considered words, go figure.
+ var emptyLineIsWord = !(forward && wordEnd);
+ for (var i = 0; i < repeat; i++) {
+ var word = findWord(cm, cur, forward, bigWord, emptyLineIsWord);
+ if (!word) {
+ var eodCh = lineLength(cm, cm.lastLine());
+ words.push(forward
+ ? {line: cm.lastLine(), from: eodCh, to: eodCh}
+ : {line: 0, from: 0, to: 0});
+ break;
+ }
+ words.push(word);
+ cur = Pos(word.line, forward ? (word.to - 1) : word.from);
+ }
+ var shortCircuit = words.length != repeat;
+ var firstWord = words[0];
+ var lastWord = words.pop();
+ if (forward && !wordEnd) {
+ // w
+ if (!shortCircuit && (firstWord.from != curStart.ch || firstWord.line != curStart.line)) {
+ // We did not start in the middle of a word. Discard the extra word at the end.
+ lastWord = words.pop();
+ }
+ return Pos(lastWord.line, lastWord.from);
+ } else if (forward && wordEnd) {
+ return Pos(lastWord.line, lastWord.to - 1);
+ } else if (!forward && wordEnd) {
+ // ge
+ if (!shortCircuit && (firstWord.to != curStart.ch || firstWord.line != curStart.line)) {
+ // We did not start in the middle of a word. Discard the extra word at the end.
+ lastWord = words.pop();
+ }
+ return Pos(lastWord.line, lastWord.to);
+ } else {
+ // b
+ return Pos(lastWord.line, lastWord.from);
+ }
+ }
+
+ function moveToCharacter(cm, repeat, forward, character) {
+ var cur = cm.getCursor();
+ var start = cur.ch;
+ var idx;
+ for (var i = 0; i < repeat; i ++) {
+ var line = cm.getLine(cur.line);
+ idx = charIdxInLine(start, line, character, forward, true);
+ if (idx == -1) {
+ return null;
+ }
+ start = idx;
+ }
+ return Pos(cm.getCursor().line, idx);
+ }
+
+ function moveToColumn(cm, repeat) {
+ // repeat is always >= 1, so repeat - 1 always corresponds
+ // to the column we want to go to.
+ var line = cm.getCursor().line;
+ return clipCursorToContent(cm, Pos(line, repeat - 1));
+ }
+
+ function updateMark(cm, vim, markName, pos) {
+ if (!inArray(markName, validMarks)) {
+ return;
+ }
+ if (vim.marks[markName]) {
+ vim.marks[markName].clear();
+ }
+ vim.marks[markName] = cm.setBookmark(pos);
+ }
+
+ function charIdxInLine(start, line, character, forward, includeChar) {
+ // Search for char in line.
+ // motion_options: {forward, includeChar}
+ // If includeChar = true, include it too.
+ // If forward = true, search forward, else search backwards.
+ // If char is not found on this line, do nothing
+ var idx;
+ if (forward) {
+ idx = line.indexOf(character, start + 1);
+ if (idx != -1 && !includeChar) {
+ idx -= 1;
+ }
+ } else {
+ idx = line.lastIndexOf(character, start - 1);
+ if (idx != -1 && !includeChar) {
+ idx += 1;
+ }
+ }
+ return idx;
+ }
+
+ // TODO: perhaps this finagling of start and end positions belonds
+ // in codmirror/replaceRange?
+ function selectCompanionObject(cm, symb, inclusive) {
+ var cur = getHead(cm), start, end;
+
+ var bracketRegexp = ({
+ '(': /[()]/, ')': /[()]/,
+ '[': /[[\]]/, ']': /[[\]]/,
+ '{': /[{}]/, '}': /[{}]/})[symb];
+ var openSym = ({
+ '(': '(', ')': '(',
+ '[': '[', ']': '[',
+ '{': '{', '}': '{'})[symb];
+ var curChar = cm.getLine(cur.line).charAt(cur.ch);
+ // Due to the behavior of scanForBracket, we need to add an offset if the
+ // cursor is on a matching open bracket.
+ var offset = curChar === openSym ? 1 : 0;
+
+ start = cm.scanForBracket(Pos(cur.line, cur.ch + offset), -1, null, {'bracketRegex': bracketRegexp});
+ end = cm.scanForBracket(Pos(cur.line, cur.ch + offset), 1, null, {'bracketRegex': bracketRegexp});
+
+ if (!start || !end) {
+ return { start: cur, end: cur };
+ }
+
+ start = start.pos;
+ end = end.pos;
+
+ if ((start.line == end.line && start.ch > end.ch)
+ || (start.line > end.line)) {
+ var tmp = start;
+ start = end;
+ end = tmp;
+ }
+
+ if (inclusive) {
+ end.ch += 1;
+ } else {
+ start.ch += 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 = copyCursor(getHead(cm));
+ var line = cm.getLine(cur.line);
+ var chars = line.split('');
+ var start, end, i, len;
+ 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;
+ // Why is this line even here???
+ // 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) {
+ start = cur.ch + 1; // assign start to ahead of the cursor
+ } else {
+ // go backwards to find the start
+ for (i = cur.ch; i > -1 && !start; i--) {
+ if (chars[i] == symb) {
+ start = i + 1;
+ }
+ }
+ }
+
+ // look forwards for the end symbol
+ if (start && !end) {
+ for (i = start, len = chars.length; i < len && !end; i++) {
+ if (chars[i] == symb) {
+ end = i;
+ }
+ }
+ }
+
+ // nothing found
+ if (!start || !end) {
+ return { start: cur, end: cur };
+ }
+
+ // include the symbols
+ if (inclusive) {
+ --start; ++end;
+ }
+
+ return {
+ start: Pos(cur.line, start),
+ end: Pos(cur.line, end)
+ };
+ }
+
+ // Search functions
+ defineOption('pcre', true, 'boolean');
+ function SearchState() {}
+ SearchState.prototype = {
+ getQuery: function() {
+ return vimGlobalState.query;
+ },
+ setQuery: function(query) {
+ vimGlobalState.query = query;
+ },
+ getOverlay: function() {
+ return this.searchOverlay;
+ },
+ setOverlay: function(overlay) {
+ this.searchOverlay = overlay;
+ },
+ isReversed: function() {
+ return vimGlobalState.isReversed;
+ },
+ setReversed: function(reversed) {
+ vimGlobalState.isReversed = reversed;
+ }
+ };
+ function getSearchState(cm) {
+ var vim = cm.state.vim;
+ return vim.searchState_ || (vim.searchState_ = new SearchState());
+ }
+ function dialog(cm, template, shortText, onClose, options) {
+ if (cm.openDialog) {
+ cm.openDialog(template, onClose, { bottom: true, value: options.value,
+ onKeyDown: options.onKeyDown, onKeyUp: options.onKeyUp });
+ }
+ else {
+ onClose(prompt(shortText, ''));
+ }
+ }
+ function splitBySlash(argString) {
+ var slashes = findUnescapedSlashes(argString) || [];
+ if (!slashes.length) return [];
+ var tokens = [];
+ // in case of strings like foo/bar
+ if (slashes[0] !== 0) return;
+ for (var i = 0; i < slashes.length; i++) {
+ if (typeof slashes[i] == 'number')
+ tokens.push(argString.substring(slashes[i] + 1, slashes[i+1]));
+ }
+ return tokens;
+ }
+
+ function findUnescapedSlashes(str) {
+ var escapeNextChar = false;
+ var slashes = [];
+ for (var i = 0; i < str.length; i++) {
+ var c = str.charAt(i);
+ if (!escapeNextChar && c == '/') {
+ slashes.push(i);
+ }
+ escapeNextChar = !escapeNextChar && (c == '\\');
+ }
+ return slashes;
+ }
+
+ // Translates a search string from ex (vim) syntax into javascript form.
+ function translateRegex(str) {
+ // When these match, add a '\' if unescaped or remove one if escaped.
+ var specials = '|(){';
+ // Remove, but never add, a '\' for these.
+ var unescape = '}';
+ var escapeNextChar = false;
+ var out = [];
+ for (var i = -1; i < str.length; i++) {
+ var c = str.charAt(i) || '';
+ var n = str.charAt(i+1) || '';
+ var specialComesNext = (n && specials.indexOf(n) != -1);
+ if (escapeNextChar) {
+ if (c !== '\\' || !specialComesNext) {
+ out.push(c);
+ }
+ escapeNextChar = false;
+ } else {
+ if (c === '\\') {
+ escapeNextChar = true;
+ // Treat the unescape list as special for removing, but not adding '\'.
+ if (n && unescape.indexOf(n) != -1) {
+ specialComesNext = true;
+ }
+ // Not passing this test means removing a '\'.
+ if (!specialComesNext || n === '\\') {
+ out.push(c);
+ }
+ } else {
+ out.push(c);
+ if (specialComesNext && n !== '\\') {
+ out.push('\\');
+ }
+ }
+ }
+ }
+ return out.join('');
+ }
+
+ // Translates the replace part of a search and replace from ex (vim) syntax into
+ // javascript form. Similar to translateRegex, but additionally fixes back references
+ // (translates '\[0..9]' to '$[0..9]') and follows different rules for escaping '$'.
+ function translateRegexReplace(str) {
+ var escapeNextChar = false;
+ var out = [];
+ for (var i = -1; i < str.length; i++) {
+ var c = str.charAt(i) || '';
+ var n = str.charAt(i+1) || '';
+ if (escapeNextChar) {
+ // At any point in the loop, escapeNextChar is true if the previous
+ // character was a '\' and was not escaped.
+ out.push(c);
+ escapeNextChar = false;
+ } else {
+ if (c === '\\') {
+ escapeNextChar = true;
+ if ((isNumber(n) || n === '$')) {
+ out.push('$');
+ } else if (n !== '/' && n !== '\\') {
+ out.push('\\');
+ }
+ } else {
+ if (c === '$') {
+ out.push('$');
+ }
+ out.push(c);
+ if (n === '/') {
+ out.push('\\');
+ }
+ }
+ }
+ }
+ return out.join('');
+ }
+
+ // Unescape \ and / in the replace part, for PCRE mode.
+ function unescapeRegexReplace(str) {
+ var stream = new CodeMirror.StringStream(str);
+ var output = [];
+ while (!stream.eol()) {
+ // Search for \.
+ while (stream.peek() && stream.peek() != '\\') {
+ output.push(stream.next());
+ }
+ if (stream.match('\\/', true)) {
+ // \/ => /
+ output.push('/');
+ } else if (stream.match('\\\\', true)) {
+ // \\ => \
+ output.push('\\');
+ } else {
+ // Don't change anything
+ output.push(stream.next());
+ }
+ }
+ return output.join('');
+ }
+
+ /**
+ * Extract the regular expression from the query and return a Regexp object.
+ * Returns null if the query is blank.
+ * If ignoreCase is passed in, the Regexp object will have the 'i' flag set.
+ * If smartCase is passed in, and the query contains upper case letters,
+ * then ignoreCase is overridden, and the 'i' flag will not be set.
+ * If the query contains the /i in the flag part of the regular expression,
+ * then both ignoreCase and smartCase are ignored, and 'i' will be passed
+ * through to the Regex object.
+ */
+ function parseQuery(query, ignoreCase, smartCase) {
+ // First update the last search register
+ var lastSearchRegister = vimGlobalState.registerController.getRegister('/');
+ lastSearchRegister.setText(query);
+ // Check if the query is already a regex.
+ if (query instanceof RegExp) { return query; }
+ // First try to extract regex + flags from the input. If no flags found,
+ // extract just the regex. IE does not accept flags directly defined in
+ // the regex string in the form /regex/flags
+ var slashes = findUnescapedSlashes(query);
+ var regexPart;
+ var forceIgnoreCase;
+ if (!slashes.length) {
+ // Query looks like 'regexp'
+ regexPart = query;
+ } else {
+ // Query looks like 'regexp/...'
+ regexPart = query.substring(0, slashes[0]);
+ var flagsPart = query.substring(slashes[0]);
+ forceIgnoreCase = (flagsPart.indexOf('i') != -1);
+ }
+ if (!regexPart) {
+ return null;
+ }
+ if (!getOption('pcre')) {
+ regexPart = translateRegex(regexPart);
+ }
+ if (smartCase) {
+ ignoreCase = (/^[^A-Z]*$/).test(regexPart);
+ }
+ var regexp = new RegExp(regexPart,
+ (ignoreCase || forceIgnoreCase) ? 'i' : undefined);
+ return regexp;
+ }
+ function showConfirm(cm, text) {
+ if (cm.openNotification) {
+ cm.openNotification('' + text + '',
+ {bottom: true, duration: 5000});
+ } else {
+ alert(text);
+ }
+ }
+ function makePrompt(prefix, desc) {
+ var raw = '';
+ if (prefix) {
+ raw += '' + prefix + '';
+ }
+ raw += ' ' +
+ '';
+ if (desc) {
+ raw += '';
+ raw += desc;
+ raw += '';
+ }
+ return raw;
+ }
+ var searchPromptDesc = '(Javascript regexp)';
+ function showPrompt(cm, options) {
+ var shortText = (options.prefix || '') + ' ' + (options.desc || '');
+ var prompt = makePrompt(options.prefix, options.desc);
+ dialog(cm, prompt, shortText, options.onClose, options);
+ }
+ function regexEqual(r1, r2) {
+ if (r1 instanceof RegExp && r2 instanceof RegExp) {
+ var props = ['global', 'multiline', 'ignoreCase', 'source'];
+ for (var i = 0; i < props.length; i++) {
+ var prop = props[i];
+ if (r1[prop] !== r2[prop]) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+ // Returns true if the query is valid.
+ function updateSearchQuery(cm, rawQuery, ignoreCase, smartCase) {
+ if (!rawQuery) {
+ return;
+ }
+ var state = getSearchState(cm);
+ var query = parseQuery(rawQuery, !!ignoreCase, !!smartCase);
+ if (!query) {
+ return;
+ }
+ highlightSearchMatches(cm, query);
+ if (regexEqual(query, state.getQuery())) {
+ return query;
+ }
+ state.setQuery(query);
+ return query;
+ }
+ function searchOverlay(query) {
+ if (query.source.charAt(0) == '^') {
+ var matchSol = true;
+ }
+ return {
+ token: function(stream) {
+ if (matchSol && !stream.sol()) {
+ stream.skipToEnd();
+ return;
+ }
+ var match = stream.match(query, false);
+ if (match) {
+ if (match[0].length == 0) {
+ // Matched empty string, skip to next.
+ stream.next();
+ return 'searching';
+ }
+ if (!stream.sol()) {
+ // Backtrack 1 to match \b
+ stream.backUp(1);
+ if (!query.exec(stream.next() + match[0])) {
+ stream.next();
+ return null;
+ }
+ }
+ stream.match(query);
+ return 'searching';
+ }
+ while (!stream.eol()) {
+ stream.next();
+ if (stream.match(query, false)) break;
+ }
+ },
+ query: query
+ };
+ }
+ function highlightSearchMatches(cm, query) {
+ var overlay = getSearchState(cm).getOverlay();
+ if (!overlay || query != overlay.query) {
+ if (overlay) {
+ cm.removeOverlay(overlay);
+ }
+ overlay = searchOverlay(query);
+ cm.addOverlay(overlay);
+ getSearchState(cm).setOverlay(overlay);
+ }
+ }
+ function findNext(cm, prev, query, repeat) {
+ if (repeat === undefined) { repeat = 1; }
+ return cm.operation(function() {
+ var pos = cm.getCursor();
+ var cursor = cm.getSearchCursor(query, pos);
+ for (var i = 0; i < repeat; i++) {
+ var found = cursor.find(prev);
+ if (i == 0 && found && cursorEqual(cursor.from(), pos)) { found = cursor.find(prev); }
+ if (!found) {
+ // SearchCursor may have returned null because it hit EOF, wrap
+ // around and try again.
+ cursor = cm.getSearchCursor(query,
+ (prev) ? Pos(cm.lastLine()) : Pos(cm.firstLine(), 0) );
+ if (!cursor.find(prev)) {
+ return;
+ }
+ }
+ }
+ return cursor.from();
+ });
+ }
+ function clearSearchHighlight(cm) {
+ cm.removeOverlay(getSearchState(cm).getOverlay());
+ getSearchState(cm).setOverlay(null);
+ }
+ /**
+ * Check if pos is in the specified range, INCLUSIVE.
+ * Range can be specified with 1 or 2 arguments.
+ * If the first range argument is an array, treat it as an array of line
+ * numbers. Match pos against any of the lines.
+ * If the first range argument is a number,
+ * if there is only 1 range argument, check if pos has the same line
+ * number
+ * if there are 2 range arguments, then check if pos is in between the two
+ * range arguments.
+ */
+ function isInRange(pos, start, end) {
+ if (typeof pos != 'number') {
+ // Assume it is a cursor position. Get the line number.
+ pos = pos.line;
+ }
+ if (start instanceof Array) {
+ return inArray(pos, start);
+ } else {
+ if (end) {
+ return (pos >= start && pos <= end);
+ } else {
+ return pos == start;
+ }
+ }
+ }
+ function getUserVisibleLines(cm) {
+ var scrollInfo = cm.getScrollInfo();
+ var occludeToleranceTop = 6;
+ var occludeToleranceBottom = 10;
+ var from = cm.coordsChar({left:0, top: occludeToleranceTop + scrollInfo.top}, 'local');
+ var bottomY = scrollInfo.clientHeight - occludeToleranceBottom + scrollInfo.top;
+ var to = cm.coordsChar({left:0, top: bottomY}, 'local');
+ return {top: from.line, bottom: to.line};
+ }
+
+ // Ex command handling
+ // Care must be taken when adding to the default Ex command map. For any
+ // pair of commands that have a shared prefix, at least one of their
+ // shortNames must not match the prefix of the other command.
+ var defaultExCommandMap = [
+ { name: 'map' },
+ { name: 'imap', shortName: 'im' },
+ { name: 'nmap', shortName: 'nm' },
+ { name: 'vmap', shortName: 'vm' },
+ { name: 'unmap' },
+ { name: 'write', shortName: 'w' },
+ { name: 'undo', shortName: 'u' },
+ { name: 'redo', shortName: 'red' },
+ { name: 'set', shortName: 'set' },
+ { name: 'sort', shortName: 'sor' },
+ { name: 'substitute', shortName: 's', possiblyAsync: true },
+ { name: 'nohlsearch', shortName: 'noh' },
+ { name: 'delmarks', shortName: 'delm' },
+ { name: 'registers', shortName: 'reg', excludeFromCommandHistory: true },
+ { name: 'global', shortName: 'g' }
+ ];
+ var ExCommandDispatcher = function() {
+ this.buildCommandMap_();
+ };
+ ExCommandDispatcher.prototype = {
+ processCommand: function(cm, input, opt_params) {
+ var vim = cm.state.vim;
+ var commandHistoryRegister = vimGlobalState.registerController.getRegister(':');
+ var previousCommand = commandHistoryRegister.toString();
+ if (vim.visualMode) {
+ exitVisualMode(cm);
+ }
+ var inputStream = new CodeMirror.StringStream(input);
+ // update ": with the latest command whether valid or invalid
+ commandHistoryRegister.setText(input);
+ var params = opt_params || {};
+ params.input = input;
+ try {
+ this.parseInput_(cm, inputStream, params);
+ } catch(e) {
+ showConfirm(cm, e);
+ throw e;
+ }
+ var command;
+ var commandName;
+ if (!params.commandName) {
+ // If only a line range is defined, move to the line.
+ if (params.line !== undefined) {
+ commandName = 'move';
+ }
+ } else {
+ command = this.matchCommand_(params.commandName);
+ if (command) {
+ commandName = command.name;
+ if (command.excludeFromCommandHistory) {
+ commandHistoryRegister.setText(previousCommand);
+ }
+ this.parseCommandArgs_(inputStream, params, command);
+ if (command.type == 'exToKey') {
+ // Handle Ex to Key mapping.
+ for (var i = 0; i < command.toKeys.length; i++) {
+ CodeMirror.Vim.handleKey(cm, command.toKeys[i], 'mapping');
+ }
+ return;
+ } else if (command.type == 'exToEx') {
+ // Handle Ex to Ex mapping.
+ this.processCommand(cm, command.toInput);
+ return;
+ }
+ }
+ }
+ if (!commandName) {
+ showConfirm(cm, 'Not an editor command ":' + input + '"');
+ return;
+ }
+ try {
+ exCommands[commandName](cm, params);
+ // Possibly asynchronous commands (e.g. substitute, which might have a
+ // user confirmation), are responsible for calling the callback when
+ // done. All others have it taken care of for them here.
+ if ((!command || !command.possiblyAsync) && params.callback) {
+ params.callback();
+ }
+ } catch(e) {
+ showConfirm(cm, e);
+ throw e;
+ }
+ },
+ parseInput_: function(cm, inputStream, result) {
+ inputStream.eatWhile(':');
+ // Parse range.
+ if (inputStream.eat('%')) {
+ result.line = cm.firstLine();
+ result.lineEnd = cm.lastLine();
+ } else {
+ result.line = this.parseLineSpec_(cm, inputStream);
+ if (result.line !== undefined && inputStream.eat(',')) {
+ result.lineEnd = this.parseLineSpec_(cm, inputStream);
+ }
+ }
+
+ // Parse command name.
+ var commandMatch = inputStream.match(/^(\w+)/);
+ if (commandMatch) {
+ result.commandName = commandMatch[1];
+ } else {
+ result.commandName = inputStream.match(/.*/)[0];
+ }
+
+ return result;
+ },
+ parseLineSpec_: function(cm, inputStream) {
+ var numberMatch = inputStream.match(/^(\d+)/);
+ if (numberMatch) {
+ return parseInt(numberMatch[1], 10) - 1;
+ }
+ switch (inputStream.next()) {
+ case '.':
+ return cm.getCursor().line;
+ case '$':
+ return cm.lastLine();
+ case '\'':
+ var mark = cm.state.vim.marks[inputStream.next()];
+ if (mark && mark.find()) {
+ return mark.find().line;
+ }
+ throw new Error('Mark not set');
+ default:
+ inputStream.backUp(1);
+ return undefined;
+ }
+ },
+ parseCommandArgs_: function(inputStream, params, command) {
+ if (inputStream.eol()) {
+ return;
+ }
+ params.argString = inputStream.match(/.*/)[0];
+ // Parse command-line arguments
+ var delim = command.argDelimiter || /\s+/;
+ var args = trim(params.argString).split(delim);
+ if (args.length && args[0]) {
+ params.args = args;
+ }
+ },
+ matchCommand_: function(commandName) {
+ // Return the command in the command map that matches the shortest
+ // prefix of the passed in command name. The match is guaranteed to be
+ // unambiguous if the defaultExCommandMap's shortNames are set up
+ // correctly. (see @code{defaultExCommandMap}).
+ for (var i = commandName.length; i > 0; i--) {
+ var prefix = commandName.substring(0, i);
+ if (this.commandMap_[prefix]) {
+ var command = this.commandMap_[prefix];
+ if (command.name.indexOf(commandName) === 0) {
+ return command;
+ }
+ }
+ }
+ return null;
+ },
+ buildCommandMap_: function() {
+ this.commandMap_ = {};
+ for (var i = 0; i < defaultExCommandMap.length; i++) {
+ var command = defaultExCommandMap[i];
+ var key = command.shortName || command.name;
+ this.commandMap_[key] = command;
+ }
+ },
+ map: function(lhs, rhs, ctx) {
+ if (lhs != ':' && lhs.charAt(0) == ':') {
+ if (ctx) { throw Error('Mode not supported for ex mappings'); }
+ var commandName = lhs.substring(1);
+ if (rhs != ':' && rhs.charAt(0) == ':') {
+ // Ex to Ex mapping
+ this.commandMap_[commandName] = {
+ name: commandName,
+ type: 'exToEx',
+ toInput: rhs.substring(1),
+ user: true
+ };
+ } else {
+ // Ex to key mapping
+ this.commandMap_[commandName] = {
+ name: commandName,
+ type: 'exToKey',
+ toKeys: rhs,
+ user: true
+ };
+ }
+ } else {
+ if (rhs != ':' && rhs.charAt(0) == ':') {
+ // Key to Ex mapping.
+ var mapping = {
+ keys: lhs,
+ type: 'keyToEx',
+ exArgs: { input: rhs.substring(1) },
+ user: true};
+ if (ctx) { mapping.context = ctx; }
+ defaultKeymap.unshift(mapping);
+ } else {
+ // Key to key mapping
+ var mapping = {
+ keys: lhs,
+ type: 'keyToKey',
+ toKeys: rhs,
+ user: true
+ };
+ if (ctx) { mapping.context = ctx; }
+ defaultKeymap.unshift(mapping);
+ }
+ }
+ },
+ unmap: function(lhs, ctx) {
+ if (lhs != ':' && lhs.charAt(0) == ':') {
+ // Ex to Ex or Ex to key mapping
+ if (ctx) { throw Error('Mode not supported for ex mappings'); }
+ var commandName = lhs.substring(1);
+ if (this.commandMap_[commandName] && this.commandMap_[commandName].user) {
+ delete this.commandMap_[commandName];
+ return;
+ }
+ } else {
+ // Key to Ex or key to key mapping
+ var keys = lhs;
+ for (var i = 0; i < defaultKeymap.length; i++) {
+ if (keys == defaultKeymap[i].keys
+ && defaultKeymap[i].context === ctx
+ && defaultKeymap[i].user) {
+ defaultKeymap.splice(i, 1);
+ return;
+ }
+ }
+ }
+ throw Error('No such mapping.');
+ }
+ };
+
+ var exCommands = {
+ map: function(cm, params, ctx) {
+ var mapArgs = params.args;
+ if (!mapArgs || mapArgs.length < 2) {
+ if (cm) {
+ showConfirm(cm, 'Invalid mapping: ' + params.input);
+ }
+ return;
+ }
+ exCommandDispatcher.map(mapArgs[0], mapArgs[1], ctx);
+ },
+ imap: function(cm, params) { this.map(cm, params, 'insert'); },
+ nmap: function(cm, params) { this.map(cm, params, 'normal'); },
+ vmap: function(cm, params) { this.map(cm, params, 'visual'); },
+ unmap: function(cm, params, ctx) {
+ var mapArgs = params.args;
+ if (!mapArgs || mapArgs.length < 1) {
+ if (cm) {
+ showConfirm(cm, 'No such mapping: ' + params.input);
+ }
+ return;
+ }
+ exCommandDispatcher.unmap(mapArgs[0], ctx);
+ },
+ move: function(cm, params) {
+ commandDispatcher.processCommand(cm, cm.state.vim, {
+ type: 'motion',
+ motion: 'moveToLineOrEdgeOfDocument',
+ motionArgs: { forward: false, explicitRepeat: true,
+ linewise: true },
+ repeatOverride: params.line+1});
+ },
+ set: function(cm, params) {
+ var setArgs = params.args;
+ if (!setArgs || setArgs.length < 1) {
+ if (cm) {
+ showConfirm(cm, 'Invalid mapping: ' + params.input);
+ }
+ return;
+ }
+ var expr = setArgs[0].split('=');
+ var optionName = expr[0];
+ var value = expr[1];
+ var forceGet = false;
+
+ if (optionName.charAt(optionName.length - 1) == '?') {
+ // If post-fixed with ?, then the set is actually a get.
+ if (value) { throw Error('Trailing characters: ' + params.argString); }
+ optionName = optionName.substring(0, optionName.length - 1);
+ forceGet = true;
+ }
+ if (value === undefined && optionName.substring(0, 2) == 'no') {
+ // To set boolean options to false, the option name is prefixed with
+ // 'no'.
+ optionName = optionName.substring(2);
+ value = false;
+ }
+ var optionIsBoolean = options[optionName] && options[optionName].type == 'boolean';
+ if (optionIsBoolean && value == undefined) {
+ // Calling set with a boolean option sets it to true.
+ value = true;
+ }
+ if (!optionIsBoolean && !value || forceGet) {
+ var oldValue = getOption(optionName);
+ // If no value is provided, then we assume this is a get.
+ if (oldValue === true || oldValue === false) {
+ showConfirm(cm, ' ' + (oldValue ? '' : 'no') + optionName);
+ } else {
+ showConfirm(cm, ' ' + optionName + '=' + oldValue);
+ }
+ } else {
+ setOption(optionName, value);
+ }
+ },
+ registers: function(cm,params) {
+ var regArgs = params.args;
+ var registers = vimGlobalState.registerController.registers;
+ var regInfo = '----------Registers----------
';
+ if (!regArgs) {
+ for (var registerName in registers) {
+ var text = registers[registerName].toString();
+ if (text.length) {
+ regInfo += '"' + registerName + ' ' + text + '
';
+ }
+ }
+ } else {
+ var registerName;
+ regArgs = regArgs.join('');
+ for (var i = 0; i < regArgs.length; i++) {
+ registerName = regArgs.charAt(i);
+ if (!vimGlobalState.registerController.isValidRegister(registerName)) {
+ continue;
+ }
+ var register = registers[registerName] || new Register();
+ regInfo += '"' + registerName + ' ' + register.toString() + '
';
+ }
+ }
+ showConfirm(cm, regInfo);
+ },
+ sort: function(cm, params) {
+ var reverse, ignoreCase, unique, number;
+ function parseArgs() {
+ if (params.argString) {
+ var args = new CodeMirror.StringStream(params.argString);
+ if (args.eat('!')) { reverse = true; }
+ if (args.eol()) { return; }
+ if (!args.eatSpace()) { return 'Invalid arguments'; }
+ var opts = args.match(/[a-z]+/);
+ if (opts) {
+ opts = opts[0];
+ ignoreCase = opts.indexOf('i') != -1;
+ unique = opts.indexOf('u') != -1;
+ var decimal = opts.indexOf('d') != -1 && 1;
+ var hex = opts.indexOf('x') != -1 && 1;
+ var octal = opts.indexOf('o') != -1 && 1;
+ if (decimal + hex + octal > 1) { return 'Invalid arguments'; }
+ number = decimal && 'decimal' || hex && 'hex' || octal && 'octal';
+ }
+ if (args.eatSpace() && args.match(/\/.*\//)) { 'patterns not supported'; }
+ }
+ }
+ var err = parseArgs();
+ if (err) {
+ showConfirm(cm, err + ': ' + params.argString);
+ return;
+ }
+ var lineStart = params.line || cm.firstLine();
+ var lineEnd = params.lineEnd || params.line || cm.lastLine();
+ if (lineStart == lineEnd) { return; }
+ var curStart = Pos(lineStart, 0);
+ var curEnd = Pos(lineEnd, lineLength(cm, lineEnd));
+ var text = cm.getRange(curStart, curEnd).split('\n');
+ var numberRegex = (number == 'decimal') ? /(-?)([\d]+)/ :
+ (number == 'hex') ? /(-?)(?:0x)?([0-9a-f]+)/i :
+ (number == 'octal') ? /([0-7]+)/ : null;
+ var radix = (number == 'decimal') ? 10 : (number == 'hex') ? 16 : (number == 'octal') ? 8 : null;
+ var numPart = [], textPart = [];
+ if (number) {
+ for (var i = 0; i < text.length; i++) {
+ if (numberRegex.exec(text[i])) {
+ numPart.push(text[i]);
+ } else {
+ textPart.push(text[i]);
+ }
+ }
+ } else {
+ textPart = text;
+ }
+ function compareFn(a, b) {
+ if (reverse) { var tmp; tmp = a; a = b; b = tmp; }
+ if (ignoreCase) { a = a.toLowerCase(); b = b.toLowerCase(); }
+ var anum = number && numberRegex.exec(a);
+ var bnum = number && numberRegex.exec(b);
+ if (!anum) { return a < b ? -1 : 1; }
+ anum = parseInt((anum[1] + anum[2]).toLowerCase(), radix);
+ bnum = parseInt((bnum[1] + bnum[2]).toLowerCase(), radix);
+ return anum - bnum;
+ }
+ numPart.sort(compareFn);
+ textPart.sort(compareFn);
+ text = (!reverse) ? textPart.concat(numPart) : numPart.concat(textPart);
+ if (unique) { // Remove duplicate lines
+ var textOld = text;
+ var lastLine;
+ text = [];
+ for (var i = 0; i < textOld.length; i++) {
+ if (textOld[i] != lastLine) {
+ text.push(textOld[i]);
+ }
+ lastLine = textOld[i];
+ }
+ }
+ cm.replaceRange(text.join('\n'), curStart, curEnd);
+ },
+ global: function(cm, params) {
+ // a global command is of the form
+ // :[range]g/pattern/[cmd]
+ // argString holds the string /pattern/[cmd]
+ var argString = params.argString;
+ if (!argString) {
+ showConfirm(cm, 'Regular Expression missing from global');
+ return;
+ }
+ // range is specified here
+ var lineStart = (params.line !== undefined) ? params.line : cm.firstLine();
+ var lineEnd = params.lineEnd || params.line || cm.lastLine();
+ // get the tokens from argString
+ var tokens = splitBySlash(argString);
+ var regexPart = argString, cmd;
+ if (tokens.length) {
+ regexPart = tokens[0];
+ cmd = tokens.slice(1, tokens.length).join('/');
+ }
+ if (regexPart) {
+ // If regex part is empty, then use the previous query. Otherwise
+ // use the regex part as the new query.
+ try {
+ updateSearchQuery(cm, regexPart, true /** ignoreCase */,
+ true /** smartCase */);
+ } catch (e) {
+ showConfirm(cm, 'Invalid regex: ' + regexPart);
+ return;
+ }
+ }
+ // now that we have the regexPart, search for regex matches in the
+ // specified range of lines
+ var query = getSearchState(cm).getQuery();
+ var matchedLines = [], content = '';
+ for (var i = lineStart; i <= lineEnd; i++) {
+ var matched = query.test(cm.getLine(i));
+ if (matched) {
+ matchedLines.push(i+1);
+ content+= cm.getLine(i) + '
';
+ }
+ }
+ // if there is no [cmd], just display the list of matched lines
+ if (!cmd) {
+ showConfirm(cm, content);
+ return;
+ }
+ var index = 0;
+ var nextCommand = function() {
+ if (index < matchedLines.length) {
+ var command = matchedLines[index] + cmd;
+ exCommandDispatcher.processCommand(cm, command, {
+ callback: nextCommand
+ });
+ }
+ index++;
+ };
+ nextCommand();
+ },
+ substitute: function(cm, params) {
+ if (!cm.getSearchCursor) {
+ throw new Error('Search feature not available. Requires searchcursor.js or ' +
+ 'any other getSearchCursor implementation.');
+ }
+ var argString = params.argString;
+ var tokens = argString ? splitBySlash(argString) : [];
+ var regexPart, replacePart = '', trailing, flagsPart, count;
+ var confirm = false; // Whether to confirm each replace.
+ var global = false; // True to replace all instances on a line, false to replace only 1.
+ if (tokens.length) {
+ regexPart = tokens[0];
+ replacePart = tokens[1];
+ if (replacePart !== undefined) {
+ if (getOption('pcre')) {
+ replacePart = unescapeRegexReplace(replacePart);
+ } else {
+ replacePart = translateRegexReplace(replacePart);
+ }
+ vimGlobalState.lastSubstituteReplacePart = replacePart;
+ }
+ trailing = tokens[2] ? tokens[2].split(' ') : [];
+ } else {
+ // either the argString is empty or its of the form ' hello/world'
+ // actually splitBySlash returns a list of tokens
+ // only if the string starts with a '/'
+ if (argString && argString.length) {
+ showConfirm(cm, 'Substitutions should be of the form ' +
+ ':s/pattern/replace/');
+ return;
+ }
+ }
+ // After the 3rd slash, we can have flags followed by a space followed
+ // by count.
+ if (trailing) {
+ flagsPart = trailing[0];
+ count = parseInt(trailing[1]);
+ if (flagsPart) {
+ if (flagsPart.indexOf('c') != -1) {
+ confirm = true;
+ flagsPart.replace('c', '');
+ }
+ if (flagsPart.indexOf('g') != -1) {
+ global = true;
+ flagsPart.replace('g', '');
+ }
+ regexPart = regexPart + '/' + flagsPart;
+ }
+ }
+ if (regexPart) {
+ // If regex part is empty, then use the previous query. Otherwise use
+ // the regex part as the new query.
+ try {
+ updateSearchQuery(cm, regexPart, true /** ignoreCase */,
+ true /** smartCase */);
+ } catch (e) {
+ showConfirm(cm, 'Invalid regex: ' + regexPart);
+ return;
+ }
+ }
+ replacePart = replacePart || vimGlobalState.lastSubstituteReplacePart;
+ if (replacePart === undefined) {
+ showConfirm(cm, 'No previous substitute regular expression');
+ return;
+ }
+ var state = getSearchState(cm);
+ var query = state.getQuery();
+ var lineStart = (params.line !== undefined) ? params.line : cm.getCursor().line;
+ var lineEnd = params.lineEnd || lineStart;
+ if (count) {
+ lineStart = lineEnd;
+ lineEnd = lineStart + count - 1;
+ }
+ var startPos = clipCursorToContent(cm, Pos(lineStart, 0));
+ var cursor = cm.getSearchCursor(query, startPos);
+ doReplace(cm, confirm, global, lineStart, lineEnd, cursor, query, replacePart, params.callback);
+ },
+ redo: CodeMirror.commands.redo,
+ undo: CodeMirror.commands.undo,
+ write: function(cm) {
+ if (CodeMirror.commands.save) {
+ // If a save command is defined, call it.
+ CodeMirror.commands.save(cm);
+ } else {
+ // Saves to text area if no save command is defined.
+ cm.save();
+ }
+ },
+ nohlsearch: function(cm) {
+ clearSearchHighlight(cm);
+ },
+ delmarks: function(cm, params) {
+ if (!params.argString || !trim(params.argString)) {
+ showConfirm(cm, 'Argument required');
+ return;
+ }
+
+ var state = cm.state.vim;
+ var stream = new CodeMirror.StringStream(trim(params.argString));
+ while (!stream.eol()) {
+ stream.eatSpace();
+
+ // Record the streams position at the beginning of the loop for use
+ // in error messages.
+ var count = stream.pos;
+
+ if (!stream.match(/[a-zA-Z]/, false)) {
+ showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));
+ return;
+ }
+
+ var sym = stream.next();
+ // Check if this symbol is part of a range
+ if (stream.match('-', true)) {
+ // This symbol is part of a range.
+
+ // The range must terminate at an alphabetic character.
+ if (!stream.match(/[a-zA-Z]/, false)) {
+ showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));
+ return;
+ }
+
+ var startMark = sym;
+ var finishMark = stream.next();
+ // The range must terminate at an alphabetic character which
+ // shares the same case as the start of the range.
+ if (isLowerCase(startMark) && isLowerCase(finishMark) ||
+ isUpperCase(startMark) && isUpperCase(finishMark)) {
+ var start = startMark.charCodeAt(0);
+ var finish = finishMark.charCodeAt(0);
+ if (start >= finish) {
+ showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));
+ return;
+ }
+
+ // Because marks are always ASCII values, and we have
+ // determined that they are the same case, we can use
+ // their char codes to iterate through the defined range.
+ for (var j = 0; j <= finish - start; j++) {
+ var mark = String.fromCharCode(start + j);
+ delete state.marks[mark];
+ }
+ } else {
+ showConfirm(cm, 'Invalid argument: ' + startMark + '-');
+ return;
+ }
+ } else {
+ // This symbol is a valid mark, and is not part of a range.
+ delete state.marks[sym];
+ }
+ }
+ }
+ };
+
+ var exCommandDispatcher = new ExCommandDispatcher();
+
+ /**
+ * @param {CodeMirror} cm CodeMirror instance we are in.
+ * @param {boolean} confirm Whether to confirm each replace.
+ * @param {Cursor} lineStart Line to start replacing from.
+ * @param {Cursor} lineEnd Line to stop replacing at.
+ * @param {RegExp} query Query for performing matches with.
+ * @param {string} replaceWith Text to replace matches with. May contain $1,
+ * $2, etc for replacing captured groups using Javascript replace.
+ * @param {function()} callback A callback for when the replace is done.
+ */
+ function doReplace(cm, confirm, global, lineStart, lineEnd, searchCursor, query,
+ replaceWith, callback) {
+ // Set up all the functions.
+ cm.state.vim.exMode = true;
+ var done = false;
+ var lastPos = searchCursor.from();
+ function replaceAll() {
+ cm.operation(function() {
+ while (!done) {
+ replace();
+ next();
+ }
+ stop();
+ });
+ }
+ function replace() {
+ var text = cm.getRange(searchCursor.from(), searchCursor.to());
+ var newText = text.replace(query, replaceWith);
+ searchCursor.replace(newText);
+ }
+ function next() {
+ var found;
+ // The below only loops to skip over multiple occurrences on the same
+ // line when 'global' is not true.
+ while(found = searchCursor.findNext() &&
+ isInRange(searchCursor.from(), lineStart, lineEnd)) {
+ if (!global && lastPos && searchCursor.from().line == lastPos.line) {
+ continue;
+ }
+ cm.scrollIntoView(searchCursor.from(), 30);
+ cm.setSelection(searchCursor.from(), searchCursor.to());
+ lastPos = searchCursor.from();
+ done = false;
+ return;
+ }
+ done = true;
+ }
+ function stop(close) {
+ if (close) { close(); }
+ cm.focus();
+ if (lastPos) {
+ cm.setCursor(lastPos);
+ var vim = cm.state.vim;
+ vim.exMode = false;
+ vim.lastHPos = vim.lastHSPos = lastPos.ch;
+ }
+ if (callback) { callback(); }
+ }
+ function onPromptKeyDown(e, _value, close) {
+ // Swallow all keys.
+ CodeMirror.e_stop(e);
+ var keyName = CodeMirror.keyName(e);
+ switch (keyName) {
+ case 'Y':
+ replace(); next(); break;
+ case 'N':
+ next(); break;
+ case 'A':
+ // replaceAll contains a call to close of its own. We don't want it
+ // to fire too early or multiple times.
+ var savedCallback = callback;
+ callback = undefined;
+ cm.operation(replaceAll);
+ callback = savedCallback;
+ break;
+ case 'L':
+ replace();
+ // fall through and exit.
+ case 'Q':
+ case 'Esc':
+ case 'Ctrl-C':
+ case 'Ctrl-[':
+ stop(close);
+ break;
+ }
+ if (done) { stop(close); }
+ return true;
+ }
+
+ // Actually do replace.
+ next();
+ if (done) {
+ showConfirm(cm, 'No matches for ' + query.source);
+ return;
+ }
+ if (!confirm) {
+ replaceAll();
+ if (callback) { callback(); };
+ return;
+ }
+ showPrompt(cm, {
+ prefix: 'replace with ' + replaceWith + ' (y/n/a/q/l)',
+ onKeyDown: onPromptKeyDown
+ });
+ }
+
+ CodeMirror.keyMap.vim = {
+ attach: attachVimMap,
+ detach: detachVimMap
+ };
+
+ function exitInsertMode(cm) {
+ var vim = cm.state.vim;
+ var macroModeState = vimGlobalState.macroModeState;
+ var insertModeChangeRegister = vimGlobalState.registerController.getRegister('.');
+ var isPlaying = macroModeState.isPlaying;
+ var lastChange = macroModeState.lastInsertModeChanges;
+ // In case of visual block, the insertModeChanges are not saved as a
+ // single word, so we convert them to a single word
+ // so as to update the ". register as expected in real vim.
+ var text = [];
+ if (!isPlaying) {
+ var selLength = lastChange.inVisualBlock ? vim.lastSelection.visualBlock.height : 1;
+ var changes = lastChange.changes;
+ var text = [];
+ var i = 0;
+ // In case of multiple selections in blockwise visual,
+ // the inserted text, for example: 'foo', is stored as
+ // 'f', 'f', InsertModeKey 'o', 'o', 'o', 'o'. (if you have a block with 2 lines).
+ // We push the contents of the changes array as per the following:
+ // 1. In case of InsertModeKey, just increment by 1.
+ // 2. In case of a character, jump by selLength (2 in the example).
+ while (i < changes.length) {
+ // This loop will convert 'ffoooo' to 'foo'.
+ text.push(changes[i]);
+ if (changes[i] instanceof InsertModeKey) {
+ i++;
+ } else {
+ i+= selLength;
+ }
+ }
+ lastChange.changes = text;
+ cm.off('change', onChange);
+ CodeMirror.off(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown);
+ }
+ if (!isPlaying && vim.insertModeRepeat > 1) {
+ // Perform insert mode repeat for commands like 3,a and 3,o.
+ repeatLastEdit(cm, vim, vim.insertModeRepeat - 1,
+ true /** repeatForInsert */);
+ vim.lastEditInputState.repeatOverride = vim.insertModeRepeat;
+ }
+ delete vim.insertModeRepeat;
+ vim.insertMode = false;
+ cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1);
+ cm.setOption('keyMap', 'vim');
+ cm.setOption('disableInput', true);
+ cm.toggleOverwrite(false); // exit replace mode if we were in it.
+ // update the ". register before exiting insert mode
+ insertModeChangeRegister.setText(lastChange.changes.join(''));
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
+ if (macroModeState.isRecording) {
+ logInsertModeChange(macroModeState);
+ }
+ }
+
+ // The timeout in milliseconds for the two-character ESC keymap should be
+ // adjusted according to your typing speed to prevent false positives.
+ defineOption('insertModeEscKeysTimeout', 200, 'number');
+
+ CodeMirror.keyMap['vim-insert'] = {
+ // TODO: override navigation keys so that Esc will cancel automatic
+ // indentation from o, O, i_
+ 'Ctrl-N': 'autocomplete',
+ 'Ctrl-P': 'autocomplete',
+ 'Enter': function(cm) {
+ var fn = CodeMirror.commands.newlineAndIndentContinueComment ||
+ CodeMirror.commands.newlineAndIndent;
+ fn(cm);
+ },
+ fallthrough: ['default'],
+ attach: attachVimMap,
+ detach: detachVimMap
+ };
+
+ CodeMirror.keyMap['await-second'] = {
+ fallthrough: ['vim-insert'],
+ attach: attachVimMap,
+ detach: detachVimMap
+ };
+
+ CodeMirror.keyMap['vim-replace'] = {
+ 'Backspace': 'goCharLeft',
+ fallthrough: ['vim-insert'],
+ attach: attachVimMap,
+ detach: detachVimMap
+ };
+
+ function executeMacroRegister(cm, vim, macroModeState, registerName) {
+ var register = vimGlobalState.registerController.getRegister(registerName);
+ var keyBuffer = register.keyBuffer;
+ var imc = 0;
+ macroModeState.isPlaying = true;
+ macroModeState.replaySearchQueries = register.searchQueries.slice(0);
+ for (var i = 0; i < keyBuffer.length; i++) {
+ var text = keyBuffer[i];
+ var match, key;
+ while (text) {
+ // Pull off one command key, which is either a single character
+ // or a special sequence wrapped in '<' and '>', e.g. ''.
+ match = (/<\w+-.+?>|<\w+>|./).exec(text);
+ key = match[0];
+ text = text.substring(match.index + key.length);
+ CodeMirror.Vim.handleKey(cm, key, 'macro');
+ if (vim.insertMode) {
+ var changes = register.insertModeChanges[imc++].changes;
+ vimGlobalState.macroModeState.lastInsertModeChanges.changes =
+ changes;
+ repeatInsertModeChanges(cm, changes, 1);
+ exitInsertMode(cm);
+ }
+ }
+ };
+ macroModeState.isPlaying = false;
+ }
+
+ function logKey(macroModeState, key) {
+ if (macroModeState.isPlaying) { return; }
+ var registerName = macroModeState.latestRegister;
+ var register = vimGlobalState.registerController.getRegister(registerName);
+ if (register) {
+ register.pushText(key);
+ }
+ }
+
+ function logInsertModeChange(macroModeState) {
+ if (macroModeState.isPlaying) { return; }
+ var registerName = macroModeState.latestRegister;
+ var register = vimGlobalState.registerController.getRegister(registerName);
+ if (register) {
+ register.pushInsertModeChanges(macroModeState.lastInsertModeChanges);
+ }
+ }
+
+ function logSearchQuery(macroModeState, query) {
+ if (macroModeState.isPlaying) { return; }
+ var registerName = macroModeState.latestRegister;
+ var register = vimGlobalState.registerController.getRegister(registerName);
+ if (register) {
+ register.pushSearchQuery(query);
+ }
+ }
+
+ /**
+ * Listens for changes made in insert mode.
+ * Should only be active in insert mode.
+ */
+ function onChange(_cm, changeObj) {
+ var macroModeState = vimGlobalState.macroModeState;
+ var lastChange = macroModeState.lastInsertModeChanges;
+ if (!macroModeState.isPlaying) {
+ while(changeObj) {
+ lastChange.expectCursorActivityForChange = true;
+ if (changeObj.origin == '+input' || changeObj.origin == 'paste'
+ || changeObj.origin === undefined /* only in testing */) {
+ var text = changeObj.text.join('\n');
+ lastChange.changes.push(text);
+ }
+ // Change objects may be chained with next.
+ changeObj = changeObj.next;
+ }
+ }
+ }
+
+ /**
+ * Listens for any kind of cursor activity on CodeMirror.
+ */
+ function onCursorActivity(cm) {
+ var vim = cm.state.vim;
+ if (vim.insertMode) {
+ // Tracking cursor activity in insert mode (for macro support).
+ var macroModeState = vimGlobalState.macroModeState;
+ if (macroModeState.isPlaying) { return; }
+ var lastChange = macroModeState.lastInsertModeChanges;
+ if (lastChange.expectCursorActivityForChange) {
+ lastChange.expectCursorActivityForChange = false;
+ } else {
+ // Cursor moved outside the context of an edit. Reset the change.
+ lastChange.changes = [];
+ }
+ } else {
+ handleExternalSelection(cm, vim);
+ }
+ if (vim.visualMode) {
+ var from, head;
+ from = head = cm.getCursor('head');
+ var anchor = cm.getCursor('anchor');
+ var to = Pos(head.line, from.ch + (cursorIsBefore(anchor, head) ? -1 : 1));
+ if (cursorIsBefore(to, from)) {
+ var temp = from;
+ from = to;
+ to = temp;
+ }
+ if (vim.fakeCursor) {
+ vim.fakeCursor.clear();
+ }
+ vim.fakeCursor = cm.markText(from, to, {className: 'cm-animate-fat-cursor'});
+ }
+ }
+
+ function handleExternalSelection(cm, vim) {
+ var anchor = cm.getCursor('anchor');
+ var head = cm.getCursor('head');
+ // Enter or exit visual mode to match mouse selection.
+ if (vim.visualMode && cursorEqual(head, anchor) && lineLength(cm, head.line) > head.ch) {
+ exitVisualMode(cm);
+ } else if (!cm.curOp.isVimOp && !vim.visualMode && !vim.insertMode && cm.somethingSelected()) {
+ vim.visualMode = true;
+ vim.visualLine = false;
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"});
+ }
+ if (!cm.curOp.isVimOp) {
+ if (vim.visualMode) {
+ updateMark(cm, vim, '<', cursorMin(head, anchor));
+ updateMark(cm, vim, '>', cursorMax(head, anchor));
+ } else if (!vim.insertMode) {
+ // Reset lastHPos if selection was modified by something outside of vim mode e.g. by mouse.
+ vim.lastHPos = cm.getCursor().ch;
+ }
+ }
+ }
+
+ /** Wrapper for special keys pressed in insert mode */
+ function InsertModeKey(keyName) {
+ this.keyName = keyName;
+ }
+
+ /**
+ * Handles raw key down events from the text area.
+ * - Should only be active in insert mode.
+ * - For recording deletes in insert mode.
+ */
+ function onKeyEventTargetKeyDown(e) {
+ var macroModeState = vimGlobalState.macroModeState;
+ var lastChange = macroModeState.lastInsertModeChanges;
+ var keyName = CodeMirror.keyName(e);
+ function onKeyFound() {
+ lastChange.changes.push(new InsertModeKey(keyName));
+ return true;
+ }
+ if (keyName.indexOf('Delete') != -1 || keyName.indexOf('Backspace') != -1) {
+ CodeMirror.lookupKey(keyName, 'vim-insert', onKeyFound);
+ }
+ }
+
+ /**
+ * Repeats the last edit, which includes exactly 1 command and at most 1
+ * insert. Operator and motion commands are read from lastEditInputState,
+ * while action commands are read from lastEditActionCommand.
+ *
+ * If repeatForInsert is true, then the function was called by
+ * exitInsertMode to repeat the insert mode changes the user just made. The
+ * corresponding enterInsertMode call was made with a count.
+ */
+ function repeatLastEdit(cm, vim, repeat, repeatForInsert) {
+ var macroModeState = vimGlobalState.macroModeState;
+ macroModeState.isPlaying = true;
+ var isAction = !!vim.lastEditActionCommand;
+ var cachedInputState = vim.inputState;
+ function repeatCommand() {
+ if (isAction) {
+ commandDispatcher.processAction(cm, vim, vim.lastEditActionCommand);
+ } else {
+ commandDispatcher.evalInput(cm, vim);
+ }
+ }
+ function repeatInsert(repeat) {
+ if (macroModeState.lastInsertModeChanges.changes.length > 0) {
+ // For some reason, repeat cw in desktop VIM does not repeat
+ // insert mode changes. Will conform to that behavior.
+ repeat = !vim.lastEditActionCommand ? 1 : repeat;
+ var changeObject = macroModeState.lastInsertModeChanges;
+ repeatInsertModeChanges(cm, changeObject.changes, repeat);
+ }
+ }
+ vim.inputState = vim.lastEditInputState;
+ if (isAction && vim.lastEditActionCommand.interlaceInsertRepeat) {
+ // o and O repeat have to be interlaced with insert repeats so that the
+ // insertions appear on separate lines instead of the last line.
+ for (var i = 0; i < repeat; i++) {
+ repeatCommand();
+ repeatInsert(1);
+ }
+ } else {
+ if (!repeatForInsert) {
+ // Hack to get the cursor to end up at the right place. If I is
+ // repeated in insert mode repeat, cursor will be 1 insert
+ // change set left of where it should be.
+ repeatCommand();
+ }
+ repeatInsert(repeat);
+ }
+ vim.inputState = cachedInputState;
+ if (vim.insertMode && !repeatForInsert) {
+ // Don't exit insert mode twice. If repeatForInsert is set, then we
+ // were called by an exitInsertMode call lower on the stack.
+ exitInsertMode(cm);
+ }
+ macroModeState.isPlaying = false;
+ };
+
+ function repeatInsertModeChanges(cm, changes, repeat) {
+ function keyHandler(binding) {
+ if (typeof binding == 'string') {
+ CodeMirror.commands[binding](cm);
+ } else {
+ binding(cm);
+ }
+ return true;
+ }
+ var curStart = cm.getCursor();
+ var inVisualBlock = vimGlobalState.macroModeState.lastInsertModeChanges.inVisualBlock;
+ if (inVisualBlock) {
+ // Set up block selection again for repeating the changes.
+ var vim = cm.state.vim;
+ var block = vim.lastSelection.visualBlock;
+ var curEnd = Pos(curStart.line + block.height-1, curStart.ch);
+ cm.setCursor(curStart);
+ selectBlock(cm, curEnd);
+ repeat = cm.listSelections().length;
+ cm.setCursor(curStart);
+ }
+ for (var i = 0; i < repeat; i++) {
+ for (var j = 0; j < changes.length; j++) {
+ var change = changes[j];
+ if (change instanceof InsertModeKey) {
+ CodeMirror.lookupKey(change.keyName, 'vim-insert', keyHandler);
+ } else {
+ var cur = cm.getCursor();
+ cm.replaceRange(change, cur, cur);
+ }
+ }
+ if (inVisualBlock) {
+ curStart.line++;
+ cm.setCursor(curStart);
+ }
+ }
+ }
+
+ resetVimGlobalState();
+ //};
+ // Initialize Vim and make it available as an API.
+ CodeMirror.Vim = Vim();
+});
diff --git a/lib/ace/keyboard/vim2_test.js b/lib/ace/keyboard/vim2_test.js
new file mode 100644
index 00000000..7f016c47
--- /dev/null
+++ b/lib/ace/keyboard/vim2_test.js
@@ -0,0 +1,3614 @@
+var code = '' +
+' wOrd1 (#%\n' +
+' word3] \n' +
+'aopop pop 0 1 2 3 4\n' +
+' (a) [b] {c} \n' +
+'int getchar(void) {\n' +
+' static char buf[BUFSIZ];\n' +
+' static char *bufp = buf;\n' +
+' if (n == 0) { /* buffer is empty */\n' +
+' n = read(0, buf, sizeof buf);\n' +
+' bufp = buf;\n' +
+' }\n' +
+'\n' +
+' return (--n >= 0) ? (unsigned char) *bufp++ : EOF;\n' +
+' \n' +
+'}\n';
+
+var lines = (function() {
+ lineText = code.split('\n');
+ var ret = [];
+ for (var i = 0; i < lineText.length; i++) {
+ ret[i] = {
+ line: i,
+ length: lineText[i].length,
+ lineText: lineText[i],
+ textStart: /^\s*/.exec(lineText[i])[0].length
+ };
+ }
+ return ret;
+})();
+var endOfDocument = makeCursor(lines.length - 1,
+ lines[lines.length - 1].length);
+var wordLine = lines[0];
+var bigWordLine = lines[1];
+var charLine = lines[2];
+var bracesLine = lines[3];
+var seekBraceLine = lines[4];
+
+var word1 = {
+ start: { line: wordLine.line, ch: 1 },
+ end: { line: wordLine.line, ch: 5 }
+};
+var word2 = {
+ start: { line: wordLine.line, ch: word1.end.ch + 2 },
+ end: { line: wordLine.line, ch: word1.end.ch + 4 }
+};
+var word3 = {
+ start: { line: bigWordLine.line, ch: 1 },
+ end: { line: bigWordLine.line, ch: 5 }
+};
+var bigWord1 = word1;
+var bigWord2 = word2;
+var bigWord3 = {
+ start: { line: bigWordLine.line, ch: 1 },
+ end: { line: bigWordLine.line, ch: 7 }
+};
+var bigWord4 = {
+ start: { line: bigWordLine.line, ch: bigWord1.end.ch + 3 },
+ end: { line: bigWordLine.line, ch: bigWord1.end.ch + 7 }
+};
+
+var oChars = [ { line: charLine.line, ch: 1 },
+ { line: charLine.line, ch: 3 },
+ { line: charLine.line, ch: 7 } ];
+var pChars = [ { line: charLine.line, ch: 2 },
+ { line: charLine.line, ch: 4 },
+ { line: charLine.line, ch: 6 },
+ { line: charLine.line, ch: 8 } ];
+var numChars = [ { line: charLine.line, ch: 10 },
+ { line: charLine.line, ch: 12 },
+ { line: charLine.line, ch: 14 },
+ { line: charLine.line, ch: 16 },
+ { line: charLine.line, ch: 18 }];
+var parens1 = {
+ start: { line: bracesLine.line, ch: 1 },
+ end: { line: bracesLine.line, ch: 3 }
+};
+var squares1 = {
+ start: { line: bracesLine.line, ch: 5 },
+ end: { line: bracesLine.line, ch: 7 }
+};
+var curlys1 = {
+ start: { line: bracesLine.line, ch: 9 },
+ end: { line: bracesLine.line, ch: 11 }
+};
+var seekOutside = {
+ start: { line: seekBraceLine.line, ch: 1 },
+ end: { line: seekBraceLine.line, ch: 16 }
+};
+var seekInside = {
+ start: { line: seekBraceLine.line, ch: 14 },
+ end: { line: seekBraceLine.line, ch: 11 }
+};
+
+function copyCursor(cur) {
+ return { ch: cur.ch, line: cur.line };
+}
+
+function forEach(arr, func) {
+ for (var i = 0; i < arr.length; i++) {
+ func(arr[i], i, arr);
+ }
+}
+
+function testVim(name, run, opts, expectedFail) {
+ var vimOpts = {
+ lineNumbers: true,
+ vimMode: true,
+ showCursorWhenSelecting: true,
+ value: code
+ };
+ for (var prop in opts) {
+ if (opts.hasOwnProperty(prop)) {
+ vimOpts[prop] = opts[prop];
+ }
+ }
+ return test('vim_' + name, function() {
+ var place = document.getElementById("testground");
+ var cm = CodeMirror(place, vimOpts);
+ var vim = CodeMirror.Vim.maybeInitVimState_(cm);
+
+ function doKeysFn(cm) {
+ return function(args) {
+ if (args instanceof Array) {
+ arguments = args;
+ }
+ for (var i = 0; i < arguments.length; i++) {
+ CodeMirror.Vim.handleKey(cm, arguments[i]);
+ }
+ }
+ }
+ function doInsertModeKeysFn(cm) {
+ return function(args) {
+ if (args instanceof Array) { arguments = args; }
+ function executeHandler(handler) {
+ if (typeof handler == 'string') {
+ CodeMirror.commands[handler](cm);
+ } else {
+ handler(cm);
+ }
+ return true;
+ }
+ for (var i = 0; i < arguments.length; i++) {
+ var key = arguments[i];
+ // Find key in keymap and handle.
+ var handled = CodeMirror.lookupKey(key, 'vim-insert', executeHandler);
+ // Record for insert mode.
+ if (handled == "handled" && cm.state.vim.insertMode && arguments[i] != 'Esc') {
+ var lastChange = CodeMirror.Vim.getVimGlobalState_().macroModeState.lastInsertModeChanges;
+ if (lastChange) {
+ lastChange.changes.push(new CodeMirror.Vim.InsertModeKey(key));
+ }
+ }
+ }
+ }
+ }
+ function doExFn(cm) {
+ return function(command) {
+ cm.openDialog = helpers.fakeOpenDialog(command);
+ helpers.doKeys(':');
+ }
+ }
+ function assertCursorAtFn(cm) {
+ return function(line, ch) {
+ var pos;
+ if (ch == null && typeof line.line == 'number') {
+ pos = line;
+ } else {
+ pos = makeCursor(line, ch);
+ }
+ eqPos(pos, cm.getCursor());
+ }
+ }
+ function fakeOpenDialog(result) {
+ return function(text, callback) {
+ return callback(result);
+ }
+ }
+ function fakeOpenNotification(matcher) {
+ return function(text) {
+ matcher(text);
+ }
+ }
+ var helpers = {
+ doKeys: doKeysFn(cm),
+ // Warning: Only emulates keymap events, not character insertions. Use
+ // replaceRange to simulate character insertions.
+ // Keys are in CodeMirror format, NOT vim format.
+ doInsertModeKeys: doInsertModeKeysFn(cm),
+ doEx: doExFn(cm),
+ assertCursorAt: assertCursorAtFn(cm),
+ fakeOpenDialog: fakeOpenDialog,
+ fakeOpenNotification: fakeOpenNotification,
+ getRegisterController: function() {
+ return CodeMirror.Vim.getRegisterController();
+ }
+ }
+ CodeMirror.Vim.resetVimGlobalState_();
+ var successful = false;
+ var savedOpenNotification = cm.openNotification;
+ try {
+ run(cm, vim, helpers);
+ successful = true;
+ } finally {
+ cm.openNotification = savedOpenNotification;
+ if (!successful || verbose) {
+ place.style.visibility = "visible";
+ } else {
+ place.removeChild(cm.getWrapperElement());
+ }
+ }
+ }, expectedFail);
+};
+testVim('qq@q', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'q', 'l', 'l', 'q');
+ helpers.assertCursorAt(0,2);
+ helpers.doKeys('@', 'q');
+ helpers.assertCursorAt(0,4);
+}, { value: ' '});
+testVim('@@', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'q', 'l', 'l', 'q');
+ helpers.assertCursorAt(0,2);
+ helpers.doKeys('@', 'q');
+ helpers.assertCursorAt(0,4);
+ helpers.doKeys('@', '@');
+ helpers.assertCursorAt(0,6);
+}, { value: ' '});
+var jumplistScene = ''+
+ 'word\n'+
+ '(word)\n'+
+ '{word\n'+
+ 'word.\n'+
+ '\n'+
+ 'word search\n'+
+ '}word\n'+
+ 'word\n'+
+ 'word\n';
+function testJumplist(name, keys, endPos, startPos, dialog) {
+ endPos = makeCursor(endPos[0], endPos[1]);
+ startPos = makeCursor(startPos[0], startPos[1]);
+ testVim(name, function(cm, vim, helpers) {
+ CodeMirror.Vim.resetVimGlobalState_();
+ if(dialog)cm.openDialog = helpers.fakeOpenDialog('word');
+ cm.setCursor(startPos);
+ helpers.doKeys.apply(null, keys);
+ helpers.assertCursorAt(endPos);
+ }, {value: jumplistScene});
+};
+testJumplist('jumplist_H', ['H', ''], [5,2], [5,2]);
+testJumplist('jumplist_M', ['M', ''], [2,2], [2,2]);
+testJumplist('jumplist_L', ['L', ''], [2,2], [2,2]);
+testJumplist('jumplist_[[', ['[', '[', ''], [5,2], [5,2]);
+testJumplist('jumplist_]]', [']', ']', ''], [2,2], [2,2]);
+testJumplist('jumplist_G', ['G', ''], [5,2], [5,2]);
+testJumplist('jumplist_gg', ['g', 'g', ''], [5,2], [5,2]);
+testJumplist('jumplist_%', ['%', ''], [1,5], [1,5]);
+testJumplist('jumplist_{', ['{', ''], [1,5], [1,5]);
+testJumplist('jumplist_}', ['}', ''], [1,5], [1,5]);
+testJumplist('jumplist_\'', ['m', 'a', 'h', '\'', 'a', 'h', ''], [1,0], [1,5]);
+testJumplist('jumplist_`', ['m', 'a', 'h', '`', 'a', 'h', ''], [1,5], [1,5]);
+testJumplist('jumplist_*_cachedCursor', ['*', ''], [1,3], [1,3]);
+testJumplist('jumplist_#_cachedCursor', ['#', ''], [1,3], [1,3]);
+testJumplist('jumplist_n', ['#', 'n', ''], [1,1], [2,3]);
+testJumplist('jumplist_N', ['#', 'N', ''], [1,1], [2,3]);
+testJumplist('jumplist_repeat_', ['*', '*', '*', '3', ''], [2,3], [2,3]);
+testJumplist('jumplist_repeat_', ['*', '*', '*', '3', '', '2', ''], [5,0], [2,3]);
+testJumplist('jumplist_repeated_motion', ['3', '*', ''], [2,3], [2,3]);
+testJumplist('jumplist_/', ['/', ''], [2,3], [2,3], 'dialog');
+testJumplist('jumplist_?', ['?', ''], [2,3], [2,3], 'dialog');
+testJumplist('jumplist_skip_delted_mark',
+ ['*', 'n', 'n', 'k', 'd', 'k', '', '', ''],
+ [0,2], [0,2]);
+testJumplist('jumplist_skip_delted_mark',
+ ['*', 'n', 'n', 'k', 'd', 'k', '', '', ''],
+ [1,0], [0,2]);
+
+/**
+ * @param name Name of the test
+ * @param keys An array of keys or a string with a single key to simulate.
+ * @param endPos The expected end position of the cursor.
+ * @param startPos The position the cursor should start at, defaults to 0, 0.
+ */
+function testMotion(name, keys, endPos, startPos) {
+ testVim(name, function(cm, vim, helpers) {
+ if (!startPos) {
+ startPos = { line: 0, ch: 0 };
+ }
+ cm.setCursor(startPos);
+ helpers.doKeys(keys);
+ helpers.assertCursorAt(endPos);
+ });
+};
+
+function makeCursor(line, ch) {
+ return { line: line, ch: ch };
+};
+
+function offsetCursor(cur, offsetLine, offsetCh) {
+ return { line: cur.line + offsetLine, ch: cur.ch + offsetCh };
+};
+
+// Motion tests
+testMotion('|', '|', makeCursor(0, 0), makeCursor(0,4));
+testMotion('|_repeat', ['3', '|'], makeCursor(0, 2), makeCursor(0,4));
+testMotion('h', 'h', makeCursor(0, 0), word1.start);
+testMotion('h_repeat', ['3', 'h'], offsetCursor(word1.end, 0, -3), word1.end);
+testMotion('l', 'l', makeCursor(0, 1));
+testMotion('l_repeat', ['2', 'l'], makeCursor(0, 2));
+testMotion('j', 'j', offsetCursor(word1.end, 1, 0), word1.end);
+testMotion('j_repeat', ['2', 'j'], offsetCursor(word1.end, 2, 0), word1.end);
+testMotion('j_repeat_clip', ['1000', 'j'], endOfDocument);
+testMotion('k', 'k', offsetCursor(word3.end, -1, 0), word3.end);
+testMotion('k_repeat', ['2', 'k'], makeCursor(0, 4), makeCursor(2, 4));
+testMotion('k_repeat_clip', ['1000', 'k'], makeCursor(0, 4), makeCursor(2, 4));
+testMotion('w', 'w', word1.start);
+testMotion('w_multiple_newlines_no_space', 'w', makeCursor(12, 2), makeCursor(11, 2));
+testMotion('w_multiple_newlines_with_space', 'w', makeCursor(14, 0), makeCursor(12, 51));
+testMotion('w_repeat', ['2', 'w'], word2.start);
+testMotion('w_wrap', ['w'], word3.start, word2.start);
+testMotion('w_endOfDocument', 'w', endOfDocument, endOfDocument);
+testMotion('w_start_to_end', ['1000', 'w'], endOfDocument, makeCursor(0, 0));
+testMotion('W', 'W', bigWord1.start);
+testMotion('W_repeat', ['2', 'W'], bigWord3.start, bigWord1.start);
+testMotion('e', 'e', word1.end);
+testMotion('e_repeat', ['2', 'e'], word2.end);
+testMotion('e_wrap', 'e', word3.end, word2.end);
+testMotion('e_endOfDocument', 'e', endOfDocument, endOfDocument);
+testMotion('e_start_to_end', ['1000', 'e'], endOfDocument, makeCursor(0, 0));
+testMotion('b', 'b', word3.start, word3.end);
+testMotion('b_repeat', ['2', 'b'], word2.start, word3.end);
+testMotion('b_wrap', 'b', word2.start, word3.start);
+testMotion('b_startOfDocument', 'b', makeCursor(0, 0), makeCursor(0, 0));
+testMotion('b_end_to_start', ['1000', 'b'], makeCursor(0, 0), endOfDocument);
+testMotion('ge', ['g', 'e'], word2.end, word3.end);
+testMotion('ge_repeat', ['2', 'g', 'e'], word1.end, word3.start);
+testMotion('ge_wrap', ['g', 'e'], word2.end, word3.start);
+testMotion('ge_startOfDocument', ['g', 'e'], makeCursor(0, 0),
+ makeCursor(0, 0));
+testMotion('ge_end_to_start', ['1000', 'g', 'e'], makeCursor(0, 0), endOfDocument);
+testMotion('gg', ['g', 'g'], makeCursor(lines[0].line, lines[0].textStart),
+ makeCursor(3, 1));
+testMotion('gg_repeat', ['3', 'g', 'g'],
+ makeCursor(lines[2].line, lines[2].textStart));
+testMotion('G', 'G',
+ makeCursor(lines[lines.length - 1].line, lines[lines.length - 1].textStart),
+ makeCursor(3, 1));
+testMotion('G_repeat', ['3', 'G'], makeCursor(lines[2].line,
+ lines[2].textStart));
+// TODO: Make the test code long enough to test Ctrl-F and Ctrl-B.
+testMotion('0', '0', makeCursor(0, 0), makeCursor(0, 8));
+testMotion('^', '^', makeCursor(0, lines[0].textStart), makeCursor(0, 8));
+testMotion('+', '+', makeCursor(1, lines[1].textStart), makeCursor(0, 8));
+testMotion('-', '-', makeCursor(0, lines[0].textStart), makeCursor(1, 4));
+testMotion('_', ['6','_'], makeCursor(5, lines[5].textStart), makeCursor(0, 8));
+testMotion('$', '$', makeCursor(0, lines[0].length - 1), makeCursor(0, 1));
+testMotion('$_repeat', ['2', '$'], makeCursor(1, lines[1].length - 1),
+ makeCursor(0, 3));
+testMotion('f', ['f', 'p'], pChars[0], makeCursor(charLine.line, 0));
+testMotion('f_repeat', ['2', 'f', 'p'], pChars[2], pChars[0]);
+testMotion('f_num', ['f', '2'], numChars[2], makeCursor(charLine.line, 0));
+testMotion('t', ['t','p'], offsetCursor(pChars[0], 0, -1),
+ makeCursor(charLine.line, 0));
+testMotion('t_repeat', ['2', 't', 'p'], offsetCursor(pChars[2], 0, -1),
+ pChars[0]);
+testMotion('F', ['F', 'p'], pChars[0], pChars[1]);
+testMotion('F_repeat', ['2', 'F', 'p'], pChars[0], pChars[2]);
+testMotion('T', ['T', 'p'], offsetCursor(pChars[0], 0, 1), pChars[1]);
+testMotion('T_repeat', ['2', 'T', 'p'], offsetCursor(pChars[0], 0, 1), pChars[2]);
+testMotion('%_parens', ['%'], parens1.end, parens1.start);
+testMotion('%_squares', ['%'], squares1.end, squares1.start);
+testMotion('%_braces', ['%'], curlys1.end, curlys1.start);
+testMotion('%_seek_outside', ['%'], seekOutside.end, seekOutside.start);
+testMotion('%_seek_inside', ['%'], seekInside.end, seekInside.start);
+testVim('%_seek_skip', function(cm, vim, helpers) {
+ cm.setCursor(0,0);
+ helpers.doKeys(['%']);
+ helpers.assertCursorAt(0,9);
+}, {value:'01234"("()'});
+testVim('%_skip_string', function(cm, vim, helpers) {
+ cm.setCursor(0,0);
+ helpers.doKeys(['%']);
+ helpers.assertCursorAt(0,4);
+ cm.setCursor(0,2);
+ helpers.doKeys(['%']);
+ helpers.assertCursorAt(0,0);
+}, {value:'(")")'});
+(')')
+testVim('%_skip_comment', function(cm, vim, helpers) {
+ cm.setCursor(0,0);
+ helpers.doKeys(['%']);
+ helpers.assertCursorAt(0,6);
+ cm.setCursor(0,3);
+ helpers.doKeys(['%']);
+ helpers.assertCursorAt(0,0);
+}, {value:'(/*)*/)'});
+// Make sure that moving down after going to the end of a line always leaves you
+// at the end of a line, but preserves the offset in other cases
+testVim('Changing lines after Eol operation', function(cm, vim, helpers) {
+ cm.setCursor(0,0);
+ helpers.doKeys(['$']);
+ helpers.doKeys(['j']);
+ // After moving to Eol and then down, we should be at Eol of line 2
+ helpers.assertCursorAt({ line: 1, ch: lines[1].length - 1 });
+ helpers.doKeys(['j']);
+ // After moving down, we should be at Eol of line 3
+ helpers.assertCursorAt({ line: 2, ch: lines[2].length - 1 });
+ helpers.doKeys(['h']);
+ helpers.doKeys(['j']);
+ // After moving back one space and then down, since line 4 is shorter than line 2, we should
+ // be at Eol of line 2 - 1
+ helpers.assertCursorAt({ line: 3, ch: lines[3].length - 1 });
+ helpers.doKeys(['j']);
+ helpers.doKeys(['j']);
+ // After moving down again, since line 3 has enough characters, we should be back to the
+ // same place we were at on line 1
+ helpers.assertCursorAt({ line: 5, ch: lines[2].length - 2 });
+});
+//making sure gj and gk recover from clipping
+testVim('gj_gk_clipping', function(cm,vim,helpers){
+ cm.setCursor(0, 1);
+ helpers.doKeys('g','j','g','j');
+ helpers.assertCursorAt(2, 1);
+ helpers.doKeys('g','k','g','k');
+ helpers.assertCursorAt(0, 1);
+},{value: 'line 1\n\nline 2'});
+//testing a mix of j/k and gj/gk
+testVim('j_k_and_gj_gk', function(cm,vim,helpers){
+ cm.setSize(120);
+ cm.setCursor(0, 0);
+ //go to the last character on the first line
+ helpers.doKeys('$');
+ //move up/down on the column within the wrapped line
+ //side-effect: cursor is not locked to eol anymore
+ helpers.doKeys('g','k');
+ var cur=cm.getCursor();
+ eq(cur.line,0);
+ is((cur.ch<176),'gk didn\'t move cursor back (1)');
+ helpers.doKeys('g','j');
+ helpers.assertCursorAt(0, 176);
+ //should move to character 177 on line 2 (j/k preserve character index within line)
+ helpers.doKeys('j');
+ //due to different line wrapping, the cursor can be on a different screen-x now
+ //gj and gk preserve screen-x on movement, much like moveV
+ helpers.doKeys('3','g','k');
+ cur=cm.getCursor();
+ eq(cur.line,1);
+ is((cur.ch<176),'gk didn\'t move cursor back (2)');
+ helpers.doKeys('g','j','2','g','j');
+ //should return to the same character-index
+ helpers.doKeys('k');
+ helpers.assertCursorAt(0, 176);
+},{ lineWrapping:true, value: 'This line is intentially long to test movement of gj and gk over wrapped lines. I will start on the end of this line, then make a step up and back to set the origin for j and k.\nThis line is supposed to be even longer than the previous. I will jump here and make another wiggle with gj and gk, before I jump back to the line above. Both wiggles should not change my cursor\'s target character but both j/k and gj/gk change each other\'s reference position.'});
+testVim('gj_gk', function(cm, vim, helpers) {
+ if (phantom) return;
+ cm.setSize(120);
+ // Test top of document edge case.
+ cm.setCursor(0, 4);
+ helpers.doKeys('g', 'j');
+ helpers.doKeys('10', 'g', 'k');
+ helpers.assertCursorAt(0, 4);
+
+ // Test moving down preserves column position.
+ helpers.doKeys('g', 'j');
+ var pos1 = cm.getCursor();
+ var expectedPos2 = { line: 0, ch: (pos1.ch - 4) * 2 + 4};
+ helpers.doKeys('g', 'j');
+ helpers.assertCursorAt(expectedPos2);
+
+ // Move to the last character
+ cm.setCursor(0, 0);
+ // Move left to reset HSPos
+ helpers.doKeys('h');
+ // Test bottom of document edge case.
+ helpers.doKeys('100', 'g', 'j');
+ var endingPos = cm.getCursor();
+ is(endingPos != 0, 'gj should not be on wrapped line 0');
+ var topLeftCharCoords = cm.charCoords(makeCursor(0, 0));
+ var endingCharCoords = cm.charCoords(endingPos);
+ is(topLeftCharCoords.left == endingCharCoords.left, 'gj should end up on column 0');
+},{ lineNumbers: false, lineWrapping:true, value: 'Thislineisintentiallylongtotestmovementofgjandgkoverwrappedlines.' });
+testVim('}', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('}');
+ helpers.assertCursorAt(1, 0);
+ cm.setCursor(0, 0);
+ helpers.doKeys('2', '}');
+ helpers.assertCursorAt(4, 0);
+ cm.setCursor(0, 0);
+ helpers.doKeys('6', '}');
+ helpers.assertCursorAt(5, 0);
+}, { value: 'a\n\nb\nc\n\nd' });
+testVim('{', function(cm, vim, helpers) {
+ cm.setCursor(5, 0);
+ helpers.doKeys('{');
+ helpers.assertCursorAt(4, 0);
+ cm.setCursor(5, 0);
+ helpers.doKeys('2', '{');
+ helpers.assertCursorAt(1, 0);
+ cm.setCursor(5, 0);
+ helpers.doKeys('6', '{');
+ helpers.assertCursorAt(0, 0);
+}, { value: 'a\n\nb\nc\n\nd' });
+
+// Operator tests
+testVim('dl', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 0);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'l');
+ eq('word1 ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq(' ', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+}, { value: ' word1 ' });
+testVim('dl_eol', function(cm, vim, helpers) {
+ cm.setCursor(0, 6);
+ helpers.doKeys('d', 'l');
+ eq(' word1', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq(' ', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 6);
+}, { value: ' word1 ' });
+testVim('dl_repeat', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 0);
+ cm.setCursor(curStart);
+ helpers.doKeys('2', 'd', 'l');
+ eq('ord1 ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq(' w', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+}, { value: ' word1 ' });
+testVim('dh', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 3);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'h');
+ eq(' wrd1 ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('o', register.toString());
+ is(!register.linewise);
+ eqPos(offsetCursor(curStart, 0 , -1), cm.getCursor());
+}, { value: ' word1 ' });
+testVim('dj', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 3);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'j');
+ eq(' word3', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq(' word1\nword2\n', register.toString());
+ is(register.linewise);
+ helpers.assertCursorAt(0, 1);
+}, { value: ' word1\nword2\n word3' });
+testVim('dj_end_of_document', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 3);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'j');
+ eq(' word1 ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 3);
+}, { value: ' word1 ' });
+testVim('dk', function(cm, vim, helpers) {
+ var curStart = makeCursor(1, 3);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'k');
+ eq(' word3', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq(' word1\nword2\n', register.toString());
+ is(register.linewise);
+ helpers.assertCursorAt(0, 1);
+}, { value: ' word1\nword2\n word3' });
+testVim('dk_start_of_document', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 3);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'k');
+ eq(' word1 ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 3);
+}, { value: ' word1 ' });
+testVim('dw_space', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 0);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'w');
+ eq('word1 ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq(' ', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+}, { value: ' word1 ' });
+testVim('dw_word', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 1);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'w');
+ eq(' word2', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1 ', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+}, { value: ' word1 word2' });
+testVim('dw_only_word', function(cm, vim, helpers) {
+ // Test that if there is only 1 word left, dw deletes till the end of the
+ // line.
+ cm.setCursor(0, 1);
+ helpers.doKeys('d', 'w');
+ eq(' ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1 ', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 1);
+}, { value: ' word1 ' });
+testVim('dw_eol', function(cm, vim, helpers) {
+ // Assert that dw does not delete the newline if last word to delete is at end
+ // of line.
+ cm.setCursor(0, 1);
+ helpers.doKeys('d', 'w');
+ eq(' \nword2', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 1);
+}, { value: ' word1\nword2' });
+testVim('dw_eol_with_multiple_newlines', function(cm, vim, helpers) {
+ // Assert that dw does not delete the newline if last word to delete is at end
+ // of line and it is followed by multiple newlines.
+ cm.setCursor(0, 1);
+ helpers.doKeys('d', 'w');
+ eq(' \n\nword2', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 1);
+}, { value: ' word1\n\nword2' });
+testVim('dw_empty_line_followed_by_whitespace', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'w');
+ eq(' \nword', cm.getValue());
+}, { value: '\n \nword' });
+testVim('dw_empty_line_followed_by_word', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'w');
+ eq('word', cm.getValue());
+}, { value: '\nword' });
+testVim('dw_empty_line_followed_by_empty_line', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'w');
+ eq('\n', cm.getValue());
+}, { value: '\n\n' });
+testVim('dw_whitespace_followed_by_whitespace', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'w');
+ eq('\n \n', cm.getValue());
+}, { value: ' \n \n' });
+testVim('dw_whitespace_followed_by_empty_line', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'w');
+ eq('\n\n', cm.getValue());
+}, { value: ' \n\n' });
+testVim('dw_word_whitespace_word', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'w');
+ eq('\n \nword2', cm.getValue());
+}, { value: 'word1\n \nword2'})
+testVim('dw_end_of_document', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('d', 'w');
+ eq('\nab', cm.getValue());
+}, { value: '\nabc' });
+testVim('dw_repeat', function(cm, vim, helpers) {
+ // Assert that dw does delete newline if it should go to the next line, and
+ // that repeat works properly.
+ cm.setCursor(0, 1);
+ helpers.doKeys('d', '2', 'w');
+ eq(' ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1\nword2', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 1);
+}, { value: ' word1\nword2' });
+testVim('de_word_start_and_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'e');
+ eq('\n\n', cm.getValue());
+}, { value: 'word\n\n' });
+testVim('de_word_end_and_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ helpers.doKeys('d', 'e');
+ eq('wor', cm.getValue());
+}, { value: 'word\n\n\n' });
+testVim('de_whitespace_and_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'e');
+ eq('', cm.getValue());
+}, { value: ' \n\n\n' });
+testVim('de_end_of_document', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('d', 'e');
+ eq('\nab', cm.getValue());
+}, { value: '\nabc' });
+testVim('db_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(2, 0);
+ helpers.doKeys('d', 'b');
+ eq('\n\n', cm.getValue());
+}, { value: '\n\n\n' });
+testVim('db_word_start_and_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(2, 0);
+ helpers.doKeys('d', 'b');
+ eq('\nword', cm.getValue());
+}, { value: '\n\nword' });
+testVim('db_word_end_and_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(2, 3);
+ helpers.doKeys('d', 'b');
+ eq('\n\nd', cm.getValue());
+}, { value: '\n\nword' });
+testVim('db_whitespace_and_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(2, 0);
+ helpers.doKeys('d', 'b');
+ eq('', cm.getValue());
+}, { value: '\n \n' });
+testVim('db_start_of_document', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'b');
+ eq('abc\n', cm.getValue());
+}, { value: 'abc\n' });
+testVim('dge_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(1, 0);
+ helpers.doKeys('d', 'g', 'e');
+ // Note: In real VIM the result should be '', but it's not quite consistent,
+ // since 2 newlines are deleted. But in the similar case of word\n\n, only
+ // 1 newline is deleted. We'll diverge from VIM's behavior since it's much
+ // easier this way.
+ eq('\n', cm.getValue());
+}, { value: '\n\n' });
+testVim('dge_word_and_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(1, 0);
+ helpers.doKeys('d', 'g', 'e');
+ eq('wor\n', cm.getValue());
+}, { value: 'word\n\n'});
+testVim('dge_whitespace_and_empty_lines', function(cm, vim, helpers) {
+ cm.setCursor(2, 0);
+ helpers.doKeys('d', 'g', 'e');
+ eq('', cm.getValue());
+}, { value: '\n \n' });
+testVim('dge_start_of_document', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', 'g', 'e');
+ eq('bc\n', cm.getValue());
+}, { value: 'abc\n' });
+testVim('d_inclusive', function(cm, vim, helpers) {
+ // Assert that when inclusive is set, the character the cursor is on gets
+ // deleted too.
+ var curStart = makeCursor(0, 1);
+ cm.setCursor(curStart);
+ helpers.doKeys('d', 'e');
+ eq(' ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+}, { value: ' word1 ' });
+testVim('d_reverse', function(cm, vim, helpers) {
+ // Test that deleting in reverse works.
+ cm.setCursor(1, 0);
+ helpers.doKeys('d', 'b');
+ eq(' word2 ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1\n', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 1);
+}, { value: ' word1\nword2 ' });
+testVim('dd', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ var expectedBuffer = cm.getRange({ line: 0, ch: 0 },
+ { line: 1, ch: 0 });
+ var expectedLineCount = cm.lineCount() - 1;
+ helpers.doKeys('d', 'd');
+ eq(expectedLineCount, cm.lineCount());
+ var register = helpers.getRegisterController().getRegister();
+ eq(expectedBuffer, register.toString());
+ is(register.linewise);
+ helpers.assertCursorAt(0, lines[1].textStart);
+});
+testVim('dd_prefix_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ var expectedBuffer = cm.getRange({ line: 0, ch: 0 },
+ { line: 2, ch: 0 });
+ var expectedLineCount = cm.lineCount() - 2;
+ helpers.doKeys('2', 'd', 'd');
+ eq(expectedLineCount, cm.lineCount());
+ var register = helpers.getRegisterController().getRegister();
+ eq(expectedBuffer, register.toString());
+ is(register.linewise);
+ helpers.assertCursorAt(0, lines[2].textStart);
+});
+testVim('dd_motion_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ var expectedBuffer = cm.getRange({ line: 0, ch: 0 },
+ { line: 2, ch: 0 });
+ var expectedLineCount = cm.lineCount() - 2;
+ helpers.doKeys('d', '2', 'd');
+ eq(expectedLineCount, cm.lineCount());
+ var register = helpers.getRegisterController().getRegister();
+ eq(expectedBuffer, register.toString());
+ is(register.linewise);
+ helpers.assertCursorAt(0, lines[2].textStart);
+});
+testVim('dd_multiply_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ var expectedBuffer = cm.getRange({ line: 0, ch: 0 },
+ { line: 6, ch: 0 });
+ var expectedLineCount = cm.lineCount() - 6;
+ helpers.doKeys('2', 'd', '3', 'd');
+ eq(expectedLineCount, cm.lineCount());
+ var register = helpers.getRegisterController().getRegister();
+ eq(expectedBuffer, register.toString());
+ is(register.linewise);
+ helpers.assertCursorAt(0, lines[6].textStart);
+});
+testVim('dd_lastline', function(cm, vim, helpers) {
+ cm.setCursor(cm.lineCount(), 0);
+ var expectedLineCount = cm.lineCount() - 1;
+ helpers.doKeys('d', 'd');
+ eq(expectedLineCount, cm.lineCount());
+ helpers.assertCursorAt(cm.lineCount() - 1, 0);
+});
+testVim('dd_only_line', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ var expectedRegister = cm.getValue() + "\n";
+ helpers.doKeys('d','d');
+ eq(1, cm.lineCount());
+ eq('', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq(expectedRegister, register.toString());
+}, { value: "thisistheonlyline" });
+// Yank commands should behave the exact same as d commands, expect that nothing
+// gets deleted.
+testVim('yw_repeat', function(cm, vim, helpers) {
+ // Assert that yw does yank newline if it should go to the next line, and
+ // that repeat works properly.
+ var curStart = makeCursor(0, 1);
+ cm.setCursor(curStart);
+ helpers.doKeys('y', '2', 'w');
+ eq(' word1\nword2', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1\nword2', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+}, { value: ' word1\nword2' });
+testVim('yy_multiply_repeat', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 3);
+ cm.setCursor(curStart);
+ var expectedBuffer = cm.getRange({ line: 0, ch: 0 },
+ { line: 6, ch: 0 });
+ var expectedLineCount = cm.lineCount();
+ helpers.doKeys('2', 'y', '3', 'y');
+ eq(expectedLineCount, cm.lineCount());
+ var register = helpers.getRegisterController().getRegister();
+ eq(expectedBuffer, register.toString());
+ is(register.linewise);
+ eqPos(curStart, cm.getCursor());
+});
+// Change commands behave like d commands except that it also enters insert
+// mode. In addition, when the change is linewise, an additional newline is
+// inserted so that insert mode starts on that line.
+testVim('cw', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('c', '2', 'w');
+ eq(' word3', cm.getValue());
+ helpers.assertCursorAt(0, 0);
+}, { value: 'word1 word2 word3'});
+testVim('cw_repeat', function(cm, vim, helpers) {
+ // Assert that cw does delete newline if it should go to the next line, and
+ // that repeat works properly.
+ var curStart = makeCursor(0, 1);
+ cm.setCursor(curStart);
+ helpers.doKeys('c', '2', 'w');
+ eq(' ', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('word1\nword2', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+ eq('vim-insert', cm.getOption('keyMap'));
+}, { value: ' word1\nword2' });
+testVim('cc_multiply_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ var expectedBuffer = cm.getRange({ line: 0, ch: 0 },
+ { line: 6, ch: 0 });
+ var expectedLineCount = cm.lineCount() - 5;
+ helpers.doKeys('2', 'c', '3', 'c');
+ eq(expectedLineCount, cm.lineCount());
+ var register = helpers.getRegisterController().getRegister();
+ eq(expectedBuffer, register.toString());
+ is(register.linewise);
+ eq('vim-insert', cm.getOption('keyMap'));
+});
+testVim('cc_append', function(cm, vim, helpers) {
+ var expectedLineCount = cm.lineCount();
+ cm.setCursor(cm.lastLine(), 0);
+ helpers.doKeys('c', 'c');
+ eq(expectedLineCount, cm.lineCount());
+});
+testVim('c_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('', '2', 'j', 'l', 'l', 'l', 'c');
+ var replacement = new Array(cm.listSelections().length+1).join('hello ').split(' ');
+ replacement.pop();
+ cm.replaceSelections(replacement);
+ eq('1hello\n5hello\nahellofg', cm.getValue());
+ helpers.doKeys('');
+ cm.setCursor(2, 3);
+ helpers.doKeys('', '2', 'k', 'h', 'C');
+ replacement = new Array(cm.listSelections().length+1).join('world ').split(' ');
+ replacement.pop();
+ cm.replaceSelections(replacement);
+ eq('1hworld\n5hworld\nahworld', cm.getValue());
+}, {value: '1234\n5678\nabcdefg'});
+testVim('c_visual_block_replay', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('', '2', 'j', 'l', 'c');
+ var replacement = new Array(cm.listSelections().length+1).join('fo ').split(' ');
+ replacement.pop();
+ cm.replaceSelections(replacement);
+ eq('1fo4\n5fo8\nafodefg', cm.getValue());
+ helpers.doKeys('');
+ cm.setCursor(0, 0);
+ helpers.doKeys('.');
+ eq('foo4\nfoo8\nfoodefg', cm.getValue());
+}, {value: '1234\n5678\nabcdefg'});
+
+// Swapcase commands edit in place and do not modify registers.
+testVim('g~w_repeat', function(cm, vim, helpers) {
+ // Assert that dw does delete newline if it should go to the next line, and
+ // that repeat works properly.
+ var curStart = makeCursor(0, 1);
+ cm.setCursor(curStart);
+ helpers.doKeys('g', '~', '2', 'w');
+ eq(' WORD1\nWORD2', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+}, { value: ' word1\nword2' });
+testVim('g~g~', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 3);
+ cm.setCursor(curStart);
+ var expectedLineCount = cm.lineCount();
+ var expectedValue = cm.getValue().toUpperCase();
+ helpers.doKeys('2', 'g', '~', '3', 'g', '~');
+ eq(expectedValue, cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('', register.toString());
+ is(!register.linewise);
+ eqPos({line: curStart.line, ch:0}, cm.getCursor());
+}, { value: ' word1\nword2\nword3\nword4\nword5\nword6' });
+testVim('gu_and_gU', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 7);
+ var value = cm.getValue();
+ cm.setCursor(curStart);
+ helpers.doKeys('2', 'g', 'U', 'w');
+ eq(cm.getValue(), 'wa wb xX WC wd');
+ eqPos(curStart, cm.getCursor());
+ helpers.doKeys('2', 'g', 'u', 'w');
+ eq(cm.getValue(), value);
+
+ helpers.doKeys('2', 'g', 'U', 'B');
+ eq(cm.getValue(), 'wa WB Xx wc wd');
+ eqPos(makeCursor(0, 3), cm.getCursor());
+
+ cm.setCursor(makeCursor(0, 4));
+ helpers.doKeys('g', 'u', 'i', 'w');
+ eq(cm.getValue(), 'wa wb Xx wc wd');
+ eqPos(makeCursor(0, 3), cm.getCursor());
+
+ // TODO: support gUgU guu
+ // eqPos(makeCursor(0, 0), cm.getCursor());
+
+ var register = helpers.getRegisterController().getRegister();
+ eq('', register.toString());
+ is(!register.linewise);
+}, { value: 'wa wb xx wc wd' });
+testVim('visual_block_~', function(cm, vim, helpers) {
+ cm.setCursor(1, 1);
+ helpers.doKeys('', 'l', 'l', 'j', '~');
+ helpers.assertCursorAt(1, 1);
+ eq('hello\nwoRLd\naBCDe', cm.getValue());
+ cm.setCursor(2, 0);
+ helpers.doKeys('v', 'l', 'l', '~');
+ helpers.assertCursorAt(2, 0);
+ eq('hello\nwoRLd\nAbcDe', cm.getValue());
+},{value: 'hello\nwOrld\nabcde' });
+testVim('._swapCase_visualBlock', function(cm, vim, helpers) {
+ helpers.doKeys('', 'j', 'j', 'l', '~');
+ cm.setCursor(0, 3);
+ helpers.doKeys('.');
+ eq('HelLO\nWorLd\nAbcdE', cm.getValue());
+},{value: 'hEllo\nwOrlD\naBcDe' });
+testVim('._delete_visualBlock', function(cm, vim, helpers) {
+ helpers.doKeys('', 'j', 'x');
+ eq('ive\ne\nsome\nsugar', cm.getValue());
+ helpers.doKeys('.');
+ eq('ve\n\nsome\nsugar', cm.getValue());
+ helpers.doKeys('j', 'j', '.');
+ eq('ve\n\nome\nugar', cm.getValue());
+ helpers.doKeys('u', '', '.');
+ eq('ve\n\nme\ngar', cm.getValue());
+},{value: 'give\nme\nsome\nsugar' });
+testVim('>{motion}', function(cm, vim, helpers) {
+ cm.setCursor(1, 3);
+ var expectedLineCount = cm.lineCount();
+ var expectedValue = ' word1\n word2\nword3 ';
+ helpers.doKeys('>', 'k');
+ eq(expectedValue, cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 3);
+}, { value: ' word1\nword2\nword3 ', indentUnit: 2 });
+testVim('>>', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ var expectedLineCount = cm.lineCount();
+ var expectedValue = ' word1\n word2\nword3 ';
+ helpers.doKeys('2', '>', '>');
+ eq(expectedValue, cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 3);
+}, { value: ' word1\nword2\nword3 ', indentUnit: 2 });
+testVim('<{motion}', function(cm, vim, helpers) {
+ cm.setCursor(1, 3);
+ var expectedLineCount = cm.lineCount();
+ var expectedValue = ' word1\nword2\nword3 ';
+ helpers.doKeys('<', 'k');
+ eq(expectedValue, cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 1);
+}, { value: ' word1\n word2\nword3 ', indentUnit: 2 });
+testVim('<<', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ var expectedLineCount = cm.lineCount();
+ var expectedValue = ' word1\nword2\nword3 ';
+ helpers.doKeys('2', '<', '<');
+ eq(expectedValue, cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 1);
+}, { value: ' word1\n word2\nword3 ', indentUnit: 2 });
+
+// Edit tests
+function testEdit(name, before, pos, edit, after) {
+ return testVim(name, function(cm, vim, helpers) {
+ var ch = before.search(pos)
+ var line = before.substring(0, ch).split('\n').length - 1;
+ if (line) {
+ ch = before.substring(0, ch).split('\n').pop().length;
+ }
+ cm.setCursor(line, ch);
+ helpers.doKeys.apply(this, edit.split(''));
+ eq(after, cm.getValue());
+ }, {value: before});
+}
+
+// These Delete tests effectively cover word-wise Change, Visual & Yank.
+// Tabs are used as differentiated whitespace to catch edge cases.
+// Normal word:
+testEdit('diw_mid_spc', 'foo \tbAr\t baz', /A/, 'diw', 'foo \t\t baz');
+testEdit('daw_mid_spc', 'foo \tbAr\t baz', /A/, 'daw', 'foo \tbaz');
+testEdit('diw_mid_punct', 'foo \tbAr.\t baz', /A/, 'diw', 'foo \t.\t baz');
+testEdit('daw_mid_punct', 'foo \tbAr.\t baz', /A/, 'daw', 'foo.\t baz');
+testEdit('diw_mid_punct2', 'foo \t,bAr.\t baz', /A/, 'diw', 'foo \t,.\t baz');
+testEdit('daw_mid_punct2', 'foo \t,bAr.\t baz', /A/, 'daw', 'foo \t,.\t baz');
+testEdit('diw_start_spc', 'bAr \tbaz', /A/, 'diw', ' \tbaz');
+testEdit('daw_start_spc', 'bAr \tbaz', /A/, 'daw', 'baz');
+testEdit('diw_start_punct', 'bAr. \tbaz', /A/, 'diw', '. \tbaz');
+testEdit('daw_start_punct', 'bAr. \tbaz', /A/, 'daw', '. \tbaz');
+testEdit('diw_end_spc', 'foo \tbAr', /A/, 'diw', 'foo \t');
+testEdit('daw_end_spc', 'foo \tbAr', /A/, 'daw', 'foo');
+testEdit('diw_end_punct', 'foo \tbAr.', /A/, 'diw', 'foo \t.');
+testEdit('daw_end_punct', 'foo \tbAr.', /A/, 'daw', 'foo.');
+// Big word:
+testEdit('diW_mid_spc', 'foo \tbAr\t baz', /A/, 'diW', 'foo \t\t baz');
+testEdit('daW_mid_spc', 'foo \tbAr\t baz', /A/, 'daW', 'foo \tbaz');
+testEdit('diW_mid_punct', 'foo \tbAr.\t baz', /A/, 'diW', 'foo \t\t baz');
+testEdit('daW_mid_punct', 'foo \tbAr.\t baz', /A/, 'daW', 'foo \tbaz');
+testEdit('diW_mid_punct2', 'foo \t,bAr.\t baz', /A/, 'diW', 'foo \t\t baz');
+testEdit('daW_mid_punct2', 'foo \t,bAr.\t baz', /A/, 'daW', 'foo \tbaz');
+testEdit('diW_start_spc', 'bAr\t baz', /A/, 'diW', '\t baz');
+testEdit('daW_start_spc', 'bAr\t baz', /A/, 'daW', 'baz');
+testEdit('diW_start_punct', 'bAr.\t baz', /A/, 'diW', '\t baz');
+testEdit('daW_start_punct', 'bAr.\t baz', /A/, 'daW', 'baz');
+testEdit('diW_end_spc', 'foo \tbAr', /A/, 'diW', 'foo \t');
+testEdit('daW_end_spc', 'foo \tbAr', /A/, 'daW', 'foo');
+testEdit('diW_end_punct', 'foo \tbAr.', /A/, 'diW', 'foo \t');
+testEdit('daW_end_punct', 'foo \tbAr.', /A/, 'daW', 'foo');
+// Deleting text objects
+// Open and close on same line
+testEdit('di(_open_spc', 'foo (bAr) baz', /\(/, 'di(', 'foo () baz');
+testEdit('di)_open_spc', 'foo (bAr) baz', /\(/, 'di)', 'foo () baz');
+testEdit('dib_open_spc', 'foo (bAr) baz', /\(/, 'dib', 'foo () baz');
+testEdit('da(_open_spc', 'foo (bAr) baz', /\(/, 'da(', 'foo baz');
+testEdit('da)_open_spc', 'foo (bAr) baz', /\(/, 'da)', 'foo baz');
+
+testEdit('di(_middle_spc', 'foo (bAr) baz', /A/, 'di(', 'foo () baz');
+testEdit('di)_middle_spc', 'foo (bAr) baz', /A/, 'di)', 'foo () baz');
+testEdit('da(_middle_spc', 'foo (bAr) baz', /A/, 'da(', 'foo baz');
+testEdit('da)_middle_spc', 'foo (bAr) baz', /A/, 'da)', 'foo baz');
+
+testEdit('di(_close_spc', 'foo (bAr) baz', /\)/, 'di(', 'foo () baz');
+testEdit('di)_close_spc', 'foo (bAr) baz', /\)/, 'di)', 'foo () baz');
+testEdit('da(_close_spc', 'foo (bAr) baz', /\)/, 'da(', 'foo baz');
+testEdit('da)_close_spc', 'foo (bAr) baz', /\)/, 'da)', 'foo baz');
+
+// delete around and inner b.
+testEdit('dab_on_(_should_delete_around_()block', 'o( in(abc) )', /\(a/, 'dab', 'o( in )');
+
+// delete around and inner B.
+testEdit('daB_on_{_should_delete_around_{}block', 'o{ in{abc} }', /{a/, 'daB', 'o{ in }');
+testEdit('diB_on_{_should_delete_inner_{}block', 'o{ in{abc} }', /{a/, 'diB', 'o{ in{} }');
+
+testEdit('da{_on_{_should_delete_inner_block', 'o{ in{abc} }', /{a/, 'da{', 'o{ in }');
+testEdit('di[_on_(_should_not_delete', 'foo (bAr) baz', /\(/, 'di[', 'foo (bAr) baz');
+testEdit('di[_on_)_should_not_delete', 'foo (bAr) baz', /\)/, 'di[', 'foo (bAr) baz');
+testEdit('da[_on_(_should_not_delete', 'foo (bAr) baz', /\(/, 'da[', 'foo (bAr) baz');
+testEdit('da[_on_)_should_not_delete', 'foo (bAr) baz', /\)/, 'da[', 'foo (bAr) baz');
+testMotion('di(_outside_should_stay', ['d', 'i', '('], { line: 0, ch: 0}, { line: 0, ch: 0});
+
+// Open and close on different lines, equally indented
+testEdit('di{_middle_spc', 'a{\n\tbar\n}b', /r/, 'di{', 'a{}b');
+testEdit('di}_middle_spc', 'a{\n\tbar\n}b', /r/, 'di}', 'a{}b');
+testEdit('da{_middle_spc', 'a{\n\tbar\n}b', /r/, 'da{', 'ab');
+testEdit('da}_middle_spc', 'a{\n\tbar\n}b', /r/, 'da}', 'ab');
+testEdit('daB_middle_spc', 'a{\n\tbar\n}b', /r/, 'daB', 'ab');
+
+// open and close on diff lines, open indented less than close
+testEdit('di{_middle_spc', 'a{\n\tbar\n\t}b', /r/, 'di{', 'a{}b');
+testEdit('di}_middle_spc', 'a{\n\tbar\n\t}b', /r/, 'di}', 'a{}b');
+testEdit('da{_middle_spc', 'a{\n\tbar\n\t}b', /r/, 'da{', 'ab');
+testEdit('da}_middle_spc', 'a{\n\tbar\n\t}b', /r/, 'da}', 'ab');
+
+// open and close on diff lines, open indented more than close
+testEdit('di[_middle_spc', 'a\t[\n\tbar\n]b', /r/, 'di[', 'a\t[]b');
+testEdit('di]_middle_spc', 'a\t[\n\tbar\n]b', /r/, 'di]', 'a\t[]b');
+testEdit('da[_middle_spc', 'a\t[\n\tbar\n]b', /r/, 'da[', 'a\tb');
+testEdit('da]_middle_spc', 'a\t[\n\tbar\n]b', /r/, 'da]', 'a\tb');
+
+function testSelection(name, before, pos, keys, sel) {
+ return testVim(name, function(cm, vim, helpers) {
+ var ch = before.search(pos)
+ var line = before.substring(0, ch).split('\n').length - 1;
+ if (line) {
+ ch = before.substring(0, ch).split('\n').pop().length;
+ }
+ cm.setCursor(line, ch);
+ helpers.doKeys.apply(this, keys.split(''));
+ eq(sel, cm.getSelection());
+ }, {value: before});
+}
+testSelection('viw_middle_spc', 'foo \tbAr\t baz', /A/, 'viw', 'bAr');
+testSelection('vaw_middle_spc', 'foo \tbAr\t baz', /A/, 'vaw', 'bAr\t ');
+testSelection('viw_middle_punct', 'foo \tbAr,\t baz', /A/, 'viw', 'bAr');
+testSelection('vaW_middle_punct', 'foo \tbAr,\t baz', /A/, 'vaW', 'bAr,\t ');
+testSelection('viw_start_spc', 'foo \tbAr\t baz', /b/, 'viw', 'bAr');
+testSelection('viw_end_spc', 'foo \tbAr\t baz', /r/, 'viw', 'bAr');
+testSelection('viw_eol', 'foo \tbAr', /r/, 'viw', 'bAr');
+testSelection('vi{_middle_spc', 'a{\n\tbar\n\t}b', /r/, 'vi{', '\n\tbar\n\t');
+testSelection('va{_middle_spc', 'a{\n\tbar\n\t}b', /r/, 'va{', '{\n\tbar\n\t}');
+
+testVim('mouse_select', function(cm, vim, helpers) {
+ cm.setSelection(Pos(0, 2), Pos(0, 4), {origin: '*mouse'});
+ is(cm.state.vim.visualMode);
+ is(!cm.state.vim.visualLine);
+ is(!cm.state.vim.visualBlock);
+ helpers.doKeys('');
+ is(!cm.somethingSelected());
+ helpers.doKeys('g', 'v');
+ eq('cd', cm.getSelection());
+}, {value: 'abcdef'});
+
+// Operator-motion tests
+testVim('D', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ helpers.doKeys('D');
+ eq(' wo\nword2\n word3', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('rd1', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 3);
+}, { value: ' word1\nword2\n word3' });
+testVim('C', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 3);
+ cm.setCursor(curStart);
+ helpers.doKeys('C');
+ eq(' wo\nword2\n word3', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('rd1', register.toString());
+ is(!register.linewise);
+ eqPos(curStart, cm.getCursor());
+ eq('vim-insert', cm.getOption('keyMap'));
+}, { value: ' word1\nword2\n word3' });
+testVim('Y', function(cm, vim, helpers) {
+ var curStart = makeCursor(0, 3);
+ cm.setCursor(curStart);
+ helpers.doKeys('Y');
+ eq(' word1\nword2\n word3', cm.getValue());
+ var register = helpers.getRegisterController().getRegister();
+ eq('rd1', register.toString());
+ is(!register.linewise);
+ helpers.assertCursorAt(0, 3);
+}, { value: ' word1\nword2\n word3' });
+testVim('~', function(cm, vim, helpers) {
+ helpers.doKeys('3', '~');
+ eq('ABCdefg', cm.getValue());
+ helpers.assertCursorAt(0, 3);
+}, { value: 'abcdefg' });
+
+// Action tests
+testVim('ctrl-a', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('');
+ eq('-9', cm.getValue());
+ helpers.assertCursorAt(0, 1);
+ helpers.doKeys('2','');
+ eq('-7', cm.getValue());
+}, {value: '-10'});
+testVim('ctrl-x', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('');
+ eq('-1', cm.getValue());
+ helpers.assertCursorAt(0, 1);
+ helpers.doKeys('2','');
+ eq('-3', cm.getValue());
+}, {value: '0'});
+testVim('/ search forward', function(cm, vim, helpers) {
+ forEach(['', ''], function(key) {
+ cm.setCursor(0, 0);
+ helpers.doKeys(key);
+ helpers.assertCursorAt(0, 5);
+ helpers.doKeys('l');
+ helpers.doKeys(key);
+ helpers.assertCursorAt(0, 10);
+ cm.setCursor(0, 11);
+ helpers.doKeys(key);
+ helpers.assertCursorAt(0, 11);
+ });
+}, {value: '__jmp1 jmp2 jmp'});
+testVim('a', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('a');
+ helpers.assertCursorAt(0, 2);
+ eq('vim-insert', cm.getOption('keyMap'));
+});
+testVim('a_eol', function(cm, vim, helpers) {
+ cm.setCursor(0, lines[0].length - 1);
+ helpers.doKeys('a');
+ helpers.assertCursorAt(0, lines[0].length);
+ eq('vim-insert', cm.getOption('keyMap'));
+});
+testVim('a_endOfSelectedArea', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('v', 'j', 'l');
+ helpers.doKeys('A');
+ helpers.assertCursorAt(1, 2);
+ eq('vim-insert', cm.getOption('keyMap'));
+}, {value: 'foo\nbar'});
+testVim('i', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('i');
+ helpers.assertCursorAt(0, 1);
+ eq('vim-insert', cm.getOption('keyMap'));
+});
+testVim('i_repeat', function(cm, vim, helpers) {
+ helpers.doKeys('3', 'i');
+ cm.replaceRange('test', cm.getCursor());
+ helpers.doKeys('');
+ eq('testtesttest', cm.getValue());
+ helpers.assertCursorAt(0, 11);
+}, { value: '' });
+testVim('i_repeat_delete', function(cm, vim, helpers) {
+ cm.setCursor(0, 4);
+ helpers.doKeys('2', 'i');
+ cm.replaceRange('z', cm.getCursor());
+ helpers.doInsertModeKeys('Backspace', 'Backspace');
+ helpers.doKeys('');
+ eq('abe', cm.getValue());
+ helpers.assertCursorAt(0, 1);
+}, { value: 'abcde' });
+testVim('A', function(cm, vim, helpers) {
+ helpers.doKeys('A');
+ helpers.assertCursorAt(0, lines[0].length);
+ eq('vim-insert', cm.getOption('keyMap'));
+});
+testVim('A_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('', '2', 'j', 'l', 'l', 'A');
+ var replacement = new Array(cm.listSelections().length+1).join('hello ').split(' ');
+ replacement.pop();
+ cm.replaceSelections(replacement);
+ eq('testhello\nmehello\npleahellose', cm.getValue());
+ helpers.doKeys('');
+ cm.setCursor(0, 0);
+ helpers.doKeys('.');
+ // TODO this doesn't work yet
+ // eq('teshellothello\nme hello hello\nplehelloahellose', cm.getValue());
+}, {value: 'test\nme\nplease'});
+testVim('I', function(cm, vim, helpers) {
+ cm.setCursor(0, 4);
+ helpers.doKeys('I');
+ helpers.assertCursorAt(0, lines[0].textStart);
+ eq('vim-insert', cm.getOption('keyMap'));
+});
+testVim('I_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('3', 'I');
+ cm.replaceRange('test', cm.getCursor());
+ helpers.doKeys('');
+ eq('testtesttestblah', cm.getValue());
+ helpers.assertCursorAt(0, 11);
+}, { value: 'blah' });
+testVim('I_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('', '2', 'j', 'l', 'l', 'I');
+ var replacement = new Array(cm.listSelections().length+1).join('hello ').split(' ');
+ replacement.pop();
+ cm.replaceSelections(replacement);
+ eq('hellotest\nhellome\nhelloplease', cm.getValue());
+}, {value: 'test\nme\nplease'});
+testVim('o', function(cm, vim, helpers) {
+ cm.setCursor(0, 4);
+ helpers.doKeys('o');
+ eq('word1\n\nword2', cm.getValue());
+ helpers.assertCursorAt(1, 0);
+ eq('vim-insert', cm.getOption('keyMap'));
+}, { value: 'word1\nword2' });
+testVim('o_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('3', 'o');
+ cm.replaceRange('test', cm.getCursor());
+ helpers.doKeys('');
+ eq('\ntest\ntest\ntest', cm.getValue());
+ helpers.assertCursorAt(3, 3);
+}, { value: '' });
+testVim('O', function(cm, vim, helpers) {
+ cm.setCursor(0, 4);
+ helpers.doKeys('O');
+ eq('\nword1\nword2', cm.getValue());
+ helpers.assertCursorAt(0, 0);
+ eq('vim-insert', cm.getOption('keyMap'));
+}, { value: 'word1\nword2' });
+testVim('J', function(cm, vim, helpers) {
+ cm.setCursor(0, 4);
+ helpers.doKeys('J');
+ var expectedValue = 'word1 word2\nword3\n word4';
+ eq(expectedValue, cm.getValue());
+ helpers.assertCursorAt(0, expectedValue.indexOf('word2') - 1);
+}, { value: 'word1 \n word2\nword3\n word4' });
+testVim('J_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 4);
+ helpers.doKeys('3', 'J');
+ var expectedValue = 'word1 word2 word3\n word4';
+ eq(expectedValue, cm.getValue());
+ helpers.assertCursorAt(0, expectedValue.indexOf('word3') - 1);
+}, { value: 'word1 \n word2\nword3\n word4' });
+testVim('p', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.getRegisterController().pushText('"', 'yank', 'abc\ndef', false);
+ helpers.doKeys('p');
+ eq('__abc\ndef_', cm.getValue());
+ helpers.assertCursorAt(1, 2);
+}, { value: '___' });
+testVim('p_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.getRegisterController().getRegister('a').setText('abc\ndef', false);
+ helpers.doKeys('"', 'a', 'p');
+ eq('__abc\ndef_', cm.getValue());
+ helpers.assertCursorAt(1, 2);
+}, { value: '___' });
+testVim('p_wrong_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.getRegisterController().getRegister('a').setText('abc\ndef', false);
+ helpers.doKeys('p');
+ eq('___', cm.getValue());
+ helpers.assertCursorAt(0, 1);
+}, { value: '___' });
+testVim('p_line', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.getRegisterController().pushText('"', 'yank', ' a\nd\n', true);
+ helpers.doKeys('2', 'p');
+ eq('___\n a\nd\n a\nd', cm.getValue());
+ helpers.assertCursorAt(1, 2);
+}, { value: '___' });
+testVim('p_lastline', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.getRegisterController().pushText('"', 'yank', ' a\nd', true);
+ helpers.doKeys('2', 'p');
+ eq('___\n a\nd\n a\nd', cm.getValue());
+ helpers.assertCursorAt(1, 2);
+}, { value: '___' });
+testVim(']p_first_indent_is_smaller', function(cm, vim, helpers) {
+ helpers.getRegisterController().pushText('"', 'yank', ' abc\n def\n', true);
+ helpers.doKeys(']', 'p');
+ eq(' ___\n abc\n def', cm.getValue());
+}, { value: ' ___' });
+testVim(']p_first_indent_is_larger', function(cm, vim, helpers) {
+ helpers.getRegisterController().pushText('"', 'yank', ' abc\n def\n', true);
+ helpers.doKeys(']', 'p');
+ eq(' ___\n abc\ndef', cm.getValue());
+}, { value: ' ___' });
+testVim(']p_with_tab_indents', function(cm, vim, helpers) {
+ helpers.getRegisterController().pushText('"', 'yank', '\t\tabc\n\t\t\tdef\n', true);
+ helpers.doKeys(']', 'p');
+ eq('\t___\n\tabc\n\t\tdef', cm.getValue());
+}, { value: '\t___', indentWithTabs: true});
+testVim(']p_with_spaces_translated_to_tabs', function(cm, vim, helpers) {
+ helpers.getRegisterController().pushText('"', 'yank', ' abc\n def\n', true);
+ helpers.doKeys(']', 'p');
+ eq('\t___\n\tabc\n\t\tdef', cm.getValue());
+}, { value: '\t___', indentWithTabs: true, tabSize: 2 });
+testVim('[p', function(cm, vim, helpers) {
+ helpers.getRegisterController().pushText('"', 'yank', ' abc\n def\n', true);
+ helpers.doKeys('[', 'p');
+ eq(' abc\n def\n ___', cm.getValue());
+}, { value: ' ___' });
+testVim('P', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.getRegisterController().pushText('"', 'yank', 'abc\ndef', false);
+ helpers.doKeys('P');
+ eq('_abc\ndef__', cm.getValue());
+ helpers.assertCursorAt(1, 3);
+}, { value: '___' });
+testVim('P_line', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.getRegisterController().pushText('"', 'yank', ' a\nd\n', true);
+ helpers.doKeys('2', 'P');
+ eq(' a\nd\n a\nd\n___', cm.getValue());
+ helpers.assertCursorAt(0, 2);
+}, { value: '___' });
+testVim('r', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('3', 'r', 'u');
+ eq('wuuuet\nanother', cm.getValue(),'3r failed');
+ helpers.assertCursorAt(0, 3);
+ cm.setCursor(0, 4);
+ helpers.doKeys('v', 'j', 'h', 'r', '');
+ eq('wuuu \n her', cm.getValue(),'Replacing selection by space-characters failed');
+}, { value: 'wordet\nanother' });
+testVim('r_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(2, 3);
+ helpers.doKeys('', 'k', 'k', 'h', 'h', 'r', 'l');
+ eq('1lll\n5lll\nalllefg', cm.getValue());
+ helpers.doKeys('', 'l', 'j', 'r', '');
+ eq('1 l\n5 l\nalllefg', cm.getValue());
+ cm.setCursor(2, 0);
+ helpers.doKeys('o');
+ helpers.doKeys('');
+ cm.replaceRange('\t\t', cm.getCursor());
+ helpers.doKeys('', 'h', 'h', 'r', 'r');
+ eq('1 l\n5 l\nalllefg\nrrrrrrrr', cm.getValue());
+}, {value: '1234\n5678\nabcdefg'});
+testVim('R', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('R');
+ helpers.assertCursorAt(0, 1);
+ eq('vim-replace', cm.getOption('keyMap'));
+ is(cm.state.overwrite, 'Setting overwrite state failed');
+});
+testVim('mark', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 't');
+ cm.setCursor(0, 0);
+ helpers.doKeys('`', 't');
+ helpers.assertCursorAt(2, 2);
+ cm.setCursor(2, 0);
+ cm.replaceRange(' h', cm.getCursor());
+ cm.setCursor(0, 0);
+ helpers.doKeys('\'', 't');
+ helpers.assertCursorAt(2, 3);
+});
+testVim('jumpToMark_next', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 't');
+ cm.setCursor(0, 0);
+ helpers.doKeys(']', '`');
+ helpers.assertCursorAt(2, 2);
+ cm.setCursor(0, 0);
+ helpers.doKeys(']', '\'');
+ helpers.assertCursorAt(2, 0);
+});
+testVim('jumpToMark_next_repeat', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(3, 2);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(4, 2);
+ helpers.doKeys('m', 'c');
+ cm.setCursor(0, 0);
+ helpers.doKeys('2', ']', '`');
+ helpers.assertCursorAt(3, 2);
+ cm.setCursor(0, 0);
+ helpers.doKeys('2', ']', '\'');
+ helpers.assertCursorAt(3, 1);
+});
+testVim('jumpToMark_next_sameline', function(cm, vim, helpers) {
+ cm.setCursor(2, 0);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(2, 4);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(2, 2);
+ helpers.doKeys(']', '`');
+ helpers.assertCursorAt(2, 4);
+});
+testVim('jumpToMark_next_onlyprev', function(cm, vim, helpers) {
+ cm.setCursor(2, 0);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(4, 0);
+ helpers.doKeys(']', '`');
+ helpers.assertCursorAt(4, 0);
+});
+testVim('jumpToMark_next_nomark', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys(']', '`');
+ helpers.assertCursorAt(2, 2);
+ helpers.doKeys(']', '\'');
+ helpers.assertCursorAt(2, 0);
+});
+testVim('jumpToMark_next_linewise_over', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(3, 4);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(2, 1);
+ helpers.doKeys(']', '\'');
+ helpers.assertCursorAt(3, 1);
+});
+testVim('jumpToMark_next_action', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 't');
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', ']', '`');
+ helpers.assertCursorAt(0, 0);
+ var actual = cm.getLine(0);
+ var expected = 'pop pop 0 1 2 3 4';
+ eq(actual, expected, "Deleting while jumping to the next mark failed.");
+});
+testVim('jumpToMark_next_line_action', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 't');
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', ']', '\'');
+ helpers.assertCursorAt(0, 1);
+ var actual = cm.getLine(0);
+ var expected = ' (a) [b] {c} '
+ eq(actual, expected, "Deleting while jumping to the next mark line failed.");
+});
+testVim('jumpToMark_prev', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 't');
+ cm.setCursor(4, 0);
+ helpers.doKeys('[', '`');
+ helpers.assertCursorAt(2, 2);
+ cm.setCursor(4, 0);
+ helpers.doKeys('[', '\'');
+ helpers.assertCursorAt(2, 0);
+});
+testVim('jumpToMark_prev_repeat', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(3, 2);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(4, 2);
+ helpers.doKeys('m', 'c');
+ cm.setCursor(5, 0);
+ helpers.doKeys('2', '[', '`');
+ helpers.assertCursorAt(3, 2);
+ cm.setCursor(5, 0);
+ helpers.doKeys('2', '[', '\'');
+ helpers.assertCursorAt(3, 1);
+});
+testVim('jumpToMark_prev_sameline', function(cm, vim, helpers) {
+ cm.setCursor(2, 0);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(2, 4);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(2, 2);
+ helpers.doKeys('[', '`');
+ helpers.assertCursorAt(2, 0);
+});
+testVim('jumpToMark_prev_onlynext', function(cm, vim, helpers) {
+ cm.setCursor(4, 4);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(2, 0);
+ helpers.doKeys('[', '`');
+ helpers.assertCursorAt(2, 0);
+});
+testVim('jumpToMark_prev_nomark', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('[', '`');
+ helpers.assertCursorAt(2, 2);
+ helpers.doKeys('[', '\'');
+ helpers.assertCursorAt(2, 0);
+});
+testVim('jumpToMark_prev_linewise_over', function(cm, vim, helpers) {
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(3, 4);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(3, 6);
+ helpers.doKeys('[', '\'');
+ helpers.assertCursorAt(2, 0);
+});
+testVim('delmark_single', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('m', 't');
+ helpers.doEx('delmarks t');
+ cm.setCursor(0, 0);
+ helpers.doKeys('`', 't');
+ helpers.assertCursorAt(0, 0);
+});
+testVim('delmark_range', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(3, 2);
+ helpers.doKeys('m', 'c');
+ cm.setCursor(4, 2);
+ helpers.doKeys('m', 'd');
+ cm.setCursor(5, 2);
+ helpers.doKeys('m', 'e');
+ helpers.doEx('delmarks b-d');
+ cm.setCursor(0, 0);
+ helpers.doKeys('`', 'a');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'b');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'c');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'd');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'e');
+ helpers.assertCursorAt(5, 2);
+});
+testVim('delmark_multi', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(3, 2);
+ helpers.doKeys('m', 'c');
+ cm.setCursor(4, 2);
+ helpers.doKeys('m', 'd');
+ cm.setCursor(5, 2);
+ helpers.doKeys('m', 'e');
+ helpers.doEx('delmarks bcd');
+ cm.setCursor(0, 0);
+ helpers.doKeys('`', 'a');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'b');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'c');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'd');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'e');
+ helpers.assertCursorAt(5, 2);
+});
+testVim('delmark_multi_space', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(3, 2);
+ helpers.doKeys('m', 'c');
+ cm.setCursor(4, 2);
+ helpers.doKeys('m', 'd');
+ cm.setCursor(5, 2);
+ helpers.doKeys('m', 'e');
+ helpers.doEx('delmarks b c d');
+ cm.setCursor(0, 0);
+ helpers.doKeys('`', 'a');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'b');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'c');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'd');
+ helpers.assertCursorAt(1, 2);
+ helpers.doKeys('`', 'e');
+ helpers.assertCursorAt(5, 2);
+});
+testVim('delmark_all', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('m', 'a');
+ cm.setCursor(2, 2);
+ helpers.doKeys('m', 'b');
+ cm.setCursor(3, 2);
+ helpers.doKeys('m', 'c');
+ cm.setCursor(4, 2);
+ helpers.doKeys('m', 'd');
+ cm.setCursor(5, 2);
+ helpers.doKeys('m', 'e');
+ helpers.doEx('delmarks a b-de');
+ cm.setCursor(0, 0);
+ helpers.doKeys('`', 'a');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('`', 'b');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('`', 'c');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('`', 'd');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('`', 'e');
+ helpers.assertCursorAt(0, 0);
+});
+testVim('visual', function(cm, vim, helpers) {
+ helpers.doKeys('l', 'v', 'l', 'l');
+ helpers.assertCursorAt(0, 4);
+ eqPos(makeCursor(0, 1), cm.getCursor('anchor'));
+ helpers.doKeys('d');
+ eq('15', cm.getValue());
+}, { value: '12345' });
+testVim('visual_exit', function(cm, vim, helpers) {
+ helpers.doKeys('', 'l', 'j', 'j', '');
+ eqPos(cm.getCursor('anchor'), cm.getCursor('head'));
+ eq(vim.visualMode, false);
+}, { value: 'hello\nworld\nfoo' });
+testVim('visual_line', function(cm, vim, helpers) {
+ helpers.doKeys('l', 'V', 'l', 'j', 'j', 'd');
+ eq(' 4\n 5', cm.getValue());
+}, { value: ' 1\n 2\n 3\n 4\n 5' });
+testVim('visual_block', function(cm, vim, helpers) {
+ // test the block selection with lines of different length
+ // i.e. extending the selection
+ // till the end of the longest line.
+ helpers.doKeys('', 'l', 'j', 'j', '6', 'l', 'd');
+ helpers.doKeys('d', 'd', 'd', 'd');
+ eq('', cm.getValue());
+ // check for left side selection in case
+ // of moving up to a shorter line.
+ cm.replaceRange('hello world\n{\nthis is\nsparta!', cm.getCursor());
+ cm.setCursor(3, 4);
+ helpers.doKeys('', 'l', 'k', 'k', 'd');
+ eq('hello world\n{\ntis\nsa!', cm.getValue());
+ cm.replaceRange('12345\n67891\nabcde', {line: 0, ch: 0}, {line: cm.lastLine(), ch: 6});
+ cm.setCursor(1, 2);
+ helpers.doKeys('', '2', 'l', 'k');
+ // circle around the anchor
+ // and check the selections
+ var selections = cm.getSelections();
+ eq('345891', selections.join(''));
+ helpers.doKeys('4', 'h');
+ selections = cm.getSelections();
+ eq('123678', selections.join(''));
+ helpers.doKeys('j', 'j');
+ selections = cm.getSelections();
+ eq('678abc', selections.join(''));
+ helpers.doKeys('4', 'l');
+ selections = cm.getSelections();
+ eq('891cde', selections.join(''));
+ // switch between visual modes
+ cm.setCursor(1, 1);
+ // blockwise to characterwise visual
+ helpers.doKeys('', 'j', 'l', 'v');
+ selections = cm.getSelections();
+ eq('7891\nabc', selections.join(''));
+ // characterwise to blockwise
+ helpers.doKeys('');
+ selections = cm.getSelections();
+ eq('78bc', selections.join(''));
+ // blockwise to linewise visual
+ helpers.doKeys('V');
+ selections = cm.getSelections();
+ eq('67891\nabcde', selections.join(''));
+}, {value: '1234\n5678\nabcdefg'});
+testVim('visual_block_crossing_short_line', function(cm, vim, helpers) {
+ // visual block with long and short lines
+ cm.setCursor(0, 3);
+ helpers.doKeys('', 'j', 'j', 'j');
+ var selections = cm.getSelections().join();
+ eq('4,,d,b', selections);
+ helpers.doKeys('3', 'k');
+ selections = cm.getSelections().join();
+ eq('4', selections);
+ helpers.doKeys('5', 'j', 'k');
+ selections = cm.getSelections().join("");
+ eq(10, selections.length);
+}, {value: '123456\n78\nabcdefg\nfoobar\n}\n'});
+testVim('visual_block_curPos_on_exit', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('', '3' , 'l', '');
+ eqPos(makeCursor(0, 3), cm.getCursor());
+ helpers.doKeys('h', '', '2' , 'j' ,'3' , 'l');
+ eq(cm.getSelections().join(), "3456,,cdef");
+ helpers.doKeys('4' , 'h');
+ eq(cm.getSelections().join(), "23,8,bc");
+ helpers.doKeys('2' , 'l');
+ eq(cm.getSelections().join(), "34,,cd");
+}, {value: '123456\n78\nabcdefg\nfoobar'});
+
+testVim('visual_marks', function(cm, vim, helpers) {
+ helpers.doKeys('l', 'v', 'l', 'l', 'j', 'j', 'v');
+ // Test visual mode marks
+ cm.setCursor(2, 1);
+ helpers.doKeys('\'', '<');
+ helpers.assertCursorAt(0, 1);
+ helpers.doKeys('\'', '>');
+ helpers.assertCursorAt(2, 0);
+});
+testVim('visual_join', function(cm, vim, helpers) {
+ helpers.doKeys('l', 'V', 'l', 'j', 'j', 'J');
+ eq(' 1 2 3\n 4\n 5', cm.getValue());
+ is(!vim.visualMode);
+}, { value: ' 1\n 2\n 3\n 4\n 5' });
+testVim('visual_blank', function(cm, vim, helpers) {
+ helpers.doKeys('v', 'k');
+ eq(vim.visualMode, true);
+}, { value: '\n' });
+testVim('reselect_visual', function(cm, vim, helpers) {
+ helpers.doKeys('l', 'v', 'l', 'l', 'l', 'y', 'g', 'v');
+ helpers.assertCursorAt(0, 5);
+ eqPos(makeCursor(0, 1), cm.getCursor('anchor'));
+ helpers.doKeys('v');
+ cm.setCursor(1, 0);
+ helpers.doKeys('v', 'l', 'l', 'p');
+ eq('123456\n2345\nbar', cm.getValue());
+ cm.setCursor(0, 0);
+ helpers.doKeys('g', 'v');
+ // here the fake cursor is at (1, 3)
+ helpers.assertCursorAt(1, 4);
+ eqPos(makeCursor(1, 0), cm.getCursor('anchor'));
+ helpers.doKeys('v');
+ cm.setCursor(2, 0);
+ helpers.doKeys('v', 'l', 'l', 'g', 'v');
+ helpers.assertCursorAt(1, 4);
+ eqPos(makeCursor(1, 0), cm.getCursor('anchor'));
+ helpers.doKeys('g', 'v');
+ helpers.assertCursorAt(2, 3);
+ eqPos(makeCursor(2, 0), cm.getCursor('anchor'));
+ eq('123456\n2345\nbar', cm.getValue());
+}, { value: '123456\nfoo\nbar' });
+testVim('reselect_visual_line', function(cm, vim, helpers) {
+ helpers.doKeys('l', 'V', 'j', 'j', 'V', 'g', 'v', 'd');
+ eq('foo\nand\nbar', cm.getValue());
+ cm.setCursor(1, 0);
+ helpers.doKeys('V', 'y', 'j');
+ helpers.doKeys('V', 'p' , 'g', 'v', 'd');
+ eq('foo\nand', cm.getValue());
+}, { value: 'hello\nthis\nis\nfoo\nand\nbar' });
+testVim('reselect_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(1, 2);
+ helpers.doKeys('', 'k', 'h', '');
+ cm.setCursor(2, 1);
+ helpers.doKeys('v', 'l', 'g', 'v');
+ helpers.assertCursorAt(0, 1);
+ // Ensure selection is done with visual block mode rather than one
+ // continuous range.
+ eq(cm.getSelections().join(''), '23oo')
+ helpers.doKeys('g', 'v');
+ helpers.assertCursorAt(2, 3);
+ // Ensure selection of deleted range
+ cm.setCursor(1, 1);
+ helpers.doKeys('v', '', 'j', 'd', 'g', 'v');
+ eq(cm.getSelections().join(''), 'or');
+}, { value: '123456\nfoo\nbar' });
+testVim('s_normal', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('s');
+ helpers.doKeys('');
+ eq('ac', cm.getValue());
+}, { value: 'abc'});
+testVim('s_visual', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('v', 's');
+ helpers.doKeys('');
+ helpers.assertCursorAt(0, 0);
+ eq('ac', cm.getValue());
+}, { value: 'abc'});
+testVim('o_visual', function(cm, vim, helpers) {
+ cm.setCursor(0,0);
+ helpers.doKeys('v','l','l','l','o');
+ helpers.assertCursorAt(0,0);
+ helpers.doKeys('v','v','j','j','j','o');
+ helpers.assertCursorAt(0,0);
+ helpers.doKeys('O');
+ helpers.doKeys('l','l')
+ helpers.assertCursorAt(3, 3);
+ helpers.doKeys('d');
+ eq('p',cm.getValue());
+}, { value: 'abcd\nefgh\nijkl\nmnop'});
+testVim('o_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('','3','j','l','l', 'o');
+ helpers.assertCursorAt(0, 1);
+ helpers.doKeys('O');
+ helpers.assertCursorAt(0, 4);
+ helpers.doKeys('o');
+ helpers.assertCursorAt(3, 1);
+}, { value: 'abcd\nefgh\nijkl\nmnop'});
+testVim('changeCase_visual', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('v', 'l', 'l');
+ helpers.doKeys('U');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('v', 'l', 'l');
+ helpers.doKeys('u');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('l', 'l', 'l', '.');
+ helpers.assertCursorAt(0, 3);
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'a', 'v', 'j', 'U', 'q');
+ helpers.assertCursorAt(0, 0);
+ helpers.doKeys('j', '@', 'a');
+ helpers.assertCursorAt(1, 0);
+ cm.setCursor(3, 0);
+ helpers.doKeys('V', 'U', 'j', '.');
+ eq('ABCDEF\nGHIJKL\nMnopq\nSHORT LINE\nLONG LINE OF TEXT', cm.getValue());
+}, { value: 'abcdef\nghijkl\nmnopq\nshort line\nlong line of text'});
+testVim('changeCase_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(2, 1);
+ helpers.doKeys('', 'k', 'k', 'h', 'U');
+ eq('ABcdef\nGHijkl\nMNopq\nfoo', cm.getValue());
+ cm.setCursor(0, 2);
+ helpers.doKeys('.');
+ eq('ABCDef\nGHIJkl\nMNOPq\nfoo', cm.getValue());
+ // check when last line is shorter.
+ cm.setCursor(2, 2);
+ helpers.doKeys('.');
+ eq('ABCDef\nGHIJkl\nMNOPq\nfoO', cm.getValue());
+}, { value: 'abcdef\nghijkl\nmnopq\nfoo'});
+testVim('visual_paste', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('v', 'l', 'l', 'y', 'j', 'v', 'l', 'p');
+ helpers.assertCursorAt(1, 5);
+ eq('this is a\nunithitest for visual paste', cm.getValue());
+ cm.setCursor(0, 0);
+ // in case of pasting whole line
+ helpers.doKeys('y', 'y');
+ cm.setCursor(1, 6);
+ helpers.doKeys('v', 'l', 'l', 'l', 'p');
+ helpers.assertCursorAt(2, 0);
+ eq('this is a\nunithi\nthis is a\n for visual paste', cm.getValue());
+}, { value: 'this is a\nunit test for visual paste'});
+
+// This checks the contents of the register used to paste the text
+testVim('v_paste_from_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('"', 'a', 'y', 'w');
+ cm.setCursor(1, 0);
+ helpers.doKeys('v', 'p');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/a\s+register/.test(text));
+ });
+}, { value: 'register contents\nare not erased'});
+testVim('S_normal', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('j', 'S');
+ helpers.doKeys('');
+ helpers.assertCursorAt(1, 0);
+ eq('aa\n\ncc', cm.getValue());
+}, { value: 'aa\nbb\ncc'});
+testVim('blockwise_paste', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('', '3', 'j', 'l', 'y');
+ cm.setCursor(0, 2);
+ // paste one char after the current cursor position
+ helpers.doKeys('p');
+ eq('helhelo\nworwold\nfoofo\nbarba', cm.getValue());
+ cm.setCursor(0, 0);
+ helpers.doKeys('v', '4', 'l', 'y');
+ cm.setCursor(0, 0);
+ helpers.doKeys('', '3', 'j', 'p');
+ eq('helheelhelo\norwold\noofo\narba', cm.getValue());
+}, { value: 'hello\nworld\nfoo\nbar'});
+testVim('blockwise_paste_long/short_line', function(cm, vim, helpers) {
+ // extend short lines in case of different line lengths.
+ cm.setCursor(0, 0);
+ helpers.doKeys('', 'j', 'j', 'y');
+ cm.setCursor(0, 3);
+ helpers.doKeys('p');
+ eq('hellho\nfoo f\nbar b', cm.getValue());
+}, { value: 'hello\nfoo\nbar'});
+testVim('blockwise_paste_cut_paste', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('', '2', 'j', 'x');
+ cm.setCursor(0, 0);
+ helpers.doKeys('P');
+ eq('cut\nand\npaste\nme', cm.getValue());
+}, { value: 'cut\nand\npaste\nme'});
+testVim('blockwise_paste_from_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('', '2', 'j', '"', 'a', 'y');
+ cm.setCursor(0, 3);
+ helpers.doKeys('"', 'a', 'p');
+ eq('foobfar\nhellho\nworlwd', cm.getValue());
+}, { value: 'foobar\nhello\nworld'});
+testVim('blockwise_paste_last_line', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('', '2', 'j', 'l', 'y');
+ cm.setCursor(3, 0);
+ helpers.doKeys('p');
+ eq('cut\nand\npaste\nmcue\n an\n pa', cm.getValue());
+}, { value: 'cut\nand\npaste\nme'});
+
+testVim('S_visual', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('v', 'j', 'S');
+ helpers.doKeys('');
+ helpers.assertCursorAt(0, 0);
+ eq('\ncc', cm.getValue());
+}, { value: 'aa\nbb\ncc'});
+
+testVim('/ and n/N', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('match');
+ helpers.doKeys('/');
+ helpers.assertCursorAt(0, 11);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(1, 6);
+ helpers.doKeys('N');
+ helpers.assertCursorAt(0, 11);
+
+ cm.setCursor(0, 0);
+ helpers.doKeys('2', '/');
+ helpers.assertCursorAt(1, 6);
+}, { value: 'match nope match \n nope Match' });
+testVim('/_case', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('Match');
+ helpers.doKeys('/');
+ helpers.assertCursorAt(1, 6);
+}, { value: 'match nope match \n nope Match' });
+testVim('/_2_pcre', function(cm, vim, helpers) {
+ CodeMirror.Vim.setOption('pcre', true);
+ cm.openDialog = helpers.fakeOpenDialog('(word){2}');
+ helpers.doKeys('/');
+ helpers.assertCursorAt(1, 9);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(2, 1);
+}, { value: 'word\n another wordword\n wordwordword\n' });
+testVim('/_2_nopcre', function(cm, vim, helpers) {
+ CodeMirror.Vim.setOption('pcre', false);
+ cm.openDialog = helpers.fakeOpenDialog('\\(word\\)\\{2}');
+ helpers.doKeys('/');
+ helpers.assertCursorAt(1, 9);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(2, 1);
+}, { value: 'word\n another wordword\n wordwordword\n' });
+testVim('/_nongreedy', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('aa');
+ helpers.doKeys('/');
+ helpers.assertCursorAt(0, 4);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(1, 3);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 0);
+}, { value: 'aaa aa \n a aa'});
+testVim('?_nongreedy', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('aa');
+ helpers.doKeys('?');
+ helpers.assertCursorAt(1, 3);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 4);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 0);
+}, { value: 'aaa aa \n a aa'});
+testVim('/_greedy', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('a+');
+ helpers.doKeys('/');
+ helpers.assertCursorAt(0, 4);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(1, 1);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(1, 3);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 0);
+}, { value: 'aaa aa \n a aa'});
+testVim('?_greedy', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('a+');
+ helpers.doKeys('?');
+ helpers.assertCursorAt(1, 3);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(1, 1);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 4);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 0);
+}, { value: 'aaa aa \n a aa'});
+testVim('/_greedy_0_or_more', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('a*');
+ helpers.doKeys('/');
+ helpers.assertCursorAt(0, 3);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 4);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 5);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(1, 0);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(1, 1);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 0);
+}, { value: 'aaa aa\n aa'});
+testVim('?_greedy_0_or_more', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('a*');
+ helpers.doKeys('?');
+ helpers.assertCursorAt(1, 1);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(1, 0);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 5);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 4);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 3);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 0);
+}, { value: 'aaa aa\n aa'});
+testVim('? and n/N', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('match');
+ helpers.doKeys('?');
+ helpers.assertCursorAt(1, 6);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 11);
+ helpers.doKeys('N');
+ helpers.assertCursorAt(1, 6);
+
+ cm.setCursor(0, 0);
+ helpers.doKeys('2', '?');
+ helpers.assertCursorAt(0, 11);
+}, { value: 'match nope match \n nope Match' });
+testVim('*', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('*');
+ helpers.assertCursorAt(0, 22);
+
+ cm.setCursor(0, 9);
+ helpers.doKeys('2', '*');
+ helpers.assertCursorAt(1, 8);
+}, { value: 'nomatch match nomatch match \nnomatch Match' });
+testVim('*_no_word', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('*');
+ helpers.assertCursorAt(0, 0);
+}, { value: ' \n match \n' });
+testVim('*_symbol', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('*');
+ helpers.assertCursorAt(1, 0);
+}, { value: ' /}\n/} match \n' });
+testVim('#', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('#');
+ helpers.assertCursorAt(1, 8);
+
+ cm.setCursor(0, 9);
+ helpers.doKeys('2', '#');
+ helpers.assertCursorAt(0, 22);
+}, { value: 'nomatch match nomatch match \nnomatch Match' });
+testVim('*_seek', function(cm, vim, helpers) {
+ // Should skip over space and symbols.
+ cm.setCursor(0, 3);
+ helpers.doKeys('*');
+ helpers.assertCursorAt(0, 22);
+}, { value: ' := match nomatch match \nnomatch Match' });
+testVim('#', function(cm, vim, helpers) {
+ // Should skip over space and symbols.
+ cm.setCursor(0, 3);
+ helpers.doKeys('#');
+ helpers.assertCursorAt(1, 8);
+}, { value: ' := match nomatch match \nnomatch Match' });
+testVim('g*', function(cm, vim, helpers) {
+ cm.setCursor(0, 8);
+ helpers.doKeys('g', '*');
+ helpers.assertCursorAt(0, 18);
+ cm.setCursor(0, 8);
+ helpers.doKeys('3', 'g', '*');
+ helpers.assertCursorAt(1, 8);
+}, { value: 'matches match alsoMatch\nmatchme matching' });
+testVim('g#', function(cm, vim, helpers) {
+ cm.setCursor(0, 8);
+ helpers.doKeys('g', '#');
+ helpers.assertCursorAt(0, 0);
+ cm.setCursor(0, 8);
+ helpers.doKeys('3', 'g', '#');
+ helpers.assertCursorAt(1, 0);
+}, { value: 'matches match alsoMatch\nmatchme matching' });
+testVim('macro_insert', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'a', '0', 'i');
+ cm.replaceRange('foo', cm.getCursor());
+ helpers.doKeys('');
+ helpers.doKeys('q', '@', 'a');
+ eq('foofoo', cm.getValue());
+}, { value: ''});
+testVim('macro_insert_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'a', '$', 'a');
+ cm.replaceRange('larry.', cm.getCursor());
+ helpers.doKeys('');
+ helpers.doKeys('a');
+ cm.replaceRange('curly.', cm.getCursor());
+ helpers.doKeys('');
+ helpers.doKeys('q');
+ helpers.doKeys('a');
+ cm.replaceRange('moe.', cm.getCursor());
+ helpers.doKeys('');
+ helpers.doKeys('@', 'a');
+ // At this point, the most recent edit should be the 2nd insert change
+ // inside the macro, i.e. "curly.".
+ helpers.doKeys('.');
+ eq('larry.curly.moe.larry.curly.curly.', cm.getValue());
+}, { value: ''});
+testVim('macro_space', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('', '');
+ helpers.assertCursorAt(0, 2);
+ helpers.doKeys('q', 'a', '', '', 'q');
+ helpers.assertCursorAt(0, 4);
+ helpers.doKeys('@', 'a');
+ helpers.assertCursorAt(0, 6);
+ helpers.doKeys('@', 'a');
+ helpers.assertCursorAt(0, 8);
+}, { value: 'one line of text.'});
+testVim('macro_t_search', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'a', 't', 'e', 'q');
+ helpers.assertCursorAt(0, 1);
+ helpers.doKeys('l', '@', 'a');
+ helpers.assertCursorAt(0, 6);
+ helpers.doKeys('l', ';');
+ helpers.assertCursorAt(0, 12);
+}, { value: 'one line of text.'});
+testVim('macro_f_search', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'b', 'f', 'e', 'q');
+ helpers.assertCursorAt(0, 2);
+ helpers.doKeys('@', 'b');
+ helpers.assertCursorAt(0, 7);
+ helpers.doKeys(';');
+ helpers.assertCursorAt(0, 13);
+}, { value: 'one line of text.'});
+testVim('macro_slash_search', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'c');
+ cm.openDialog = helpers.fakeOpenDialog('e');
+ helpers.doKeys('/', 'q');
+ helpers.assertCursorAt(0, 2);
+ helpers.doKeys('@', 'c');
+ helpers.assertCursorAt(0, 7);
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 13);
+}, { value: 'one line of text.'});
+testVim('macro_multislash_search', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'd');
+ cm.openDialog = helpers.fakeOpenDialog('e');
+ helpers.doKeys('/');
+ cm.openDialog = helpers.fakeOpenDialog('t');
+ helpers.doKeys('/', 'q');
+ helpers.assertCursorAt(0, 12);
+ helpers.doKeys('@', 'd');
+ helpers.assertCursorAt(0, 15);
+}, { value: 'one line of text to rule them all.'});
+testVim('macro_parens', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'z', 'i');
+ cm.replaceRange('(', cm.getCursor());
+ helpers.doKeys('');
+ helpers.doKeys('e', 'a');
+ cm.replaceRange(')', cm.getCursor());
+ helpers.doKeys('');
+ helpers.doKeys('q');
+ helpers.doKeys('w', '@', 'z');
+ helpers.doKeys('w', '@', 'z');
+ eq('(see) (spot) (run)', cm.getValue());
+}, { value: 'see spot run'});
+testVim('macro_overwrite', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'z', '0', 'i');
+ cm.replaceRange('I ', cm.getCursor());
+ helpers.doKeys('');
+ helpers.doKeys('q');
+ helpers.doKeys('e');
+ // Now replace the macro with something else.
+ helpers.doKeys('q', 'z', 'a');
+ cm.replaceRange('.', cm.getCursor());
+ helpers.doKeys('');
+ helpers.doKeys('q');
+ helpers.doKeys('e', '@', 'z');
+ helpers.doKeys('e', '@', 'z');
+ eq('I see. spot. run.', cm.getValue());
+}, { value: 'see spot run'});
+testVim('macro_search_f', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'a', 'f', ' ');
+ helpers.assertCursorAt(0,3);
+ helpers.doKeys('q', '0');
+ helpers.assertCursorAt(0,0);
+ helpers.doKeys('@', 'a');
+ helpers.assertCursorAt(0,3);
+}, { value: 'The quick brown fox jumped over the lazy dog.'});
+testVim('macro_search_2f', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'a', '2', 'f', ' ');
+ helpers.assertCursorAt(0,9);
+ helpers.doKeys('q', '0');
+ helpers.assertCursorAt(0,0);
+ helpers.doKeys('@', 'a');
+ helpers.assertCursorAt(0,9);
+}, { value: 'The quick brown fox jumped over the lazy dog.'});
+testVim('yank_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('"', 'a', 'y', 'y');
+ helpers.doKeys('j', '"', 'b', 'y', 'y');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/a\s+foo/.test(text));
+ is(/b\s+bar/.test(text));
+ });
+ helpers.doKeys(':');
+}, { value: 'foo\nbar'});
+testVim('yank_visual_block', function(cm, vim, helpers) {
+ cm.setCursor(0, 1);
+ helpers.doKeys('', 'l', 'j', '"', 'a', 'y');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/a\s+oo\nar/.test(text));
+ });
+ helpers.doKeys(':');
+}, { value: 'foo\nbar'});
+testVim('yank_append_line_to_line_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('"', 'a', 'y', 'y');
+ helpers.doKeys('j', '"', 'A', 'y', 'y');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/a\s+foo\nbar/.test(text));
+ is(/"\s+foo\nbar/.test(text));
+ });
+ helpers.doKeys(':');
+}, { value: 'foo\nbar'});
+testVim('yank_append_word_to_word_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('"', 'a', 'y', 'w');
+ helpers.doKeys('j', '"', 'A', 'y', 'w');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/a\s+foobar/.test(text));
+ is(/"\s+foobar/.test(text));
+ });
+ helpers.doKeys(':');
+}, { value: 'foo\nbar'});
+testVim('yank_append_line_to_word_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('"', 'a', 'y', 'w');
+ helpers.doKeys('j', '"', 'A', 'y', 'y');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/a\s+foo\nbar/.test(text));
+ is(/"\s+foo\nbar/.test(text));
+ });
+ helpers.doKeys(':');
+}, { value: 'foo\nbar'});
+testVim('yank_append_word_to_line_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('"', 'a', 'y', 'y');
+ helpers.doKeys('j', '"', 'A', 'y', 'w');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/a\s+foo\nbar/.test(text));
+ is(/"\s+foo\nbar/.test(text));
+ });
+ helpers.doKeys(':');
+}, { value: 'foo\nbar'});
+testVim('macro_register', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('q', 'a', 'i');
+ cm.replaceRange('gangnam', cm.getCursor());
+ helpers.doKeys('');
+ helpers.doKeys('q');
+ helpers.doKeys('q', 'b', 'o');
+ cm.replaceRange('style', cm.getCursor());
+ helpers.doKeys('');
+ helpers.doKeys('q');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/a\s+i/.test(text));
+ is(/b\s+o/.test(text));
+ });
+ helpers.doKeys(':');
+}, { value: ''});
+testVim('._register', function(cm,vim,helpers) {
+ cm.setCursor(0,0);
+ helpers.doKeys('i');
+ cm.replaceRange('foo',cm.getCursor());
+ helpers.doKeys('');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/\.\s+foo/.test(text));
+ });
+ helpers.doKeys(':');
+}, {value: ''});
+testVim(':_register', function(cm,vim,helpers) {
+ helpers.doEx('bar');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/:\s+bar/.test(text));
+ });
+ helpers.doKeys(':');
+}, {value: ''});
+testVim('search_register_escape', function(cm, vim, helpers) {
+ // Check that the register is restored if the user escapes rather than confirms.
+ cm.openDialog = helpers.fakeOpenDialog('waldo');
+ helpers.doKeys('/');
+ var onKeyDown;
+ var onKeyUp;
+ var KEYCODES = {
+ f: 70,
+ o: 79,
+ Esc: 27
+ };
+ cm.openDialog = function(template, callback, options) {
+ onKeyDown = options.onKeyDown;
+ onKeyUp = options.onKeyUp;
+ };
+ var close = function() {};
+ helpers.doKeys('/');
+ // Fake some keyboard events coming in.
+ onKeyDown({keyCode: KEYCODES.f}, '', close);
+ onKeyUp({keyCode: KEYCODES.f}, '', close);
+ onKeyDown({keyCode: KEYCODES.o}, 'f', close);
+ onKeyUp({keyCode: KEYCODES.o}, 'f', close);
+ onKeyDown({keyCode: KEYCODES.o}, 'fo', close);
+ onKeyUp({keyCode: KEYCODES.o}, 'fo', close);
+ onKeyDown({keyCode: KEYCODES.Esc}, 'foo', close);
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/waldo/.test(text));
+ is(!/foo/.test(text));
+ });
+ helpers.doKeys(':');
+}, {value: ''});
+testVim('search_register', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('foo');
+ helpers.doKeys('/');
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ cm.openNotification = helpers.fakeOpenNotification(function(text) {
+ is(/\/\s+foo/.test(text));
+ });
+ helpers.doKeys(':');
+}, {value: ''});
+testVim('search_history', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('this');
+ helpers.doKeys('/');
+ cm.openDialog = helpers.fakeOpenDialog('checks');
+ helpers.doKeys('/');
+ cm.openDialog = helpers.fakeOpenDialog('search');
+ helpers.doKeys('/');
+ cm.openDialog = helpers.fakeOpenDialog('history');
+ helpers.doKeys('/');
+ cm.openDialog = helpers.fakeOpenDialog('checks');
+ helpers.doKeys('/');
+ var onKeyDown;
+ var onKeyUp;
+ var query = '';
+ var keyCodes = {
+ Up: 38,
+ Down: 40
+ };
+ cm.openDialog = function(template, callback, options) {
+ onKeyUp = options.onKeyUp;
+ onKeyDown = options.onKeyDown;
+ };
+ var close = function(newVal) {
+ if (typeof newVal == 'string') query = newVal;
+ }
+ helpers.doKeys('/');
+ onKeyDown({keyCode: keyCodes.Up}, query, close);
+ onKeyUp({keyCode: keyCodes.Up}, query, close);
+ eq(query, 'checks');
+ onKeyDown({keyCode: keyCodes.Up}, query, close);
+ onKeyUp({keyCode: keyCodes.Up}, query, close);
+ eq(query, 'history');
+ onKeyDown({keyCode: keyCodes.Up}, query, close);
+ onKeyUp({keyCode: keyCodes.Up}, query, close);
+ eq(query, 'search');
+ onKeyDown({keyCode: keyCodes.Up}, query, close);
+ onKeyUp({keyCode: keyCodes.Up}, query, close);
+ eq(query, 'this');
+ onKeyDown({keyCode: keyCodes.Down}, query, close);
+ onKeyUp({keyCode: keyCodes.Down}, query, close);
+ eq(query, 'search');
+}, {value: ''});
+testVim('exCommand_history', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('registers');
+ helpers.doKeys(':');
+ cm.openDialog = helpers.fakeOpenDialog('sort');
+ helpers.doKeys(':');
+ cm.openDialog = helpers.fakeOpenDialog('map');
+ helpers.doKeys(':');
+ cm.openDialog = helpers.fakeOpenDialog('invalid');
+ helpers.doKeys(':');
+ var onKeyDown;
+ var onKeyUp;
+ var input = '';
+ var keyCodes = {
+ Up: 38,
+ Down: 40,
+ s: 115
+ };
+ cm.openDialog = function(template, callback, options) {
+ onKeyUp = options.onKeyUp;
+ onKeyDown = options.onKeyDown;
+ };
+ var close = function(newVal) {
+ if (typeof newVal == 'string') input = newVal;
+ }
+ helpers.doKeys(':');
+ onKeyDown({keyCode: keyCodes.Up}, input, close);
+ eq(input, 'invalid');
+ onKeyDown({keyCode: keyCodes.Up}, input, close);
+ eq(input, 'map');
+ onKeyDown({keyCode: keyCodes.Up}, input, close);
+ eq(input, 'sort');
+ onKeyDown({keyCode: keyCodes.Up}, input, close);
+ eq(input, 'registers');
+ onKeyDown({keyCode: keyCodes.s}, '', close);
+ input = 's';
+ onKeyDown({keyCode: keyCodes.Up}, input, close);
+ eq(input, 'sort');
+}, {value: ''});
+testVim('.', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('2', 'd', 'w');
+ helpers.doKeys('.');
+ eq('5 6', cm.getValue());
+}, { value: '1 2 3 4 5 6'});
+testVim('._repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('2', 'd', 'w');
+ helpers.doKeys('3', '.');
+ eq('6', cm.getValue());
+}, { value: '1 2 3 4 5 6'});
+testVim('._insert', function(cm, vim, helpers) {
+ helpers.doKeys('i');
+ cm.replaceRange('test', cm.getCursor());
+ helpers.doKeys('');
+ helpers.doKeys('.');
+ eq('testestt', cm.getValue());
+ helpers.assertCursorAt(0, 6);
+}, { value: ''});
+testVim('._insert_repeat', function(cm, vim, helpers) {
+ helpers.doKeys('i');
+ cm.replaceRange('test', cm.getCursor());
+ cm.setCursor(0, 4);
+ helpers.doKeys('');
+ helpers.doKeys('2', '.');
+ eq('testesttestt', cm.getValue());
+ helpers.assertCursorAt(0, 10);
+}, { value: ''});
+testVim('._repeat_insert', function(cm, vim, helpers) {
+ helpers.doKeys('3', 'i');
+ cm.replaceRange('te', cm.getCursor());
+ cm.setCursor(0, 2);
+ helpers.doKeys('');
+ helpers.doKeys('.');
+ eq('tetettetetee', cm.getValue());
+ helpers.assertCursorAt(0, 10);
+}, { value: ''});
+testVim('._insert_o', function(cm, vim, helpers) {
+ helpers.doKeys('o');
+ cm.replaceRange('z', cm.getCursor());
+ cm.setCursor(1, 1);
+ helpers.doKeys('');
+ helpers.doKeys('.');
+ eq('\nz\nz', cm.getValue());
+ helpers.assertCursorAt(2, 0);
+}, { value: ''});
+testVim('._insert_o_repeat', function(cm, vim, helpers) {
+ helpers.doKeys('o');
+ cm.replaceRange('z', cm.getCursor());
+ helpers.doKeys('');
+ cm.setCursor(1, 0);
+ helpers.doKeys('2', '.');
+ eq('\nz\nz\nz', cm.getValue());
+ helpers.assertCursorAt(3, 0);
+}, { value: ''});
+testVim('._insert_o_indent', function(cm, vim, helpers) {
+ helpers.doKeys('o');
+ cm.replaceRange('z', cm.getCursor());
+ helpers.doKeys('');
+ cm.setCursor(1, 2);
+ helpers.doKeys('.');
+ eq('{\n z\n z', cm.getValue());
+ helpers.assertCursorAt(2, 2);
+}, { value: '{'});
+testVim('._insert_cw', function(cm, vim, helpers) {
+ helpers.doKeys('c', 'w');
+ cm.replaceRange('test', cm.getCursor());
+ helpers.doKeys('');
+ cm.setCursor(0, 3);
+ helpers.doKeys('2', 'l');
+ helpers.doKeys('.');
+ eq('test test word3', cm.getValue());
+ helpers.assertCursorAt(0, 8);
+}, { value: 'word1 word2 word3' });
+testVim('._insert_cw_repeat', function(cm, vim, helpers) {
+ // For some reason, repeat cw in desktop VIM will does not repeat insert mode
+ // changes. Will conform to that behavior.
+ helpers.doKeys('c', 'w');
+ cm.replaceRange('test', cm.getCursor());
+ helpers.doKeys('');
+ cm.setCursor(0, 4);
+ helpers.doKeys('l');
+ helpers.doKeys('2', '.');
+ eq('test test', cm.getValue());
+ helpers.assertCursorAt(0, 8);
+}, { value: 'word1 word2 word3' });
+testVim('._delete', function(cm, vim, helpers) {
+ cm.setCursor(0, 5);
+ helpers.doKeys('i');
+ helpers.doInsertModeKeys('Backspace');
+ helpers.doKeys('');
+ helpers.doKeys('.');
+ eq('zace', cm.getValue());
+ helpers.assertCursorAt(0, 1);
+}, { value: 'zabcde'});
+testVim('._delete_repeat', function(cm, vim, helpers) {
+ cm.setCursor(0, 6);
+ helpers.doKeys('i');
+ helpers.doInsertModeKeys('Backspace');
+ helpers.doKeys('');
+ helpers.doKeys('2', '.');
+ eq('zzce', cm.getValue());
+ helpers.assertCursorAt(0, 1);
+}, { value: 'zzabcde'});
+testVim('._visual_>', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('V', 'j', '>');
+ cm.setCursor(2, 0)
+ helpers.doKeys('.');
+ eq(' 1\n 2\n 3\n 4', cm.getValue());
+ helpers.assertCursorAt(2, 2);
+}, { value: '1\n2\n3\n4'});
+testVim('f;', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('f', 'x');
+ helpers.doKeys(';');
+ helpers.doKeys('2', ';');
+ eq(9, cm.getCursor().ch);
+}, { value: '01x3xx678x'});
+testVim('F;', function(cm, vim, helpers) {
+ cm.setCursor(0, 8);
+ helpers.doKeys('F', 'x');
+ helpers.doKeys(';');
+ helpers.doKeys('2', ';');
+ eq(2, cm.getCursor().ch);
+}, { value: '01x3xx6x8x'});
+testVim('t;', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('t', 'x');
+ helpers.doKeys(';');
+ helpers.doKeys('2', ';');
+ eq(8, cm.getCursor().ch);
+}, { value: '01x3xx678x'});
+testVim('T;', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('T', 'x');
+ helpers.doKeys(';');
+ helpers.doKeys('2', ';');
+ eq(2, cm.getCursor().ch);
+}, { value: '0xx3xx678x'});
+testVim('f,', function(cm, vim, helpers) {
+ cm.setCursor(0, 6);
+ helpers.doKeys('f', 'x');
+ helpers.doKeys(',');
+ helpers.doKeys('2', ',');
+ eq(2, cm.getCursor().ch);
+}, { value: '01x3xx678x'});
+testVim('F,', function(cm, vim, helpers) {
+ cm.setCursor(0, 3);
+ helpers.doKeys('F', 'x');
+ helpers.doKeys(',');
+ helpers.doKeys('2', ',');
+ eq(9, cm.getCursor().ch);
+}, { value: '01x3xx678x'});
+testVim('t,', function(cm, vim, helpers) {
+ cm.setCursor(0, 6);
+ helpers.doKeys('t', 'x');
+ helpers.doKeys(',');
+ helpers.doKeys('2', ',');
+ eq(3, cm.getCursor().ch);
+}, { value: '01x3xx678x'});
+testVim('T,', function(cm, vim, helpers) {
+ cm.setCursor(0, 4);
+ helpers.doKeys('T', 'x');
+ helpers.doKeys(',');
+ helpers.doKeys('2', ',');
+ eq(8, cm.getCursor().ch);
+}, { value: '01x3xx67xx'});
+testVim('fd,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('f', '4');
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', ';');
+ eq('56789', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 9);
+ helpers.doKeys('d', ',');
+ eq('01239', cm.getValue());
+}, { value: '0123456789'});
+testVim('Fd,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('F', '4');
+ cm.setCursor(0, 9);
+ helpers.doKeys('d', ';');
+ eq('01239', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', ',');
+ eq('56789', cm.getValue());
+}, { value: '0123456789'});
+testVim('td,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('t', '4');
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', ';');
+ eq('456789', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 9);
+ helpers.doKeys('d', ',');
+ eq('012349', cm.getValue());
+}, { value: '0123456789'});
+testVim('Td,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('T', '4');
+ cm.setCursor(0, 9);
+ helpers.doKeys('d', ';');
+ eq('012349', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 0);
+ helpers.doKeys('d', ',');
+ eq('456789', cm.getValue());
+}, { value: '0123456789'});
+testVim('fc,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('f', '4');
+ cm.setCursor(0, 0);
+ helpers.doKeys('c', ';', '');
+ eq('56789', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 9);
+ helpers.doKeys('c', ',');
+ eq('01239', cm.getValue());
+}, { value: '0123456789'});
+testVim('Fc,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('F', '4');
+ cm.setCursor(0, 9);
+ helpers.doKeys('c', ';', '');
+ eq('01239', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 0);
+ helpers.doKeys('c', ',');
+ eq('56789', cm.getValue());
+}, { value: '0123456789'});
+testVim('tc,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('t', '4');
+ cm.setCursor(0, 0);
+ helpers.doKeys('c', ';', '');
+ eq('456789', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 9);
+ helpers.doKeys('c', ',');
+ eq('012349', cm.getValue());
+}, { value: '0123456789'});
+testVim('Tc,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('T', '4');
+ cm.setCursor(0, 9);
+ helpers.doKeys('c', ';', '');
+ eq('012349', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 0);
+ helpers.doKeys('c', ',');
+ eq('456789', cm.getValue());
+}, { value: '0123456789'});
+testVim('fy,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('f', '4');
+ cm.setCursor(0, 0);
+ helpers.doKeys('y', ';', 'P');
+ eq('012340123456789', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 9);
+ helpers.doKeys('y', ',', 'P');
+ eq('012345678456789', cm.getValue());
+}, { value: '0123456789'});
+testVim('Fy,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('F', '4');
+ cm.setCursor(0, 9);
+ helpers.doKeys('y', ';', 'p');
+ eq('012345678945678', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 0);
+ helpers.doKeys('y', ',', 'P');
+ eq('012340123456789', cm.getValue());
+}, { value: '0123456789'});
+testVim('ty,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys('t', '4');
+ cm.setCursor(0, 0);
+ helpers.doKeys('y', ';', 'P');
+ eq('01230123456789', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 9);
+ helpers.doKeys('y', ',', 'p');
+ eq('01234567895678', cm.getValue());
+}, { value: '0123456789'});
+testVim('Ty,;', function(cm, vim, helpers) {
+ cm.setCursor(0, 9);
+ helpers.doKeys('T', '4');
+ cm.setCursor(0, 9);
+ helpers.doKeys('y', ';', 'p');
+ eq('01234567895678', cm.getValue());
+ helpers.doKeys('u');
+ cm.setCursor(0, 0);
+ helpers.doKeys('y', ',', 'P');
+ eq('01230123456789', cm.getValue());
+}, { value: '0123456789'});
+testVim('HML', function(cm, vim, helpers) {
+ var lines = 35;
+ var textHeight = cm.defaultTextHeight();
+ cm.setSize(600, lines*textHeight);
+ cm.setCursor(120, 0);
+ helpers.doKeys('H');
+ helpers.assertCursorAt(86, 2);
+ helpers.doKeys('L');
+ helpers.assertCursorAt(120, 4);
+ helpers.doKeys('M');
+ helpers.assertCursorAt(103,4);
+}, { value: (function(){
+ var lines = new Array(100);
+ var upper = ' xx\n';
+ var lower = ' xx\n';
+ upper = lines.join(upper);
+ lower = lines.join(lower);
+ return upper + lower;
+})()});
+
+var zVals = [];
+forEach(['zb','zz','zt','z-','z.','z'], function(e, idx){
+ var lineNum = 250;
+ var lines = 35;
+ testVim(e, function(cm, vim, helpers) {
+ var k1 = e[0];
+ var k2 = e.substring(1);
+ var textHeight = cm.defaultTextHeight();
+ cm.setSize(600, lines*textHeight);
+ cm.setCursor(lineNum, 0);
+ helpers.doKeys(k1, k2);
+ zVals[idx] = cm.getScrollInfo().top;
+ }, { value: (function(){
+ return new Array(500).join('\n');
+ })()});
+});
+testVim('zb', function(cm, vim, helpers){
+ eq(zVals[2], zVals[5]);
+});
+
+var moveTillCharacterSandbox =
+ 'The quick brown fox \n'
+ 'jumped over the lazy dog.'
+testVim('moveTillCharacter', function(cm, vim, helpers){
+ cm.setCursor(0, 0);
+ // Search for the 'q'.
+ cm.openDialog = helpers.fakeOpenDialog('q');
+ helpers.doKeys('/');
+ eq(4, cm.getCursor().ch);
+ // Jump to just before the first o in the list.
+ helpers.doKeys('t');
+ helpers.doKeys('o');
+ eq('The quick brown fox \n', cm.getValue());
+ // Delete that one character.
+ helpers.doKeys('d');
+ helpers.doKeys('t');
+ helpers.doKeys('o');
+ eq('The quick bown fox \n', cm.getValue());
+ // Delete everything until the next 'o'.
+ helpers.doKeys('.');
+ eq('The quick box \n', cm.getValue());
+ // An unmatched character should have no effect.
+ helpers.doKeys('d');
+ helpers.doKeys('t');
+ helpers.doKeys('q');
+ eq('The quick box \n', cm.getValue());
+ // Matches should only be possible on single lines.
+ helpers.doKeys('d');
+ helpers.doKeys('t');
+ helpers.doKeys('z');
+ eq('The quick box \n', cm.getValue());
+ // After all that, the search for 'q' should still be active, so the 'N' command
+ // can run it again in reverse. Use that to delete everything back to the 'q'.
+ helpers.doKeys('d');
+ helpers.doKeys('N');
+ eq('The ox \n', cm.getValue());
+ eq(4, cm.getCursor().ch);
+}, { value: moveTillCharacterSandbox});
+testVim('searchForPipe', function(cm, vim, helpers){
+ CodeMirror.Vim.setOption('pcre', false);
+ cm.setCursor(0, 0);
+ // Search for the '|'.
+ cm.openDialog = helpers.fakeOpenDialog('|');
+ helpers.doKeys('/');
+ eq(4, cm.getCursor().ch);
+}, { value: 'this|that'});
+
+
+var scrollMotionSandbox =
+ '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'
+ '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'
+ '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'
+ '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n';
+testVim('scrollMotion', function(cm, vim, helpers){
+ var prevCursor, prevScrollInfo;
+ cm.setCursor(0, 0);
+ // ctrl-y at the top of the file should have no effect.
+ helpers.doKeys('');
+ eq(0, cm.getCursor().line);
+ prevScrollInfo = cm.getScrollInfo();
+ helpers.doKeys('');
+ eq(1, cm.getCursor().line);
+ is(prevScrollInfo.top < cm.getScrollInfo().top);
+ // Jump to the end of the sandbox.
+ cm.setCursor(1000, 0);
+ prevCursor = cm.getCursor();
+ // ctrl-e at the bottom of the file should have no effect.
+ helpers.doKeys('');
+ eq(prevCursor.line, cm.getCursor().line);
+ prevScrollInfo = cm.getScrollInfo();
+ helpers.doKeys('');
+ eq(prevCursor.line - 1, cm.getCursor().line);
+ is(prevScrollInfo.top > cm.getScrollInfo().top);
+}, { value: scrollMotionSandbox});
+
+var squareBracketMotionSandbox = ''+
+ '({\n'+//0
+ ' ({\n'+//11
+ ' /*comment {\n'+//2
+ ' */(\n'+//3
+ '#else \n'+//4
+ ' /* )\n'+//5
+ '#if }\n'+//6
+ ' )}*/\n'+//7
+ ')}\n'+//8
+ '{}\n'+//9
+ '#else {{\n'+//10
+ '{}\n'+//11
+ '}\n'+//12
+ '{\n'+//13
+ '#endif\n'+//14
+ '}\n'+//15
+ '}\n'+//16
+ '#else';//17
+testVim('[[, ]]', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys(']', ']');
+ helpers.assertCursorAt(9,0);
+ helpers.doKeys('2', ']', ']');
+ helpers.assertCursorAt(13,0);
+ helpers.doKeys(']', ']');
+ helpers.assertCursorAt(17,0);
+ helpers.doKeys('[', '[');
+ helpers.assertCursorAt(13,0);
+ helpers.doKeys('2', '[', '[');
+ helpers.assertCursorAt(9,0);
+ helpers.doKeys('[', '[');
+ helpers.assertCursorAt(0,0);
+}, { value: squareBracketMotionSandbox});
+testVim('[], ][', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doKeys(']', '[');
+ helpers.assertCursorAt(12,0);
+ helpers.doKeys('2', ']', '[');
+ helpers.assertCursorAt(16,0);
+ helpers.doKeys(']', '[');
+ helpers.assertCursorAt(17,0);
+ helpers.doKeys('[', ']');
+ helpers.assertCursorAt(16,0);
+ helpers.doKeys('2', '[', ']');
+ helpers.assertCursorAt(12,0);
+ helpers.doKeys('[', ']');
+ helpers.assertCursorAt(0,0);
+}, { value: squareBracketMotionSandbox});
+testVim('[{, ]}', function(cm, vim, helpers) {
+ cm.setCursor(4, 10);
+ helpers.doKeys('[', '{');
+ helpers.assertCursorAt(2,12);
+ helpers.doKeys('2', '[', '{');
+ helpers.assertCursorAt(0,1);
+ cm.setCursor(4, 10);
+ helpers.doKeys(']', '}');
+ helpers.assertCursorAt(6,11);
+ helpers.doKeys('2', ']', '}');
+ helpers.assertCursorAt(8,1);
+ cm.setCursor(0,1);
+ helpers.doKeys(']', '}');
+ helpers.assertCursorAt(8,1);
+ helpers.doKeys('[', '{');
+ helpers.assertCursorAt(0,1);
+}, { value: squareBracketMotionSandbox});
+testVim('[(, ])', function(cm, vim, helpers) {
+ cm.setCursor(4, 10);
+ helpers.doKeys('[', '(');
+ helpers.assertCursorAt(3,14);
+ helpers.doKeys('2', '[', '(');
+ helpers.assertCursorAt(0,0);
+ cm.setCursor(4, 10);
+ helpers.doKeys(']', ')');
+ helpers.assertCursorAt(5,11);
+ helpers.doKeys('2', ']', ')');
+ helpers.assertCursorAt(8,0);
+ helpers.doKeys('[', '(');
+ helpers.assertCursorAt(0,0);
+ helpers.doKeys(']', ')');
+ helpers.assertCursorAt(8,0);
+}, { value: squareBracketMotionSandbox});
+testVim('[*, ]*, [/, ]/', function(cm, vim, helpers) {
+ forEach(['*', '/'], function(key){
+ cm.setCursor(7, 0);
+ helpers.doKeys('2', '[', key);
+ helpers.assertCursorAt(2,2);
+ helpers.doKeys('2', ']', key);
+ helpers.assertCursorAt(7,5);
+ });
+}, { value: squareBracketMotionSandbox});
+testVim('[#, ]#', function(cm, vim, helpers) {
+ cm.setCursor(10, 3);
+ helpers.doKeys('2', '[', '#');
+ helpers.assertCursorAt(4,0);
+ helpers.doKeys('5', ']', '#');
+ helpers.assertCursorAt(17,0);
+ cm.setCursor(10, 3);
+ helpers.doKeys(']', '#');
+ helpers.assertCursorAt(14,0);
+}, { value: squareBracketMotionSandbox});
+testVim('[m, ]m, [M, ]M', function(cm, vim, helpers) {
+ cm.setCursor(11, 0);
+ helpers.doKeys('[', 'm');
+ helpers.assertCursorAt(10,7);
+ helpers.doKeys('4', '[', 'm');
+ helpers.assertCursorAt(1,3);
+ helpers.doKeys('5', ']', 'm');
+ helpers.assertCursorAt(11,0);
+ helpers.doKeys('[', 'M');
+ helpers.assertCursorAt(9,1);
+ helpers.doKeys('3', ']', 'M');
+ helpers.assertCursorAt(15,0);
+ helpers.doKeys('5', '[', 'M');
+ helpers.assertCursorAt(7,3);
+}, { value: squareBracketMotionSandbox});
+
+// Ex mode tests
+testVim('ex_go_to_line', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doEx('4');
+ helpers.assertCursorAt(3, 0);
+}, { value: 'a\nb\nc\nd\ne\n'});
+testVim('ex_write', function(cm, vim, helpers) {
+ var tmp = CodeMirror.commands.save;
+ var written;
+ var actualCm;
+ CodeMirror.commands.save = function(cm) {
+ written = true;
+ actualCm = cm;
+ };
+ // Test that w, wr, wri ... write all trigger :write.
+ var command = 'write';
+ for (var i = 1; i < command.length; i++) {
+ written = false;
+ actualCm = null;
+ helpers.doEx(command.substring(0, i));
+ eq(written, true);
+ eq(actualCm, cm);
+ }
+ CodeMirror.commands.save = tmp;
+});
+testVim('ex_sort', function(cm, vim, helpers) {
+ helpers.doEx('sort');
+ eq('Z\na\nb\nc\nd', cm.getValue());
+}, { value: 'b\nZ\nd\nc\na'});
+testVim('ex_sort_reverse', function(cm, vim, helpers) {
+ helpers.doEx('sort!');
+ eq('d\nc\nb\na', cm.getValue());
+}, { value: 'b\nd\nc\na'});
+testVim('ex_sort_range', function(cm, vim, helpers) {
+ helpers.doEx('2,3sort');
+ eq('b\nc\nd\na', cm.getValue());
+}, { value: 'b\nd\nc\na'});
+testVim('ex_sort_oneline', function(cm, vim, helpers) {
+ helpers.doEx('2sort');
+ // Expect no change.
+ eq('b\nd\nc\na', cm.getValue());
+}, { value: 'b\nd\nc\na'});
+testVim('ex_sort_ignoreCase', function(cm, vim, helpers) {
+ helpers.doEx('sort i');
+ eq('a\nb\nc\nd\nZ', cm.getValue());
+}, { value: 'b\nZ\nd\nc\na'});
+testVim('ex_sort_unique', function(cm, vim, helpers) {
+ helpers.doEx('sort u');
+ eq('Z\na\nb\nc\nd', cm.getValue());
+}, { value: 'b\nZ\na\na\nd\na\nc\na'});
+testVim('ex_sort_decimal', function(cm, vim, helpers) {
+ helpers.doEx('sort d');
+ eq('d3\n s5\n6\n.9', cm.getValue());
+}, { value: '6\nd3\n s5\n.9'});
+testVim('ex_sort_decimal_negative', function(cm, vim, helpers) {
+ helpers.doEx('sort d');
+ eq('z-9\nd3\n s5\n6\n.9', cm.getValue());
+}, { value: '6\nd3\n s5\n.9\nz-9'});
+testVim('ex_sort_decimal_reverse', function(cm, vim, helpers) {
+ helpers.doEx('sort! d');
+ eq('.9\n6\n s5\nd3', cm.getValue());
+}, { value: '6\nd3\n s5\n.9'});
+testVim('ex_sort_hex', function(cm, vim, helpers) {
+ helpers.doEx('sort x');
+ eq(' s5\n6\n.9\n&0xB\nd3', cm.getValue());
+}, { value: '6\nd3\n s5\n&0xB\n.9'});
+testVim('ex_sort_octal', function(cm, vim, helpers) {
+ helpers.doEx('sort o');
+ eq('.8\n.9\nd3\n s5\n6', cm.getValue());
+}, { value: '6\nd3\n s5\n.9\n.8'});
+testVim('ex_sort_decimal_mixed', function(cm, vim, helpers) {
+ helpers.doEx('sort d');
+ eq('y\nz\nc1\nb2\na3', cm.getValue());
+}, { value: 'a3\nz\nc1\ny\nb2'});
+testVim('ex_sort_decimal_mixed_reverse', function(cm, vim, helpers) {
+ helpers.doEx('sort! d');
+ eq('a3\nb2\nc1\nz\ny', cm.getValue());
+}, { value: 'a3\nz\nc1\ny\nb2'});
+// test for :global command
+testVim('ex_global', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ helpers.doEx('g/one/s//two');
+ eq('two two\n two two\n two two', cm.getValue());
+ helpers.doEx('1,2g/two/s//one');
+ eq('one one\n one one\n two two', cm.getValue());
+}, {value: 'one one\n one one\n one one'});
+testVim('ex_global_confirm', function(cm, vim, helpers) {
+ cm.setCursor(0, 0);
+ var onKeyDown;
+ var openDialogSave = cm.openDialog;
+ var KEYCODES = {
+ a: 65,
+ n: 78,
+ q: 81,
+ y: 89
+ };
+ // Intercept the ex command, 'global'
+ cm.openDialog = function(template, callback, options) {
+ // Intercept the prompt for the embedded ex command, 'substitute'
+ cm.openDialog = function(template, callback, options) {
+ onKeyDown = options.onKeyDown;
+ };
+ callback('g/one/s//two/gc');
+ };
+ helpers.doKeys(':');
+ var close = function() {};
+ onKeyDown({keyCode: KEYCODES.n}, '', close);
+ onKeyDown({keyCode: KEYCODES.y}, '', close);
+ onKeyDown({keyCode: KEYCODES.a}, '', close);
+ onKeyDown({keyCode: KEYCODES.q}, '', close);
+ onKeyDown({keyCode: KEYCODES.y}, '', close);
+ eq('one two\n two two\n one one\n two one\n one one', cm.getValue());
+}, {value: 'one one\n one one\n one one\n one one\n one one'});
+// Basic substitute tests.
+testVim('ex_substitute_same_line', function(cm, vim, helpers) {
+ cm.setCursor(1, 0);
+ helpers.doEx('s/one/two/g');
+ eq('one one\n two two', cm.getValue());
+}, { value: 'one one\n one one'});
+testVim('ex_substitute_full_file', function(cm, vim, helpers) {
+ cm.setCursor(1, 0);
+ helpers.doEx('%s/one/two/g');
+ eq('two two\n two two', cm.getValue());
+}, { value: 'one one\n one one'});
+testVim('ex_substitute_input_range', function(cm, vim, helpers) {
+ cm.setCursor(1, 0);
+ helpers.doEx('1,3s/\\d/0/g');
+ eq('0\n0\n0\n4', cm.getValue());
+}, { value: '1\n2\n3\n4' });
+testVim('ex_substitute_visual_range', function(cm, vim, helpers) {
+ cm.setCursor(1, 0);
+ // Set last visual mode selection marks '< and '> at lines 2 and 4
+ helpers.doKeys('V', '2', 'j', 'v');
+ helpers.doEx('\'<,\'>s/\\d/0/g');
+ eq('1\n0\n0\n0\n5', cm.getValue());
+}, { value: '1\n2\n3\n4\n5' });
+testVim('ex_substitute_empty_query', function(cm, vim, helpers) {
+ // If the query is empty, use last query.
+ cm.setCursor(1, 0);
+ cm.openDialog = helpers.fakeOpenDialog('1');
+ helpers.doKeys('/');
+ helpers.doEx('s//b/g');
+ eq('abb ab2 ab3', cm.getValue());
+}, { value: 'a11 a12 a13' });
+testVim('ex_substitute_javascript', function(cm, vim, helpers) {
+ CodeMirror.Vim.setOption('pcre', false);
+ cm.setCursor(1, 0);
+ // Throw all the things that javascript likes to treat as special values
+ // into the replace part. All should be literal (this is VIM).
+ helpers.doEx('s/\\(\\d+\\)/$$ $\' $` $& \\1/g')
+ eq('a $$ $\' $` $& 0 b', cm.getValue());
+}, { value: 'a 0 b' });
+testVim('ex_substitute_empty_arguments', function(cm,vim,helpers) {
+ cm.setCursor(0, 0);
+ helpers.doEx('s/a/b/g');
+ cm.setCursor(1, 0);
+ helpers.doEx('s');
+ eq('b b\nb a', cm.getValue());
+}, {value: 'a a\na a'});
+
+// More complex substitute tests that test both pcre and nopcre options.
+function testSubstitute(name, options) {
+ testVim(name + '_pcre', function(cm, vim, helpers) {
+ cm.setCursor(1, 0);
+ CodeMirror.Vim.setOption('pcre', true);
+ helpers.doEx(options.expr);
+ eq(options.expectedValue, cm.getValue());
+ }, options);
+ // If no noPcreExpr is defined, assume that it's the same as the expr.
+ var noPcreExpr = options.noPcreExpr ? options.noPcreExpr : options.expr;
+ testVim(name + '_nopcre', function(cm, vim, helpers) {
+ cm.setCursor(1, 0);
+ CodeMirror.Vim.setOption('pcre', false);
+ helpers.doEx(noPcreExpr);
+ eq(options.expectedValue, cm.getValue());
+ }, options);
+}
+testSubstitute('ex_substitute_capture', {
+ value: 'a11 a12 a13',
+ expectedValue: 'a1111 a1212 a1313',
+ // $n is a backreference
+ expr: 's/(\\d+)/$1$1/g',
+ // \n is a backreference.
+ noPcreExpr: 's/\\(\\d+\\)/\\1\\1/g'});
+testSubstitute('ex_substitute_capture2', {
+ value: 'a 0 b',
+ expectedValue: 'a $00 b',
+ expr: 's/(\\d+)/$$$1$1/g',
+ noPcreExpr: 's/\\(\\d+\\)/$\\1\\1/g'});
+testSubstitute('ex_substitute_nocapture', {
+ value: 'a11 a12 a13',
+ expectedValue: 'a$1$1 a$1$1 a$1$1',
+ expr: 's/(\\d+)/$$1$$1/g',
+ noPcreExpr: 's/\\(\\d+\\)/$1$1/g'});
+testSubstitute('ex_substitute_nocapture2', {
+ value: 'a 0 b',
+ expectedValue: 'a $10 b',
+ expr: 's/(\\d+)/$$1$1/g',
+ noPcreExpr: 's/\\(\\d+\\)/\\$1\\1/g'});
+testSubstitute('ex_substitute_nocapture', {
+ value: 'a b c',
+ expectedValue: 'a $ c',
+ expr: 's/b/$$/',
+ noPcreExpr: 's/b/$/'});
+testSubstitute('ex_substitute_slash_regex', {
+ value: 'one/two \n three/four',
+ expectedValue: 'one|two \n three|four',
+ expr: '%s/\\//|'});
+testSubstitute('ex_substitute_pipe_regex', {
+ value: 'one|two \n three|four',
+ expectedValue: 'one,two \n three,four',
+ expr: '%s/\\|/,/',
+ noPcreExpr: '%s/|/,/'});
+testSubstitute('ex_substitute_or_regex', {
+ value: 'one|two \n three|four',
+ expectedValue: 'ana|twa \n thraa|faar',
+ expr: '%s/o|e|u/a/g',
+ noPcreExpr: '%s/o\\|e\\|u/a/g'});
+testSubstitute('ex_substitute_or_word_regex', {
+ value: 'one|two \n three|four',
+ expectedValue: 'five|five \n three|four',
+ expr: '%s/(one|two)/five/g',
+ noPcreExpr: '%s/\\(one\\|two\\)/five/g'});
+testSubstitute('ex_substitute_backslashslash_regex', {
+ value: 'one\\two \n three\\four',
+ expectedValue: 'one,two \n three,four',
+ expr: '%s/\\\\/,'});
+testSubstitute('ex_substitute_slash_replacement', {
+ value: 'one,two \n three,four',
+ expectedValue: 'one/two \n three/four',
+ expr: '%s/,/\\/'});
+testSubstitute('ex_substitute_backslash_replacement', {
+ value: 'one,two \n three,four',
+ expectedValue: 'one\\two \n three\\four',
+ expr: '%s/,/\\\\/g'});
+testSubstitute('ex_substitute_multibackslash_replacement', {
+ value: 'one,two \n three,four',
+ expectedValue: 'one\\\\\\\\two \n three\\\\\\\\four', // 2*8 backslashes.
+ expr: '%s/,/\\\\\\\\\\\\\\\\/g'}); // 16 backslashes.
+testSubstitute('ex_substitute_braces_word', {
+ value: 'ababab abb ab{2}',
+ expectedValue: 'ab abb ab{2}',
+ expr: '%s/(ab){2}//g',
+ noPcreExpr: '%s/\\(ab\\)\\{2\\}//g'});
+testSubstitute('ex_substitute_braces_range', {
+ value: 'a aa aaa aaaa',
+ expectedValue: 'a a',
+ expr: '%s/a{2,3}//g',
+ noPcreExpr: '%s/a\\{2,3\\}//g'});
+testSubstitute('ex_substitute_braces_literal', {
+ value: 'ababab abb ab{2}',
+ expectedValue: 'ababab abb ',
+ expr: '%s/ab\\{2\\}//g',
+ noPcreExpr: '%s/ab{2}//g'});
+testSubstitute('ex_substitute_braces_char', {
+ value: 'ababab abb ab{2}',
+ expectedValue: 'ababab ab{2}',
+ expr: '%s/ab{2}//g',
+ noPcreExpr: '%s/ab\\{2\\}//g'});
+testSubstitute('ex_substitute_braces_no_escape', {
+ value: 'ababab abb ab{2}',
+ expectedValue: 'ababab ab{2}',
+ expr: '%s/ab{2}//g',
+ noPcreExpr: '%s/ab\\{2}//g'});
+testSubstitute('ex_substitute_count', {
+ value: '1\n2\n3\n4',
+ expectedValue: '1\n0\n0\n4',
+ expr: 's/\\d/0/i 2'});
+testSubstitute('ex_substitute_count_with_range', {
+ value: '1\n2\n3\n4',
+ expectedValue: '1\n2\n0\n0',
+ expr: '1,3s/\\d/0/ 3'});
+testSubstitute('ex_substitute_not_global', {
+ value: 'aaa\nbaa\ncaa',
+ expectedValue: 'xaa\nbxa\ncxa',
+ expr: '%s/a/x/'});
+function testSubstituteConfirm(name, command, initialValue, expectedValue, keys, finalPos) {
+ testVim(name, function(cm, vim, helpers) {
+ var savedOpenDialog = cm.openDialog;
+ var savedKeyName = CodeMirror.keyName;
+ var onKeyDown;
+ var recordedCallback;
+ var closed = true; // Start out closed, set false on second openDialog.
+ function close() {
+ closed = true;
+ }
+ // First openDialog should save callback.
+ cm.openDialog = function(template, callback, options) {
+ recordedCallback = callback;
+ }
+ // Do first openDialog.
+ helpers.doKeys(':');
+ // Second openDialog should save keyDown handler.
+ cm.openDialog = function(template, callback, options) {
+ onKeyDown = options.onKeyDown;
+ closed = false;
+ };
+ // Return the command to Vim and trigger second openDialog.
+ recordedCallback(command);
+ // The event should really use keyCode, but here just mock it out and use
+ // key and replace keyName to just return key.
+ CodeMirror.keyName = function (e) { return e.key; }
+ keys = keys.toUpperCase();
+ for (var i = 0; i < keys.length; i++) {
+ is(!closed);
+ onKeyDown({ key: keys.charAt(i) }, '', close);
+ }
+ try {
+ eq(expectedValue, cm.getValue());
+ helpers.assertCursorAt(finalPos);
+ is(closed);
+ } catch(e) {
+ throw e
+ } finally {
+ // Restore overriden functions.
+ CodeMirror.keyName = savedKeyName;
+ cm.openDialog = savedOpenDialog;
+ }
+ }, { value: initialValue });
+};
+testSubstituteConfirm('ex_substitute_confirm_emptydoc',
+ '%s/x/b/c', '', '', '', makeCursor(0, 0));
+testSubstituteConfirm('ex_substitute_confirm_nomatch',
+ '%s/x/b/c', 'ba a\nbab', 'ba a\nbab', '', makeCursor(0, 0));
+testSubstituteConfirm('ex_substitute_confirm_accept',
+ '%s/a/b/cg', 'ba a\nbab', 'bb b\nbbb', 'yyy', makeCursor(1, 1));
+testSubstituteConfirm('ex_substitute_confirm_random_keys',
+ '%s/a/b/cg', 'ba a\nbab', 'bb b\nbbb', 'ysdkywerty', makeCursor(1, 1));
+testSubstituteConfirm('ex_substitute_confirm_some',
+ '%s/a/b/cg', 'ba a\nbab', 'bb a\nbbb', 'yny', makeCursor(1, 1));
+testSubstituteConfirm('ex_substitute_confirm_all',
+ '%s/a/b/cg', 'ba a\nbab', 'bb b\nbbb', 'a', makeCursor(1, 1));
+testSubstituteConfirm('ex_substitute_confirm_accept_then_all',
+ '%s/a/b/cg', 'ba a\nbab', 'bb b\nbbb', 'ya', makeCursor(1, 1));
+testSubstituteConfirm('ex_substitute_confirm_quit',
+ '%s/a/b/cg', 'ba a\nbab', 'bb a\nbab', 'yq', makeCursor(0, 3));
+testSubstituteConfirm('ex_substitute_confirm_last',
+ '%s/a/b/cg', 'ba a\nbab', 'bb b\nbab', 'yl', makeCursor(0, 3));
+testSubstituteConfirm('ex_substitute_confirm_oneline',
+ '1s/a/b/cg', 'ba a\nbab', 'bb b\nbab', 'yl', makeCursor(0, 3));
+testSubstituteConfirm('ex_substitute_confirm_range_accept',
+ '1,2s/a/b/cg', 'aa\na \na\na', 'bb\nb \na\na', 'yyy', makeCursor(1, 0));
+testSubstituteConfirm('ex_substitute_confirm_range_some',
+ '1,3s/a/b/cg', 'aa\na \na\na', 'ba\nb \nb\na', 'ynyy', makeCursor(2, 0));
+testSubstituteConfirm('ex_substitute_confirm_range_all',
+ '1,3s/a/b/cg', 'aa\na \na\na', 'bb\nb \nb\na', 'a', makeCursor(2, 0));
+testSubstituteConfirm('ex_substitute_confirm_range_last',
+ '1,3s/a/b/cg', 'aa\na \na\na', 'bb\nb \na\na', 'yyl', makeCursor(1, 0));
+//:noh should clear highlighting of search-results but allow to resume search through n
+testVim('ex_noh_clearSearchHighlight', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('match');
+ helpers.doKeys('?');
+ helpers.doEx('noh');
+ eq(vim.searchState_.getOverlay(),null,'match-highlighting wasn\'t cleared');
+ helpers.doKeys('n');
+ helpers.assertCursorAt(0, 11,'can\'t resume search after clearing highlighting');
+}, { value: 'match nope match \n nope Match' });
+testVim('set_boolean', function(cm, vim, helpers) {
+ CodeMirror.Vim.defineOption('testoption', true, 'boolean');
+ // Test default value is set.
+ is(CodeMirror.Vim.getOption('testoption'));
+ try {
+ // Test fail to set to non-boolean
+ CodeMirror.Vim.setOption('testoption', '5');
+ fail();
+ } catch (expected) {};
+ // Test setOption
+ CodeMirror.Vim.setOption('testoption', false);
+ is(!CodeMirror.Vim.getOption('testoption'));
+});
+testVim('ex_set_boolean', function(cm, vim, helpers) {
+ CodeMirror.Vim.defineOption('testoption', true, 'boolean');
+ // Test default value is set.
+ is(CodeMirror.Vim.getOption('testoption'));
+ try {
+ // Test fail to set to non-boolean
+ helpers.doEx('set testoption=22');
+ fail();
+ } catch (expected) {};
+ // Test setOption
+ helpers.doEx('set notestoption');
+ is(!CodeMirror.Vim.getOption('testoption'));
+});
+testVim('set_string', function(cm, vim, helpers) {
+ CodeMirror.Vim.defineOption('testoption', 'a', 'string');
+ // Test default value is set.
+ eq('a', CodeMirror.Vim.getOption('testoption'));
+ try {
+ // Test fail to set non-string.
+ CodeMirror.Vim.setOption('testoption', true);
+ fail();
+ } catch (expected) {};
+ try {
+ // Test fail to set 'notestoption'
+ CodeMirror.Vim.setOption('notestoption', 'b');
+ fail();
+ } catch (expected) {};
+ // Test setOption
+ CodeMirror.Vim.setOption('testoption', 'c');
+ eq('c', CodeMirror.Vim.getOption('testoption'));
+});
+testVim('ex_set_string', function(cm, vim, helpers) {
+ CodeMirror.Vim.defineOption('testoption', 'a', 'string');
+ // Test default value is set.
+ eq('a', CodeMirror.Vim.getOption('testoption'));
+ try {
+ // Test fail to set 'notestoption'
+ helpers.doEx('set notestoption=b');
+ fail();
+ } catch (expected) {};
+ // Test setOption
+ helpers.doEx('set testoption=c')
+ eq('c', CodeMirror.Vim.getOption('testoption'));
+});
+// TODO: Reset key maps after each test.
+testVim('ex_map_key2key', function(cm, vim, helpers) {
+ helpers.doEx('map a x');
+ helpers.doKeys('a');
+ helpers.assertCursorAt(0, 0);
+ eq('bc', cm.getValue());
+}, { value: 'abc' });
+testVim('ex_unmap_key2key', function(cm, vim, helpers) {
+ helpers.doEx('unmap a');
+ helpers.doKeys('a');
+ eq('vim-insert', cm.getOption('keyMap'));
+}, { value: 'abc' });
+testVim('ex_unmap_key2key_does_not_remove_default', function(cm, vim, helpers) {
+ try {
+ helpers.doEx('unmap a');
+ fail();
+ } catch (expected) {}
+ helpers.doKeys('a');
+ eq('vim-insert', cm.getOption('keyMap'));
+}, { value: 'abc' });
+testVim('ex_map_key2key_to_colon', function(cm, vim, helpers) {
+ helpers.doEx('map ; :');
+ var dialogOpened = false;
+ cm.openDialog = function() {
+ dialogOpened = true;
+ }
+ helpers.doKeys(';');
+ eq(dialogOpened, true);
+});
+testVim('ex_map_ex2key:', function(cm, vim, helpers) {
+ helpers.doEx('map :del x');
+ helpers.doEx('del');
+ helpers.assertCursorAt(0, 0);
+ eq('bc', cm.getValue());
+}, { value: 'abc' });
+testVim('ex_map_ex2ex', function(cm, vim, helpers) {
+ helpers.doEx('map :del :w');
+ var tmp = CodeMirror.commands.save;
+ var written = false;
+ var actualCm;
+ CodeMirror.commands.save = function(cm) {
+ written = true;
+ actualCm = cm;
+ };
+ helpers.doEx('del');
+ CodeMirror.commands.save = tmp;
+ eq(written, true);
+ eq(actualCm, cm);
+});
+testVim('ex_map_key2ex', function(cm, vim, helpers) {
+ helpers.doEx('map a :w');
+ var tmp = CodeMirror.commands.save;
+ var written = false;
+ var actualCm;
+ CodeMirror.commands.save = function(cm) {
+ written = true;
+ actualCm = cm;
+ };
+ helpers.doKeys('a');
+ CodeMirror.commands.save = tmp;
+ eq(written, true);
+ eq(actualCm, cm);
+});
+testVim('ex_map_key2key_visual_api', function(cm, vim, helpers) {
+ CodeMirror.Vim.map('b', ':w', 'visual');
+ var tmp = CodeMirror.commands.save;
+ var written = false;
+ var actualCm;
+ CodeMirror.commands.save = function(cm) {
+ written = true;
+ actualCm = cm;
+ };
+ // Mapping should not work in normal mode.
+ helpers.doKeys('b');
+ eq(written, false);
+ // Mapping should work in visual mode.
+ helpers.doKeys('v', 'b');
+ eq(written, true);
+ eq(actualCm, cm);
+
+ CodeMirror.commands.save = tmp;
+});
+testVim('ex_imap', function(cm, vim, helpers) {
+ CodeMirror.Vim.map('jk', '', 'insert');
+ helpers.doKeys('i');
+ is(vim.insertMode);
+ helpers.doKeys('j', 'k');
+ is(!vim.insertMode);
+})
+
+// Testing registration of functions as ex-commands and mapping to -keys
+testVim('ex_api_test', function(cm, vim, helpers) {
+ var res=false;
+ var val='from';
+ CodeMirror.Vim.defineEx('extest','ext',function(cm,params){
+ if(params.args)val=params.args[0];
+ else res=true;
+ });
+ helpers.doEx(':ext to');
+ eq(val,'to','Defining ex-command failed');
+ CodeMirror.Vim.map('',':ext');
+ helpers.doKeys('','');
+ is(res,'Mapping to key failed');
+});
+// For now, this test needs to be last because it messes up : for future tests.
+testVim('ex_map_key2key_from_colon', function(cm, vim, helpers) {
+ helpers.doEx('map : x');
+ helpers.doKeys(':');
+ helpers.assertCursorAt(0, 0);
+ eq('bc', cm.getValue());
+}, { value: 'abc' });
+
+// Test event handlers
+testVim('beforeSelectionChange', function(cm, vim, helpers) {
+ cm.setCursor(0, 100);
+ eqPos(cm.getCursor('head'), cm.getCursor('anchor'));
+}, { value: 'abc' });
+
+
diff --git a/tool/update_deps.js b/tool/update_deps.js
index 4b340ec2..d230fde6 100644
--- a/tool/update_deps.js
+++ b/tool/update_deps.js
@@ -1,7 +1,7 @@
-var https = require("https")
- , http = require("http")
- , url = require("url")
- , fs = require("fs");
+var https = require("https");
+var http = require("http");
+var url = require("url");
+var fs = require("fs");
var Path = require("path");
var spawn = require("child_process").spawn;
@@ -119,6 +119,22 @@ var deps = {
});
}
},
+ vim: {
+ fetch: function(){
+ var rootHref = "https://raw.githubusercontent.com/codemirror/CodeMirror/master/"
+ var fileMap = {"keymap/vim.js": "keyboard/vim2.js", "test/vim_test.js": "keyboard/vim2_test.js"};
+ async.forEach(Object.keys(fileMap), function(x, next) {
+ download(rootHref + x, function(e, d) {
+ d = d.replace(/^\(function.*{[^{}]+^}[^{}]+{/m, "define(function(require, exports, module) {");
+ d = d.replace(/^\s*return vimApi;\s*};/gm, " //};")
+ .replace("var Vim = function() {", "$& return vimApi; } //{")
+ fs.writeFile(rootDir + fileMap[x], d, next)
+ })
+ }, function() {
+ console.log("done")
+ });
+ }
+ },
coffee: {
fetch: function(){
var rootHref = "https://raw.github.com/jashkenas/coffee-script/master/";