Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /** |
michael@0 | 2 | * Supported keybindings: |
michael@0 | 3 | * |
michael@0 | 4 | * Motion: |
michael@0 | 5 | * h, j, k, l |
michael@0 | 6 | * gj, gk |
michael@0 | 7 | * e, E, w, W, b, B, ge, gE |
michael@0 | 8 | * f<character>, F<character>, t<character>, T<character> |
michael@0 | 9 | * $, ^, 0, -, +, _ |
michael@0 | 10 | * gg, G |
michael@0 | 11 | * % |
michael@0 | 12 | * '<character>, `<character> |
michael@0 | 13 | * |
michael@0 | 14 | * Operator: |
michael@0 | 15 | * d, y, c |
michael@0 | 16 | * dd, yy, cc |
michael@0 | 17 | * g~, g~g~ |
michael@0 | 18 | * >, <, >>, << |
michael@0 | 19 | * |
michael@0 | 20 | * Operator-Motion: |
michael@0 | 21 | * x, X, D, Y, C, ~ |
michael@0 | 22 | * |
michael@0 | 23 | * Action: |
michael@0 | 24 | * a, i, s, A, I, S, o, O |
michael@0 | 25 | * zz, z., z<CR>, zt, zb, z- |
michael@0 | 26 | * J |
michael@0 | 27 | * u, Ctrl-r |
michael@0 | 28 | * m<character> |
michael@0 | 29 | * r<character> |
michael@0 | 30 | * |
michael@0 | 31 | * Modes: |
michael@0 | 32 | * ESC - leave insert mode, visual mode, and clear input state. |
michael@0 | 33 | * Ctrl-[, Ctrl-c - same as ESC. |
michael@0 | 34 | * |
michael@0 | 35 | * Registers: unnamed, -, a-z, A-Z, 0-9 |
michael@0 | 36 | * (Does not respect the special case for number registers when delete |
michael@0 | 37 | * operator is made with these commands: %, (, ), , /, ?, n, N, {, } ) |
michael@0 | 38 | * TODO: Implement the remaining registers. |
michael@0 | 39 | * Marks: a-z, A-Z, and 0-9 |
michael@0 | 40 | * TODO: Implement the remaining special marks. They have more complex |
michael@0 | 41 | * behavior. |
michael@0 | 42 | * |
michael@0 | 43 | * Events: |
michael@0 | 44 | * 'vim-mode-change' - raised on the editor anytime the current mode changes, |
michael@0 | 45 | * Event object: {mode: "visual", subMode: "linewise"} |
michael@0 | 46 | * |
michael@0 | 47 | * Code structure: |
michael@0 | 48 | * 1. Default keymap |
michael@0 | 49 | * 2. Variable declarations and short basic helpers |
michael@0 | 50 | * 3. Instance (External API) implementation |
michael@0 | 51 | * 4. Internal state tracking objects (input state, counter) implementation |
michael@0 | 52 | * and instanstiation |
michael@0 | 53 | * 5. Key handler (the main command dispatcher) implementation |
michael@0 | 54 | * 6. Motion, operator, and action implementations |
michael@0 | 55 | * 7. Helper functions for the key handler, motions, operators, and actions |
michael@0 | 56 | * 8. Set up Vim to work as a keymap for CodeMirror. |
michael@0 | 57 | */ |
michael@0 | 58 | |
michael@0 | 59 | (function(mod) { |
michael@0 | 60 | if (typeof exports == "object" && typeof module == "object") // CommonJS |
michael@0 | 61 | mod(require("../lib/codemirror"), require("../addon/search/searchcursor"), require("../addon/dialog/dialog")); |
michael@0 | 62 | else if (typeof define == "function" && define.amd) // AMD |
michael@0 | 63 | define(["../lib/codemirror", "../addon/search/searchcursor", "../addon/dialog/dialog"], mod); |
michael@0 | 64 | else // Plain browser env |
michael@0 | 65 | mod(CodeMirror); |
michael@0 | 66 | })(function(CodeMirror) { |
michael@0 | 67 | 'use strict'; |
michael@0 | 68 | |
michael@0 | 69 | var defaultKeymap = [ |
michael@0 | 70 | // Key to key mapping. This goes first to make it possible to override |
michael@0 | 71 | // existing mappings. |
michael@0 | 72 | { keys: ['<Left>'], type: 'keyToKey', toKeys: ['h'] }, |
michael@0 | 73 | { keys: ['<Right>'], type: 'keyToKey', toKeys: ['l'] }, |
michael@0 | 74 | { keys: ['<Up>'], type: 'keyToKey', toKeys: ['k'] }, |
michael@0 | 75 | { keys: ['<Down>'], type: 'keyToKey', toKeys: ['j'] }, |
michael@0 | 76 | { keys: ['<Space>'], type: 'keyToKey', toKeys: ['l'] }, |
michael@0 | 77 | { keys: ['<BS>'], type: 'keyToKey', toKeys: ['h'] }, |
michael@0 | 78 | { keys: ['<C-Space>'], type: 'keyToKey', toKeys: ['W'] }, |
michael@0 | 79 | { keys: ['<C-BS>'], type: 'keyToKey', toKeys: ['B'] }, |
michael@0 | 80 | { keys: ['<S-Space>'], type: 'keyToKey', toKeys: ['w'] }, |
michael@0 | 81 | { keys: ['<S-BS>'], type: 'keyToKey', toKeys: ['b'] }, |
michael@0 | 82 | { keys: ['<C-n>'], type: 'keyToKey', toKeys: ['j'] }, |
michael@0 | 83 | { keys: ['<C-p>'], type: 'keyToKey', toKeys: ['k'] }, |
michael@0 | 84 | { keys: ['<C-[>'], type: 'keyToKey', toKeys: ['<Esc>'] }, |
michael@0 | 85 | { keys: ['<C-c>'], type: 'keyToKey', toKeys: ['<Esc>'] }, |
michael@0 | 86 | { keys: ['s'], type: 'keyToKey', toKeys: ['c', 'l'], context: 'normal' }, |
michael@0 | 87 | { keys: ['s'], type: 'keyToKey', toKeys: ['x', 'i'], context: 'visual'}, |
michael@0 | 88 | { keys: ['S'], type: 'keyToKey', toKeys: ['c', 'c'], context: 'normal' }, |
michael@0 | 89 | { keys: ['S'], type: 'keyToKey', toKeys: ['d', 'c', 'c'], context: 'visual' }, |
michael@0 | 90 | { keys: ['<Home>'], type: 'keyToKey', toKeys: ['0'] }, |
michael@0 | 91 | { keys: ['<End>'], type: 'keyToKey', toKeys: ['$'] }, |
michael@0 | 92 | { keys: ['<PageUp>'], type: 'keyToKey', toKeys: ['<C-b>'] }, |
michael@0 | 93 | { keys: ['<PageDown>'], type: 'keyToKey', toKeys: ['<C-f>'] }, |
michael@0 | 94 | { keys: ['<CR>'], type: 'keyToKey', toKeys: ['j', '^'], context: 'normal' }, |
michael@0 | 95 | // Motions |
michael@0 | 96 | { keys: ['H'], type: 'motion', |
michael@0 | 97 | motion: 'moveToTopLine', |
michael@0 | 98 | motionArgs: { linewise: true, toJumplist: true }}, |
michael@0 | 99 | { keys: ['M'], type: 'motion', |
michael@0 | 100 | motion: 'moveToMiddleLine', |
michael@0 | 101 | motionArgs: { linewise: true, toJumplist: true }}, |
michael@0 | 102 | { keys: ['L'], type: 'motion', |
michael@0 | 103 | motion: 'moveToBottomLine', |
michael@0 | 104 | motionArgs: { linewise: true, toJumplist: true }}, |
michael@0 | 105 | { keys: ['h'], type: 'motion', |
michael@0 | 106 | motion: 'moveByCharacters', |
michael@0 | 107 | motionArgs: { forward: false }}, |
michael@0 | 108 | { keys: ['l'], type: 'motion', |
michael@0 | 109 | motion: 'moveByCharacters', |
michael@0 | 110 | motionArgs: { forward: true }}, |
michael@0 | 111 | { keys: ['j'], type: 'motion', |
michael@0 | 112 | motion: 'moveByLines', |
michael@0 | 113 | motionArgs: { forward: true, linewise: true }}, |
michael@0 | 114 | { keys: ['k'], type: 'motion', |
michael@0 | 115 | motion: 'moveByLines', |
michael@0 | 116 | motionArgs: { forward: false, linewise: true }}, |
michael@0 | 117 | { keys: ['g','j'], type: 'motion', |
michael@0 | 118 | motion: 'moveByDisplayLines', |
michael@0 | 119 | motionArgs: { forward: true }}, |
michael@0 | 120 | { keys: ['g','k'], type: 'motion', |
michael@0 | 121 | motion: 'moveByDisplayLines', |
michael@0 | 122 | motionArgs: { forward: false }}, |
michael@0 | 123 | { keys: ['w'], type: 'motion', |
michael@0 | 124 | motion: 'moveByWords', |
michael@0 | 125 | motionArgs: { forward: true, wordEnd: false }}, |
michael@0 | 126 | { keys: ['W'], type: 'motion', |
michael@0 | 127 | motion: 'moveByWords', |
michael@0 | 128 | motionArgs: { forward: true, wordEnd: false, bigWord: true }}, |
michael@0 | 129 | { keys: ['e'], type: 'motion', |
michael@0 | 130 | motion: 'moveByWords', |
michael@0 | 131 | motionArgs: { forward: true, wordEnd: true, inclusive: true }}, |
michael@0 | 132 | { keys: ['E'], type: 'motion', |
michael@0 | 133 | motion: 'moveByWords', |
michael@0 | 134 | motionArgs: { forward: true, wordEnd: true, bigWord: true, |
michael@0 | 135 | inclusive: true }}, |
michael@0 | 136 | { keys: ['b'], type: 'motion', |
michael@0 | 137 | motion: 'moveByWords', |
michael@0 | 138 | motionArgs: { forward: false, wordEnd: false }}, |
michael@0 | 139 | { keys: ['B'], type: 'motion', |
michael@0 | 140 | motion: 'moveByWords', |
michael@0 | 141 | motionArgs: { forward: false, wordEnd: false, bigWord: true }}, |
michael@0 | 142 | { keys: ['g', 'e'], type: 'motion', |
michael@0 | 143 | motion: 'moveByWords', |
michael@0 | 144 | motionArgs: { forward: false, wordEnd: true, inclusive: true }}, |
michael@0 | 145 | { keys: ['g', 'E'], type: 'motion', |
michael@0 | 146 | motion: 'moveByWords', |
michael@0 | 147 | motionArgs: { forward: false, wordEnd: true, bigWord: true, |
michael@0 | 148 | inclusive: true }}, |
michael@0 | 149 | { keys: ['{'], type: 'motion', motion: 'moveByParagraph', |
michael@0 | 150 | motionArgs: { forward: false, toJumplist: true }}, |
michael@0 | 151 | { keys: ['}'], type: 'motion', motion: 'moveByParagraph', |
michael@0 | 152 | motionArgs: { forward: true, toJumplist: true }}, |
michael@0 | 153 | { keys: ['<C-f>'], type: 'motion', |
michael@0 | 154 | motion: 'moveByPage', motionArgs: { forward: true }}, |
michael@0 | 155 | { keys: ['<C-b>'], type: 'motion', |
michael@0 | 156 | motion: 'moveByPage', motionArgs: { forward: false }}, |
michael@0 | 157 | { keys: ['<C-d>'], type: 'motion', |
michael@0 | 158 | motion: 'moveByScroll', |
michael@0 | 159 | motionArgs: { forward: true, explicitRepeat: true }}, |
michael@0 | 160 | { keys: ['<C-u>'], type: 'motion', |
michael@0 | 161 | motion: 'moveByScroll', |
michael@0 | 162 | motionArgs: { forward: false, explicitRepeat: true }}, |
michael@0 | 163 | { keys: ['g', 'g'], type: 'motion', |
michael@0 | 164 | motion: 'moveToLineOrEdgeOfDocument', |
michael@0 | 165 | motionArgs: { forward: false, explicitRepeat: true, linewise: true, toJumplist: true }}, |
michael@0 | 166 | { keys: ['G'], type: 'motion', |
michael@0 | 167 | motion: 'moveToLineOrEdgeOfDocument', |
michael@0 | 168 | motionArgs: { forward: true, explicitRepeat: true, linewise: true, toJumplist: true }}, |
michael@0 | 169 | { keys: ['0'], type: 'motion', motion: 'moveToStartOfLine' }, |
michael@0 | 170 | { keys: ['^'], type: 'motion', |
michael@0 | 171 | motion: 'moveToFirstNonWhiteSpaceCharacter' }, |
michael@0 | 172 | { keys: ['+'], type: 'motion', |
michael@0 | 173 | motion: 'moveByLines', |
michael@0 | 174 | motionArgs: { forward: true, toFirstChar:true }}, |
michael@0 | 175 | { keys: ['-'], type: 'motion', |
michael@0 | 176 | motion: 'moveByLines', |
michael@0 | 177 | motionArgs: { forward: false, toFirstChar:true }}, |
michael@0 | 178 | { keys: ['_'], type: 'motion', |
michael@0 | 179 | motion: 'moveByLines', |
michael@0 | 180 | motionArgs: { forward: true, toFirstChar:true, repeatOffset:-1 }}, |
michael@0 | 181 | { keys: ['$'], type: 'motion', |
michael@0 | 182 | motion: 'moveToEol', |
michael@0 | 183 | motionArgs: { inclusive: true }}, |
michael@0 | 184 | { keys: ['%'], type: 'motion', |
michael@0 | 185 | motion: 'moveToMatchedSymbol', |
michael@0 | 186 | motionArgs: { inclusive: true, toJumplist: true }}, |
michael@0 | 187 | { keys: ['f', 'character'], type: 'motion', |
michael@0 | 188 | motion: 'moveToCharacter', |
michael@0 | 189 | motionArgs: { forward: true , inclusive: true }}, |
michael@0 | 190 | { keys: ['F', 'character'], type: 'motion', |
michael@0 | 191 | motion: 'moveToCharacter', |
michael@0 | 192 | motionArgs: { forward: false }}, |
michael@0 | 193 | { keys: ['t', 'character'], type: 'motion', |
michael@0 | 194 | motion: 'moveTillCharacter', |
michael@0 | 195 | motionArgs: { forward: true, inclusive: true }}, |
michael@0 | 196 | { keys: ['T', 'character'], type: 'motion', |
michael@0 | 197 | motion: 'moveTillCharacter', |
michael@0 | 198 | motionArgs: { forward: false }}, |
michael@0 | 199 | { keys: [';'], type: 'motion', motion: 'repeatLastCharacterSearch', |
michael@0 | 200 | motionArgs: { forward: true }}, |
michael@0 | 201 | { keys: [','], type: 'motion', motion: 'repeatLastCharacterSearch', |
michael@0 | 202 | motionArgs: { forward: false }}, |
michael@0 | 203 | { keys: ['\'', 'character'], type: 'motion', motion: 'goToMark', |
michael@0 | 204 | motionArgs: {toJumplist: true}}, |
michael@0 | 205 | { keys: ['`', 'character'], type: 'motion', motion: 'goToMark', |
michael@0 | 206 | motionArgs: {toJumplist: true}}, |
michael@0 | 207 | { keys: [']', '`'], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true } }, |
michael@0 | 208 | { keys: ['[', '`'], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false } }, |
michael@0 | 209 | { keys: [']', '\''], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true, linewise: true } }, |
michael@0 | 210 | { keys: ['[', '\''], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false, linewise: true } }, |
michael@0 | 211 | { keys: [']', 'character'], type: 'motion', |
michael@0 | 212 | motion: 'moveToSymbol', |
michael@0 | 213 | motionArgs: { forward: true, toJumplist: true}}, |
michael@0 | 214 | { keys: ['[', 'character'], type: 'motion', |
michael@0 | 215 | motion: 'moveToSymbol', |
michael@0 | 216 | motionArgs: { forward: false, toJumplist: true}}, |
michael@0 | 217 | { keys: ['|'], type: 'motion', |
michael@0 | 218 | motion: 'moveToColumn', |
michael@0 | 219 | motionArgs: { }}, |
michael@0 | 220 | { keys: ['o'], type: 'motion', motion: 'moveToOtherHighlightedEnd', motionArgs: { },context:'visual'}, |
michael@0 | 221 | // Operators |
michael@0 | 222 | { keys: ['d'], type: 'operator', operator: 'delete' }, |
michael@0 | 223 | { keys: ['y'], type: 'operator', operator: 'yank' }, |
michael@0 | 224 | { keys: ['c'], type: 'operator', operator: 'change' }, |
michael@0 | 225 | { keys: ['>'], type: 'operator', operator: 'indent', |
michael@0 | 226 | operatorArgs: { indentRight: true }}, |
michael@0 | 227 | { keys: ['<'], type: 'operator', operator: 'indent', |
michael@0 | 228 | operatorArgs: { indentRight: false }}, |
michael@0 | 229 | { keys: ['g', '~'], type: 'operator', operator: 'swapcase' }, |
michael@0 | 230 | { keys: ['n'], type: 'motion', motion: 'findNext', |
michael@0 | 231 | motionArgs: { forward: true, toJumplist: true }}, |
michael@0 | 232 | { keys: ['N'], type: 'motion', motion: 'findNext', |
michael@0 | 233 | motionArgs: { forward: false, toJumplist: true }}, |
michael@0 | 234 | // Operator-Motion dual commands |
michael@0 | 235 | { keys: ['x'], type: 'operatorMotion', operator: 'delete', |
michael@0 | 236 | motion: 'moveByCharacters', motionArgs: { forward: true }, |
michael@0 | 237 | operatorMotionArgs: { visualLine: false }}, |
michael@0 | 238 | { keys: ['X'], type: 'operatorMotion', operator: 'delete', |
michael@0 | 239 | motion: 'moveByCharacters', motionArgs: { forward: false }, |
michael@0 | 240 | operatorMotionArgs: { visualLine: true }}, |
michael@0 | 241 | { keys: ['D'], type: 'operatorMotion', operator: 'delete', |
michael@0 | 242 | motion: 'moveToEol', motionArgs: { inclusive: true }, |
michael@0 | 243 | operatorMotionArgs: { visualLine: true }}, |
michael@0 | 244 | { keys: ['Y'], type: 'operatorMotion', operator: 'yank', |
michael@0 | 245 | motion: 'moveToEol', motionArgs: { inclusive: true }, |
michael@0 | 246 | operatorMotionArgs: { visualLine: true }}, |
michael@0 | 247 | { keys: ['C'], type: 'operatorMotion', |
michael@0 | 248 | operator: 'change', |
michael@0 | 249 | motion: 'moveToEol', motionArgs: { inclusive: true }, |
michael@0 | 250 | operatorMotionArgs: { visualLine: true }}, |
michael@0 | 251 | { keys: ['~'], type: 'operatorMotion', |
michael@0 | 252 | operator: 'swapcase', operatorArgs: { shouldMoveCursor: true }, |
michael@0 | 253 | motion: 'moveByCharacters', motionArgs: { forward: true }}, |
michael@0 | 254 | // Actions |
michael@0 | 255 | { keys: ['<C-i>'], type: 'action', action: 'jumpListWalk', |
michael@0 | 256 | actionArgs: { forward: true }}, |
michael@0 | 257 | { keys: ['<C-o>'], type: 'action', action: 'jumpListWalk', |
michael@0 | 258 | actionArgs: { forward: false }}, |
michael@0 | 259 | { keys: ['<C-e>'], type: 'action', |
michael@0 | 260 | action: 'scroll', |
michael@0 | 261 | actionArgs: { forward: true, linewise: true }}, |
michael@0 | 262 | { keys: ['<C-y>'], type: 'action', |
michael@0 | 263 | action: 'scroll', |
michael@0 | 264 | actionArgs: { forward: false, linewise: true }}, |
michael@0 | 265 | { keys: ['a'], type: 'action', action: 'enterInsertMode', isEdit: true, |
michael@0 | 266 | actionArgs: { insertAt: 'charAfter' }}, |
michael@0 | 267 | { keys: ['A'], type: 'action', action: 'enterInsertMode', isEdit: true, |
michael@0 | 268 | actionArgs: { insertAt: 'eol' }}, |
michael@0 | 269 | { keys: ['i'], type: 'action', action: 'enterInsertMode', isEdit: true, |
michael@0 | 270 | actionArgs: { insertAt: 'inplace' }}, |
michael@0 | 271 | { keys: ['I'], type: 'action', action: 'enterInsertMode', isEdit: true, |
michael@0 | 272 | actionArgs: { insertAt: 'firstNonBlank' }}, |
michael@0 | 273 | { keys: ['o'], type: 'action', action: 'newLineAndEnterInsertMode', |
michael@0 | 274 | isEdit: true, interlaceInsertRepeat: true, |
michael@0 | 275 | actionArgs: { after: true }}, |
michael@0 | 276 | { keys: ['O'], type: 'action', action: 'newLineAndEnterInsertMode', |
michael@0 | 277 | isEdit: true, interlaceInsertRepeat: true, |
michael@0 | 278 | actionArgs: { after: false }}, |
michael@0 | 279 | { keys: ['v'], type: 'action', action: 'toggleVisualMode' }, |
michael@0 | 280 | { keys: ['V'], type: 'action', action: 'toggleVisualMode', |
michael@0 | 281 | actionArgs: { linewise: true }}, |
michael@0 | 282 | { keys: ['g', 'v'], type: 'action', action: 'reselectLastSelection' }, |
michael@0 | 283 | { keys: ['J'], type: 'action', action: 'joinLines', isEdit: true }, |
michael@0 | 284 | { keys: ['p'], type: 'action', action: 'paste', isEdit: true, |
michael@0 | 285 | actionArgs: { after: true, isEdit: true }}, |
michael@0 | 286 | { keys: ['P'], type: 'action', action: 'paste', isEdit: true, |
michael@0 | 287 | actionArgs: { after: false, isEdit: true }}, |
michael@0 | 288 | { keys: ['r', 'character'], type: 'action', action: 'replace', isEdit: true }, |
michael@0 | 289 | { keys: ['@', 'character'], type: 'action', action: 'replayMacro' }, |
michael@0 | 290 | { keys: ['q', 'character'], type: 'action', action: 'enterMacroRecordMode' }, |
michael@0 | 291 | // Handle Replace-mode as a special case of insert mode. |
michael@0 | 292 | { keys: ['R'], type: 'action', action: 'enterInsertMode', isEdit: true, |
michael@0 | 293 | actionArgs: { replace: true }}, |
michael@0 | 294 | { keys: ['u'], type: 'action', action: 'undo' }, |
michael@0 | 295 | { keys: ['<C-r>'], type: 'action', action: 'redo' }, |
michael@0 | 296 | { keys: ['m', 'character'], type: 'action', action: 'setMark' }, |
michael@0 | 297 | { keys: ['"', 'character'], type: 'action', action: 'setRegister' }, |
michael@0 | 298 | { keys: ['z', 'z'], type: 'action', action: 'scrollToCursor', |
michael@0 | 299 | actionArgs: { position: 'center' }}, |
michael@0 | 300 | { keys: ['z', '.'], type: 'action', action: 'scrollToCursor', |
michael@0 | 301 | actionArgs: { position: 'center' }, |
michael@0 | 302 | motion: 'moveToFirstNonWhiteSpaceCharacter' }, |
michael@0 | 303 | { keys: ['z', 't'], type: 'action', action: 'scrollToCursor', |
michael@0 | 304 | actionArgs: { position: 'top' }}, |
michael@0 | 305 | { keys: ['z', '<CR>'], type: 'action', action: 'scrollToCursor', |
michael@0 | 306 | actionArgs: { position: 'top' }, |
michael@0 | 307 | motion: 'moveToFirstNonWhiteSpaceCharacter' }, |
michael@0 | 308 | { keys: ['z', '-'], type: 'action', action: 'scrollToCursor', |
michael@0 | 309 | actionArgs: { position: 'bottom' }}, |
michael@0 | 310 | { keys: ['z', 'b'], type: 'action', action: 'scrollToCursor', |
michael@0 | 311 | actionArgs: { position: 'bottom' }, |
michael@0 | 312 | motion: 'moveToFirstNonWhiteSpaceCharacter' }, |
michael@0 | 313 | { keys: ['.'], type: 'action', action: 'repeatLastEdit' }, |
michael@0 | 314 | { keys: ['<C-a>'], type: 'action', action: 'incrementNumberToken', |
michael@0 | 315 | isEdit: true, |
michael@0 | 316 | actionArgs: {increase: true, backtrack: false}}, |
michael@0 | 317 | { keys: ['<C-x>'], type: 'action', action: 'incrementNumberToken', |
michael@0 | 318 | isEdit: true, |
michael@0 | 319 | actionArgs: {increase: false, backtrack: false}}, |
michael@0 | 320 | // Text object motions |
michael@0 | 321 | { keys: ['a', 'character'], type: 'motion', |
michael@0 | 322 | motion: 'textObjectManipulation' }, |
michael@0 | 323 | { keys: ['i', 'character'], type: 'motion', |
michael@0 | 324 | motion: 'textObjectManipulation', |
michael@0 | 325 | motionArgs: { textObjectInner: true }}, |
michael@0 | 326 | // Search |
michael@0 | 327 | { keys: ['/'], type: 'search', |
michael@0 | 328 | searchArgs: { forward: true, querySrc: 'prompt', toJumplist: true }}, |
michael@0 | 329 | { keys: ['?'], type: 'search', |
michael@0 | 330 | searchArgs: { forward: false, querySrc: 'prompt', toJumplist: true }}, |
michael@0 | 331 | { keys: ['*'], type: 'search', |
michael@0 | 332 | searchArgs: { forward: true, querySrc: 'wordUnderCursor', toJumplist: true }}, |
michael@0 | 333 | { keys: ['#'], type: 'search', |
michael@0 | 334 | searchArgs: { forward: false, querySrc: 'wordUnderCursor', toJumplist: true }}, |
michael@0 | 335 | // Ex command |
michael@0 | 336 | { keys: [':'], type: 'ex' } |
michael@0 | 337 | ]; |
michael@0 | 338 | |
michael@0 | 339 | var Pos = CodeMirror.Pos; |
michael@0 | 340 | |
michael@0 | 341 | var Vim = function() { |
michael@0 | 342 | CodeMirror.defineOption('vimMode', false, function(cm, val) { |
michael@0 | 343 | if (val) { |
michael@0 | 344 | cm.setOption('keyMap', 'vim'); |
michael@0 | 345 | cm.setOption('disableInput', true); |
michael@0 | 346 | CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); |
michael@0 | 347 | cm.on('beforeSelectionChange', beforeSelectionChange); |
michael@0 | 348 | maybeInitVimState(cm); |
michael@0 | 349 | CodeMirror.on(cm.getInputField(), 'paste', getOnPasteFn(cm)); |
michael@0 | 350 | } else if (cm.state.vim) { |
michael@0 | 351 | cm.setOption('keyMap', 'default'); |
michael@0 | 352 | cm.setOption('disableInput', false); |
michael@0 | 353 | cm.off('beforeSelectionChange', beforeSelectionChange); |
michael@0 | 354 | CodeMirror.off(cm.getInputField(), 'paste', getOnPasteFn(cm)); |
michael@0 | 355 | cm.state.vim = null; |
michael@0 | 356 | } |
michael@0 | 357 | }); |
michael@0 | 358 | function beforeSelectionChange(cm, obj) { |
michael@0 | 359 | var vim = cm.state.vim; |
michael@0 | 360 | if (vim.insertMode || vim.exMode) return; |
michael@0 | 361 | |
michael@0 | 362 | var head = obj.ranges[0].head; |
michael@0 | 363 | var anchor = obj.ranges[0].anchor; |
michael@0 | 364 | if (head.ch && head.ch == cm.doc.getLine(head.line).length) { |
michael@0 | 365 | var pos = Pos(head.line, head.ch - 1); |
michael@0 | 366 | obj.update([{anchor: cursorEqual(head, anchor) ? pos : anchor, |
michael@0 | 367 | head: pos}]); |
michael@0 | 368 | } |
michael@0 | 369 | } |
michael@0 | 370 | function getOnPasteFn(cm) { |
michael@0 | 371 | var vim = cm.state.vim; |
michael@0 | 372 | if (!vim.onPasteFn) { |
michael@0 | 373 | vim.onPasteFn = function() { |
michael@0 | 374 | if (!vim.insertMode) { |
michael@0 | 375 | cm.setCursor(offsetCursor(cm.getCursor(), 0, 1)); |
michael@0 | 376 | actions.enterInsertMode(cm, {}, vim); |
michael@0 | 377 | } |
michael@0 | 378 | }; |
michael@0 | 379 | } |
michael@0 | 380 | return vim.onPasteFn; |
michael@0 | 381 | } |
michael@0 | 382 | |
michael@0 | 383 | var numberRegex = /[\d]/; |
michael@0 | 384 | var wordRegexp = [(/\w/), (/[^\w\s]/)], bigWordRegexp = [(/\S/)]; |
michael@0 | 385 | function makeKeyRange(start, size) { |
michael@0 | 386 | var keys = []; |
michael@0 | 387 | for (var i = start; i < start + size; i++) { |
michael@0 | 388 | keys.push(String.fromCharCode(i)); |
michael@0 | 389 | } |
michael@0 | 390 | return keys; |
michael@0 | 391 | } |
michael@0 | 392 | var upperCaseAlphabet = makeKeyRange(65, 26); |
michael@0 | 393 | var lowerCaseAlphabet = makeKeyRange(97, 26); |
michael@0 | 394 | var numbers = makeKeyRange(48, 10); |
michael@0 | 395 | var specialSymbols = '~`!@#$%^&*()_-+=[{}]\\|/?.,<>:;"\''.split(''); |
michael@0 | 396 | var specialKeys = ['Left', 'Right', 'Up', 'Down', 'Space', 'Backspace', |
michael@0 | 397 | 'Esc', 'Home', 'End', 'PageUp', 'PageDown', 'Enter']; |
michael@0 | 398 | var validMarks = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['<', '>']); |
michael@0 | 399 | var validRegisters = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['-', '"']); |
michael@0 | 400 | |
michael@0 | 401 | function isLine(cm, line) { |
michael@0 | 402 | return line >= cm.firstLine() && line <= cm.lastLine(); |
michael@0 | 403 | } |
michael@0 | 404 | function isLowerCase(k) { |
michael@0 | 405 | return (/^[a-z]$/).test(k); |
michael@0 | 406 | } |
michael@0 | 407 | function isMatchableSymbol(k) { |
michael@0 | 408 | return '()[]{}'.indexOf(k) != -1; |
michael@0 | 409 | } |
michael@0 | 410 | function isNumber(k) { |
michael@0 | 411 | return numberRegex.test(k); |
michael@0 | 412 | } |
michael@0 | 413 | function isUpperCase(k) { |
michael@0 | 414 | return (/^[A-Z]$/).test(k); |
michael@0 | 415 | } |
michael@0 | 416 | function isWhiteSpaceString(k) { |
michael@0 | 417 | return (/^\s*$/).test(k); |
michael@0 | 418 | } |
michael@0 | 419 | function inArray(val, arr) { |
michael@0 | 420 | for (var i = 0; i < arr.length; i++) { |
michael@0 | 421 | if (arr[i] == val) { |
michael@0 | 422 | return true; |
michael@0 | 423 | } |
michael@0 | 424 | } |
michael@0 | 425 | return false; |
michael@0 | 426 | } |
michael@0 | 427 | |
michael@0 | 428 | var options = {}; |
michael@0 | 429 | function defineOption(name, defaultValue, type) { |
michael@0 | 430 | if (defaultValue === undefined) { throw Error('defaultValue is required'); } |
michael@0 | 431 | if (!type) { type = 'string'; } |
michael@0 | 432 | options[name] = { |
michael@0 | 433 | type: type, |
michael@0 | 434 | defaultValue: defaultValue |
michael@0 | 435 | }; |
michael@0 | 436 | setOption(name, defaultValue); |
michael@0 | 437 | } |
michael@0 | 438 | |
michael@0 | 439 | function setOption(name, value) { |
michael@0 | 440 | var option = options[name]; |
michael@0 | 441 | if (!option) { |
michael@0 | 442 | throw Error('Unknown option: ' + name); |
michael@0 | 443 | } |
michael@0 | 444 | if (option.type == 'boolean') { |
michael@0 | 445 | if (value && value !== true) { |
michael@0 | 446 | throw Error('Invalid argument: ' + name + '=' + value); |
michael@0 | 447 | } else if (value !== false) { |
michael@0 | 448 | // Boolean options are set to true if value is not defined. |
michael@0 | 449 | value = true; |
michael@0 | 450 | } |
michael@0 | 451 | } |
michael@0 | 452 | option.value = option.type == 'boolean' ? !!value : value; |
michael@0 | 453 | } |
michael@0 | 454 | |
michael@0 | 455 | function getOption(name) { |
michael@0 | 456 | var option = options[name]; |
michael@0 | 457 | if (!option) { |
michael@0 | 458 | throw Error('Unknown option: ' + name); |
michael@0 | 459 | } |
michael@0 | 460 | return option.value; |
michael@0 | 461 | } |
michael@0 | 462 | |
michael@0 | 463 | var createCircularJumpList = function() { |
michael@0 | 464 | var size = 100; |
michael@0 | 465 | var pointer = -1; |
michael@0 | 466 | var head = 0; |
michael@0 | 467 | var tail = 0; |
michael@0 | 468 | var buffer = new Array(size); |
michael@0 | 469 | function add(cm, oldCur, newCur) { |
michael@0 | 470 | var current = pointer % size; |
michael@0 | 471 | var curMark = buffer[current]; |
michael@0 | 472 | function useNextSlot(cursor) { |
michael@0 | 473 | var next = ++pointer % size; |
michael@0 | 474 | var trashMark = buffer[next]; |
michael@0 | 475 | if (trashMark) { |
michael@0 | 476 | trashMark.clear(); |
michael@0 | 477 | } |
michael@0 | 478 | buffer[next] = cm.setBookmark(cursor); |
michael@0 | 479 | } |
michael@0 | 480 | if (curMark) { |
michael@0 | 481 | var markPos = curMark.find(); |
michael@0 | 482 | // avoid recording redundant cursor position |
michael@0 | 483 | if (markPos && !cursorEqual(markPos, oldCur)) { |
michael@0 | 484 | useNextSlot(oldCur); |
michael@0 | 485 | } |
michael@0 | 486 | } else { |
michael@0 | 487 | useNextSlot(oldCur); |
michael@0 | 488 | } |
michael@0 | 489 | useNextSlot(newCur); |
michael@0 | 490 | head = pointer; |
michael@0 | 491 | tail = pointer - size + 1; |
michael@0 | 492 | if (tail < 0) { |
michael@0 | 493 | tail = 0; |
michael@0 | 494 | } |
michael@0 | 495 | } |
michael@0 | 496 | function move(cm, offset) { |
michael@0 | 497 | pointer += offset; |
michael@0 | 498 | if (pointer > head) { |
michael@0 | 499 | pointer = head; |
michael@0 | 500 | } else if (pointer < tail) { |
michael@0 | 501 | pointer = tail; |
michael@0 | 502 | } |
michael@0 | 503 | var mark = buffer[(size + pointer) % size]; |
michael@0 | 504 | // skip marks that are temporarily removed from text buffer |
michael@0 | 505 | if (mark && !mark.find()) { |
michael@0 | 506 | var inc = offset > 0 ? 1 : -1; |
michael@0 | 507 | var newCur; |
michael@0 | 508 | var oldCur = cm.getCursor(); |
michael@0 | 509 | do { |
michael@0 | 510 | pointer += inc; |
michael@0 | 511 | mark = buffer[(size + pointer) % size]; |
michael@0 | 512 | // skip marks that are the same as current position |
michael@0 | 513 | if (mark && |
michael@0 | 514 | (newCur = mark.find()) && |
michael@0 | 515 | !cursorEqual(oldCur, newCur)) { |
michael@0 | 516 | break; |
michael@0 | 517 | } |
michael@0 | 518 | } while (pointer < head && pointer > tail); |
michael@0 | 519 | } |
michael@0 | 520 | return mark; |
michael@0 | 521 | } |
michael@0 | 522 | return { |
michael@0 | 523 | cachedCursor: undefined, //used for # and * jumps |
michael@0 | 524 | add: add, |
michael@0 | 525 | move: move |
michael@0 | 526 | }; |
michael@0 | 527 | }; |
michael@0 | 528 | |
michael@0 | 529 | // Returns an object to track the changes associated insert mode. It |
michael@0 | 530 | // clones the object that is passed in, or creates an empty object one if |
michael@0 | 531 | // none is provided. |
michael@0 | 532 | var createInsertModeChanges = function(c) { |
michael@0 | 533 | if (c) { |
michael@0 | 534 | // Copy construction |
michael@0 | 535 | return { |
michael@0 | 536 | changes: c.changes, |
michael@0 | 537 | expectCursorActivityForChange: c.expectCursorActivityForChange |
michael@0 | 538 | }; |
michael@0 | 539 | } |
michael@0 | 540 | return { |
michael@0 | 541 | // Change list |
michael@0 | 542 | changes: [], |
michael@0 | 543 | // Set to true on change, false on cursorActivity. |
michael@0 | 544 | expectCursorActivityForChange: false |
michael@0 | 545 | }; |
michael@0 | 546 | }; |
michael@0 | 547 | |
michael@0 | 548 | function MacroModeState() { |
michael@0 | 549 | this.latestRegister = undefined; |
michael@0 | 550 | this.isPlaying = false; |
michael@0 | 551 | this.isRecording = false; |
michael@0 | 552 | this.onRecordingDone = undefined; |
michael@0 | 553 | this.lastInsertModeChanges = createInsertModeChanges(); |
michael@0 | 554 | } |
michael@0 | 555 | MacroModeState.prototype = { |
michael@0 | 556 | exitMacroRecordMode: function() { |
michael@0 | 557 | var macroModeState = vimGlobalState.macroModeState; |
michael@0 | 558 | macroModeState.onRecordingDone(); // close dialog |
michael@0 | 559 | macroModeState.onRecordingDone = undefined; |
michael@0 | 560 | macroModeState.isRecording = false; |
michael@0 | 561 | }, |
michael@0 | 562 | enterMacroRecordMode: function(cm, registerName) { |
michael@0 | 563 | var register = |
michael@0 | 564 | vimGlobalState.registerController.getRegister(registerName); |
michael@0 | 565 | if (register) { |
michael@0 | 566 | register.clear(); |
michael@0 | 567 | this.latestRegister = registerName; |
michael@0 | 568 | this.onRecordingDone = cm.openDialog( |
michael@0 | 569 | '(recording)['+registerName+']', null, {bottom:true}); |
michael@0 | 570 | this.isRecording = true; |
michael@0 | 571 | } |
michael@0 | 572 | } |
michael@0 | 573 | }; |
michael@0 | 574 | |
michael@0 | 575 | function maybeInitVimState(cm) { |
michael@0 | 576 | if (!cm.state.vim) { |
michael@0 | 577 | // Store instance state in the CodeMirror object. |
michael@0 | 578 | cm.state.vim = { |
michael@0 | 579 | inputState: new InputState(), |
michael@0 | 580 | // Vim's input state that triggered the last edit, used to repeat |
michael@0 | 581 | // motions and operators with '.'. |
michael@0 | 582 | lastEditInputState: undefined, |
michael@0 | 583 | // Vim's action command before the last edit, used to repeat actions |
michael@0 | 584 | // with '.' and insert mode repeat. |
michael@0 | 585 | lastEditActionCommand: undefined, |
michael@0 | 586 | // When using jk for navigation, if you move from a longer line to a |
michael@0 | 587 | // shorter line, the cursor may clip to the end of the shorter line. |
michael@0 | 588 | // If j is pressed again and cursor goes to the next line, the |
michael@0 | 589 | // cursor should go back to its horizontal position on the longer |
michael@0 | 590 | // line if it can. This is to keep track of the horizontal position. |
michael@0 | 591 | lastHPos: -1, |
michael@0 | 592 | // Doing the same with screen-position for gj/gk |
michael@0 | 593 | lastHSPos: -1, |
michael@0 | 594 | // The last motion command run. Cleared if a non-motion command gets |
michael@0 | 595 | // executed in between. |
michael@0 | 596 | lastMotion: null, |
michael@0 | 597 | marks: {}, |
michael@0 | 598 | insertMode: false, |
michael@0 | 599 | // Repeat count for changes made in insert mode, triggered by key |
michael@0 | 600 | // sequences like 3,i. Only exists when insertMode is true. |
michael@0 | 601 | insertModeRepeat: undefined, |
michael@0 | 602 | visualMode: false, |
michael@0 | 603 | // If we are in visual line mode. No effect if visualMode is false. |
michael@0 | 604 | visualLine: false, |
michael@0 | 605 | lastSelection: null |
michael@0 | 606 | }; |
michael@0 | 607 | } |
michael@0 | 608 | return cm.state.vim; |
michael@0 | 609 | } |
michael@0 | 610 | var vimGlobalState; |
michael@0 | 611 | function resetVimGlobalState() { |
michael@0 | 612 | vimGlobalState = { |
michael@0 | 613 | // The current search query. |
michael@0 | 614 | searchQuery: null, |
michael@0 | 615 | // Whether we are searching backwards. |
michael@0 | 616 | searchIsReversed: false, |
michael@0 | 617 | jumpList: createCircularJumpList(), |
michael@0 | 618 | macroModeState: new MacroModeState, |
michael@0 | 619 | // Recording latest f, t, F or T motion command. |
michael@0 | 620 | lastChararacterSearch: {increment:0, forward:true, selectedCharacter:''}, |
michael@0 | 621 | registerController: new RegisterController({}) |
michael@0 | 622 | }; |
michael@0 | 623 | for (var optionName in options) { |
michael@0 | 624 | var option = options[optionName]; |
michael@0 | 625 | option.value = option.defaultValue; |
michael@0 | 626 | } |
michael@0 | 627 | } |
michael@0 | 628 | |
michael@0 | 629 | var vimApi= { |
michael@0 | 630 | buildKeyMap: function() { |
michael@0 | 631 | // TODO: Convert keymap into dictionary format for fast lookup. |
michael@0 | 632 | }, |
michael@0 | 633 | // Testing hook, though it might be useful to expose the register |
michael@0 | 634 | // controller anyways. |
michael@0 | 635 | getRegisterController: function() { |
michael@0 | 636 | return vimGlobalState.registerController; |
michael@0 | 637 | }, |
michael@0 | 638 | // Testing hook. |
michael@0 | 639 | resetVimGlobalState_: resetVimGlobalState, |
michael@0 | 640 | |
michael@0 | 641 | // Testing hook. |
michael@0 | 642 | getVimGlobalState_: function() { |
michael@0 | 643 | return vimGlobalState; |
michael@0 | 644 | }, |
michael@0 | 645 | |
michael@0 | 646 | // Testing hook. |
michael@0 | 647 | maybeInitVimState_: maybeInitVimState, |
michael@0 | 648 | |
michael@0 | 649 | InsertModeKey: InsertModeKey, |
michael@0 | 650 | map: function(lhs, rhs, ctx) { |
michael@0 | 651 | // Add user defined key bindings. |
michael@0 | 652 | exCommandDispatcher.map(lhs, rhs, ctx); |
michael@0 | 653 | }, |
michael@0 | 654 | setOption: setOption, |
michael@0 | 655 | getOption: getOption, |
michael@0 | 656 | defineOption: defineOption, |
michael@0 | 657 | defineEx: function(name, prefix, func){ |
michael@0 | 658 | if (name.indexOf(prefix) !== 0) { |
michael@0 | 659 | throw new Error('(Vim.defineEx) "'+prefix+'" is not a prefix of "'+name+'", command not registered'); |
michael@0 | 660 | } |
michael@0 | 661 | exCommands[name]=func; |
michael@0 | 662 | exCommandDispatcher.commandMap_[prefix]={name:name, shortName:prefix, type:'api'}; |
michael@0 | 663 | }, |
michael@0 | 664 | // This is the outermost function called by CodeMirror, after keys have |
michael@0 | 665 | // been mapped to their Vim equivalents. |
michael@0 | 666 | handleKey: function(cm, key) { |
michael@0 | 667 | var command; |
michael@0 | 668 | var vim = maybeInitVimState(cm); |
michael@0 | 669 | var macroModeState = vimGlobalState.macroModeState; |
michael@0 | 670 | if (macroModeState.isRecording) { |
michael@0 | 671 | if (key == 'q') { |
michael@0 | 672 | macroModeState.exitMacroRecordMode(); |
michael@0 | 673 | vim.inputState = new InputState(); |
michael@0 | 674 | return; |
michael@0 | 675 | } |
michael@0 | 676 | } |
michael@0 | 677 | if (key == '<Esc>') { |
michael@0 | 678 | // Clear input state and get back to normal mode. |
michael@0 | 679 | vim.inputState = new InputState(); |
michael@0 | 680 | if (vim.visualMode) { |
michael@0 | 681 | exitVisualMode(cm); |
michael@0 | 682 | } |
michael@0 | 683 | return; |
michael@0 | 684 | } |
michael@0 | 685 | // Enter visual mode when the mouse selects text. |
michael@0 | 686 | if (!vim.visualMode && |
michael@0 | 687 | !cursorEqual(cm.getCursor('head'), cm.getCursor('anchor'))) { |
michael@0 | 688 | vim.visualMode = true; |
michael@0 | 689 | vim.visualLine = false; |
michael@0 | 690 | CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"}); |
michael@0 | 691 | cm.on('mousedown', exitVisualMode); |
michael@0 | 692 | } |
michael@0 | 693 | if (key != '0' || (key == '0' && vim.inputState.getRepeat() === 0)) { |
michael@0 | 694 | // Have to special case 0 since it's both a motion and a number. |
michael@0 | 695 | command = commandDispatcher.matchCommand(key, defaultKeymap, vim); |
michael@0 | 696 | } |
michael@0 | 697 | if (!command) { |
michael@0 | 698 | if (isNumber(key)) { |
michael@0 | 699 | // Increment count unless count is 0 and key is 0. |
michael@0 | 700 | vim.inputState.pushRepeatDigit(key); |
michael@0 | 701 | } |
michael@0 | 702 | if (macroModeState.isRecording) { |
michael@0 | 703 | logKey(macroModeState, key); |
michael@0 | 704 | } |
michael@0 | 705 | return; |
michael@0 | 706 | } |
michael@0 | 707 | if (command.type == 'keyToKey') { |
michael@0 | 708 | // TODO: prevent infinite recursion. |
michael@0 | 709 | for (var i = 0; i < command.toKeys.length; i++) { |
michael@0 | 710 | this.handleKey(cm, command.toKeys[i]); |
michael@0 | 711 | } |
michael@0 | 712 | } else { |
michael@0 | 713 | if (macroModeState.isRecording) { |
michael@0 | 714 | logKey(macroModeState, key); |
michael@0 | 715 | } |
michael@0 | 716 | commandDispatcher.processCommand(cm, vim, command); |
michael@0 | 717 | } |
michael@0 | 718 | }, |
michael@0 | 719 | handleEx: function(cm, input) { |
michael@0 | 720 | exCommandDispatcher.processCommand(cm, input); |
michael@0 | 721 | } |
michael@0 | 722 | }; |
michael@0 | 723 | |
michael@0 | 724 | // Represents the current input state. |
michael@0 | 725 | function InputState() { |
michael@0 | 726 | this.prefixRepeat = []; |
michael@0 | 727 | this.motionRepeat = []; |
michael@0 | 728 | |
michael@0 | 729 | this.operator = null; |
michael@0 | 730 | this.operatorArgs = null; |
michael@0 | 731 | this.motion = null; |
michael@0 | 732 | this.motionArgs = null; |
michael@0 | 733 | this.keyBuffer = []; // For matching multi-key commands. |
michael@0 | 734 | this.registerName = null; // Defaults to the unnamed register. |
michael@0 | 735 | } |
michael@0 | 736 | InputState.prototype.pushRepeatDigit = function(n) { |
michael@0 | 737 | if (!this.operator) { |
michael@0 | 738 | this.prefixRepeat = this.prefixRepeat.concat(n); |
michael@0 | 739 | } else { |
michael@0 | 740 | this.motionRepeat = this.motionRepeat.concat(n); |
michael@0 | 741 | } |
michael@0 | 742 | }; |
michael@0 | 743 | InputState.prototype.getRepeat = function() { |
michael@0 | 744 | var repeat = 0; |
michael@0 | 745 | if (this.prefixRepeat.length > 0 || this.motionRepeat.length > 0) { |
michael@0 | 746 | repeat = 1; |
michael@0 | 747 | if (this.prefixRepeat.length > 0) { |
michael@0 | 748 | repeat *= parseInt(this.prefixRepeat.join(''), 10); |
michael@0 | 749 | } |
michael@0 | 750 | if (this.motionRepeat.length > 0) { |
michael@0 | 751 | repeat *= parseInt(this.motionRepeat.join(''), 10); |
michael@0 | 752 | } |
michael@0 | 753 | } |
michael@0 | 754 | return repeat; |
michael@0 | 755 | }; |
michael@0 | 756 | |
michael@0 | 757 | /* |
michael@0 | 758 | * Register stores information about copy and paste registers. Besides |
michael@0 | 759 | * text, a register must store whether it is linewise (i.e., when it is |
michael@0 | 760 | * pasted, should it insert itself into a new line, or should the text be |
michael@0 | 761 | * inserted at the cursor position.) |
michael@0 | 762 | */ |
michael@0 | 763 | function Register(text, linewise) { |
michael@0 | 764 | this.clear(); |
michael@0 | 765 | this.keyBuffer = [text || '']; |
michael@0 | 766 | this.insertModeChanges = []; |
michael@0 | 767 | this.linewise = !!linewise; |
michael@0 | 768 | } |
michael@0 | 769 | Register.prototype = { |
michael@0 | 770 | setText: function(text, linewise) { |
michael@0 | 771 | this.keyBuffer = [text || '']; |
michael@0 | 772 | this.linewise = !!linewise; |
michael@0 | 773 | }, |
michael@0 | 774 | pushText: function(text, linewise) { |
michael@0 | 775 | // if this register has ever been set to linewise, use linewise. |
michael@0 | 776 | if (linewise || this.linewise) { |
michael@0 | 777 | this.keyBuffer.push('\n'); |
michael@0 | 778 | this.linewise = true; |
michael@0 | 779 | } |
michael@0 | 780 | this.keyBuffer.push(text); |
michael@0 | 781 | }, |
michael@0 | 782 | pushInsertModeChanges: function(changes) { |
michael@0 | 783 | this.insertModeChanges.push(createInsertModeChanges(changes)); |
michael@0 | 784 | }, |
michael@0 | 785 | clear: function() { |
michael@0 | 786 | this.keyBuffer = []; |
michael@0 | 787 | this.insertModeChanges = []; |
michael@0 | 788 | this.linewise = false; |
michael@0 | 789 | }, |
michael@0 | 790 | toString: function() { |
michael@0 | 791 | return this.keyBuffer.join(''); |
michael@0 | 792 | } |
michael@0 | 793 | }; |
michael@0 | 794 | |
michael@0 | 795 | /* |
michael@0 | 796 | * vim registers allow you to keep many independent copy and paste buffers. |
michael@0 | 797 | * See http://usevim.com/2012/04/13/registers/ for an introduction. |
michael@0 | 798 | * |
michael@0 | 799 | * RegisterController keeps the state of all the registers. An initial |
michael@0 | 800 | * state may be passed in. The unnamed register '"' will always be |
michael@0 | 801 | * overridden. |
michael@0 | 802 | */ |
michael@0 | 803 | function RegisterController(registers) { |
michael@0 | 804 | this.registers = registers; |
michael@0 | 805 | this.unnamedRegister = registers['"'] = new Register(); |
michael@0 | 806 | } |
michael@0 | 807 | RegisterController.prototype = { |
michael@0 | 808 | pushText: function(registerName, operator, text, linewise) { |
michael@0 | 809 | if (linewise && text.charAt(0) == '\n') { |
michael@0 | 810 | text = text.slice(1) + '\n'; |
michael@0 | 811 | } |
michael@0 | 812 | if (linewise && text.charAt(text.length - 1) !== '\n'){ |
michael@0 | 813 | text += '\n'; |
michael@0 | 814 | } |
michael@0 | 815 | // Lowercase and uppercase registers refer to the same register. |
michael@0 | 816 | // Uppercase just means append. |
michael@0 | 817 | var register = this.isValidRegister(registerName) ? |
michael@0 | 818 | this.getRegister(registerName) : null; |
michael@0 | 819 | // if no register/an invalid register was specified, things go to the |
michael@0 | 820 | // default registers |
michael@0 | 821 | if (!register) { |
michael@0 | 822 | switch (operator) { |
michael@0 | 823 | case 'yank': |
michael@0 | 824 | // The 0 register contains the text from the most recent yank. |
michael@0 | 825 | this.registers['0'] = new Register(text, linewise); |
michael@0 | 826 | break; |
michael@0 | 827 | case 'delete': |
michael@0 | 828 | case 'change': |
michael@0 | 829 | if (text.indexOf('\n') == -1) { |
michael@0 | 830 | // Delete less than 1 line. Update the small delete register. |
michael@0 | 831 | this.registers['-'] = new Register(text, linewise); |
michael@0 | 832 | } else { |
michael@0 | 833 | // Shift down the contents of the numbered registers and put the |
michael@0 | 834 | // deleted text into register 1. |
michael@0 | 835 | this.shiftNumericRegisters_(); |
michael@0 | 836 | this.registers['1'] = new Register(text, linewise); |
michael@0 | 837 | } |
michael@0 | 838 | break; |
michael@0 | 839 | } |
michael@0 | 840 | // Make sure the unnamed register is set to what just happened |
michael@0 | 841 | this.unnamedRegister.setText(text, linewise); |
michael@0 | 842 | return; |
michael@0 | 843 | } |
michael@0 | 844 | |
michael@0 | 845 | // If we've gotten to this point, we've actually specified a register |
michael@0 | 846 | var append = isUpperCase(registerName); |
michael@0 | 847 | if (append) { |
michael@0 | 848 | register.append(text, linewise); |
michael@0 | 849 | // The unnamed register always has the same value as the last used |
michael@0 | 850 | // register. |
michael@0 | 851 | this.unnamedRegister.append(text, linewise); |
michael@0 | 852 | } else { |
michael@0 | 853 | register.setText(text, linewise); |
michael@0 | 854 | this.unnamedRegister.setText(text, linewise); |
michael@0 | 855 | } |
michael@0 | 856 | }, |
michael@0 | 857 | // Gets the register named @name. If one of @name doesn't already exist, |
michael@0 | 858 | // create it. If @name is invalid, return the unnamedRegister. |
michael@0 | 859 | getRegister: function(name) { |
michael@0 | 860 | if (!this.isValidRegister(name)) { |
michael@0 | 861 | return this.unnamedRegister; |
michael@0 | 862 | } |
michael@0 | 863 | name = name.toLowerCase(); |
michael@0 | 864 | if (!this.registers[name]) { |
michael@0 | 865 | this.registers[name] = new Register(); |
michael@0 | 866 | } |
michael@0 | 867 | return this.registers[name]; |
michael@0 | 868 | }, |
michael@0 | 869 | isValidRegister: function(name) { |
michael@0 | 870 | return name && inArray(name, validRegisters); |
michael@0 | 871 | }, |
michael@0 | 872 | shiftNumericRegisters_: function() { |
michael@0 | 873 | for (var i = 9; i >= 2; i--) { |
michael@0 | 874 | this.registers[i] = this.getRegister('' + (i - 1)); |
michael@0 | 875 | } |
michael@0 | 876 | } |
michael@0 | 877 | }; |
michael@0 | 878 | |
michael@0 | 879 | var commandDispatcher = { |
michael@0 | 880 | matchCommand: function(key, keyMap, vim) { |
michael@0 | 881 | var inputState = vim.inputState; |
michael@0 | 882 | var keys = inputState.keyBuffer.concat(key); |
michael@0 | 883 | var matchedCommands = []; |
michael@0 | 884 | var selectedCharacter; |
michael@0 | 885 | for (var i = 0; i < keyMap.length; i++) { |
michael@0 | 886 | var command = keyMap[i]; |
michael@0 | 887 | if (matchKeysPartial(keys, command.keys)) { |
michael@0 | 888 | if (inputState.operator && command.type == 'action') { |
michael@0 | 889 | // Ignore matched action commands after an operator. Operators |
michael@0 | 890 | // only operate on motions. This check is really for text |
michael@0 | 891 | // objects since aW, a[ etcs conflicts with a. |
michael@0 | 892 | continue; |
michael@0 | 893 | } |
michael@0 | 894 | // Match commands that take <character> as an argument. |
michael@0 | 895 | if (command.keys[keys.length - 1] == 'character') { |
michael@0 | 896 | selectedCharacter = keys[keys.length - 1]; |
michael@0 | 897 | if (selectedCharacter.length>1){ |
michael@0 | 898 | switch(selectedCharacter){ |
michael@0 | 899 | case '<CR>': |
michael@0 | 900 | selectedCharacter='\n'; |
michael@0 | 901 | break; |
michael@0 | 902 | case '<Space>': |
michael@0 | 903 | selectedCharacter=' '; |
michael@0 | 904 | break; |
michael@0 | 905 | default: |
michael@0 | 906 | continue; |
michael@0 | 907 | } |
michael@0 | 908 | } |
michael@0 | 909 | } |
michael@0 | 910 | // Add the command to the list of matched commands. Choose the best |
michael@0 | 911 | // command later. |
michael@0 | 912 | matchedCommands.push(command); |
michael@0 | 913 | } |
michael@0 | 914 | } |
michael@0 | 915 | |
michael@0 | 916 | // Returns the command if it is a full match, or null if not. |
michael@0 | 917 | function getFullyMatchedCommandOrNull(command) { |
michael@0 | 918 | if (keys.length < command.keys.length) { |
michael@0 | 919 | // Matches part of a multi-key command. Buffer and wait for next |
michael@0 | 920 | // stroke. |
michael@0 | 921 | inputState.keyBuffer.push(key); |
michael@0 | 922 | return null; |
michael@0 | 923 | } else { |
michael@0 | 924 | if (command.keys[keys.length - 1] == 'character') { |
michael@0 | 925 | inputState.selectedCharacter = selectedCharacter; |
michael@0 | 926 | } |
michael@0 | 927 | // Clear the buffer since a full match was found. |
michael@0 | 928 | inputState.keyBuffer = []; |
michael@0 | 929 | return command; |
michael@0 | 930 | } |
michael@0 | 931 | } |
michael@0 | 932 | |
michael@0 | 933 | if (!matchedCommands.length) { |
michael@0 | 934 | // Clear the buffer since there were no matches. |
michael@0 | 935 | inputState.keyBuffer = []; |
michael@0 | 936 | return null; |
michael@0 | 937 | } else if (matchedCommands.length == 1) { |
michael@0 | 938 | return getFullyMatchedCommandOrNull(matchedCommands[0]); |
michael@0 | 939 | } else { |
michael@0 | 940 | // Find the best match in the list of matchedCommands. |
michael@0 | 941 | var context = vim.visualMode ? 'visual' : 'normal'; |
michael@0 | 942 | var bestMatch; // Default to first in the list. |
michael@0 | 943 | for (var i = 0; i < matchedCommands.length; i++) { |
michael@0 | 944 | var current = matchedCommands[i]; |
michael@0 | 945 | if (current.context == context) { |
michael@0 | 946 | bestMatch = current; |
michael@0 | 947 | break; |
michael@0 | 948 | } else if (!bestMatch && !current.context) { |
michael@0 | 949 | // Only set an imperfect match to best match if no best match is |
michael@0 | 950 | // set and the imperfect match is not restricted to another |
michael@0 | 951 | // context. |
michael@0 | 952 | bestMatch = current; |
michael@0 | 953 | } |
michael@0 | 954 | } |
michael@0 | 955 | return getFullyMatchedCommandOrNull(bestMatch); |
michael@0 | 956 | } |
michael@0 | 957 | }, |
michael@0 | 958 | processCommand: function(cm, vim, command) { |
michael@0 | 959 | vim.inputState.repeatOverride = command.repeatOverride; |
michael@0 | 960 | switch (command.type) { |
michael@0 | 961 | case 'motion': |
michael@0 | 962 | this.processMotion(cm, vim, command); |
michael@0 | 963 | break; |
michael@0 | 964 | case 'operator': |
michael@0 | 965 | this.processOperator(cm, vim, command); |
michael@0 | 966 | break; |
michael@0 | 967 | case 'operatorMotion': |
michael@0 | 968 | this.processOperatorMotion(cm, vim, command); |
michael@0 | 969 | break; |
michael@0 | 970 | case 'action': |
michael@0 | 971 | this.processAction(cm, vim, command); |
michael@0 | 972 | break; |
michael@0 | 973 | case 'search': |
michael@0 | 974 | this.processSearch(cm, vim, command); |
michael@0 | 975 | break; |
michael@0 | 976 | case 'ex': |
michael@0 | 977 | case 'keyToEx': |
michael@0 | 978 | this.processEx(cm, vim, command); |
michael@0 | 979 | break; |
michael@0 | 980 | default: |
michael@0 | 981 | break; |
michael@0 | 982 | } |
michael@0 | 983 | }, |
michael@0 | 984 | processMotion: function(cm, vim, command) { |
michael@0 | 985 | vim.inputState.motion = command.motion; |
michael@0 | 986 | vim.inputState.motionArgs = copyArgs(command.motionArgs); |
michael@0 | 987 | this.evalInput(cm, vim); |
michael@0 | 988 | }, |
michael@0 | 989 | processOperator: function(cm, vim, command) { |
michael@0 | 990 | var inputState = vim.inputState; |
michael@0 | 991 | if (inputState.operator) { |
michael@0 | 992 | if (inputState.operator == command.operator) { |
michael@0 | 993 | // Typing an operator twice like 'dd' makes the operator operate |
michael@0 | 994 | // linewise |
michael@0 | 995 | inputState.motion = 'expandToLine'; |
michael@0 | 996 | inputState.motionArgs = { linewise: true }; |
michael@0 | 997 | this.evalInput(cm, vim); |
michael@0 | 998 | return; |
michael@0 | 999 | } else { |
michael@0 | 1000 | // 2 different operators in a row doesn't make sense. |
michael@0 | 1001 | vim.inputState = new InputState(); |
michael@0 | 1002 | } |
michael@0 | 1003 | } |
michael@0 | 1004 | inputState.operator = command.operator; |
michael@0 | 1005 | inputState.operatorArgs = copyArgs(command.operatorArgs); |
michael@0 | 1006 | if (vim.visualMode) { |
michael@0 | 1007 | // Operating on a selection in visual mode. We don't need a motion. |
michael@0 | 1008 | this.evalInput(cm, vim); |
michael@0 | 1009 | } |
michael@0 | 1010 | }, |
michael@0 | 1011 | processOperatorMotion: function(cm, vim, command) { |
michael@0 | 1012 | var visualMode = vim.visualMode; |
michael@0 | 1013 | var operatorMotionArgs = copyArgs(command.operatorMotionArgs); |
michael@0 | 1014 | if (operatorMotionArgs) { |
michael@0 | 1015 | // Operator motions may have special behavior in visual mode. |
michael@0 | 1016 | if (visualMode && operatorMotionArgs.visualLine) { |
michael@0 | 1017 | vim.visualLine = true; |
michael@0 | 1018 | } |
michael@0 | 1019 | } |
michael@0 | 1020 | this.processOperator(cm, vim, command); |
michael@0 | 1021 | if (!visualMode) { |
michael@0 | 1022 | this.processMotion(cm, vim, command); |
michael@0 | 1023 | } |
michael@0 | 1024 | }, |
michael@0 | 1025 | processAction: function(cm, vim, command) { |
michael@0 | 1026 | var inputState = vim.inputState; |
michael@0 | 1027 | var repeat = inputState.getRepeat(); |
michael@0 | 1028 | var repeatIsExplicit = !!repeat; |
michael@0 | 1029 | var actionArgs = copyArgs(command.actionArgs) || {}; |
michael@0 | 1030 | if (inputState.selectedCharacter) { |
michael@0 | 1031 | actionArgs.selectedCharacter = inputState.selectedCharacter; |
michael@0 | 1032 | } |
michael@0 | 1033 | // Actions may or may not have motions and operators. Do these first. |
michael@0 | 1034 | if (command.operator) { |
michael@0 | 1035 | this.processOperator(cm, vim, command); |
michael@0 | 1036 | } |
michael@0 | 1037 | if (command.motion) { |
michael@0 | 1038 | this.processMotion(cm, vim, command); |
michael@0 | 1039 | } |
michael@0 | 1040 | if (command.motion || command.operator) { |
michael@0 | 1041 | this.evalInput(cm, vim); |
michael@0 | 1042 | } |
michael@0 | 1043 | actionArgs.repeat = repeat || 1; |
michael@0 | 1044 | actionArgs.repeatIsExplicit = repeatIsExplicit; |
michael@0 | 1045 | actionArgs.registerName = inputState.registerName; |
michael@0 | 1046 | vim.inputState = new InputState(); |
michael@0 | 1047 | vim.lastMotion = null; |
michael@0 | 1048 | if (command.isEdit) { |
michael@0 | 1049 | this.recordLastEdit(vim, inputState, command); |
michael@0 | 1050 | } |
michael@0 | 1051 | actions[command.action](cm, actionArgs, vim); |
michael@0 | 1052 | }, |
michael@0 | 1053 | processSearch: function(cm, vim, command) { |
michael@0 | 1054 | if (!cm.getSearchCursor) { |
michael@0 | 1055 | // Search depends on SearchCursor. |
michael@0 | 1056 | return; |
michael@0 | 1057 | } |
michael@0 | 1058 | var forward = command.searchArgs.forward; |
michael@0 | 1059 | getSearchState(cm).setReversed(!forward); |
michael@0 | 1060 | var promptPrefix = (forward) ? '/' : '?'; |
michael@0 | 1061 | var originalQuery = getSearchState(cm).getQuery(); |
michael@0 | 1062 | var originalScrollPos = cm.getScrollInfo(); |
michael@0 | 1063 | function handleQuery(query, ignoreCase, smartCase) { |
michael@0 | 1064 | try { |
michael@0 | 1065 | updateSearchQuery(cm, query, ignoreCase, smartCase); |
michael@0 | 1066 | } catch (e) { |
michael@0 | 1067 | showConfirm(cm, 'Invalid regex: ' + query); |
michael@0 | 1068 | return; |
michael@0 | 1069 | } |
michael@0 | 1070 | commandDispatcher.processMotion(cm, vim, { |
michael@0 | 1071 | type: 'motion', |
michael@0 | 1072 | motion: 'findNext', |
michael@0 | 1073 | motionArgs: { forward: true, toJumplist: command.searchArgs.toJumplist } |
michael@0 | 1074 | }); |
michael@0 | 1075 | } |
michael@0 | 1076 | function onPromptClose(query) { |
michael@0 | 1077 | cm.scrollTo(originalScrollPos.left, originalScrollPos.top); |
michael@0 | 1078 | handleQuery(query, true /** ignoreCase */, true /** smartCase */); |
michael@0 | 1079 | } |
michael@0 | 1080 | function onPromptKeyUp(_e, query) { |
michael@0 | 1081 | var parsedQuery; |
michael@0 | 1082 | try { |
michael@0 | 1083 | parsedQuery = updateSearchQuery(cm, query, |
michael@0 | 1084 | true /** ignoreCase */, true /** smartCase */); |
michael@0 | 1085 | } catch (e) { |
michael@0 | 1086 | // Swallow bad regexes for incremental search. |
michael@0 | 1087 | } |
michael@0 | 1088 | if (parsedQuery) { |
michael@0 | 1089 | cm.scrollIntoView(findNext(cm, !forward, parsedQuery), 30); |
michael@0 | 1090 | } else { |
michael@0 | 1091 | clearSearchHighlight(cm); |
michael@0 | 1092 | cm.scrollTo(originalScrollPos.left, originalScrollPos.top); |
michael@0 | 1093 | } |
michael@0 | 1094 | } |
michael@0 | 1095 | function onPromptKeyDown(e, _query, close) { |
michael@0 | 1096 | var keyName = CodeMirror.keyName(e); |
michael@0 | 1097 | if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[') { |
michael@0 | 1098 | updateSearchQuery(cm, originalQuery); |
michael@0 | 1099 | clearSearchHighlight(cm); |
michael@0 | 1100 | cm.scrollTo(originalScrollPos.left, originalScrollPos.top); |
michael@0 | 1101 | |
michael@0 | 1102 | CodeMirror.e_stop(e); |
michael@0 | 1103 | close(); |
michael@0 | 1104 | cm.focus(); |
michael@0 | 1105 | } |
michael@0 | 1106 | } |
michael@0 | 1107 | switch (command.searchArgs.querySrc) { |
michael@0 | 1108 | case 'prompt': |
michael@0 | 1109 | showPrompt(cm, { |
michael@0 | 1110 | onClose: onPromptClose, |
michael@0 | 1111 | prefix: promptPrefix, |
michael@0 | 1112 | desc: searchPromptDesc, |
michael@0 | 1113 | onKeyUp: onPromptKeyUp, |
michael@0 | 1114 | onKeyDown: onPromptKeyDown |
michael@0 | 1115 | }); |
michael@0 | 1116 | break; |
michael@0 | 1117 | case 'wordUnderCursor': |
michael@0 | 1118 | var word = expandWordUnderCursor(cm, false /** inclusive */, |
michael@0 | 1119 | true /** forward */, false /** bigWord */, |
michael@0 | 1120 | true /** noSymbol */); |
michael@0 | 1121 | var isKeyword = true; |
michael@0 | 1122 | if (!word) { |
michael@0 | 1123 | word = expandWordUnderCursor(cm, false /** inclusive */, |
michael@0 | 1124 | true /** forward */, false /** bigWord */, |
michael@0 | 1125 | false /** noSymbol */); |
michael@0 | 1126 | isKeyword = false; |
michael@0 | 1127 | } |
michael@0 | 1128 | if (!word) { |
michael@0 | 1129 | return; |
michael@0 | 1130 | } |
michael@0 | 1131 | var query = cm.getLine(word.start.line).substring(word.start.ch, |
michael@0 | 1132 | word.end.ch); |
michael@0 | 1133 | if (isKeyword) { |
michael@0 | 1134 | query = '\\b' + query + '\\b'; |
michael@0 | 1135 | } else { |
michael@0 | 1136 | query = escapeRegex(query); |
michael@0 | 1137 | } |
michael@0 | 1138 | |
michael@0 | 1139 | // cachedCursor is used to save the old position of the cursor |
michael@0 | 1140 | // when * or # causes vim to seek for the nearest word and shift |
michael@0 | 1141 | // the cursor before entering the motion. |
michael@0 | 1142 | vimGlobalState.jumpList.cachedCursor = cm.getCursor(); |
michael@0 | 1143 | cm.setCursor(word.start); |
michael@0 | 1144 | |
michael@0 | 1145 | handleQuery(query, true /** ignoreCase */, false /** smartCase */); |
michael@0 | 1146 | break; |
michael@0 | 1147 | } |
michael@0 | 1148 | }, |
michael@0 | 1149 | processEx: function(cm, vim, command) { |
michael@0 | 1150 | function onPromptClose(input) { |
michael@0 | 1151 | // Give the prompt some time to close so that if processCommand shows |
michael@0 | 1152 | // an error, the elements don't overlap. |
michael@0 | 1153 | exCommandDispatcher.processCommand(cm, input); |
michael@0 | 1154 | } |
michael@0 | 1155 | function onPromptKeyDown(e, _input, close) { |
michael@0 | 1156 | var keyName = CodeMirror.keyName(e); |
michael@0 | 1157 | if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[') { |
michael@0 | 1158 | CodeMirror.e_stop(e); |
michael@0 | 1159 | close(); |
michael@0 | 1160 | cm.focus(); |
michael@0 | 1161 | } |
michael@0 | 1162 | } |
michael@0 | 1163 | if (command.type == 'keyToEx') { |
michael@0 | 1164 | // Handle user defined Ex to Ex mappings |
michael@0 | 1165 | exCommandDispatcher.processCommand(cm, command.exArgs.input); |
michael@0 | 1166 | } else { |
michael@0 | 1167 | if (vim.visualMode) { |
michael@0 | 1168 | showPrompt(cm, { onClose: onPromptClose, prefix: ':', value: '\'<,\'>', |
michael@0 | 1169 | onKeyDown: onPromptKeyDown}); |
michael@0 | 1170 | } else { |
michael@0 | 1171 | showPrompt(cm, { onClose: onPromptClose, prefix: ':', |
michael@0 | 1172 | onKeyDown: onPromptKeyDown}); |
michael@0 | 1173 | } |
michael@0 | 1174 | } |
michael@0 | 1175 | }, |
michael@0 | 1176 | evalInput: function(cm, vim) { |
michael@0 | 1177 | // If the motion comand is set, execute both the operator and motion. |
michael@0 | 1178 | // Otherwise return. |
michael@0 | 1179 | var inputState = vim.inputState; |
michael@0 | 1180 | var motion = inputState.motion; |
michael@0 | 1181 | var motionArgs = inputState.motionArgs || {}; |
michael@0 | 1182 | var operator = inputState.operator; |
michael@0 | 1183 | var operatorArgs = inputState.operatorArgs || {}; |
michael@0 | 1184 | var registerName = inputState.registerName; |
michael@0 | 1185 | var selectionEnd = copyCursor(cm.getCursor('head')); |
michael@0 | 1186 | var selectionStart = copyCursor(cm.getCursor('anchor')); |
michael@0 | 1187 | // The difference between cur and selection cursors are that cur is |
michael@0 | 1188 | // being operated on and ignores that there is a selection. |
michael@0 | 1189 | var curStart = copyCursor(selectionEnd); |
michael@0 | 1190 | var curOriginal = copyCursor(curStart); |
michael@0 | 1191 | var curEnd; |
michael@0 | 1192 | var repeat; |
michael@0 | 1193 | if (operator) { |
michael@0 | 1194 | this.recordLastEdit(vim, inputState); |
michael@0 | 1195 | } |
michael@0 | 1196 | if (inputState.repeatOverride !== undefined) { |
michael@0 | 1197 | // If repeatOverride is specified, that takes precedence over the |
michael@0 | 1198 | // input state's repeat. Used by Ex mode and can be user defined. |
michael@0 | 1199 | repeat = inputState.repeatOverride; |
michael@0 | 1200 | } else { |
michael@0 | 1201 | repeat = inputState.getRepeat(); |
michael@0 | 1202 | } |
michael@0 | 1203 | if (repeat > 0 && motionArgs.explicitRepeat) { |
michael@0 | 1204 | motionArgs.repeatIsExplicit = true; |
michael@0 | 1205 | } else if (motionArgs.noRepeat || |
michael@0 | 1206 | (!motionArgs.explicitRepeat && repeat === 0)) { |
michael@0 | 1207 | repeat = 1; |
michael@0 | 1208 | motionArgs.repeatIsExplicit = false; |
michael@0 | 1209 | } |
michael@0 | 1210 | if (inputState.selectedCharacter) { |
michael@0 | 1211 | // If there is a character input, stick it in all of the arg arrays. |
michael@0 | 1212 | motionArgs.selectedCharacter = operatorArgs.selectedCharacter = |
michael@0 | 1213 | inputState.selectedCharacter; |
michael@0 | 1214 | } |
michael@0 | 1215 | motionArgs.repeat = repeat; |
michael@0 | 1216 | vim.inputState = new InputState(); |
michael@0 | 1217 | if (motion) { |
michael@0 | 1218 | var motionResult = motions[motion](cm, motionArgs, vim); |
michael@0 | 1219 | vim.lastMotion = motions[motion]; |
michael@0 | 1220 | if (!motionResult) { |
michael@0 | 1221 | return; |
michael@0 | 1222 | } |
michael@0 | 1223 | if (motionArgs.toJumplist) { |
michael@0 | 1224 | var jumpList = vimGlobalState.jumpList; |
michael@0 | 1225 | // if the current motion is # or *, use cachedCursor |
michael@0 | 1226 | var cachedCursor = jumpList.cachedCursor; |
michael@0 | 1227 | if (cachedCursor) { |
michael@0 | 1228 | recordJumpPosition(cm, cachedCursor, motionResult); |
michael@0 | 1229 | delete jumpList.cachedCursor; |
michael@0 | 1230 | } else { |
michael@0 | 1231 | recordJumpPosition(cm, curOriginal, motionResult); |
michael@0 | 1232 | } |
michael@0 | 1233 | } |
michael@0 | 1234 | if (motionResult instanceof Array) { |
michael@0 | 1235 | curStart = motionResult[0]; |
michael@0 | 1236 | curEnd = motionResult[1]; |
michael@0 | 1237 | } else { |
michael@0 | 1238 | curEnd = motionResult; |
michael@0 | 1239 | } |
michael@0 | 1240 | // TODO: Handle null returns from motion commands better. |
michael@0 | 1241 | if (!curEnd) { |
michael@0 | 1242 | curEnd = Pos(curStart.line, curStart.ch); |
michael@0 | 1243 | } |
michael@0 | 1244 | if (vim.visualMode) { |
michael@0 | 1245 | // Check if the selection crossed over itself. Will need to shift |
michael@0 | 1246 | // the start point if that happened. |
michael@0 | 1247 | if (cursorIsBefore(selectionStart, selectionEnd) && |
michael@0 | 1248 | (cursorEqual(selectionStart, curEnd) || |
michael@0 | 1249 | cursorIsBefore(curEnd, selectionStart))) { |
michael@0 | 1250 | // The end of the selection has moved from after the start to |
michael@0 | 1251 | // before the start. We will shift the start right by 1. |
michael@0 | 1252 | selectionStart.ch += 1; |
michael@0 | 1253 | } else if (cursorIsBefore(selectionEnd, selectionStart) && |
michael@0 | 1254 | (cursorEqual(selectionStart, curEnd) || |
michael@0 | 1255 | cursorIsBefore(selectionStart, curEnd))) { |
michael@0 | 1256 | // The opposite happened. We will shift the start left by 1. |
michael@0 | 1257 | selectionStart.ch -= 1; |
michael@0 | 1258 | } |
michael@0 | 1259 | selectionEnd = curEnd; |
michael@0 | 1260 | selectionStart = (motionResult instanceof Array) ? curStart : selectionStart; |
michael@0 | 1261 | if (vim.visualLine) { |
michael@0 | 1262 | if (cursorIsBefore(selectionStart, selectionEnd)) { |
michael@0 | 1263 | selectionStart.ch = 0; |
michael@0 | 1264 | |
michael@0 | 1265 | var lastLine = cm.lastLine(); |
michael@0 | 1266 | if (selectionEnd.line > lastLine) { |
michael@0 | 1267 | selectionEnd.line = lastLine; |
michael@0 | 1268 | } |
michael@0 | 1269 | selectionEnd.ch = lineLength(cm, selectionEnd.line); |
michael@0 | 1270 | } else { |
michael@0 | 1271 | selectionEnd.ch = 0; |
michael@0 | 1272 | selectionStart.ch = lineLength(cm, selectionStart.line); |
michael@0 | 1273 | } |
michael@0 | 1274 | } |
michael@0 | 1275 | cm.setSelection(selectionStart, selectionEnd); |
michael@0 | 1276 | updateMark(cm, vim, '<', |
michael@0 | 1277 | cursorIsBefore(selectionStart, selectionEnd) ? selectionStart |
michael@0 | 1278 | : selectionEnd); |
michael@0 | 1279 | updateMark(cm, vim, '>', |
michael@0 | 1280 | cursorIsBefore(selectionStart, selectionEnd) ? selectionEnd |
michael@0 | 1281 | : selectionStart); |
michael@0 | 1282 | } else if (!operator) { |
michael@0 | 1283 | curEnd = clipCursorToContent(cm, curEnd); |
michael@0 | 1284 | cm.setCursor(curEnd.line, curEnd.ch); |
michael@0 | 1285 | } |
michael@0 | 1286 | } |
michael@0 | 1287 | |
michael@0 | 1288 | if (operator) { |
michael@0 | 1289 | var inverted = false; |
michael@0 | 1290 | vim.lastMotion = null; |
michael@0 | 1291 | operatorArgs.repeat = repeat; // Indent in visual mode needs this. |
michael@0 | 1292 | if (vim.visualMode) { |
michael@0 | 1293 | curStart = selectionStart; |
michael@0 | 1294 | curEnd = selectionEnd; |
michael@0 | 1295 | motionArgs.inclusive = true; |
michael@0 | 1296 | } |
michael@0 | 1297 | // Swap start and end if motion was backward. |
michael@0 | 1298 | if (cursorIsBefore(curEnd, curStart)) { |
michael@0 | 1299 | var tmp = curStart; |
michael@0 | 1300 | curStart = curEnd; |
michael@0 | 1301 | curEnd = tmp; |
michael@0 | 1302 | inverted = true; |
michael@0 | 1303 | } |
michael@0 | 1304 | if (motionArgs.inclusive && !(vim.visualMode && inverted)) { |
michael@0 | 1305 | // Move the selection end one to the right to include the last |
michael@0 | 1306 | // character. |
michael@0 | 1307 | curEnd.ch++; |
michael@0 | 1308 | } |
michael@0 | 1309 | var linewise = motionArgs.linewise || |
michael@0 | 1310 | (vim.visualMode && vim.visualLine); |
michael@0 | 1311 | if (linewise) { |
michael@0 | 1312 | // Expand selection to entire line. |
michael@0 | 1313 | expandSelectionToLine(cm, curStart, curEnd); |
michael@0 | 1314 | } else if (motionArgs.forward) { |
michael@0 | 1315 | // Clip to trailing newlines only if the motion goes forward. |
michael@0 | 1316 | clipToLine(cm, curStart, curEnd); |
michael@0 | 1317 | } |
michael@0 | 1318 | operatorArgs.registerName = registerName; |
michael@0 | 1319 | // Keep track of linewise as it affects how paste and change behave. |
michael@0 | 1320 | operatorArgs.linewise = linewise; |
michael@0 | 1321 | operators[operator](cm, operatorArgs, vim, curStart, |
michael@0 | 1322 | curEnd, curOriginal); |
michael@0 | 1323 | if (vim.visualMode) { |
michael@0 | 1324 | exitVisualMode(cm); |
michael@0 | 1325 | } |
michael@0 | 1326 | } |
michael@0 | 1327 | }, |
michael@0 | 1328 | recordLastEdit: function(vim, inputState, actionCommand) { |
michael@0 | 1329 | var macroModeState = vimGlobalState.macroModeState; |
michael@0 | 1330 | if (macroModeState.isPlaying) { return; } |
michael@0 | 1331 | vim.lastEditInputState = inputState; |
michael@0 | 1332 | vim.lastEditActionCommand = actionCommand; |
michael@0 | 1333 | macroModeState.lastInsertModeChanges.changes = []; |
michael@0 | 1334 | macroModeState.lastInsertModeChanges.expectCursorActivityForChange = false; |
michael@0 | 1335 | } |
michael@0 | 1336 | }; |
michael@0 | 1337 | |
michael@0 | 1338 | /** |
michael@0 | 1339 | * typedef {Object{line:number,ch:number}} Cursor An object containing the |
michael@0 | 1340 | * position of the cursor. |
michael@0 | 1341 | */ |
michael@0 | 1342 | // All of the functions below return Cursor objects. |
michael@0 | 1343 | var motions = { |
michael@0 | 1344 | moveToTopLine: function(cm, motionArgs) { |
michael@0 | 1345 | var line = getUserVisibleLines(cm).top + motionArgs.repeat -1; |
michael@0 | 1346 | return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); |
michael@0 | 1347 | }, |
michael@0 | 1348 | moveToMiddleLine: function(cm) { |
michael@0 | 1349 | var range = getUserVisibleLines(cm); |
michael@0 | 1350 | var line = Math.floor((range.top + range.bottom) * 0.5); |
michael@0 | 1351 | return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); |
michael@0 | 1352 | }, |
michael@0 | 1353 | moveToBottomLine: function(cm, motionArgs) { |
michael@0 | 1354 | var line = getUserVisibleLines(cm).bottom - motionArgs.repeat +1; |
michael@0 | 1355 | return Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); |
michael@0 | 1356 | }, |
michael@0 | 1357 | expandToLine: function(cm, motionArgs) { |
michael@0 | 1358 | // Expands forward to end of line, and then to next line if repeat is |
michael@0 | 1359 | // >1. Does not handle backward motion! |
michael@0 | 1360 | var cur = cm.getCursor(); |
michael@0 | 1361 | return Pos(cur.line + motionArgs.repeat - 1, Infinity); |
michael@0 | 1362 | }, |
michael@0 | 1363 | findNext: function(cm, motionArgs) { |
michael@0 | 1364 | var state = getSearchState(cm); |
michael@0 | 1365 | var query = state.getQuery(); |
michael@0 | 1366 | if (!query) { |
michael@0 | 1367 | return; |
michael@0 | 1368 | } |
michael@0 | 1369 | var prev = !motionArgs.forward; |
michael@0 | 1370 | // If search is initiated with ? instead of /, negate direction. |
michael@0 | 1371 | prev = (state.isReversed()) ? !prev : prev; |
michael@0 | 1372 | highlightSearchMatches(cm, query); |
michael@0 | 1373 | return findNext(cm, prev/** prev */, query, motionArgs.repeat); |
michael@0 | 1374 | }, |
michael@0 | 1375 | goToMark: function(_cm, motionArgs, vim) { |
michael@0 | 1376 | var mark = vim.marks[motionArgs.selectedCharacter]; |
michael@0 | 1377 | if (mark) { |
michael@0 | 1378 | return mark.find(); |
michael@0 | 1379 | } |
michael@0 | 1380 | return null; |
michael@0 | 1381 | }, |
michael@0 | 1382 | moveToOtherHighlightedEnd: function(cm) { |
michael@0 | 1383 | var curEnd = copyCursor(cm.getCursor('head')); |
michael@0 | 1384 | var curStart = copyCursor(cm.getCursor('anchor')); |
michael@0 | 1385 | if (cursorIsBefore(curStart, curEnd)) { |
michael@0 | 1386 | curEnd.ch += 1; |
michael@0 | 1387 | } else if (cursorIsBefore(curEnd, curStart)) { |
michael@0 | 1388 | curStart.ch -= 1; |
michael@0 | 1389 | } |
michael@0 | 1390 | return ([curEnd,curStart]); |
michael@0 | 1391 | }, |
michael@0 | 1392 | jumpToMark: function(cm, motionArgs, vim) { |
michael@0 | 1393 | var best = cm.getCursor(); |
michael@0 | 1394 | for (var i = 0; i < motionArgs.repeat; i++) { |
michael@0 | 1395 | var cursor = best; |
michael@0 | 1396 | for (var key in vim.marks) { |
michael@0 | 1397 | if (!isLowerCase(key)) { |
michael@0 | 1398 | continue; |
michael@0 | 1399 | } |
michael@0 | 1400 | var mark = vim.marks[key].find(); |
michael@0 | 1401 | var isWrongDirection = (motionArgs.forward) ? |
michael@0 | 1402 | cursorIsBefore(mark, cursor) : cursorIsBefore(cursor, mark); |
michael@0 | 1403 | |
michael@0 | 1404 | if (isWrongDirection) { |
michael@0 | 1405 | continue; |
michael@0 | 1406 | } |
michael@0 | 1407 | if (motionArgs.linewise && (mark.line == cursor.line)) { |
michael@0 | 1408 | continue; |
michael@0 | 1409 | } |
michael@0 | 1410 | |
michael@0 | 1411 | var equal = cursorEqual(cursor, best); |
michael@0 | 1412 | var between = (motionArgs.forward) ? |
michael@0 | 1413 | cusrorIsBetween(cursor, mark, best) : |
michael@0 | 1414 | cusrorIsBetween(best, mark, cursor); |
michael@0 | 1415 | |
michael@0 | 1416 | if (equal || between) { |
michael@0 | 1417 | best = mark; |
michael@0 | 1418 | } |
michael@0 | 1419 | } |
michael@0 | 1420 | } |
michael@0 | 1421 | |
michael@0 | 1422 | if (motionArgs.linewise) { |
michael@0 | 1423 | // Vim places the cursor on the first non-whitespace character of |
michael@0 | 1424 | // the line if there is one, else it places the cursor at the end |
michael@0 | 1425 | // of the line, regardless of whether a mark was found. |
michael@0 | 1426 | best = Pos(best.line, findFirstNonWhiteSpaceCharacter(cm.getLine(best.line))); |
michael@0 | 1427 | } |
michael@0 | 1428 | return best; |
michael@0 | 1429 | }, |
michael@0 | 1430 | moveByCharacters: function(cm, motionArgs) { |
michael@0 | 1431 | var cur = cm.getCursor(); |
michael@0 | 1432 | var repeat = motionArgs.repeat; |
michael@0 | 1433 | var ch = motionArgs.forward ? cur.ch + repeat : cur.ch - repeat; |
michael@0 | 1434 | return Pos(cur.line, ch); |
michael@0 | 1435 | }, |
michael@0 | 1436 | moveByLines: function(cm, motionArgs, vim) { |
michael@0 | 1437 | var cur = cm.getCursor(); |
michael@0 | 1438 | var endCh = cur.ch; |
michael@0 | 1439 | // Depending what our last motion was, we may want to do different |
michael@0 | 1440 | // things. If our last motion was moving vertically, we want to |
michael@0 | 1441 | // preserve the HPos from our last horizontal move. If our last motion |
michael@0 | 1442 | // was going to the end of a line, moving vertically we should go to |
michael@0 | 1443 | // the end of the line, etc. |
michael@0 | 1444 | switch (vim.lastMotion) { |
michael@0 | 1445 | case this.moveByLines: |
michael@0 | 1446 | case this.moveByDisplayLines: |
michael@0 | 1447 | case this.moveByScroll: |
michael@0 | 1448 | case this.moveToColumn: |
michael@0 | 1449 | case this.moveToEol: |
michael@0 | 1450 | endCh = vim.lastHPos; |
michael@0 | 1451 | break; |
michael@0 | 1452 | default: |
michael@0 | 1453 | vim.lastHPos = endCh; |
michael@0 | 1454 | } |
michael@0 | 1455 | var repeat = motionArgs.repeat+(motionArgs.repeatOffset||0); |
michael@0 | 1456 | var line = motionArgs.forward ? cur.line + repeat : cur.line - repeat; |
michael@0 | 1457 | var first = cm.firstLine(); |
michael@0 | 1458 | var last = cm.lastLine(); |
michael@0 | 1459 | // Vim cancels linewise motions that start on an edge and move beyond |
michael@0 | 1460 | // that edge. It does not cancel motions that do not start on an edge. |
michael@0 | 1461 | if ((line < first && cur.line == first) || |
michael@0 | 1462 | (line > last && cur.line == last)) { |
michael@0 | 1463 | return; |
michael@0 | 1464 | } |
michael@0 | 1465 | if (motionArgs.toFirstChar){ |
michael@0 | 1466 | endCh=findFirstNonWhiteSpaceCharacter(cm.getLine(line)); |
michael@0 | 1467 | vim.lastHPos = endCh; |
michael@0 | 1468 | } |
michael@0 | 1469 | vim.lastHSPos = cm.charCoords(Pos(line, endCh),'div').left; |
michael@0 | 1470 | return Pos(line, endCh); |
michael@0 | 1471 | }, |
michael@0 | 1472 | moveByDisplayLines: function(cm, motionArgs, vim) { |
michael@0 | 1473 | var cur = cm.getCursor(); |
michael@0 | 1474 | switch (vim.lastMotion) { |
michael@0 | 1475 | case this.moveByDisplayLines: |
michael@0 | 1476 | case this.moveByScroll: |
michael@0 | 1477 | case this.moveByLines: |
michael@0 | 1478 | case this.moveToColumn: |
michael@0 | 1479 | case this.moveToEol: |
michael@0 | 1480 | break; |
michael@0 | 1481 | default: |
michael@0 | 1482 | vim.lastHSPos = cm.charCoords(cur,'div').left; |
michael@0 | 1483 | } |
michael@0 | 1484 | var repeat = motionArgs.repeat; |
michael@0 | 1485 | var res=cm.findPosV(cur,(motionArgs.forward ? repeat : -repeat),'line',vim.lastHSPos); |
michael@0 | 1486 | if (res.hitSide) { |
michael@0 | 1487 | if (motionArgs.forward) { |
michael@0 | 1488 | var lastCharCoords = cm.charCoords(res, 'div'); |
michael@0 | 1489 | var goalCoords = { top: lastCharCoords.top + 8, left: vim.lastHSPos }; |
michael@0 | 1490 | var res = cm.coordsChar(goalCoords, 'div'); |
michael@0 | 1491 | } else { |
michael@0 | 1492 | var resCoords = cm.charCoords(Pos(cm.firstLine(), 0), 'div'); |
michael@0 | 1493 | resCoords.left = vim.lastHSPos; |
michael@0 | 1494 | res = cm.coordsChar(resCoords, 'div'); |
michael@0 | 1495 | } |
michael@0 | 1496 | } |
michael@0 | 1497 | vim.lastHPos = res.ch; |
michael@0 | 1498 | return res; |
michael@0 | 1499 | }, |
michael@0 | 1500 | moveByPage: function(cm, motionArgs) { |
michael@0 | 1501 | // CodeMirror only exposes functions that move the cursor page down, so |
michael@0 | 1502 | // doing this bad hack to move the cursor and move it back. evalInput |
michael@0 | 1503 | // will move the cursor to where it should be in the end. |
michael@0 | 1504 | var curStart = cm.getCursor(); |
michael@0 | 1505 | var repeat = motionArgs.repeat; |
michael@0 | 1506 | cm.moveV((motionArgs.forward ? repeat : -repeat), 'page'); |
michael@0 | 1507 | var curEnd = cm.getCursor(); |
michael@0 | 1508 | cm.setCursor(curStart); |
michael@0 | 1509 | return curEnd; |
michael@0 | 1510 | }, |
michael@0 | 1511 | moveByParagraph: function(cm, motionArgs) { |
michael@0 | 1512 | var line = cm.getCursor().line; |
michael@0 | 1513 | var repeat = motionArgs.repeat; |
michael@0 | 1514 | var inc = motionArgs.forward ? 1 : -1; |
michael@0 | 1515 | for (var i = 0; i < repeat; i++) { |
michael@0 | 1516 | if ((!motionArgs.forward && line === cm.firstLine() ) || |
michael@0 | 1517 | (motionArgs.forward && line == cm.lastLine())) { |
michael@0 | 1518 | break; |
michael@0 | 1519 | } |
michael@0 | 1520 | line += inc; |
michael@0 | 1521 | while (line !== cm.firstLine() && line != cm.lastLine() && cm.getLine(line)) { |
michael@0 | 1522 | line += inc; |
michael@0 | 1523 | } |
michael@0 | 1524 | } |
michael@0 | 1525 | return Pos(line, 0); |
michael@0 | 1526 | }, |
michael@0 | 1527 | moveByScroll: function(cm, motionArgs, vim) { |
michael@0 | 1528 | var scrollbox = cm.getScrollInfo(); |
michael@0 | 1529 | var curEnd = null; |
michael@0 | 1530 | var repeat = motionArgs.repeat; |
michael@0 | 1531 | if (!repeat) { |
michael@0 | 1532 | repeat = scrollbox.clientHeight / (2 * cm.defaultTextHeight()); |
michael@0 | 1533 | } |
michael@0 | 1534 | var orig = cm.charCoords(cm.getCursor(), 'local'); |
michael@0 | 1535 | motionArgs.repeat = repeat; |
michael@0 | 1536 | var curEnd = motions.moveByDisplayLines(cm, motionArgs, vim); |
michael@0 | 1537 | if (!curEnd) { |
michael@0 | 1538 | return null; |
michael@0 | 1539 | } |
michael@0 | 1540 | var dest = cm.charCoords(curEnd, 'local'); |
michael@0 | 1541 | cm.scrollTo(null, scrollbox.top + dest.top - orig.top); |
michael@0 | 1542 | return curEnd; |
michael@0 | 1543 | }, |
michael@0 | 1544 | moveByWords: function(cm, motionArgs) { |
michael@0 | 1545 | return moveToWord(cm, motionArgs.repeat, !!motionArgs.forward, |
michael@0 | 1546 | !!motionArgs.wordEnd, !!motionArgs.bigWord); |
michael@0 | 1547 | }, |
michael@0 | 1548 | moveTillCharacter: function(cm, motionArgs) { |
michael@0 | 1549 | var repeat = motionArgs.repeat; |
michael@0 | 1550 | var curEnd = moveToCharacter(cm, repeat, motionArgs.forward, |
michael@0 | 1551 | motionArgs.selectedCharacter); |
michael@0 | 1552 | var increment = motionArgs.forward ? -1 : 1; |
michael@0 | 1553 | recordLastCharacterSearch(increment, motionArgs); |
michael@0 | 1554 | if (!curEnd) return null; |
michael@0 | 1555 | curEnd.ch += increment; |
michael@0 | 1556 | return curEnd; |
michael@0 | 1557 | }, |
michael@0 | 1558 | moveToCharacter: function(cm, motionArgs) { |
michael@0 | 1559 | var repeat = motionArgs.repeat; |
michael@0 | 1560 | recordLastCharacterSearch(0, motionArgs); |
michael@0 | 1561 | return moveToCharacter(cm, repeat, motionArgs.forward, |
michael@0 | 1562 | motionArgs.selectedCharacter) || cm.getCursor(); |
michael@0 | 1563 | }, |
michael@0 | 1564 | moveToSymbol: function(cm, motionArgs) { |
michael@0 | 1565 | var repeat = motionArgs.repeat; |
michael@0 | 1566 | return findSymbol(cm, repeat, motionArgs.forward, |
michael@0 | 1567 | motionArgs.selectedCharacter) || cm.getCursor(); |
michael@0 | 1568 | }, |
michael@0 | 1569 | moveToColumn: function(cm, motionArgs, vim) { |
michael@0 | 1570 | var repeat = motionArgs.repeat; |
michael@0 | 1571 | // repeat is equivalent to which column we want to move to! |
michael@0 | 1572 | vim.lastHPos = repeat - 1; |
michael@0 | 1573 | vim.lastHSPos = cm.charCoords(cm.getCursor(),'div').left; |
michael@0 | 1574 | return moveToColumn(cm, repeat); |
michael@0 | 1575 | }, |
michael@0 | 1576 | moveToEol: function(cm, motionArgs, vim) { |
michael@0 | 1577 | var cur = cm.getCursor(); |
michael@0 | 1578 | vim.lastHPos = Infinity; |
michael@0 | 1579 | var retval= Pos(cur.line + motionArgs.repeat - 1, Infinity); |
michael@0 | 1580 | var end=cm.clipPos(retval); |
michael@0 | 1581 | end.ch--; |
michael@0 | 1582 | vim.lastHSPos = cm.charCoords(end,'div').left; |
michael@0 | 1583 | return retval; |
michael@0 | 1584 | }, |
michael@0 | 1585 | moveToFirstNonWhiteSpaceCharacter: function(cm) { |
michael@0 | 1586 | // Go to the start of the line where the text begins, or the end for |
michael@0 | 1587 | // whitespace-only lines |
michael@0 | 1588 | var cursor = cm.getCursor(); |
michael@0 | 1589 | return Pos(cursor.line, |
michael@0 | 1590 | findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line))); |
michael@0 | 1591 | }, |
michael@0 | 1592 | moveToMatchedSymbol: function(cm) { |
michael@0 | 1593 | var cursor = cm.getCursor(); |
michael@0 | 1594 | var line = cursor.line; |
michael@0 | 1595 | var ch = cursor.ch; |
michael@0 | 1596 | var lineText = cm.getLine(line); |
michael@0 | 1597 | var symbol; |
michael@0 | 1598 | var startContext = cm.getTokenAt(cursor).type; |
michael@0 | 1599 | var startCtxLevel = getContextLevel(startContext); |
michael@0 | 1600 | do { |
michael@0 | 1601 | symbol = lineText.charAt(ch++); |
michael@0 | 1602 | if (symbol && isMatchableSymbol(symbol)) { |
michael@0 | 1603 | var endContext = cm.getTokenAt(Pos(line, ch)).type; |
michael@0 | 1604 | var endCtxLevel = getContextLevel(endContext); |
michael@0 | 1605 | if (startCtxLevel >= endCtxLevel) { |
michael@0 | 1606 | break; |
michael@0 | 1607 | } |
michael@0 | 1608 | } |
michael@0 | 1609 | } while (symbol); |
michael@0 | 1610 | if (symbol) { |
michael@0 | 1611 | return findMatchedSymbol(cm, Pos(line, ch-1), symbol); |
michael@0 | 1612 | } else { |
michael@0 | 1613 | return cursor; |
michael@0 | 1614 | } |
michael@0 | 1615 | }, |
michael@0 | 1616 | moveToStartOfLine: function(cm) { |
michael@0 | 1617 | var cursor = cm.getCursor(); |
michael@0 | 1618 | return Pos(cursor.line, 0); |
michael@0 | 1619 | }, |
michael@0 | 1620 | moveToLineOrEdgeOfDocument: function(cm, motionArgs) { |
michael@0 | 1621 | var lineNum = motionArgs.forward ? cm.lastLine() : cm.firstLine(); |
michael@0 | 1622 | if (motionArgs.repeatIsExplicit) { |
michael@0 | 1623 | lineNum = motionArgs.repeat - cm.getOption('firstLineNumber'); |
michael@0 | 1624 | } |
michael@0 | 1625 | return Pos(lineNum, |
michael@0 | 1626 | findFirstNonWhiteSpaceCharacter(cm.getLine(lineNum))); |
michael@0 | 1627 | }, |
michael@0 | 1628 | textObjectManipulation: function(cm, motionArgs) { |
michael@0 | 1629 | // TODO: lots of possible exceptions that can be thrown here. Try da( |
michael@0 | 1630 | // outside of a () block. |
michael@0 | 1631 | |
michael@0 | 1632 | // TODO: adding <> >< to this map doesn't work, presumably because |
michael@0 | 1633 | // they're operators |
michael@0 | 1634 | var mirroredPairs = {'(': ')', ')': '(', |
michael@0 | 1635 | '{': '}', '}': '{', |
michael@0 | 1636 | '[': ']', ']': '['}; |
michael@0 | 1637 | var selfPaired = {'\'': true, '"': true}; |
michael@0 | 1638 | |
michael@0 | 1639 | var character = motionArgs.selectedCharacter; |
michael@0 | 1640 | |
michael@0 | 1641 | // Inclusive is the difference between a and i |
michael@0 | 1642 | // TODO: Instead of using the additional text object map to perform text |
michael@0 | 1643 | // object operations, merge the map into the defaultKeyMap and use |
michael@0 | 1644 | // motionArgs to define behavior. Define separate entries for 'aw', |
michael@0 | 1645 | // 'iw', 'a[', 'i[', etc. |
michael@0 | 1646 | var inclusive = !motionArgs.textObjectInner; |
michael@0 | 1647 | |
michael@0 | 1648 | var tmp; |
michael@0 | 1649 | if (mirroredPairs[character]) { |
michael@0 | 1650 | tmp = selectCompanionObject(cm, mirroredPairs[character], inclusive); |
michael@0 | 1651 | } else if (selfPaired[character]) { |
michael@0 | 1652 | tmp = findBeginningAndEnd(cm, character, inclusive); |
michael@0 | 1653 | } else if (character === 'W') { |
michael@0 | 1654 | tmp = expandWordUnderCursor(cm, inclusive, true /** forward */, |
michael@0 | 1655 | true /** bigWord */); |
michael@0 | 1656 | } else if (character === 'w') { |
michael@0 | 1657 | tmp = expandWordUnderCursor(cm, inclusive, true /** forward */, |
michael@0 | 1658 | false /** bigWord */); |
michael@0 | 1659 | } else { |
michael@0 | 1660 | // No text object defined for this, don't move. |
michael@0 | 1661 | return null; |
michael@0 | 1662 | } |
michael@0 | 1663 | |
michael@0 | 1664 | return [tmp.start, tmp.end]; |
michael@0 | 1665 | }, |
michael@0 | 1666 | |
michael@0 | 1667 | repeatLastCharacterSearch: function(cm, motionArgs) { |
michael@0 | 1668 | var lastSearch = vimGlobalState.lastChararacterSearch; |
michael@0 | 1669 | var repeat = motionArgs.repeat; |
michael@0 | 1670 | var forward = motionArgs.forward === lastSearch.forward; |
michael@0 | 1671 | var increment = (lastSearch.increment ? 1 : 0) * (forward ? -1 : 1); |
michael@0 | 1672 | cm.moveH(-increment, 'char'); |
michael@0 | 1673 | motionArgs.inclusive = forward ? true : false; |
michael@0 | 1674 | var curEnd = moveToCharacter(cm, repeat, forward, lastSearch.selectedCharacter); |
michael@0 | 1675 | if (!curEnd) { |
michael@0 | 1676 | cm.moveH(increment, 'char'); |
michael@0 | 1677 | return cm.getCursor(); |
michael@0 | 1678 | } |
michael@0 | 1679 | curEnd.ch += increment; |
michael@0 | 1680 | return curEnd; |
michael@0 | 1681 | } |
michael@0 | 1682 | }; |
michael@0 | 1683 | |
michael@0 | 1684 | var operators = { |
michael@0 | 1685 | change: function(cm, operatorArgs, _vim, curStart, curEnd) { |
michael@0 | 1686 | vimGlobalState.registerController.pushText( |
michael@0 | 1687 | operatorArgs.registerName, 'change', cm.getRange(curStart, curEnd), |
michael@0 | 1688 | operatorArgs.linewise); |
michael@0 | 1689 | if (operatorArgs.linewise) { |
michael@0 | 1690 | // Push the next line back down, if there is a next line. |
michael@0 | 1691 | var replacement = curEnd.line > cm.lastLine() ? '' : '\n'; |
michael@0 | 1692 | cm.replaceRange(replacement, curStart, curEnd); |
michael@0 | 1693 | cm.indentLine(curStart.line, 'smart'); |
michael@0 | 1694 | // null ch so setCursor moves to end of line. |
michael@0 | 1695 | curStart.ch = null; |
michael@0 | 1696 | } else { |
michael@0 | 1697 | // Exclude trailing whitespace if the range is not all whitespace. |
michael@0 | 1698 | var text = cm.getRange(curStart, curEnd); |
michael@0 | 1699 | if (!isWhiteSpaceString(text)) { |
michael@0 | 1700 | var match = (/\s+$/).exec(text); |
michael@0 | 1701 | if (match) { |
michael@0 | 1702 | curEnd = offsetCursor(curEnd, 0, - match[0].length); |
michael@0 | 1703 | } |
michael@0 | 1704 | } |
michael@0 | 1705 | cm.replaceRange('', curStart, curEnd); |
michael@0 | 1706 | } |
michael@0 | 1707 | actions.enterInsertMode(cm, {}, cm.state.vim); |
michael@0 | 1708 | cm.setCursor(curStart); |
michael@0 | 1709 | }, |
michael@0 | 1710 | // delete is a javascript keyword. |
michael@0 | 1711 | 'delete': function(cm, operatorArgs, _vim, curStart, curEnd) { |
michael@0 | 1712 | // If the ending line is past the last line, inclusive, instead of |
michael@0 | 1713 | // including the trailing \n, include the \n before the starting line |
michael@0 | 1714 | if (operatorArgs.linewise && |
michael@0 | 1715 | curEnd.line > cm.lastLine() && curStart.line > cm.firstLine()) { |
michael@0 | 1716 | curStart.line--; |
michael@0 | 1717 | curStart.ch = lineLength(cm, curStart.line); |
michael@0 | 1718 | } |
michael@0 | 1719 | vimGlobalState.registerController.pushText( |
michael@0 | 1720 | operatorArgs.registerName, 'delete', cm.getRange(curStart, curEnd), |
michael@0 | 1721 | operatorArgs.linewise); |
michael@0 | 1722 | cm.replaceRange('', curStart, curEnd); |
michael@0 | 1723 | if (operatorArgs.linewise) { |
michael@0 | 1724 | cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm)); |
michael@0 | 1725 | } else { |
michael@0 | 1726 | cm.setCursor(curStart); |
michael@0 | 1727 | } |
michael@0 | 1728 | }, |
michael@0 | 1729 | indent: function(cm, operatorArgs, vim, curStart, curEnd) { |
michael@0 | 1730 | var startLine = curStart.line; |
michael@0 | 1731 | var endLine = curEnd.line; |
michael@0 | 1732 | // In visual mode, n> shifts the selection right n times, instead of |
michael@0 | 1733 | // shifting n lines right once. |
michael@0 | 1734 | var repeat = (vim.visualMode) ? operatorArgs.repeat : 1; |
michael@0 | 1735 | if (operatorArgs.linewise) { |
michael@0 | 1736 | // The only way to delete a newline is to delete until the start of |
michael@0 | 1737 | // the next line, so in linewise mode evalInput will include the next |
michael@0 | 1738 | // line. We don't want this in indent, so we go back a line. |
michael@0 | 1739 | endLine--; |
michael@0 | 1740 | } |
michael@0 | 1741 | for (var i = startLine; i <= endLine; i++) { |
michael@0 | 1742 | for (var j = 0; j < repeat; j++) { |
michael@0 | 1743 | cm.indentLine(i, operatorArgs.indentRight); |
michael@0 | 1744 | } |
michael@0 | 1745 | } |
michael@0 | 1746 | cm.setCursor(curStart); |
michael@0 | 1747 | cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm)); |
michael@0 | 1748 | }, |
michael@0 | 1749 | swapcase: function(cm, operatorArgs, _vim, curStart, curEnd, curOriginal) { |
michael@0 | 1750 | var toSwap = cm.getRange(curStart, curEnd); |
michael@0 | 1751 | var swapped = ''; |
michael@0 | 1752 | for (var i = 0; i < toSwap.length; i++) { |
michael@0 | 1753 | var character = toSwap.charAt(i); |
michael@0 | 1754 | swapped += isUpperCase(character) ? character.toLowerCase() : |
michael@0 | 1755 | character.toUpperCase(); |
michael@0 | 1756 | } |
michael@0 | 1757 | cm.replaceRange(swapped, curStart, curEnd); |
michael@0 | 1758 | if (!operatorArgs.shouldMoveCursor) { |
michael@0 | 1759 | cm.setCursor(curOriginal); |
michael@0 | 1760 | } |
michael@0 | 1761 | }, |
michael@0 | 1762 | yank: function(cm, operatorArgs, _vim, curStart, curEnd, curOriginal) { |
michael@0 | 1763 | vimGlobalState.registerController.pushText( |
michael@0 | 1764 | operatorArgs.registerName, 'yank', |
michael@0 | 1765 | cm.getRange(curStart, curEnd), operatorArgs.linewise); |
michael@0 | 1766 | cm.setCursor(curOriginal); |
michael@0 | 1767 | } |
michael@0 | 1768 | }; |
michael@0 | 1769 | |
michael@0 | 1770 | var actions = { |
michael@0 | 1771 | jumpListWalk: function(cm, actionArgs, vim) { |
michael@0 | 1772 | if (vim.visualMode) { |
michael@0 | 1773 | return; |
michael@0 | 1774 | } |
michael@0 | 1775 | var repeat = actionArgs.repeat; |
michael@0 | 1776 | var forward = actionArgs.forward; |
michael@0 | 1777 | var jumpList = vimGlobalState.jumpList; |
michael@0 | 1778 | |
michael@0 | 1779 | var mark = jumpList.move(cm, forward ? repeat : -repeat); |
michael@0 | 1780 | var markPos = mark ? mark.find() : undefined; |
michael@0 | 1781 | markPos = markPos ? markPos : cm.getCursor(); |
michael@0 | 1782 | cm.setCursor(markPos); |
michael@0 | 1783 | }, |
michael@0 | 1784 | scroll: function(cm, actionArgs, vim) { |
michael@0 | 1785 | if (vim.visualMode) { |
michael@0 | 1786 | return; |
michael@0 | 1787 | } |
michael@0 | 1788 | var repeat = actionArgs.repeat || 1; |
michael@0 | 1789 | var lineHeight = cm.defaultTextHeight(); |
michael@0 | 1790 | var top = cm.getScrollInfo().top; |
michael@0 | 1791 | var delta = lineHeight * repeat; |
michael@0 | 1792 | var newPos = actionArgs.forward ? top + delta : top - delta; |
michael@0 | 1793 | var cursor = copyCursor(cm.getCursor()); |
michael@0 | 1794 | var cursorCoords = cm.charCoords(cursor, 'local'); |
michael@0 | 1795 | if (actionArgs.forward) { |
michael@0 | 1796 | if (newPos > cursorCoords.top) { |
michael@0 | 1797 | cursor.line += (newPos - cursorCoords.top) / lineHeight; |
michael@0 | 1798 | cursor.line = Math.ceil(cursor.line); |
michael@0 | 1799 | cm.setCursor(cursor); |
michael@0 | 1800 | cursorCoords = cm.charCoords(cursor, 'local'); |
michael@0 | 1801 | cm.scrollTo(null, cursorCoords.top); |
michael@0 | 1802 | } else { |
michael@0 | 1803 | // Cursor stays within bounds. Just reposition the scroll window. |
michael@0 | 1804 | cm.scrollTo(null, newPos); |
michael@0 | 1805 | } |
michael@0 | 1806 | } else { |
michael@0 | 1807 | var newBottom = newPos + cm.getScrollInfo().clientHeight; |
michael@0 | 1808 | if (newBottom < cursorCoords.bottom) { |
michael@0 | 1809 | cursor.line -= (cursorCoords.bottom - newBottom) / lineHeight; |
michael@0 | 1810 | cursor.line = Math.floor(cursor.line); |
michael@0 | 1811 | cm.setCursor(cursor); |
michael@0 | 1812 | cursorCoords = cm.charCoords(cursor, 'local'); |
michael@0 | 1813 | cm.scrollTo( |
michael@0 | 1814 | null, cursorCoords.bottom - cm.getScrollInfo().clientHeight); |
michael@0 | 1815 | } else { |
michael@0 | 1816 | // Cursor stays within bounds. Just reposition the scroll window. |
michael@0 | 1817 | cm.scrollTo(null, newPos); |
michael@0 | 1818 | } |
michael@0 | 1819 | } |
michael@0 | 1820 | }, |
michael@0 | 1821 | scrollToCursor: function(cm, actionArgs) { |
michael@0 | 1822 | var lineNum = cm.getCursor().line; |
michael@0 | 1823 | var charCoords = cm.charCoords(Pos(lineNum, 0), 'local'); |
michael@0 | 1824 | var height = cm.getScrollInfo().clientHeight; |
michael@0 | 1825 | var y = charCoords.top; |
michael@0 | 1826 | var lineHeight = charCoords.bottom - y; |
michael@0 | 1827 | switch (actionArgs.position) { |
michael@0 | 1828 | case 'center': y = y - (height / 2) + lineHeight; |
michael@0 | 1829 | break; |
michael@0 | 1830 | case 'bottom': y = y - height + lineHeight*1.4; |
michael@0 | 1831 | break; |
michael@0 | 1832 | case 'top': y = y + lineHeight*0.4; |
michael@0 | 1833 | break; |
michael@0 | 1834 | } |
michael@0 | 1835 | cm.scrollTo(null, y); |
michael@0 | 1836 | }, |
michael@0 | 1837 | replayMacro: function(cm, actionArgs, vim) { |
michael@0 | 1838 | var registerName = actionArgs.selectedCharacter; |
michael@0 | 1839 | var repeat = actionArgs.repeat; |
michael@0 | 1840 | var macroModeState = vimGlobalState.macroModeState; |
michael@0 | 1841 | if (registerName == '@') { |
michael@0 | 1842 | registerName = macroModeState.latestRegister; |
michael@0 | 1843 | } |
michael@0 | 1844 | while(repeat--){ |
michael@0 | 1845 | executeMacroRegister(cm, vim, macroModeState, registerName); |
michael@0 | 1846 | } |
michael@0 | 1847 | }, |
michael@0 | 1848 | enterMacroRecordMode: function(cm, actionArgs) { |
michael@0 | 1849 | var macroModeState = vimGlobalState.macroModeState; |
michael@0 | 1850 | var registerName = actionArgs.selectedCharacter; |
michael@0 | 1851 | macroModeState.enterMacroRecordMode(cm, registerName); |
michael@0 | 1852 | }, |
michael@0 | 1853 | enterInsertMode: function(cm, actionArgs, vim) { |
michael@0 | 1854 | if (cm.getOption('readOnly')) { return; } |
michael@0 | 1855 | vim.insertMode = true; |
michael@0 | 1856 | vim.insertModeRepeat = actionArgs && actionArgs.repeat || 1; |
michael@0 | 1857 | var insertAt = (actionArgs) ? actionArgs.insertAt : null; |
michael@0 | 1858 | if (insertAt == 'eol') { |
michael@0 | 1859 | var cursor = cm.getCursor(); |
michael@0 | 1860 | cursor = Pos(cursor.line, lineLength(cm, cursor.line)); |
michael@0 | 1861 | cm.setCursor(cursor); |
michael@0 | 1862 | } else if (insertAt == 'charAfter') { |
michael@0 | 1863 | cm.setCursor(offsetCursor(cm.getCursor(), 0, 1)); |
michael@0 | 1864 | } else if (insertAt == 'firstNonBlank') { |
michael@0 | 1865 | cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm)); |
michael@0 | 1866 | } |
michael@0 | 1867 | cm.setOption('keyMap', 'vim-insert'); |
michael@0 | 1868 | cm.setOption('disableInput', false); |
michael@0 | 1869 | if (actionArgs && actionArgs.replace) { |
michael@0 | 1870 | // Handle Replace-mode as a special case of insert mode. |
michael@0 | 1871 | cm.toggleOverwrite(true); |
michael@0 | 1872 | cm.setOption('keyMap', 'vim-replace'); |
michael@0 | 1873 | CodeMirror.signal(cm, "vim-mode-change", {mode: "replace"}); |
michael@0 | 1874 | } else { |
michael@0 | 1875 | cm.setOption('keyMap', 'vim-insert'); |
michael@0 | 1876 | CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"}); |
michael@0 | 1877 | } |
michael@0 | 1878 | if (!vimGlobalState.macroModeState.isPlaying) { |
michael@0 | 1879 | // Only record if not replaying. |
michael@0 | 1880 | cm.on('change', onChange); |
michael@0 | 1881 | cm.on('cursorActivity', onCursorActivity); |
michael@0 | 1882 | CodeMirror.on(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown); |
michael@0 | 1883 | } |
michael@0 | 1884 | }, |
michael@0 | 1885 | toggleVisualMode: function(cm, actionArgs, vim) { |
michael@0 | 1886 | var repeat = actionArgs.repeat; |
michael@0 | 1887 | var curStart = cm.getCursor(); |
michael@0 | 1888 | var curEnd; |
michael@0 | 1889 | // TODO: The repeat should actually select number of characters/lines |
michael@0 | 1890 | // equal to the repeat times the size of the previous visual |
michael@0 | 1891 | // operation. |
michael@0 | 1892 | if (!vim.visualMode) { |
michael@0 | 1893 | cm.on('mousedown', exitVisualMode); |
michael@0 | 1894 | vim.visualMode = true; |
michael@0 | 1895 | vim.visualLine = !!actionArgs.linewise; |
michael@0 | 1896 | if (vim.visualLine) { |
michael@0 | 1897 | curStart.ch = 0; |
michael@0 | 1898 | curEnd = clipCursorToContent( |
michael@0 | 1899 | cm, Pos(curStart.line + repeat - 1, lineLength(cm, curStart.line)), |
michael@0 | 1900 | true /** includeLineBreak */); |
michael@0 | 1901 | } else { |
michael@0 | 1902 | curEnd = clipCursorToContent( |
michael@0 | 1903 | cm, Pos(curStart.line, curStart.ch + repeat), |
michael@0 | 1904 | true /** includeLineBreak */); |
michael@0 | 1905 | } |
michael@0 | 1906 | // Make the initial selection. |
michael@0 | 1907 | if (!actionArgs.repeatIsExplicit && !vim.visualLine) { |
michael@0 | 1908 | // This is a strange case. Here the implicit repeat is 1. The |
michael@0 | 1909 | // following commands lets the cursor hover over the 1 character |
michael@0 | 1910 | // selection. |
michael@0 | 1911 | cm.setCursor(curEnd); |
michael@0 | 1912 | cm.setSelection(curEnd, curStart); |
michael@0 | 1913 | } else { |
michael@0 | 1914 | cm.setSelection(curStart, curEnd); |
michael@0 | 1915 | } |
michael@0 | 1916 | CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : ""}); |
michael@0 | 1917 | } else { |
michael@0 | 1918 | curStart = cm.getCursor('anchor'); |
michael@0 | 1919 | curEnd = cm.getCursor('head'); |
michael@0 | 1920 | if (!vim.visualLine && actionArgs.linewise) { |
michael@0 | 1921 | // Shift-V pressed in characterwise visual mode. Switch to linewise |
michael@0 | 1922 | // visual mode instead of exiting visual mode. |
michael@0 | 1923 | vim.visualLine = true; |
michael@0 | 1924 | curStart.ch = cursorIsBefore(curStart, curEnd) ? 0 : |
michael@0 | 1925 | lineLength(cm, curStart.line); |
michael@0 | 1926 | curEnd.ch = cursorIsBefore(curStart, curEnd) ? |
michael@0 | 1927 | lineLength(cm, curEnd.line) : 0; |
michael@0 | 1928 | cm.setSelection(curStart, curEnd); |
michael@0 | 1929 | CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: "linewise"}); |
michael@0 | 1930 | } else if (vim.visualLine && !actionArgs.linewise) { |
michael@0 | 1931 | // v pressed in linewise visual mode. Switch to characterwise visual |
michael@0 | 1932 | // mode instead of exiting visual mode. |
michael@0 | 1933 | vim.visualLine = false; |
michael@0 | 1934 | CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"}); |
michael@0 | 1935 | } else { |
michael@0 | 1936 | exitVisualMode(cm); |
michael@0 | 1937 | } |
michael@0 | 1938 | } |
michael@0 | 1939 | updateMark(cm, vim, '<', cursorIsBefore(curStart, curEnd) ? curStart |
michael@0 | 1940 | : curEnd); |
michael@0 | 1941 | updateMark(cm, vim, '>', cursorIsBefore(curStart, curEnd) ? curEnd |
michael@0 | 1942 | : curStart); |
michael@0 | 1943 | }, |
michael@0 | 1944 | reselectLastSelection: function(cm, _actionArgs, vim) { |
michael@0 | 1945 | if (vim.lastSelection) { |
michael@0 | 1946 | var lastSelection = vim.lastSelection; |
michael@0 | 1947 | cm.setSelection(lastSelection.curStart, lastSelection.curEnd); |
michael@0 | 1948 | if (lastSelection.visualLine) { |
michael@0 | 1949 | vim.visualMode = true; |
michael@0 | 1950 | vim.visualLine = true; |
michael@0 | 1951 | } |
michael@0 | 1952 | else { |
michael@0 | 1953 | vim.visualMode = true; |
michael@0 | 1954 | vim.visualLine = false; |
michael@0 | 1955 | } |
michael@0 | 1956 | CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : ""}); |
michael@0 | 1957 | } |
michael@0 | 1958 | }, |
michael@0 | 1959 | joinLines: function(cm, actionArgs, vim) { |
michael@0 | 1960 | var curStart, curEnd; |
michael@0 | 1961 | if (vim.visualMode) { |
michael@0 | 1962 | curStart = cm.getCursor('anchor'); |
michael@0 | 1963 | curEnd = cm.getCursor('head'); |
michael@0 | 1964 | curEnd.ch = lineLength(cm, curEnd.line) - 1; |
michael@0 | 1965 | } else { |
michael@0 | 1966 | // Repeat is the number of lines to join. Minimum 2 lines. |
michael@0 | 1967 | var repeat = Math.max(actionArgs.repeat, 2); |
michael@0 | 1968 | curStart = cm.getCursor(); |
michael@0 | 1969 | curEnd = clipCursorToContent(cm, Pos(curStart.line + repeat - 1, |
michael@0 | 1970 | Infinity)); |
michael@0 | 1971 | } |
michael@0 | 1972 | var finalCh = 0; |
michael@0 | 1973 | cm.operation(function() { |
michael@0 | 1974 | for (var i = curStart.line; i < curEnd.line; i++) { |
michael@0 | 1975 | finalCh = lineLength(cm, curStart.line); |
michael@0 | 1976 | var tmp = Pos(curStart.line + 1, |
michael@0 | 1977 | lineLength(cm, curStart.line + 1)); |
michael@0 | 1978 | var text = cm.getRange(curStart, tmp); |
michael@0 | 1979 | text = text.replace(/\n\s*/g, ' '); |
michael@0 | 1980 | cm.replaceRange(text, curStart, tmp); |
michael@0 | 1981 | } |
michael@0 | 1982 | var curFinalPos = Pos(curStart.line, finalCh); |
michael@0 | 1983 | cm.setCursor(curFinalPos); |
michael@0 | 1984 | }); |
michael@0 | 1985 | }, |
michael@0 | 1986 | newLineAndEnterInsertMode: function(cm, actionArgs, vim) { |
michael@0 | 1987 | vim.insertMode = true; |
michael@0 | 1988 | var insertAt = copyCursor(cm.getCursor()); |
michael@0 | 1989 | if (insertAt.line === cm.firstLine() && !actionArgs.after) { |
michael@0 | 1990 | // Special case for inserting newline before start of document. |
michael@0 | 1991 | cm.replaceRange('\n', Pos(cm.firstLine(), 0)); |
michael@0 | 1992 | cm.setCursor(cm.firstLine(), 0); |
michael@0 | 1993 | } else { |
michael@0 | 1994 | insertAt.line = (actionArgs.after) ? insertAt.line : |
michael@0 | 1995 | insertAt.line - 1; |
michael@0 | 1996 | insertAt.ch = lineLength(cm, insertAt.line); |
michael@0 | 1997 | cm.setCursor(insertAt); |
michael@0 | 1998 | var newlineFn = CodeMirror.commands.newlineAndIndentContinueComment || |
michael@0 | 1999 | CodeMirror.commands.newlineAndIndent; |
michael@0 | 2000 | newlineFn(cm); |
michael@0 | 2001 | } |
michael@0 | 2002 | this.enterInsertMode(cm, { repeat: actionArgs.repeat }, vim); |
michael@0 | 2003 | }, |
michael@0 | 2004 | paste: function(cm, actionArgs) { |
michael@0 | 2005 | var cur = copyCursor(cm.getCursor()); |
michael@0 | 2006 | var register = vimGlobalState.registerController.getRegister( |
michael@0 | 2007 | actionArgs.registerName); |
michael@0 | 2008 | var text = register.toString(); |
michael@0 | 2009 | if (!text) { |
michael@0 | 2010 | return; |
michael@0 | 2011 | } |
michael@0 | 2012 | if (actionArgs.repeat > 1) { |
michael@0 | 2013 | var text = Array(actionArgs.repeat + 1).join(text); |
michael@0 | 2014 | } |
michael@0 | 2015 | var linewise = register.linewise; |
michael@0 | 2016 | if (linewise) { |
michael@0 | 2017 | if (actionArgs.after) { |
michael@0 | 2018 | // Move the newline at the end to the start instead, and paste just |
michael@0 | 2019 | // before the newline character of the line we are on right now. |
michael@0 | 2020 | text = '\n' + text.slice(0, text.length - 1); |
michael@0 | 2021 | cur.ch = lineLength(cm, cur.line); |
michael@0 | 2022 | } else { |
michael@0 | 2023 | cur.ch = 0; |
michael@0 | 2024 | } |
michael@0 | 2025 | } else { |
michael@0 | 2026 | cur.ch += actionArgs.after ? 1 : 0; |
michael@0 | 2027 | } |
michael@0 | 2028 | cm.replaceRange(text, cur); |
michael@0 | 2029 | // Now fine tune the cursor to where we want it. |
michael@0 | 2030 | var curPosFinal; |
michael@0 | 2031 | var idx; |
michael@0 | 2032 | if (linewise && actionArgs.after) { |
michael@0 | 2033 | curPosFinal = Pos( |
michael@0 | 2034 | cur.line + 1, |
michael@0 | 2035 | findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line + 1))); |
michael@0 | 2036 | } else if (linewise && !actionArgs.after) { |
michael@0 | 2037 | curPosFinal = Pos( |
michael@0 | 2038 | cur.line, |
michael@0 | 2039 | findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line))); |
michael@0 | 2040 | } else if (!linewise && actionArgs.after) { |
michael@0 | 2041 | idx = cm.indexFromPos(cur); |
michael@0 | 2042 | curPosFinal = cm.posFromIndex(idx + text.length - 1); |
michael@0 | 2043 | } else { |
michael@0 | 2044 | idx = cm.indexFromPos(cur); |
michael@0 | 2045 | curPosFinal = cm.posFromIndex(idx + text.length); |
michael@0 | 2046 | } |
michael@0 | 2047 | cm.setCursor(curPosFinal); |
michael@0 | 2048 | }, |
michael@0 | 2049 | undo: function(cm, actionArgs) { |
michael@0 | 2050 | cm.operation(function() { |
michael@0 | 2051 | repeatFn(cm, CodeMirror.commands.undo, actionArgs.repeat)(); |
michael@0 | 2052 | cm.setCursor(cm.getCursor('anchor')); |
michael@0 | 2053 | }); |
michael@0 | 2054 | }, |
michael@0 | 2055 | redo: function(cm, actionArgs) { |
michael@0 | 2056 | repeatFn(cm, CodeMirror.commands.redo, actionArgs.repeat)(); |
michael@0 | 2057 | }, |
michael@0 | 2058 | setRegister: function(_cm, actionArgs, vim) { |
michael@0 | 2059 | vim.inputState.registerName = actionArgs.selectedCharacter; |
michael@0 | 2060 | }, |
michael@0 | 2061 | setMark: function(cm, actionArgs, vim) { |
michael@0 | 2062 | var markName = actionArgs.selectedCharacter; |
michael@0 | 2063 | updateMark(cm, vim, markName, cm.getCursor()); |
michael@0 | 2064 | }, |
michael@0 | 2065 | replace: function(cm, actionArgs, vim) { |
michael@0 | 2066 | var replaceWith = actionArgs.selectedCharacter; |
michael@0 | 2067 | var curStart = cm.getCursor(); |
michael@0 | 2068 | var replaceTo; |
michael@0 | 2069 | var curEnd; |
michael@0 | 2070 | if (vim.visualMode){ |
michael@0 | 2071 | curStart=cm.getCursor('start'); |
michael@0 | 2072 | curEnd=cm.getCursor('end'); |
michael@0 | 2073 | // workaround to catch the character under the cursor |
michael@0 | 2074 | // existing workaround doesn't cover actions |
michael@0 | 2075 | curEnd=cm.clipPos(Pos(curEnd.line, curEnd.ch+1)); |
michael@0 | 2076 | }else{ |
michael@0 | 2077 | var line = cm.getLine(curStart.line); |
michael@0 | 2078 | replaceTo = curStart.ch + actionArgs.repeat; |
michael@0 | 2079 | if (replaceTo > line.length) { |
michael@0 | 2080 | replaceTo=line.length; |
michael@0 | 2081 | } |
michael@0 | 2082 | curEnd = Pos(curStart.line, replaceTo); |
michael@0 | 2083 | } |
michael@0 | 2084 | if (replaceWith=='\n'){ |
michael@0 | 2085 | if (!vim.visualMode) cm.replaceRange('', curStart, curEnd); |
michael@0 | 2086 | // special case, where vim help says to replace by just one line-break |
michael@0 | 2087 | (CodeMirror.commands.newlineAndIndentContinueComment || CodeMirror.commands.newlineAndIndent)(cm); |
michael@0 | 2088 | }else { |
michael@0 | 2089 | var replaceWithStr=cm.getRange(curStart, curEnd); |
michael@0 | 2090 | //replace all characters in range by selected, but keep linebreaks |
michael@0 | 2091 | replaceWithStr=replaceWithStr.replace(/[^\n]/g,replaceWith); |
michael@0 | 2092 | cm.replaceRange(replaceWithStr, curStart, curEnd); |
michael@0 | 2093 | if (vim.visualMode){ |
michael@0 | 2094 | cm.setCursor(curStart); |
michael@0 | 2095 | exitVisualMode(cm); |
michael@0 | 2096 | }else{ |
michael@0 | 2097 | cm.setCursor(offsetCursor(curEnd, 0, -1)); |
michael@0 | 2098 | } |
michael@0 | 2099 | } |
michael@0 | 2100 | }, |
michael@0 | 2101 | incrementNumberToken: function(cm, actionArgs) { |
michael@0 | 2102 | var cur = cm.getCursor(); |
michael@0 | 2103 | var lineStr = cm.getLine(cur.line); |
michael@0 | 2104 | var re = /-?\d+/g; |
michael@0 | 2105 | var match; |
michael@0 | 2106 | var start; |
michael@0 | 2107 | var end; |
michael@0 | 2108 | var numberStr; |
michael@0 | 2109 | var token; |
michael@0 | 2110 | while ((match = re.exec(lineStr)) !== null) { |
michael@0 | 2111 | token = match[0]; |
michael@0 | 2112 | start = match.index; |
michael@0 | 2113 | end = start + token.length; |
michael@0 | 2114 | if (cur.ch < end)break; |
michael@0 | 2115 | } |
michael@0 | 2116 | if (!actionArgs.backtrack && (end <= cur.ch))return; |
michael@0 | 2117 | if (token) { |
michael@0 | 2118 | var increment = actionArgs.increase ? 1 : -1; |
michael@0 | 2119 | var number = parseInt(token) + (increment * actionArgs.repeat); |
michael@0 | 2120 | var from = Pos(cur.line, start); |
michael@0 | 2121 | var to = Pos(cur.line, end); |
michael@0 | 2122 | numberStr = number.toString(); |
michael@0 | 2123 | cm.replaceRange(numberStr, from, to); |
michael@0 | 2124 | } else { |
michael@0 | 2125 | return; |
michael@0 | 2126 | } |
michael@0 | 2127 | cm.setCursor(Pos(cur.line, start + numberStr.length - 1)); |
michael@0 | 2128 | }, |
michael@0 | 2129 | repeatLastEdit: function(cm, actionArgs, vim) { |
michael@0 | 2130 | var lastEditInputState = vim.lastEditInputState; |
michael@0 | 2131 | if (!lastEditInputState) { return; } |
michael@0 | 2132 | var repeat = actionArgs.repeat; |
michael@0 | 2133 | if (repeat && actionArgs.repeatIsExplicit) { |
michael@0 | 2134 | vim.lastEditInputState.repeatOverride = repeat; |
michael@0 | 2135 | } else { |
michael@0 | 2136 | repeat = vim.lastEditInputState.repeatOverride || repeat; |
michael@0 | 2137 | } |
michael@0 | 2138 | repeatLastEdit(cm, vim, repeat, false /** repeatForInsert */); |
michael@0 | 2139 | } |
michael@0 | 2140 | }; |
michael@0 | 2141 | |
michael@0 | 2142 | /* |
michael@0 | 2143 | * Below are miscellaneous utility functions used by vim.js |
michael@0 | 2144 | */ |
michael@0 | 2145 | |
michael@0 | 2146 | /** |
michael@0 | 2147 | * Clips cursor to ensure that line is within the buffer's range |
michael@0 | 2148 | * If includeLineBreak is true, then allow cur.ch == lineLength. |
michael@0 | 2149 | */ |
michael@0 | 2150 | function clipCursorToContent(cm, cur, includeLineBreak) { |
michael@0 | 2151 | var line = Math.min(Math.max(cm.firstLine(), cur.line), cm.lastLine() ); |
michael@0 | 2152 | var maxCh = lineLength(cm, line) - 1; |
michael@0 | 2153 | maxCh = (includeLineBreak) ? maxCh + 1 : maxCh; |
michael@0 | 2154 | var ch = Math.min(Math.max(0, cur.ch), maxCh); |
michael@0 | 2155 | return Pos(line, ch); |
michael@0 | 2156 | } |
michael@0 | 2157 | function copyArgs(args) { |
michael@0 | 2158 | var ret = {}; |
michael@0 | 2159 | for (var prop in args) { |
michael@0 | 2160 | if (args.hasOwnProperty(prop)) { |
michael@0 | 2161 | ret[prop] = args[prop]; |
michael@0 | 2162 | } |
michael@0 | 2163 | } |
michael@0 | 2164 | return ret; |
michael@0 | 2165 | } |
michael@0 | 2166 | function offsetCursor(cur, offsetLine, offsetCh) { |
michael@0 | 2167 | return Pos(cur.line + offsetLine, cur.ch + offsetCh); |
michael@0 | 2168 | } |
michael@0 | 2169 | function matchKeysPartial(pressed, mapped) { |
michael@0 | 2170 | for (var i = 0; i < pressed.length; i++) { |
michael@0 | 2171 | // 'character' means any character. For mark, register commads, etc. |
michael@0 | 2172 | if (pressed[i] != mapped[i] && mapped[i] != 'character') { |
michael@0 | 2173 | return false; |
michael@0 | 2174 | } |
michael@0 | 2175 | } |
michael@0 | 2176 | return true; |
michael@0 | 2177 | } |
michael@0 | 2178 | function repeatFn(cm, fn, repeat) { |
michael@0 | 2179 | return function() { |
michael@0 | 2180 | for (var i = 0; i < repeat; i++) { |
michael@0 | 2181 | fn(cm); |
michael@0 | 2182 | } |
michael@0 | 2183 | }; |
michael@0 | 2184 | } |
michael@0 | 2185 | function copyCursor(cur) { |
michael@0 | 2186 | return Pos(cur.line, cur.ch); |
michael@0 | 2187 | } |
michael@0 | 2188 | function cursorEqual(cur1, cur2) { |
michael@0 | 2189 | return cur1.ch == cur2.ch && cur1.line == cur2.line; |
michael@0 | 2190 | } |
michael@0 | 2191 | function cursorIsBefore(cur1, cur2) { |
michael@0 | 2192 | if (cur1.line < cur2.line) { |
michael@0 | 2193 | return true; |
michael@0 | 2194 | } |
michael@0 | 2195 | if (cur1.line == cur2.line && cur1.ch < cur2.ch) { |
michael@0 | 2196 | return true; |
michael@0 | 2197 | } |
michael@0 | 2198 | return false; |
michael@0 | 2199 | } |
michael@0 | 2200 | function cusrorIsBetween(cur1, cur2, cur3) { |
michael@0 | 2201 | // returns true if cur2 is between cur1 and cur3. |
michael@0 | 2202 | var cur1before2 = cursorIsBefore(cur1, cur2); |
michael@0 | 2203 | var cur2before3 = cursorIsBefore(cur2, cur3); |
michael@0 | 2204 | return cur1before2 && cur2before3; |
michael@0 | 2205 | } |
michael@0 | 2206 | function lineLength(cm, lineNum) { |
michael@0 | 2207 | return cm.getLine(lineNum).length; |
michael@0 | 2208 | } |
michael@0 | 2209 | function reverse(s){ |
michael@0 | 2210 | return s.split('').reverse().join(''); |
michael@0 | 2211 | } |
michael@0 | 2212 | function trim(s) { |
michael@0 | 2213 | if (s.trim) { |
michael@0 | 2214 | return s.trim(); |
michael@0 | 2215 | } |
michael@0 | 2216 | return s.replace(/^\s+|\s+$/g, ''); |
michael@0 | 2217 | } |
michael@0 | 2218 | function escapeRegex(s) { |
michael@0 | 2219 | return s.replace(/([.?*+$\[\]\/\\(){}|\-])/g, '\\$1'); |
michael@0 | 2220 | } |
michael@0 | 2221 | |
michael@0 | 2222 | function exitVisualMode(cm) { |
michael@0 | 2223 | cm.off('mousedown', exitVisualMode); |
michael@0 | 2224 | var vim = cm.state.vim; |
michael@0 | 2225 | // can't use selection state here because yank has already reset its cursor |
michael@0 | 2226 | vim.lastSelection = {'curStart': vim.marks['<'].find(), |
michael@0 | 2227 | 'curEnd': vim.marks['>'].find(), 'visualMode': vim.visualMode, |
michael@0 | 2228 | 'visualLine': vim.visualLine}; |
michael@0 | 2229 | vim.visualMode = false; |
michael@0 | 2230 | vim.visualLine = false; |
michael@0 | 2231 | var selectionStart = cm.getCursor('anchor'); |
michael@0 | 2232 | var selectionEnd = cm.getCursor('head'); |
michael@0 | 2233 | if (!cursorEqual(selectionStart, selectionEnd)) { |
michael@0 | 2234 | // Clear the selection and set the cursor only if the selection has not |
michael@0 | 2235 | // already been cleared. Otherwise we risk moving the cursor somewhere |
michael@0 | 2236 | // it's not supposed to be. |
michael@0 | 2237 | cm.setCursor(clipCursorToContent(cm, selectionEnd)); |
michael@0 | 2238 | } |
michael@0 | 2239 | CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); |
michael@0 | 2240 | } |
michael@0 | 2241 | |
michael@0 | 2242 | // Remove any trailing newlines from the selection. For |
michael@0 | 2243 | // example, with the caret at the start of the last word on the line, |
michael@0 | 2244 | // 'dw' should word, but not the newline, while 'w' should advance the |
michael@0 | 2245 | // caret to the first character of the next line. |
michael@0 | 2246 | function clipToLine(cm, curStart, curEnd) { |
michael@0 | 2247 | var selection = cm.getRange(curStart, curEnd); |
michael@0 | 2248 | // Only clip if the selection ends with trailing newline + whitespace |
michael@0 | 2249 | if (/\n\s*$/.test(selection)) { |
michael@0 | 2250 | var lines = selection.split('\n'); |
michael@0 | 2251 | // We know this is all whitepsace. |
michael@0 | 2252 | lines.pop(); |
michael@0 | 2253 | |
michael@0 | 2254 | // Cases: |
michael@0 | 2255 | // 1. Last word is an empty line - do not clip the trailing '\n' |
michael@0 | 2256 | // 2. Last word is not an empty line - clip the trailing '\n' |
michael@0 | 2257 | var line; |
michael@0 | 2258 | // Find the line containing the last word, and clip all whitespace up |
michael@0 | 2259 | // to it. |
michael@0 | 2260 | for (var line = lines.pop(); lines.length > 0 && line && isWhiteSpaceString(line); line = lines.pop()) { |
michael@0 | 2261 | curEnd.line--; |
michael@0 | 2262 | curEnd.ch = 0; |
michael@0 | 2263 | } |
michael@0 | 2264 | // If the last word is not an empty line, clip an additional newline |
michael@0 | 2265 | if (line) { |
michael@0 | 2266 | curEnd.line--; |
michael@0 | 2267 | curEnd.ch = lineLength(cm, curEnd.line); |
michael@0 | 2268 | } else { |
michael@0 | 2269 | curEnd.ch = 0; |
michael@0 | 2270 | } |
michael@0 | 2271 | } |
michael@0 | 2272 | } |
michael@0 | 2273 | |
michael@0 | 2274 | // Expand the selection to line ends. |
michael@0 | 2275 | function expandSelectionToLine(_cm, curStart, curEnd) { |
michael@0 | 2276 | curStart.ch = 0; |
michael@0 | 2277 | curEnd.ch = 0; |
michael@0 | 2278 | curEnd.line++; |
michael@0 | 2279 | } |
michael@0 | 2280 | |
michael@0 | 2281 | function findFirstNonWhiteSpaceCharacter(text) { |
michael@0 | 2282 | if (!text) { |
michael@0 | 2283 | return 0; |
michael@0 | 2284 | } |
michael@0 | 2285 | var firstNonWS = text.search(/\S/); |
michael@0 | 2286 | return firstNonWS == -1 ? text.length : firstNonWS; |
michael@0 | 2287 | } |
michael@0 | 2288 | |
michael@0 | 2289 | function expandWordUnderCursor(cm, inclusive, _forward, bigWord, noSymbol) { |
michael@0 | 2290 | var cur = cm.getCursor(); |
michael@0 | 2291 | var line = cm.getLine(cur.line); |
michael@0 | 2292 | var idx = cur.ch; |
michael@0 | 2293 | |
michael@0 | 2294 | // Seek to first word or non-whitespace character, depending on if |
michael@0 | 2295 | // noSymbol is true. |
michael@0 | 2296 | var textAfterIdx = line.substring(idx); |
michael@0 | 2297 | var firstMatchedChar; |
michael@0 | 2298 | if (noSymbol) { |
michael@0 | 2299 | firstMatchedChar = textAfterIdx.search(/\w/); |
michael@0 | 2300 | } else { |
michael@0 | 2301 | firstMatchedChar = textAfterIdx.search(/\S/); |
michael@0 | 2302 | } |
michael@0 | 2303 | if (firstMatchedChar == -1) { |
michael@0 | 2304 | return null; |
michael@0 | 2305 | } |
michael@0 | 2306 | idx += firstMatchedChar; |
michael@0 | 2307 | textAfterIdx = line.substring(idx); |
michael@0 | 2308 | var textBeforeIdx = line.substring(0, idx); |
michael@0 | 2309 | |
michael@0 | 2310 | var matchRegex; |
michael@0 | 2311 | // Greedy matchers for the "word" we are trying to expand. |
michael@0 | 2312 | if (bigWord) { |
michael@0 | 2313 | matchRegex = /^\S+/; |
michael@0 | 2314 | } else { |
michael@0 | 2315 | if ((/\w/).test(line.charAt(idx))) { |
michael@0 | 2316 | matchRegex = /^\w+/; |
michael@0 | 2317 | } else { |
michael@0 | 2318 | matchRegex = /^[^\w\s]+/; |
michael@0 | 2319 | } |
michael@0 | 2320 | } |
michael@0 | 2321 | |
michael@0 | 2322 | var wordAfterRegex = matchRegex.exec(textAfterIdx); |
michael@0 | 2323 | var wordStart = idx; |
michael@0 | 2324 | var wordEnd = idx + wordAfterRegex[0].length; |
michael@0 | 2325 | // TODO: Find a better way to do this. It will be slow on very long lines. |
michael@0 | 2326 | var revTextBeforeIdx = reverse(textBeforeIdx); |
michael@0 | 2327 | var wordBeforeRegex = matchRegex.exec(revTextBeforeIdx); |
michael@0 | 2328 | if (wordBeforeRegex) { |
michael@0 | 2329 | wordStart -= wordBeforeRegex[0].length; |
michael@0 | 2330 | } |
michael@0 | 2331 | |
michael@0 | 2332 | if (inclusive) { |
michael@0 | 2333 | // If present, trim all whitespace after word. |
michael@0 | 2334 | // Otherwise, trim all whitespace before word. |
michael@0 | 2335 | var textAfterWordEnd = line.substring(wordEnd); |
michael@0 | 2336 | var whitespacesAfterWord = textAfterWordEnd.match(/^\s*/)[0].length; |
michael@0 | 2337 | if (whitespacesAfterWord > 0) { |
michael@0 | 2338 | wordEnd += whitespacesAfterWord; |
michael@0 | 2339 | } else { |
michael@0 | 2340 | var revTrim = revTextBeforeIdx.length - wordStart; |
michael@0 | 2341 | var textBeforeWordStart = revTextBeforeIdx.substring(revTrim); |
michael@0 | 2342 | var whitespacesBeforeWord = textBeforeWordStart.match(/^\s*/)[0].length; |
michael@0 | 2343 | wordStart -= whitespacesBeforeWord; |
michael@0 | 2344 | } |
michael@0 | 2345 | } |
michael@0 | 2346 | |
michael@0 | 2347 | return { start: Pos(cur.line, wordStart), |
michael@0 | 2348 | end: Pos(cur.line, wordEnd) }; |
michael@0 | 2349 | } |
michael@0 | 2350 | |
michael@0 | 2351 | function recordJumpPosition(cm, oldCur, newCur) { |
michael@0 | 2352 | if (!cursorEqual(oldCur, newCur)) { |
michael@0 | 2353 | vimGlobalState.jumpList.add(cm, oldCur, newCur); |
michael@0 | 2354 | } |
michael@0 | 2355 | } |
michael@0 | 2356 | |
michael@0 | 2357 | function recordLastCharacterSearch(increment, args) { |
michael@0 | 2358 | vimGlobalState.lastChararacterSearch.increment = increment; |
michael@0 | 2359 | vimGlobalState.lastChararacterSearch.forward = args.forward; |
michael@0 | 2360 | vimGlobalState.lastChararacterSearch.selectedCharacter = args.selectedCharacter; |
michael@0 | 2361 | } |
michael@0 | 2362 | |
michael@0 | 2363 | var symbolToMode = { |
michael@0 | 2364 | '(': 'bracket', ')': 'bracket', '{': 'bracket', '}': 'bracket', |
michael@0 | 2365 | '[': 'section', ']': 'section', |
michael@0 | 2366 | '*': 'comment', '/': 'comment', |
michael@0 | 2367 | 'm': 'method', 'M': 'method', |
michael@0 | 2368 | '#': 'preprocess' |
michael@0 | 2369 | }; |
michael@0 | 2370 | var findSymbolModes = { |
michael@0 | 2371 | bracket: { |
michael@0 | 2372 | isComplete: function(state) { |
michael@0 | 2373 | if (state.nextCh === state.symb) { |
michael@0 | 2374 | state.depth++; |
michael@0 | 2375 | if (state.depth >= 1)return true; |
michael@0 | 2376 | } else if (state.nextCh === state.reverseSymb) { |
michael@0 | 2377 | state.depth--; |
michael@0 | 2378 | } |
michael@0 | 2379 | return false; |
michael@0 | 2380 | } |
michael@0 | 2381 | }, |
michael@0 | 2382 | section: { |
michael@0 | 2383 | init: function(state) { |
michael@0 | 2384 | state.curMoveThrough = true; |
michael@0 | 2385 | state.symb = (state.forward ? ']' : '[') === state.symb ? '{' : '}'; |
michael@0 | 2386 | }, |
michael@0 | 2387 | isComplete: function(state) { |
michael@0 | 2388 | return state.index === 0 && state.nextCh === state.symb; |
michael@0 | 2389 | } |
michael@0 | 2390 | }, |
michael@0 | 2391 | comment: { |
michael@0 | 2392 | isComplete: function(state) { |
michael@0 | 2393 | var found = state.lastCh === '*' && state.nextCh === '/'; |
michael@0 | 2394 | state.lastCh = state.nextCh; |
michael@0 | 2395 | return found; |
michael@0 | 2396 | } |
michael@0 | 2397 | }, |
michael@0 | 2398 | // TODO: The original Vim implementation only operates on level 1 and 2. |
michael@0 | 2399 | // The current implementation doesn't check for code block level and |
michael@0 | 2400 | // therefore it operates on any levels. |
michael@0 | 2401 | method: { |
michael@0 | 2402 | init: function(state) { |
michael@0 | 2403 | state.symb = (state.symb === 'm' ? '{' : '}'); |
michael@0 | 2404 | state.reverseSymb = state.symb === '{' ? '}' : '{'; |
michael@0 | 2405 | }, |
michael@0 | 2406 | isComplete: function(state) { |
michael@0 | 2407 | if (state.nextCh === state.symb)return true; |
michael@0 | 2408 | return false; |
michael@0 | 2409 | } |
michael@0 | 2410 | }, |
michael@0 | 2411 | preprocess: { |
michael@0 | 2412 | init: function(state) { |
michael@0 | 2413 | state.index = 0; |
michael@0 | 2414 | }, |
michael@0 | 2415 | isComplete: function(state) { |
michael@0 | 2416 | if (state.nextCh === '#') { |
michael@0 | 2417 | var token = state.lineText.match(/#(\w+)/)[1]; |
michael@0 | 2418 | if (token === 'endif') { |
michael@0 | 2419 | if (state.forward && state.depth === 0) { |
michael@0 | 2420 | return true; |
michael@0 | 2421 | } |
michael@0 | 2422 | state.depth++; |
michael@0 | 2423 | } else if (token === 'if') { |
michael@0 | 2424 | if (!state.forward && state.depth === 0) { |
michael@0 | 2425 | return true; |
michael@0 | 2426 | } |
michael@0 | 2427 | state.depth--; |
michael@0 | 2428 | } |
michael@0 | 2429 | if (token === 'else' && state.depth === 0)return true; |
michael@0 | 2430 | } |
michael@0 | 2431 | return false; |
michael@0 | 2432 | } |
michael@0 | 2433 | } |
michael@0 | 2434 | }; |
michael@0 | 2435 | function findSymbol(cm, repeat, forward, symb) { |
michael@0 | 2436 | var cur = copyCursor(cm.getCursor()); |
michael@0 | 2437 | var increment = forward ? 1 : -1; |
michael@0 | 2438 | var endLine = forward ? cm.lineCount() : -1; |
michael@0 | 2439 | var curCh = cur.ch; |
michael@0 | 2440 | var line = cur.line; |
michael@0 | 2441 | var lineText = cm.getLine(line); |
michael@0 | 2442 | var state = { |
michael@0 | 2443 | lineText: lineText, |
michael@0 | 2444 | nextCh: lineText.charAt(curCh), |
michael@0 | 2445 | lastCh: null, |
michael@0 | 2446 | index: curCh, |
michael@0 | 2447 | symb: symb, |
michael@0 | 2448 | reverseSymb: (forward ? { ')': '(', '}': '{' } : { '(': ')', '{': '}' })[symb], |
michael@0 | 2449 | forward: forward, |
michael@0 | 2450 | depth: 0, |
michael@0 | 2451 | curMoveThrough: false |
michael@0 | 2452 | }; |
michael@0 | 2453 | var mode = symbolToMode[symb]; |
michael@0 | 2454 | if (!mode)return cur; |
michael@0 | 2455 | var init = findSymbolModes[mode].init; |
michael@0 | 2456 | var isComplete = findSymbolModes[mode].isComplete; |
michael@0 | 2457 | if (init) { init(state); } |
michael@0 | 2458 | while (line !== endLine && repeat) { |
michael@0 | 2459 | state.index += increment; |
michael@0 | 2460 | state.nextCh = state.lineText.charAt(state.index); |
michael@0 | 2461 | if (!state.nextCh) { |
michael@0 | 2462 | line += increment; |
michael@0 | 2463 | state.lineText = cm.getLine(line) || ''; |
michael@0 | 2464 | if (increment > 0) { |
michael@0 | 2465 | state.index = 0; |
michael@0 | 2466 | } else { |
michael@0 | 2467 | var lineLen = state.lineText.length; |
michael@0 | 2468 | state.index = (lineLen > 0) ? (lineLen-1) : 0; |
michael@0 | 2469 | } |
michael@0 | 2470 | state.nextCh = state.lineText.charAt(state.index); |
michael@0 | 2471 | } |
michael@0 | 2472 | if (isComplete(state)) { |
michael@0 | 2473 | cur.line = line; |
michael@0 | 2474 | cur.ch = state.index; |
michael@0 | 2475 | repeat--; |
michael@0 | 2476 | } |
michael@0 | 2477 | } |
michael@0 | 2478 | if (state.nextCh || state.curMoveThrough) { |
michael@0 | 2479 | return Pos(line, state.index); |
michael@0 | 2480 | } |
michael@0 | 2481 | return cur; |
michael@0 | 2482 | } |
michael@0 | 2483 | |
michael@0 | 2484 | /* |
michael@0 | 2485 | * Returns the boundaries of the next word. If the cursor in the middle of |
michael@0 | 2486 | * the word, then returns the boundaries of the current word, starting at |
michael@0 | 2487 | * the cursor. If the cursor is at the start/end of a word, and we are going |
michael@0 | 2488 | * forward/backward, respectively, find the boundaries of the next word. |
michael@0 | 2489 | * |
michael@0 | 2490 | * @param {CodeMirror} cm CodeMirror object. |
michael@0 | 2491 | * @param {Cursor} cur The cursor position. |
michael@0 | 2492 | * @param {boolean} forward True to search forward. False to search |
michael@0 | 2493 | * backward. |
michael@0 | 2494 | * @param {boolean} bigWord True if punctuation count as part of the word. |
michael@0 | 2495 | * False if only [a-zA-Z0-9] characters count as part of the word. |
michael@0 | 2496 | * @param {boolean} emptyLineIsWord True if empty lines should be treated |
michael@0 | 2497 | * as words. |
michael@0 | 2498 | * @return {Object{from:number, to:number, line: number}} The boundaries of |
michael@0 | 2499 | * the word, or null if there are no more words. |
michael@0 | 2500 | */ |
michael@0 | 2501 | function findWord(cm, cur, forward, bigWord, emptyLineIsWord) { |
michael@0 | 2502 | var lineNum = cur.line; |
michael@0 | 2503 | var pos = cur.ch; |
michael@0 | 2504 | var line = cm.getLine(lineNum); |
michael@0 | 2505 | var dir = forward ? 1 : -1; |
michael@0 | 2506 | var regexps = bigWord ? bigWordRegexp : wordRegexp; |
michael@0 | 2507 | |
michael@0 | 2508 | if (emptyLineIsWord && line == '') { |
michael@0 | 2509 | lineNum += dir; |
michael@0 | 2510 | line = cm.getLine(lineNum); |
michael@0 | 2511 | if (!isLine(cm, lineNum)) { |
michael@0 | 2512 | return null; |
michael@0 | 2513 | } |
michael@0 | 2514 | pos = (forward) ? 0 : line.length; |
michael@0 | 2515 | } |
michael@0 | 2516 | |
michael@0 | 2517 | while (true) { |
michael@0 | 2518 | if (emptyLineIsWord && line == '') { |
michael@0 | 2519 | return { from: 0, to: 0, line: lineNum }; |
michael@0 | 2520 | } |
michael@0 | 2521 | var stop = (dir > 0) ? line.length : -1; |
michael@0 | 2522 | var wordStart = stop, wordEnd = stop; |
michael@0 | 2523 | // Find bounds of next word. |
michael@0 | 2524 | while (pos != stop) { |
michael@0 | 2525 | var foundWord = false; |
michael@0 | 2526 | for (var i = 0; i < regexps.length && !foundWord; ++i) { |
michael@0 | 2527 | if (regexps[i].test(line.charAt(pos))) { |
michael@0 | 2528 | wordStart = pos; |
michael@0 | 2529 | // Advance to end of word. |
michael@0 | 2530 | while (pos != stop && regexps[i].test(line.charAt(pos))) { |
michael@0 | 2531 | pos += dir; |
michael@0 | 2532 | } |
michael@0 | 2533 | wordEnd = pos; |
michael@0 | 2534 | foundWord = wordStart != wordEnd; |
michael@0 | 2535 | if (wordStart == cur.ch && lineNum == cur.line && |
michael@0 | 2536 | wordEnd == wordStart + dir) { |
michael@0 | 2537 | // We started at the end of a word. Find the next one. |
michael@0 | 2538 | continue; |
michael@0 | 2539 | } else { |
michael@0 | 2540 | return { |
michael@0 | 2541 | from: Math.min(wordStart, wordEnd + 1), |
michael@0 | 2542 | to: Math.max(wordStart, wordEnd), |
michael@0 | 2543 | line: lineNum }; |
michael@0 | 2544 | } |
michael@0 | 2545 | } |
michael@0 | 2546 | } |
michael@0 | 2547 | if (!foundWord) { |
michael@0 | 2548 | pos += dir; |
michael@0 | 2549 | } |
michael@0 | 2550 | } |
michael@0 | 2551 | // Advance to next/prev line. |
michael@0 | 2552 | lineNum += dir; |
michael@0 | 2553 | if (!isLine(cm, lineNum)) { |
michael@0 | 2554 | return null; |
michael@0 | 2555 | } |
michael@0 | 2556 | line = cm.getLine(lineNum); |
michael@0 | 2557 | pos = (dir > 0) ? 0 : line.length; |
michael@0 | 2558 | } |
michael@0 | 2559 | // Should never get here. |
michael@0 | 2560 | throw new Error('The impossible happened.'); |
michael@0 | 2561 | } |
michael@0 | 2562 | |
michael@0 | 2563 | /** |
michael@0 | 2564 | * @param {CodeMirror} cm CodeMirror object. |
michael@0 | 2565 | * @param {int} repeat Number of words to move past. |
michael@0 | 2566 | * @param {boolean} forward True to search forward. False to search |
michael@0 | 2567 | * backward. |
michael@0 | 2568 | * @param {boolean} wordEnd True to move to end of word. False to move to |
michael@0 | 2569 | * beginning of word. |
michael@0 | 2570 | * @param {boolean} bigWord True if punctuation count as part of the word. |
michael@0 | 2571 | * False if only alphabet characters count as part of the word. |
michael@0 | 2572 | * @return {Cursor} The position the cursor should move to. |
michael@0 | 2573 | */ |
michael@0 | 2574 | function moveToWord(cm, repeat, forward, wordEnd, bigWord) { |
michael@0 | 2575 | var cur = cm.getCursor(); |
michael@0 | 2576 | var curStart = copyCursor(cur); |
michael@0 | 2577 | var words = []; |
michael@0 | 2578 | if (forward && !wordEnd || !forward && wordEnd) { |
michael@0 | 2579 | repeat++; |
michael@0 | 2580 | } |
michael@0 | 2581 | // For 'e', empty lines are not considered words, go figure. |
michael@0 | 2582 | var emptyLineIsWord = !(forward && wordEnd); |
michael@0 | 2583 | for (var i = 0; i < repeat; i++) { |
michael@0 | 2584 | var word = findWord(cm, cur, forward, bigWord, emptyLineIsWord); |
michael@0 | 2585 | if (!word) { |
michael@0 | 2586 | var eodCh = lineLength(cm, cm.lastLine()); |
michael@0 | 2587 | words.push(forward |
michael@0 | 2588 | ? {line: cm.lastLine(), from: eodCh, to: eodCh} |
michael@0 | 2589 | : {line: 0, from: 0, to: 0}); |
michael@0 | 2590 | break; |
michael@0 | 2591 | } |
michael@0 | 2592 | words.push(word); |
michael@0 | 2593 | cur = Pos(word.line, forward ? (word.to - 1) : word.from); |
michael@0 | 2594 | } |
michael@0 | 2595 | var shortCircuit = words.length != repeat; |
michael@0 | 2596 | var firstWord = words[0]; |
michael@0 | 2597 | var lastWord = words.pop(); |
michael@0 | 2598 | if (forward && !wordEnd) { |
michael@0 | 2599 | // w |
michael@0 | 2600 | if (!shortCircuit && (firstWord.from != curStart.ch || firstWord.line != curStart.line)) { |
michael@0 | 2601 | // We did not start in the middle of a word. Discard the extra word at the end. |
michael@0 | 2602 | lastWord = words.pop(); |
michael@0 | 2603 | } |
michael@0 | 2604 | return Pos(lastWord.line, lastWord.from); |
michael@0 | 2605 | } else if (forward && wordEnd) { |
michael@0 | 2606 | return Pos(lastWord.line, lastWord.to - 1); |
michael@0 | 2607 | } else if (!forward && wordEnd) { |
michael@0 | 2608 | // ge |
michael@0 | 2609 | if (!shortCircuit && (firstWord.to != curStart.ch || firstWord.line != curStart.line)) { |
michael@0 | 2610 | // We did not start in the middle of a word. Discard the extra word at the end. |
michael@0 | 2611 | lastWord = words.pop(); |
michael@0 | 2612 | } |
michael@0 | 2613 | return Pos(lastWord.line, lastWord.to); |
michael@0 | 2614 | } else { |
michael@0 | 2615 | // b |
michael@0 | 2616 | return Pos(lastWord.line, lastWord.from); |
michael@0 | 2617 | } |
michael@0 | 2618 | } |
michael@0 | 2619 | |
michael@0 | 2620 | function moveToCharacter(cm, repeat, forward, character) { |
michael@0 | 2621 | var cur = cm.getCursor(); |
michael@0 | 2622 | var start = cur.ch; |
michael@0 | 2623 | var idx; |
michael@0 | 2624 | for (var i = 0; i < repeat; i ++) { |
michael@0 | 2625 | var line = cm.getLine(cur.line); |
michael@0 | 2626 | idx = charIdxInLine(start, line, character, forward, true); |
michael@0 | 2627 | if (idx == -1) { |
michael@0 | 2628 | return null; |
michael@0 | 2629 | } |
michael@0 | 2630 | start = idx; |
michael@0 | 2631 | } |
michael@0 | 2632 | return Pos(cm.getCursor().line, idx); |
michael@0 | 2633 | } |
michael@0 | 2634 | |
michael@0 | 2635 | function moveToColumn(cm, repeat) { |
michael@0 | 2636 | // repeat is always >= 1, so repeat - 1 always corresponds |
michael@0 | 2637 | // to the column we want to go to. |
michael@0 | 2638 | var line = cm.getCursor().line; |
michael@0 | 2639 | return clipCursorToContent(cm, Pos(line, repeat - 1)); |
michael@0 | 2640 | } |
michael@0 | 2641 | |
michael@0 | 2642 | function updateMark(cm, vim, markName, pos) { |
michael@0 | 2643 | if (!inArray(markName, validMarks)) { |
michael@0 | 2644 | return; |
michael@0 | 2645 | } |
michael@0 | 2646 | if (vim.marks[markName]) { |
michael@0 | 2647 | vim.marks[markName].clear(); |
michael@0 | 2648 | } |
michael@0 | 2649 | vim.marks[markName] = cm.setBookmark(pos); |
michael@0 | 2650 | } |
michael@0 | 2651 | |
michael@0 | 2652 | function charIdxInLine(start, line, character, forward, includeChar) { |
michael@0 | 2653 | // Search for char in line. |
michael@0 | 2654 | // motion_options: {forward, includeChar} |
michael@0 | 2655 | // If includeChar = true, include it too. |
michael@0 | 2656 | // If forward = true, search forward, else search backwards. |
michael@0 | 2657 | // If char is not found on this line, do nothing |
michael@0 | 2658 | var idx; |
michael@0 | 2659 | if (forward) { |
michael@0 | 2660 | idx = line.indexOf(character, start + 1); |
michael@0 | 2661 | if (idx != -1 && !includeChar) { |
michael@0 | 2662 | idx -= 1; |
michael@0 | 2663 | } |
michael@0 | 2664 | } else { |
michael@0 | 2665 | idx = line.lastIndexOf(character, start - 1); |
michael@0 | 2666 | if (idx != -1 && !includeChar) { |
michael@0 | 2667 | idx += 1; |
michael@0 | 2668 | } |
michael@0 | 2669 | } |
michael@0 | 2670 | return idx; |
michael@0 | 2671 | } |
michael@0 | 2672 | |
michael@0 | 2673 | function getContextLevel(ctx) { |
michael@0 | 2674 | return (ctx === 'string' || ctx === 'comment') ? 1 : 0; |
michael@0 | 2675 | } |
michael@0 | 2676 | |
michael@0 | 2677 | function findMatchedSymbol(cm, cur, symb) { |
michael@0 | 2678 | var line = cur.line; |
michael@0 | 2679 | var ch = cur.ch; |
michael@0 | 2680 | symb = symb ? symb : cm.getLine(line).charAt(ch); |
michael@0 | 2681 | |
michael@0 | 2682 | var symbContext = cm.getTokenAt(Pos(line, ch + 1)).type; |
michael@0 | 2683 | var symbCtxLevel = getContextLevel(symbContext); |
michael@0 | 2684 | |
michael@0 | 2685 | var reverseSymb = ({ |
michael@0 | 2686 | '(': ')', ')': '(', |
michael@0 | 2687 | '[': ']', ']': '[', |
michael@0 | 2688 | '{': '}', '}': '{'})[symb]; |
michael@0 | 2689 | |
michael@0 | 2690 | // Couldn't find a matching symbol, abort |
michael@0 | 2691 | if (!reverseSymb) { |
michael@0 | 2692 | return cur; |
michael@0 | 2693 | } |
michael@0 | 2694 | |
michael@0 | 2695 | // set our increment to move forward (+1) or backwards (-1) |
michael@0 | 2696 | // depending on which bracket we're matching |
michael@0 | 2697 | var increment = ({'(': 1, '{': 1, '[': 1})[symb] || -1; |
michael@0 | 2698 | var endLine = increment === 1 ? cm.lineCount() : -1; |
michael@0 | 2699 | var depth = 1, nextCh = symb, index = ch, lineText = cm.getLine(line); |
michael@0 | 2700 | // Simple search for closing paren--just count openings and closings till |
michael@0 | 2701 | // we find our match |
michael@0 | 2702 | // TODO: use info from CodeMirror to ignore closing brackets in comments |
michael@0 | 2703 | // and quotes, etc. |
michael@0 | 2704 | while (line !== endLine && depth > 0) { |
michael@0 | 2705 | index += increment; |
michael@0 | 2706 | nextCh = lineText.charAt(index); |
michael@0 | 2707 | if (!nextCh) { |
michael@0 | 2708 | line += increment; |
michael@0 | 2709 | lineText = cm.getLine(line) || ''; |
michael@0 | 2710 | if (increment > 0) { |
michael@0 | 2711 | index = 0; |
michael@0 | 2712 | } else { |
michael@0 | 2713 | var lineLen = lineText.length; |
michael@0 | 2714 | index = (lineLen > 0) ? (lineLen-1) : 0; |
michael@0 | 2715 | } |
michael@0 | 2716 | nextCh = lineText.charAt(index); |
michael@0 | 2717 | } |
michael@0 | 2718 | var revSymbContext = cm.getTokenAt(Pos(line, index + 1)).type; |
michael@0 | 2719 | var revSymbCtxLevel = getContextLevel(revSymbContext); |
michael@0 | 2720 | if (symbCtxLevel >= revSymbCtxLevel) { |
michael@0 | 2721 | if (nextCh === symb) { |
michael@0 | 2722 | depth++; |
michael@0 | 2723 | } else if (nextCh === reverseSymb) { |
michael@0 | 2724 | depth--; |
michael@0 | 2725 | } |
michael@0 | 2726 | } |
michael@0 | 2727 | } |
michael@0 | 2728 | |
michael@0 | 2729 | if (nextCh) { |
michael@0 | 2730 | return Pos(line, index); |
michael@0 | 2731 | } |
michael@0 | 2732 | return cur; |
michael@0 | 2733 | } |
michael@0 | 2734 | |
michael@0 | 2735 | // TODO: perhaps this finagling of start and end positions belonds |
michael@0 | 2736 | // in codmirror/replaceRange? |
michael@0 | 2737 | function selectCompanionObject(cm, revSymb, inclusive) { |
michael@0 | 2738 | var cur = copyCursor(cm.getCursor()); |
michael@0 | 2739 | var end = findMatchedSymbol(cm, cur, revSymb); |
michael@0 | 2740 | var start = findMatchedSymbol(cm, end); |
michael@0 | 2741 | |
michael@0 | 2742 | if ((start.line == end.line && start.ch > end.ch) |
michael@0 | 2743 | || (start.line > end.line)) { |
michael@0 | 2744 | var tmp = start; |
michael@0 | 2745 | start = end; |
michael@0 | 2746 | end = tmp; |
michael@0 | 2747 | } |
michael@0 | 2748 | |
michael@0 | 2749 | if (inclusive) { |
michael@0 | 2750 | end.ch += 1; |
michael@0 | 2751 | } else { |
michael@0 | 2752 | start.ch += 1; |
michael@0 | 2753 | } |
michael@0 | 2754 | |
michael@0 | 2755 | return { start: start, end: end }; |
michael@0 | 2756 | } |
michael@0 | 2757 | |
michael@0 | 2758 | // Takes in a symbol and a cursor and tries to simulate text objects that |
michael@0 | 2759 | // have identical opening and closing symbols |
michael@0 | 2760 | // TODO support across multiple lines |
michael@0 | 2761 | function findBeginningAndEnd(cm, symb, inclusive) { |
michael@0 | 2762 | var cur = copyCursor(cm.getCursor()); |
michael@0 | 2763 | var line = cm.getLine(cur.line); |
michael@0 | 2764 | var chars = line.split(''); |
michael@0 | 2765 | var start, end, i, len; |
michael@0 | 2766 | var firstIndex = chars.indexOf(symb); |
michael@0 | 2767 | |
michael@0 | 2768 | // the decision tree is to always look backwards for the beginning first, |
michael@0 | 2769 | // but if the cursor is in front of the first instance of the symb, |
michael@0 | 2770 | // then move the cursor forward |
michael@0 | 2771 | if (cur.ch < firstIndex) { |
michael@0 | 2772 | cur.ch = firstIndex; |
michael@0 | 2773 | // Why is this line even here??? |
michael@0 | 2774 | // cm.setCursor(cur.line, firstIndex+1); |
michael@0 | 2775 | } |
michael@0 | 2776 | // otherwise if the cursor is currently on the closing symbol |
michael@0 | 2777 | else if (firstIndex < cur.ch && chars[cur.ch] == symb) { |
michael@0 | 2778 | end = cur.ch; // assign end to the current cursor |
michael@0 | 2779 | --cur.ch; // make sure to look backwards |
michael@0 | 2780 | } |
michael@0 | 2781 | |
michael@0 | 2782 | // if we're currently on the symbol, we've got a start |
michael@0 | 2783 | if (chars[cur.ch] == symb && !end) { |
michael@0 | 2784 | start = cur.ch + 1; // assign start to ahead of the cursor |
michael@0 | 2785 | } else { |
michael@0 | 2786 | // go backwards to find the start |
michael@0 | 2787 | for (i = cur.ch; i > -1 && !start; i--) { |
michael@0 | 2788 | if (chars[i] == symb) { |
michael@0 | 2789 | start = i + 1; |
michael@0 | 2790 | } |
michael@0 | 2791 | } |
michael@0 | 2792 | } |
michael@0 | 2793 | |
michael@0 | 2794 | // look forwards for the end symbol |
michael@0 | 2795 | if (start && !end) { |
michael@0 | 2796 | for (i = start, len = chars.length; i < len && !end; i++) { |
michael@0 | 2797 | if (chars[i] == symb) { |
michael@0 | 2798 | end = i; |
michael@0 | 2799 | } |
michael@0 | 2800 | } |
michael@0 | 2801 | } |
michael@0 | 2802 | |
michael@0 | 2803 | // nothing found |
michael@0 | 2804 | if (!start || !end) { |
michael@0 | 2805 | return { start: cur, end: cur }; |
michael@0 | 2806 | } |
michael@0 | 2807 | |
michael@0 | 2808 | // include the symbols |
michael@0 | 2809 | if (inclusive) { |
michael@0 | 2810 | --start; ++end; |
michael@0 | 2811 | } |
michael@0 | 2812 | |
michael@0 | 2813 | return { |
michael@0 | 2814 | start: Pos(cur.line, start), |
michael@0 | 2815 | end: Pos(cur.line, end) |
michael@0 | 2816 | }; |
michael@0 | 2817 | } |
michael@0 | 2818 | |
michael@0 | 2819 | // Search functions |
michael@0 | 2820 | defineOption('pcre', true, 'boolean'); |
michael@0 | 2821 | function SearchState() {} |
michael@0 | 2822 | SearchState.prototype = { |
michael@0 | 2823 | getQuery: function() { |
michael@0 | 2824 | return vimGlobalState.query; |
michael@0 | 2825 | }, |
michael@0 | 2826 | setQuery: function(query) { |
michael@0 | 2827 | vimGlobalState.query = query; |
michael@0 | 2828 | }, |
michael@0 | 2829 | getOverlay: function() { |
michael@0 | 2830 | return this.searchOverlay; |
michael@0 | 2831 | }, |
michael@0 | 2832 | setOverlay: function(overlay) { |
michael@0 | 2833 | this.searchOverlay = overlay; |
michael@0 | 2834 | }, |
michael@0 | 2835 | isReversed: function() { |
michael@0 | 2836 | return vimGlobalState.isReversed; |
michael@0 | 2837 | }, |
michael@0 | 2838 | setReversed: function(reversed) { |
michael@0 | 2839 | vimGlobalState.isReversed = reversed; |
michael@0 | 2840 | } |
michael@0 | 2841 | }; |
michael@0 | 2842 | function getSearchState(cm) { |
michael@0 | 2843 | var vim = cm.state.vim; |
michael@0 | 2844 | return vim.searchState_ || (vim.searchState_ = new SearchState()); |
michael@0 | 2845 | } |
michael@0 | 2846 | function dialog(cm, template, shortText, onClose, options) { |
michael@0 | 2847 | if (cm.openDialog) { |
michael@0 | 2848 | cm.openDialog(template, onClose, { bottom: true, value: options.value, |
michael@0 | 2849 | onKeyDown: options.onKeyDown, onKeyUp: options.onKeyUp }); |
michael@0 | 2850 | } |
michael@0 | 2851 | else { |
michael@0 | 2852 | onClose(prompt(shortText, '')); |
michael@0 | 2853 | } |
michael@0 | 2854 | } |
michael@0 | 2855 | |
michael@0 | 2856 | function findUnescapedSlashes(str) { |
michael@0 | 2857 | var escapeNextChar = false; |
michael@0 | 2858 | var slashes = []; |
michael@0 | 2859 | for (var i = 0; i < str.length; i++) { |
michael@0 | 2860 | var c = str.charAt(i); |
michael@0 | 2861 | if (!escapeNextChar && c == '/') { |
michael@0 | 2862 | slashes.push(i); |
michael@0 | 2863 | } |
michael@0 | 2864 | escapeNextChar = !escapeNextChar && (c == '\\'); |
michael@0 | 2865 | } |
michael@0 | 2866 | return slashes; |
michael@0 | 2867 | } |
michael@0 | 2868 | |
michael@0 | 2869 | // Translates a search string from ex (vim) syntax into javascript form. |
michael@0 | 2870 | function translateRegex(str) { |
michael@0 | 2871 | // When these match, add a '\' if unescaped or remove one if escaped. |
michael@0 | 2872 | var specials = ['|', '(', ')', '{']; |
michael@0 | 2873 | // Remove, but never add, a '\' for these. |
michael@0 | 2874 | var unescape = ['}']; |
michael@0 | 2875 | var escapeNextChar = false; |
michael@0 | 2876 | var out = []; |
michael@0 | 2877 | for (var i = -1; i < str.length; i++) { |
michael@0 | 2878 | var c = str.charAt(i) || ''; |
michael@0 | 2879 | var n = str.charAt(i+1) || ''; |
michael@0 | 2880 | var specialComesNext = (specials.indexOf(n) != -1); |
michael@0 | 2881 | if (escapeNextChar) { |
michael@0 | 2882 | if (c !== '\\' || !specialComesNext) { |
michael@0 | 2883 | out.push(c); |
michael@0 | 2884 | } |
michael@0 | 2885 | escapeNextChar = false; |
michael@0 | 2886 | } else { |
michael@0 | 2887 | if (c === '\\') { |
michael@0 | 2888 | escapeNextChar = true; |
michael@0 | 2889 | // Treat the unescape list as special for removing, but not adding '\'. |
michael@0 | 2890 | if (unescape.indexOf(n) != -1) { |
michael@0 | 2891 | specialComesNext = true; |
michael@0 | 2892 | } |
michael@0 | 2893 | // Not passing this test means removing a '\'. |
michael@0 | 2894 | if (!specialComesNext || n === '\\') { |
michael@0 | 2895 | out.push(c); |
michael@0 | 2896 | } |
michael@0 | 2897 | } else { |
michael@0 | 2898 | out.push(c); |
michael@0 | 2899 | if (specialComesNext && n !== '\\') { |
michael@0 | 2900 | out.push('\\'); |
michael@0 | 2901 | } |
michael@0 | 2902 | } |
michael@0 | 2903 | } |
michael@0 | 2904 | } |
michael@0 | 2905 | return out.join(''); |
michael@0 | 2906 | } |
michael@0 | 2907 | |
michael@0 | 2908 | // Translates the replace part of a search and replace from ex (vim) syntax into |
michael@0 | 2909 | // javascript form. Similar to translateRegex, but additionally fixes back references |
michael@0 | 2910 | // (translates '\[0..9]' to '$[0..9]') and follows different rules for escaping '$'. |
michael@0 | 2911 | function translateRegexReplace(str) { |
michael@0 | 2912 | var escapeNextChar = false; |
michael@0 | 2913 | var out = []; |
michael@0 | 2914 | for (var i = -1; i < str.length; i++) { |
michael@0 | 2915 | var c = str.charAt(i) || ''; |
michael@0 | 2916 | var n = str.charAt(i+1) || ''; |
michael@0 | 2917 | if (escapeNextChar) { |
michael@0 | 2918 | // At any point in the loop, escapeNextChar is true if the previous |
michael@0 | 2919 | // character was a '\' and was not escaped. |
michael@0 | 2920 | out.push(c); |
michael@0 | 2921 | escapeNextChar = false; |
michael@0 | 2922 | } else { |
michael@0 | 2923 | if (c === '\\') { |
michael@0 | 2924 | escapeNextChar = true; |
michael@0 | 2925 | if ((isNumber(n) || n === '$')) { |
michael@0 | 2926 | out.push('$'); |
michael@0 | 2927 | } else if (n !== '/' && n !== '\\') { |
michael@0 | 2928 | out.push('\\'); |
michael@0 | 2929 | } |
michael@0 | 2930 | } else { |
michael@0 | 2931 | if (c === '$') { |
michael@0 | 2932 | out.push('$'); |
michael@0 | 2933 | } |
michael@0 | 2934 | out.push(c); |
michael@0 | 2935 | if (n === '/') { |
michael@0 | 2936 | out.push('\\'); |
michael@0 | 2937 | } |
michael@0 | 2938 | } |
michael@0 | 2939 | } |
michael@0 | 2940 | } |
michael@0 | 2941 | return out.join(''); |
michael@0 | 2942 | } |
michael@0 | 2943 | |
michael@0 | 2944 | // Unescape \ and / in the replace part, for PCRE mode. |
michael@0 | 2945 | function unescapeRegexReplace(str) { |
michael@0 | 2946 | var stream = new CodeMirror.StringStream(str); |
michael@0 | 2947 | var output = []; |
michael@0 | 2948 | while (!stream.eol()) { |
michael@0 | 2949 | // Search for \. |
michael@0 | 2950 | while (stream.peek() && stream.peek() != '\\') { |
michael@0 | 2951 | output.push(stream.next()); |
michael@0 | 2952 | } |
michael@0 | 2953 | if (stream.match('\\/', true)) { |
michael@0 | 2954 | // \/ => / |
michael@0 | 2955 | output.push('/'); |
michael@0 | 2956 | } else if (stream.match('\\\\', true)) { |
michael@0 | 2957 | // \\ => \ |
michael@0 | 2958 | output.push('\\'); |
michael@0 | 2959 | } else { |
michael@0 | 2960 | // Don't change anything |
michael@0 | 2961 | output.push(stream.next()); |
michael@0 | 2962 | } |
michael@0 | 2963 | } |
michael@0 | 2964 | return output.join(''); |
michael@0 | 2965 | } |
michael@0 | 2966 | |
michael@0 | 2967 | /** |
michael@0 | 2968 | * Extract the regular expression from the query and return a Regexp object. |
michael@0 | 2969 | * Returns null if the query is blank. |
michael@0 | 2970 | * If ignoreCase is passed in, the Regexp object will have the 'i' flag set. |
michael@0 | 2971 | * If smartCase is passed in, and the query contains upper case letters, |
michael@0 | 2972 | * then ignoreCase is overridden, and the 'i' flag will not be set. |
michael@0 | 2973 | * If the query contains the /i in the flag part of the regular expression, |
michael@0 | 2974 | * then both ignoreCase and smartCase are ignored, and 'i' will be passed |
michael@0 | 2975 | * through to the Regex object. |
michael@0 | 2976 | */ |
michael@0 | 2977 | function parseQuery(query, ignoreCase, smartCase) { |
michael@0 | 2978 | // Check if the query is already a regex. |
michael@0 | 2979 | if (query instanceof RegExp) { return query; } |
michael@0 | 2980 | // First try to extract regex + flags from the input. If no flags found, |
michael@0 | 2981 | // extract just the regex. IE does not accept flags directly defined in |
michael@0 | 2982 | // the regex string in the form /regex/flags |
michael@0 | 2983 | var slashes = findUnescapedSlashes(query); |
michael@0 | 2984 | var regexPart; |
michael@0 | 2985 | var forceIgnoreCase; |
michael@0 | 2986 | if (!slashes.length) { |
michael@0 | 2987 | // Query looks like 'regexp' |
michael@0 | 2988 | regexPart = query; |
michael@0 | 2989 | } else { |
michael@0 | 2990 | // Query looks like 'regexp/...' |
michael@0 | 2991 | regexPart = query.substring(0, slashes[0]); |
michael@0 | 2992 | var flagsPart = query.substring(slashes[0]); |
michael@0 | 2993 | forceIgnoreCase = (flagsPart.indexOf('i') != -1); |
michael@0 | 2994 | } |
michael@0 | 2995 | if (!regexPart) { |
michael@0 | 2996 | return null; |
michael@0 | 2997 | } |
michael@0 | 2998 | if (!getOption('pcre')) { |
michael@0 | 2999 | regexPart = translateRegex(regexPart); |
michael@0 | 3000 | } |
michael@0 | 3001 | if (smartCase) { |
michael@0 | 3002 | ignoreCase = (/^[^A-Z]*$/).test(regexPart); |
michael@0 | 3003 | } |
michael@0 | 3004 | var regexp = new RegExp(regexPart, |
michael@0 | 3005 | (ignoreCase || forceIgnoreCase) ? 'i' : undefined); |
michael@0 | 3006 | return regexp; |
michael@0 | 3007 | } |
michael@0 | 3008 | function showConfirm(cm, text) { |
michael@0 | 3009 | if (cm.openNotification) { |
michael@0 | 3010 | cm.openNotification('<span style="color: red">' + text + '</span>', |
michael@0 | 3011 | {bottom: true, duration: 5000}); |
michael@0 | 3012 | } else { |
michael@0 | 3013 | alert(text); |
michael@0 | 3014 | } |
michael@0 | 3015 | } |
michael@0 | 3016 | function makePrompt(prefix, desc) { |
michael@0 | 3017 | var raw = ''; |
michael@0 | 3018 | if (prefix) { |
michael@0 | 3019 | raw += '<span style="font-family: monospace">' + prefix + '</span>'; |
michael@0 | 3020 | } |
michael@0 | 3021 | raw += '<input type="text"/> ' + |
michael@0 | 3022 | '<span style="color: #888">'; |
michael@0 | 3023 | if (desc) { |
michael@0 | 3024 | raw += '<span style="color: #888">'; |
michael@0 | 3025 | raw += desc; |
michael@0 | 3026 | raw += '</span>'; |
michael@0 | 3027 | } |
michael@0 | 3028 | return raw; |
michael@0 | 3029 | } |
michael@0 | 3030 | var searchPromptDesc = '(Javascript regexp)'; |
michael@0 | 3031 | function showPrompt(cm, options) { |
michael@0 | 3032 | var shortText = (options.prefix || '') + ' ' + (options.desc || ''); |
michael@0 | 3033 | var prompt = makePrompt(options.prefix, options.desc); |
michael@0 | 3034 | dialog(cm, prompt, shortText, options.onClose, options); |
michael@0 | 3035 | } |
michael@0 | 3036 | function regexEqual(r1, r2) { |
michael@0 | 3037 | if (r1 instanceof RegExp && r2 instanceof RegExp) { |
michael@0 | 3038 | var props = ['global', 'multiline', 'ignoreCase', 'source']; |
michael@0 | 3039 | for (var i = 0; i < props.length; i++) { |
michael@0 | 3040 | var prop = props[i]; |
michael@0 | 3041 | if (r1[prop] !== r2[prop]) { |
michael@0 | 3042 | return false; |
michael@0 | 3043 | } |
michael@0 | 3044 | } |
michael@0 | 3045 | return true; |
michael@0 | 3046 | } |
michael@0 | 3047 | return false; |
michael@0 | 3048 | } |
michael@0 | 3049 | // Returns true if the query is valid. |
michael@0 | 3050 | function updateSearchQuery(cm, rawQuery, ignoreCase, smartCase) { |
michael@0 | 3051 | if (!rawQuery) { |
michael@0 | 3052 | return; |
michael@0 | 3053 | } |
michael@0 | 3054 | var state = getSearchState(cm); |
michael@0 | 3055 | var query = parseQuery(rawQuery, !!ignoreCase, !!smartCase); |
michael@0 | 3056 | if (!query) { |
michael@0 | 3057 | return; |
michael@0 | 3058 | } |
michael@0 | 3059 | highlightSearchMatches(cm, query); |
michael@0 | 3060 | if (regexEqual(query, state.getQuery())) { |
michael@0 | 3061 | return query; |
michael@0 | 3062 | } |
michael@0 | 3063 | state.setQuery(query); |
michael@0 | 3064 | return query; |
michael@0 | 3065 | } |
michael@0 | 3066 | function searchOverlay(query) { |
michael@0 | 3067 | if (query.source.charAt(0) == '^') { |
michael@0 | 3068 | var matchSol = true; |
michael@0 | 3069 | } |
michael@0 | 3070 | return { |
michael@0 | 3071 | token: function(stream) { |
michael@0 | 3072 | if (matchSol && !stream.sol()) { |
michael@0 | 3073 | stream.skipToEnd(); |
michael@0 | 3074 | return; |
michael@0 | 3075 | } |
michael@0 | 3076 | var match = stream.match(query, false); |
michael@0 | 3077 | if (match) { |
michael@0 | 3078 | if (match[0].length == 0) { |
michael@0 | 3079 | // Matched empty string, skip to next. |
michael@0 | 3080 | stream.next(); |
michael@0 | 3081 | return 'searching'; |
michael@0 | 3082 | } |
michael@0 | 3083 | if (!stream.sol()) { |
michael@0 | 3084 | // Backtrack 1 to match \b |
michael@0 | 3085 | stream.backUp(1); |
michael@0 | 3086 | if (!query.exec(stream.next() + match[0])) { |
michael@0 | 3087 | stream.next(); |
michael@0 | 3088 | return null; |
michael@0 | 3089 | } |
michael@0 | 3090 | } |
michael@0 | 3091 | stream.match(query); |
michael@0 | 3092 | return 'searching'; |
michael@0 | 3093 | } |
michael@0 | 3094 | while (!stream.eol()) { |
michael@0 | 3095 | stream.next(); |
michael@0 | 3096 | if (stream.match(query, false)) break; |
michael@0 | 3097 | } |
michael@0 | 3098 | }, |
michael@0 | 3099 | query: query |
michael@0 | 3100 | }; |
michael@0 | 3101 | } |
michael@0 | 3102 | function highlightSearchMatches(cm, query) { |
michael@0 | 3103 | var overlay = getSearchState(cm).getOverlay(); |
michael@0 | 3104 | if (!overlay || query != overlay.query) { |
michael@0 | 3105 | if (overlay) { |
michael@0 | 3106 | cm.removeOverlay(overlay); |
michael@0 | 3107 | } |
michael@0 | 3108 | overlay = searchOverlay(query); |
michael@0 | 3109 | cm.addOverlay(overlay); |
michael@0 | 3110 | getSearchState(cm).setOverlay(overlay); |
michael@0 | 3111 | } |
michael@0 | 3112 | } |
michael@0 | 3113 | function findNext(cm, prev, query, repeat) { |
michael@0 | 3114 | if (repeat === undefined) { repeat = 1; } |
michael@0 | 3115 | return cm.operation(function() { |
michael@0 | 3116 | var pos = cm.getCursor(); |
michael@0 | 3117 | var cursor = cm.getSearchCursor(query, pos); |
michael@0 | 3118 | for (var i = 0; i < repeat; i++) { |
michael@0 | 3119 | var found = cursor.find(prev); |
michael@0 | 3120 | if (i == 0 && found && cursorEqual(cursor.from(), pos)) { found = cursor.find(prev); } |
michael@0 | 3121 | if (!found) { |
michael@0 | 3122 | // SearchCursor may have returned null because it hit EOF, wrap |
michael@0 | 3123 | // around and try again. |
michael@0 | 3124 | cursor = cm.getSearchCursor(query, |
michael@0 | 3125 | (prev) ? Pos(cm.lastLine()) : Pos(cm.firstLine(), 0) ); |
michael@0 | 3126 | if (!cursor.find(prev)) { |
michael@0 | 3127 | return; |
michael@0 | 3128 | } |
michael@0 | 3129 | } |
michael@0 | 3130 | } |
michael@0 | 3131 | return cursor.from(); |
michael@0 | 3132 | }); |
michael@0 | 3133 | } |
michael@0 | 3134 | function clearSearchHighlight(cm) { |
michael@0 | 3135 | cm.removeOverlay(getSearchState(cm).getOverlay()); |
michael@0 | 3136 | getSearchState(cm).setOverlay(null); |
michael@0 | 3137 | } |
michael@0 | 3138 | /** |
michael@0 | 3139 | * Check if pos is in the specified range, INCLUSIVE. |
michael@0 | 3140 | * Range can be specified with 1 or 2 arguments. |
michael@0 | 3141 | * If the first range argument is an array, treat it as an array of line |
michael@0 | 3142 | * numbers. Match pos against any of the lines. |
michael@0 | 3143 | * If the first range argument is a number, |
michael@0 | 3144 | * if there is only 1 range argument, check if pos has the same line |
michael@0 | 3145 | * number |
michael@0 | 3146 | * if there are 2 range arguments, then check if pos is in between the two |
michael@0 | 3147 | * range arguments. |
michael@0 | 3148 | */ |
michael@0 | 3149 | function isInRange(pos, start, end) { |
michael@0 | 3150 | if (typeof pos != 'number') { |
michael@0 | 3151 | // Assume it is a cursor position. Get the line number. |
michael@0 | 3152 | pos = pos.line; |
michael@0 | 3153 | } |
michael@0 | 3154 | if (start instanceof Array) { |
michael@0 | 3155 | return inArray(pos, start); |
michael@0 | 3156 | } else { |
michael@0 | 3157 | if (end) { |
michael@0 | 3158 | return (pos >= start && pos <= end); |
michael@0 | 3159 | } else { |
michael@0 | 3160 | return pos == start; |
michael@0 | 3161 | } |
michael@0 | 3162 | } |
michael@0 | 3163 | } |
michael@0 | 3164 | function getUserVisibleLines(cm) { |
michael@0 | 3165 | var scrollInfo = cm.getScrollInfo(); |
michael@0 | 3166 | var occludeToleranceTop = 6; |
michael@0 | 3167 | var occludeToleranceBottom = 10; |
michael@0 | 3168 | var from = cm.coordsChar({left:0, top: occludeToleranceTop + scrollInfo.top}, 'local'); |
michael@0 | 3169 | var bottomY = scrollInfo.clientHeight - occludeToleranceBottom + scrollInfo.top; |
michael@0 | 3170 | var to = cm.coordsChar({left:0, top: bottomY}, 'local'); |
michael@0 | 3171 | return {top: from.line, bottom: to.line}; |
michael@0 | 3172 | } |
michael@0 | 3173 | |
michael@0 | 3174 | // Ex command handling |
michael@0 | 3175 | // Care must be taken when adding to the default Ex command map. For any |
michael@0 | 3176 | // pair of commands that have a shared prefix, at least one of their |
michael@0 | 3177 | // shortNames must not match the prefix of the other command. |
michael@0 | 3178 | var defaultExCommandMap = [ |
michael@0 | 3179 | { name: 'map' }, |
michael@0 | 3180 | { name: 'nmap', shortName: 'nm' }, |
michael@0 | 3181 | { name: 'vmap', shortName: 'vm' }, |
michael@0 | 3182 | { name: 'unmap' }, |
michael@0 | 3183 | { name: 'write', shortName: 'w' }, |
michael@0 | 3184 | { name: 'undo', shortName: 'u' }, |
michael@0 | 3185 | { name: 'redo', shortName: 'red' }, |
michael@0 | 3186 | { name: 'set', shortName: 'set' }, |
michael@0 | 3187 | { name: 'sort', shortName: 'sor' }, |
michael@0 | 3188 | { name: 'substitute', shortName: 's' }, |
michael@0 | 3189 | { name: 'nohlsearch', shortName: 'noh' }, |
michael@0 | 3190 | { name: 'delmarks', shortName: 'delm' }, |
michael@0 | 3191 | { name: 'registers', shortName: 'reg' } |
michael@0 | 3192 | ]; |
michael@0 | 3193 | Vim.ExCommandDispatcher = function() { |
michael@0 | 3194 | this.buildCommandMap_(); |
michael@0 | 3195 | }; |
michael@0 | 3196 | Vim.ExCommandDispatcher.prototype = { |
michael@0 | 3197 | processCommand: function(cm, input) { |
michael@0 | 3198 | var vim = cm.state.vim; |
michael@0 | 3199 | if (vim.visualMode) { |
michael@0 | 3200 | exitVisualMode(cm); |
michael@0 | 3201 | } |
michael@0 | 3202 | var inputStream = new CodeMirror.StringStream(input); |
michael@0 | 3203 | var params = {}; |
michael@0 | 3204 | params.input = input; |
michael@0 | 3205 | try { |
michael@0 | 3206 | this.parseInput_(cm, inputStream, params); |
michael@0 | 3207 | } catch(e) { |
michael@0 | 3208 | showConfirm(cm, e); |
michael@0 | 3209 | throw e; |
michael@0 | 3210 | } |
michael@0 | 3211 | var commandName; |
michael@0 | 3212 | if (!params.commandName) { |
michael@0 | 3213 | // If only a line range is defined, move to the line. |
michael@0 | 3214 | if (params.line !== undefined) { |
michael@0 | 3215 | commandName = 'move'; |
michael@0 | 3216 | } |
michael@0 | 3217 | } else { |
michael@0 | 3218 | var command = this.matchCommand_(params.commandName); |
michael@0 | 3219 | if (command) { |
michael@0 | 3220 | commandName = command.name; |
michael@0 | 3221 | this.parseCommandArgs_(inputStream, params, command); |
michael@0 | 3222 | if (command.type == 'exToKey') { |
michael@0 | 3223 | // Handle Ex to Key mapping. |
michael@0 | 3224 | for (var i = 0; i < command.toKeys.length; i++) { |
michael@0 | 3225 | CodeMirror.Vim.handleKey(cm, command.toKeys[i]); |
michael@0 | 3226 | } |
michael@0 | 3227 | return; |
michael@0 | 3228 | } else if (command.type == 'exToEx') { |
michael@0 | 3229 | // Handle Ex to Ex mapping. |
michael@0 | 3230 | this.processCommand(cm, command.toInput); |
michael@0 | 3231 | return; |
michael@0 | 3232 | } |
michael@0 | 3233 | } |
michael@0 | 3234 | } |
michael@0 | 3235 | if (!commandName) { |
michael@0 | 3236 | showConfirm(cm, 'Not an editor command ":' + input + '"'); |
michael@0 | 3237 | return; |
michael@0 | 3238 | } |
michael@0 | 3239 | try { |
michael@0 | 3240 | exCommands[commandName](cm, params); |
michael@0 | 3241 | } catch(e) { |
michael@0 | 3242 | showConfirm(cm, e); |
michael@0 | 3243 | throw e; |
michael@0 | 3244 | } |
michael@0 | 3245 | }, |
michael@0 | 3246 | parseInput_: function(cm, inputStream, result) { |
michael@0 | 3247 | inputStream.eatWhile(':'); |
michael@0 | 3248 | // Parse range. |
michael@0 | 3249 | if (inputStream.eat('%')) { |
michael@0 | 3250 | result.line = cm.firstLine(); |
michael@0 | 3251 | result.lineEnd = cm.lastLine(); |
michael@0 | 3252 | } else { |
michael@0 | 3253 | result.line = this.parseLineSpec_(cm, inputStream); |
michael@0 | 3254 | if (result.line !== undefined && inputStream.eat(',')) { |
michael@0 | 3255 | result.lineEnd = this.parseLineSpec_(cm, inputStream); |
michael@0 | 3256 | } |
michael@0 | 3257 | } |
michael@0 | 3258 | |
michael@0 | 3259 | // Parse command name. |
michael@0 | 3260 | var commandMatch = inputStream.match(/^(\w+)/); |
michael@0 | 3261 | if (commandMatch) { |
michael@0 | 3262 | result.commandName = commandMatch[1]; |
michael@0 | 3263 | } else { |
michael@0 | 3264 | result.commandName = inputStream.match(/.*/)[0]; |
michael@0 | 3265 | } |
michael@0 | 3266 | |
michael@0 | 3267 | return result; |
michael@0 | 3268 | }, |
michael@0 | 3269 | parseLineSpec_: function(cm, inputStream) { |
michael@0 | 3270 | var numberMatch = inputStream.match(/^(\d+)/); |
michael@0 | 3271 | if (numberMatch) { |
michael@0 | 3272 | return parseInt(numberMatch[1], 10) - 1; |
michael@0 | 3273 | } |
michael@0 | 3274 | switch (inputStream.next()) { |
michael@0 | 3275 | case '.': |
michael@0 | 3276 | return cm.getCursor().line; |
michael@0 | 3277 | case '$': |
michael@0 | 3278 | return cm.lastLine(); |
michael@0 | 3279 | case '\'': |
michael@0 | 3280 | var mark = cm.state.vim.marks[inputStream.next()]; |
michael@0 | 3281 | if (mark && mark.find()) { |
michael@0 | 3282 | return mark.find().line; |
michael@0 | 3283 | } |
michael@0 | 3284 | throw new Error('Mark not set'); |
michael@0 | 3285 | default: |
michael@0 | 3286 | inputStream.backUp(1); |
michael@0 | 3287 | return undefined; |
michael@0 | 3288 | } |
michael@0 | 3289 | }, |
michael@0 | 3290 | parseCommandArgs_: function(inputStream, params, command) { |
michael@0 | 3291 | if (inputStream.eol()) { |
michael@0 | 3292 | return; |
michael@0 | 3293 | } |
michael@0 | 3294 | params.argString = inputStream.match(/.*/)[0]; |
michael@0 | 3295 | // Parse command-line arguments |
michael@0 | 3296 | var delim = command.argDelimiter || /\s+/; |
michael@0 | 3297 | var args = trim(params.argString).split(delim); |
michael@0 | 3298 | if (args.length && args[0]) { |
michael@0 | 3299 | params.args = args; |
michael@0 | 3300 | } |
michael@0 | 3301 | }, |
michael@0 | 3302 | matchCommand_: function(commandName) { |
michael@0 | 3303 | // Return the command in the command map that matches the shortest |
michael@0 | 3304 | // prefix of the passed in command name. The match is guaranteed to be |
michael@0 | 3305 | // unambiguous if the defaultExCommandMap's shortNames are set up |
michael@0 | 3306 | // correctly. (see @code{defaultExCommandMap}). |
michael@0 | 3307 | for (var i = commandName.length; i > 0; i--) { |
michael@0 | 3308 | var prefix = commandName.substring(0, i); |
michael@0 | 3309 | if (this.commandMap_[prefix]) { |
michael@0 | 3310 | var command = this.commandMap_[prefix]; |
michael@0 | 3311 | if (command.name.indexOf(commandName) === 0) { |
michael@0 | 3312 | return command; |
michael@0 | 3313 | } |
michael@0 | 3314 | } |
michael@0 | 3315 | } |
michael@0 | 3316 | return null; |
michael@0 | 3317 | }, |
michael@0 | 3318 | buildCommandMap_: function() { |
michael@0 | 3319 | this.commandMap_ = {}; |
michael@0 | 3320 | for (var i = 0; i < defaultExCommandMap.length; i++) { |
michael@0 | 3321 | var command = defaultExCommandMap[i]; |
michael@0 | 3322 | var key = command.shortName || command.name; |
michael@0 | 3323 | this.commandMap_[key] = command; |
michael@0 | 3324 | } |
michael@0 | 3325 | }, |
michael@0 | 3326 | map: function(lhs, rhs, ctx) { |
michael@0 | 3327 | if (lhs != ':' && lhs.charAt(0) == ':') { |
michael@0 | 3328 | if (ctx) { throw Error('Mode not supported for ex mappings'); } |
michael@0 | 3329 | var commandName = lhs.substring(1); |
michael@0 | 3330 | if (rhs != ':' && rhs.charAt(0) == ':') { |
michael@0 | 3331 | // Ex to Ex mapping |
michael@0 | 3332 | this.commandMap_[commandName] = { |
michael@0 | 3333 | name: commandName, |
michael@0 | 3334 | type: 'exToEx', |
michael@0 | 3335 | toInput: rhs.substring(1), |
michael@0 | 3336 | user: true |
michael@0 | 3337 | }; |
michael@0 | 3338 | } else { |
michael@0 | 3339 | // Ex to key mapping |
michael@0 | 3340 | this.commandMap_[commandName] = { |
michael@0 | 3341 | name: commandName, |
michael@0 | 3342 | type: 'exToKey', |
michael@0 | 3343 | toKeys: parseKeyString(rhs), |
michael@0 | 3344 | user: true |
michael@0 | 3345 | }; |
michael@0 | 3346 | } |
michael@0 | 3347 | } else { |
michael@0 | 3348 | if (rhs != ':' && rhs.charAt(0) == ':') { |
michael@0 | 3349 | // Key to Ex mapping. |
michael@0 | 3350 | var mapping = { |
michael@0 | 3351 | keys: parseKeyString(lhs), |
michael@0 | 3352 | type: 'keyToEx', |
michael@0 | 3353 | exArgs: { input: rhs.substring(1) }, |
michael@0 | 3354 | user: true}; |
michael@0 | 3355 | if (ctx) { mapping.context = ctx; } |
michael@0 | 3356 | defaultKeymap.unshift(mapping); |
michael@0 | 3357 | } else { |
michael@0 | 3358 | // Key to key mapping |
michael@0 | 3359 | var mapping = { |
michael@0 | 3360 | keys: parseKeyString(lhs), |
michael@0 | 3361 | type: 'keyToKey', |
michael@0 | 3362 | toKeys: parseKeyString(rhs), |
michael@0 | 3363 | user: true |
michael@0 | 3364 | }; |
michael@0 | 3365 | if (ctx) { mapping.context = ctx; } |
michael@0 | 3366 | defaultKeymap.unshift(mapping); |
michael@0 | 3367 | } |
michael@0 | 3368 | } |
michael@0 | 3369 | }, |
michael@0 | 3370 | unmap: function(lhs, ctx) { |
michael@0 | 3371 | var arrayEquals = function(a, b) { |
michael@0 | 3372 | if (a === b) return true; |
michael@0 | 3373 | if (a == null || b == null) return true; |
michael@0 | 3374 | if (a.length != b.length) return false; |
michael@0 | 3375 | for (var i = 0; i < a.length; i++) { |
michael@0 | 3376 | if (a[i] !== b[i]) return false; |
michael@0 | 3377 | } |
michael@0 | 3378 | return true; |
michael@0 | 3379 | }; |
michael@0 | 3380 | if (lhs != ':' && lhs.charAt(0) == ':') { |
michael@0 | 3381 | // Ex to Ex or Ex to key mapping |
michael@0 | 3382 | if (ctx) { throw Error('Mode not supported for ex mappings'); } |
michael@0 | 3383 | var commandName = lhs.substring(1); |
michael@0 | 3384 | if (this.commandMap_[commandName] && this.commandMap_[commandName].user) { |
michael@0 | 3385 | delete this.commandMap_[commandName]; |
michael@0 | 3386 | return; |
michael@0 | 3387 | } |
michael@0 | 3388 | } else { |
michael@0 | 3389 | // Key to Ex or key to key mapping |
michael@0 | 3390 | var keys = parseKeyString(lhs); |
michael@0 | 3391 | for (var i = 0; i < defaultKeymap.length; i++) { |
michael@0 | 3392 | if (arrayEquals(keys, defaultKeymap[i].keys) |
michael@0 | 3393 | && defaultKeymap[i].context === ctx |
michael@0 | 3394 | && defaultKeymap[i].user) { |
michael@0 | 3395 | defaultKeymap.splice(i, 1); |
michael@0 | 3396 | return; |
michael@0 | 3397 | } |
michael@0 | 3398 | } |
michael@0 | 3399 | } |
michael@0 | 3400 | throw Error('No such mapping.'); |
michael@0 | 3401 | } |
michael@0 | 3402 | }; |
michael@0 | 3403 | |
michael@0 | 3404 | // Converts a key string sequence of the form a<C-w>bd<Left> into Vim's |
michael@0 | 3405 | // keymap representation. |
michael@0 | 3406 | function parseKeyString(str) { |
michael@0 | 3407 | var key, match; |
michael@0 | 3408 | var keys = []; |
michael@0 | 3409 | while (str) { |
michael@0 | 3410 | match = (/<\w+-.+?>|<\w+>|./).exec(str); |
michael@0 | 3411 | if (match === null)break; |
michael@0 | 3412 | key = match[0]; |
michael@0 | 3413 | str = str.substring(match.index + key.length); |
michael@0 | 3414 | keys.push(key); |
michael@0 | 3415 | } |
michael@0 | 3416 | return keys; |
michael@0 | 3417 | } |
michael@0 | 3418 | |
michael@0 | 3419 | var exCommands = { |
michael@0 | 3420 | map: function(cm, params, ctx) { |
michael@0 | 3421 | var mapArgs = params.args; |
michael@0 | 3422 | if (!mapArgs || mapArgs.length < 2) { |
michael@0 | 3423 | if (cm) { |
michael@0 | 3424 | showConfirm(cm, 'Invalid mapping: ' + params.input); |
michael@0 | 3425 | } |
michael@0 | 3426 | return; |
michael@0 | 3427 | } |
michael@0 | 3428 | exCommandDispatcher.map(mapArgs[0], mapArgs[1], ctx); |
michael@0 | 3429 | }, |
michael@0 | 3430 | nmap: function(cm, params) { this.map(cm, params, 'normal'); }, |
michael@0 | 3431 | vmap: function(cm, params) { this.map(cm, params, 'visual'); }, |
michael@0 | 3432 | unmap: function(cm, params, ctx) { |
michael@0 | 3433 | var mapArgs = params.args; |
michael@0 | 3434 | if (!mapArgs || mapArgs.length < 1) { |
michael@0 | 3435 | if (cm) { |
michael@0 | 3436 | showConfirm(cm, 'No such mapping: ' + params.input); |
michael@0 | 3437 | } |
michael@0 | 3438 | return; |
michael@0 | 3439 | } |
michael@0 | 3440 | exCommandDispatcher.unmap(mapArgs[0], ctx); |
michael@0 | 3441 | }, |
michael@0 | 3442 | move: function(cm, params) { |
michael@0 | 3443 | commandDispatcher.processCommand(cm, cm.state.vim, { |
michael@0 | 3444 | type: 'motion', |
michael@0 | 3445 | motion: 'moveToLineOrEdgeOfDocument', |
michael@0 | 3446 | motionArgs: { forward: false, explicitRepeat: true, |
michael@0 | 3447 | linewise: true }, |
michael@0 | 3448 | repeatOverride: params.line+1}); |
michael@0 | 3449 | }, |
michael@0 | 3450 | set: function(cm, params) { |
michael@0 | 3451 | var setArgs = params.args; |
michael@0 | 3452 | if (!setArgs || setArgs.length < 1) { |
michael@0 | 3453 | if (cm) { |
michael@0 | 3454 | showConfirm(cm, 'Invalid mapping: ' + params.input); |
michael@0 | 3455 | } |
michael@0 | 3456 | return; |
michael@0 | 3457 | } |
michael@0 | 3458 | var expr = setArgs[0].split('='); |
michael@0 | 3459 | var optionName = expr[0]; |
michael@0 | 3460 | var value = expr[1]; |
michael@0 | 3461 | var forceGet = false; |
michael@0 | 3462 | |
michael@0 | 3463 | if (optionName.charAt(optionName.length - 1) == '?') { |
michael@0 | 3464 | // If post-fixed with ?, then the set is actually a get. |
michael@0 | 3465 | if (value) { throw Error('Trailing characters: ' + params.argString); } |
michael@0 | 3466 | optionName = optionName.substring(0, optionName.length - 1); |
michael@0 | 3467 | forceGet = true; |
michael@0 | 3468 | } |
michael@0 | 3469 | if (value === undefined && optionName.substring(0, 2) == 'no') { |
michael@0 | 3470 | // To set boolean options to false, the option name is prefixed with |
michael@0 | 3471 | // 'no'. |
michael@0 | 3472 | optionName = optionName.substring(2); |
michael@0 | 3473 | value = false; |
michael@0 | 3474 | } |
michael@0 | 3475 | var optionIsBoolean = options[optionName] && options[optionName].type == 'boolean'; |
michael@0 | 3476 | if (optionIsBoolean && value == undefined) { |
michael@0 | 3477 | // Calling set with a boolean option sets it to true. |
michael@0 | 3478 | value = true; |
michael@0 | 3479 | } |
michael@0 | 3480 | if (!optionIsBoolean && !value || forceGet) { |
michael@0 | 3481 | var oldValue = getOption(optionName); |
michael@0 | 3482 | // If no value is provided, then we assume this is a get. |
michael@0 | 3483 | if (oldValue === true || oldValue === false) { |
michael@0 | 3484 | showConfirm(cm, ' ' + (oldValue ? '' : 'no') + optionName); |
michael@0 | 3485 | } else { |
michael@0 | 3486 | showConfirm(cm, ' ' + optionName + '=' + oldValue); |
michael@0 | 3487 | } |
michael@0 | 3488 | } else { |
michael@0 | 3489 | setOption(optionName, value); |
michael@0 | 3490 | } |
michael@0 | 3491 | }, |
michael@0 | 3492 | registers: function(cm,params) { |
michael@0 | 3493 | var regArgs = params.args; |
michael@0 | 3494 | var registers = vimGlobalState.registerController.registers; |
michael@0 | 3495 | var regInfo = '----------Registers----------<br><br>'; |
michael@0 | 3496 | if (!regArgs) { |
michael@0 | 3497 | for (var registerName in registers) { |
michael@0 | 3498 | var text = registers[registerName].toString(); |
michael@0 | 3499 | if (text.length) { |
michael@0 | 3500 | regInfo += '"' + registerName + ' ' + text + '<br>'; |
michael@0 | 3501 | } |
michael@0 | 3502 | } |
michael@0 | 3503 | } else { |
michael@0 | 3504 | var registerName; |
michael@0 | 3505 | regArgs = regArgs.join(''); |
michael@0 | 3506 | for (var i = 0; i < regArgs.length; i++) { |
michael@0 | 3507 | registerName = regArgs.charAt(i); |
michael@0 | 3508 | if (!vimGlobalState.registerController.isValidRegister(registerName)) { |
michael@0 | 3509 | continue; |
michael@0 | 3510 | } |
michael@0 | 3511 | var register = registers[registerName] || new Register(); |
michael@0 | 3512 | regInfo += '"' + registerName + ' ' + register.text + '<br>'; |
michael@0 | 3513 | } |
michael@0 | 3514 | } |
michael@0 | 3515 | showConfirm(cm, regInfo); |
michael@0 | 3516 | }, |
michael@0 | 3517 | sort: function(cm, params) { |
michael@0 | 3518 | var reverse, ignoreCase, unique, number; |
michael@0 | 3519 | function parseArgs() { |
michael@0 | 3520 | if (params.argString) { |
michael@0 | 3521 | var args = new CodeMirror.StringStream(params.argString); |
michael@0 | 3522 | if (args.eat('!')) { reverse = true; } |
michael@0 | 3523 | if (args.eol()) { return; } |
michael@0 | 3524 | if (!args.eatSpace()) { return 'Invalid arguments'; } |
michael@0 | 3525 | var opts = args.match(/[a-z]+/); |
michael@0 | 3526 | if (opts) { |
michael@0 | 3527 | opts = opts[0]; |
michael@0 | 3528 | ignoreCase = opts.indexOf('i') != -1; |
michael@0 | 3529 | unique = opts.indexOf('u') != -1; |
michael@0 | 3530 | var decimal = opts.indexOf('d') != -1 && 1; |
michael@0 | 3531 | var hex = opts.indexOf('x') != -1 && 1; |
michael@0 | 3532 | var octal = opts.indexOf('o') != -1 && 1; |
michael@0 | 3533 | if (decimal + hex + octal > 1) { return 'Invalid arguments'; } |
michael@0 | 3534 | number = decimal && 'decimal' || hex && 'hex' || octal && 'octal'; |
michael@0 | 3535 | } |
michael@0 | 3536 | if (args.eatSpace() && args.match(/\/.*\//)) { 'patterns not supported'; } |
michael@0 | 3537 | } |
michael@0 | 3538 | } |
michael@0 | 3539 | var err = parseArgs(); |
michael@0 | 3540 | if (err) { |
michael@0 | 3541 | showConfirm(cm, err + ': ' + params.argString); |
michael@0 | 3542 | return; |
michael@0 | 3543 | } |
michael@0 | 3544 | var lineStart = params.line || cm.firstLine(); |
michael@0 | 3545 | var lineEnd = params.lineEnd || params.line || cm.lastLine(); |
michael@0 | 3546 | if (lineStart == lineEnd) { return; } |
michael@0 | 3547 | var curStart = Pos(lineStart, 0); |
michael@0 | 3548 | var curEnd = Pos(lineEnd, lineLength(cm, lineEnd)); |
michael@0 | 3549 | var text = cm.getRange(curStart, curEnd).split('\n'); |
michael@0 | 3550 | var numberRegex = (number == 'decimal') ? /(-?)([\d]+)/ : |
michael@0 | 3551 | (number == 'hex') ? /(-?)(?:0x)?([0-9a-f]+)/i : |
michael@0 | 3552 | (number == 'octal') ? /([0-7]+)/ : null; |
michael@0 | 3553 | var radix = (number == 'decimal') ? 10 : (number == 'hex') ? 16 : (number == 'octal') ? 8 : null; |
michael@0 | 3554 | var numPart = [], textPart = []; |
michael@0 | 3555 | if (number) { |
michael@0 | 3556 | for (var i = 0; i < text.length; i++) { |
michael@0 | 3557 | if (numberRegex.exec(text[i])) { |
michael@0 | 3558 | numPart.push(text[i]); |
michael@0 | 3559 | } else { |
michael@0 | 3560 | textPart.push(text[i]); |
michael@0 | 3561 | } |
michael@0 | 3562 | } |
michael@0 | 3563 | } else { |
michael@0 | 3564 | textPart = text; |
michael@0 | 3565 | } |
michael@0 | 3566 | function compareFn(a, b) { |
michael@0 | 3567 | if (reverse) { var tmp; tmp = a; a = b; b = tmp; } |
michael@0 | 3568 | if (ignoreCase) { a = a.toLowerCase(); b = b.toLowerCase(); } |
michael@0 | 3569 | var anum = number && numberRegex.exec(a); |
michael@0 | 3570 | var bnum = number && numberRegex.exec(b); |
michael@0 | 3571 | if (!anum) { return a < b ? -1 : 1; } |
michael@0 | 3572 | anum = parseInt((anum[1] + anum[2]).toLowerCase(), radix); |
michael@0 | 3573 | bnum = parseInt((bnum[1] + bnum[2]).toLowerCase(), radix); |
michael@0 | 3574 | return anum - bnum; |
michael@0 | 3575 | } |
michael@0 | 3576 | numPart.sort(compareFn); |
michael@0 | 3577 | textPart.sort(compareFn); |
michael@0 | 3578 | text = (!reverse) ? textPart.concat(numPart) : numPart.concat(textPart); |
michael@0 | 3579 | if (unique) { // Remove duplicate lines |
michael@0 | 3580 | var textOld = text; |
michael@0 | 3581 | var lastLine; |
michael@0 | 3582 | text = []; |
michael@0 | 3583 | for (var i = 0; i < textOld.length; i++) { |
michael@0 | 3584 | if (textOld[i] != lastLine) { |
michael@0 | 3585 | text.push(textOld[i]); |
michael@0 | 3586 | } |
michael@0 | 3587 | lastLine = textOld[i]; |
michael@0 | 3588 | } |
michael@0 | 3589 | } |
michael@0 | 3590 | cm.replaceRange(text.join('\n'), curStart, curEnd); |
michael@0 | 3591 | }, |
michael@0 | 3592 | substitute: function(cm, params) { |
michael@0 | 3593 | if (!cm.getSearchCursor) { |
michael@0 | 3594 | throw new Error('Search feature not available. Requires searchcursor.js or ' + |
michael@0 | 3595 | 'any other getSearchCursor implementation.'); |
michael@0 | 3596 | } |
michael@0 | 3597 | var argString = params.argString; |
michael@0 | 3598 | var slashes = findUnescapedSlashes(argString); |
michael@0 | 3599 | if (slashes[0] !== 0) { |
michael@0 | 3600 | showConfirm(cm, 'Substitutions should be of the form ' + |
michael@0 | 3601 | ':s/pattern/replace/'); |
michael@0 | 3602 | return; |
michael@0 | 3603 | } |
michael@0 | 3604 | var regexPart = argString.substring(slashes[0] + 1, slashes[1]); |
michael@0 | 3605 | var replacePart = ''; |
michael@0 | 3606 | var flagsPart; |
michael@0 | 3607 | var count; |
michael@0 | 3608 | var confirm = false; // Whether to confirm each replace. |
michael@0 | 3609 | if (slashes[1]) { |
michael@0 | 3610 | replacePart = argString.substring(slashes[1] + 1, slashes[2]); |
michael@0 | 3611 | if (getOption('pcre')) { |
michael@0 | 3612 | replacePart = unescapeRegexReplace(replacePart); |
michael@0 | 3613 | } else { |
michael@0 | 3614 | replacePart = translateRegexReplace(replacePart); |
michael@0 | 3615 | } |
michael@0 | 3616 | } |
michael@0 | 3617 | if (slashes[2]) { |
michael@0 | 3618 | // After the 3rd slash, we can have flags followed by a space followed |
michael@0 | 3619 | // by count. |
michael@0 | 3620 | var trailing = argString.substring(slashes[2] + 1).split(' '); |
michael@0 | 3621 | flagsPart = trailing[0]; |
michael@0 | 3622 | count = parseInt(trailing[1]); |
michael@0 | 3623 | } |
michael@0 | 3624 | if (flagsPart) { |
michael@0 | 3625 | if (flagsPart.indexOf('c') != -1) { |
michael@0 | 3626 | confirm = true; |
michael@0 | 3627 | flagsPart.replace('c', ''); |
michael@0 | 3628 | } |
michael@0 | 3629 | regexPart = regexPart + '/' + flagsPart; |
michael@0 | 3630 | } |
michael@0 | 3631 | if (regexPart) { |
michael@0 | 3632 | // If regex part is empty, then use the previous query. Otherwise use |
michael@0 | 3633 | // the regex part as the new query. |
michael@0 | 3634 | try { |
michael@0 | 3635 | updateSearchQuery(cm, regexPart, true /** ignoreCase */, |
michael@0 | 3636 | true /** smartCase */); |
michael@0 | 3637 | } catch (e) { |
michael@0 | 3638 | showConfirm(cm, 'Invalid regex: ' + regexPart); |
michael@0 | 3639 | return; |
michael@0 | 3640 | } |
michael@0 | 3641 | } |
michael@0 | 3642 | var state = getSearchState(cm); |
michael@0 | 3643 | var query = state.getQuery(); |
michael@0 | 3644 | var lineStart = (params.line !== undefined) ? params.line : cm.getCursor().line; |
michael@0 | 3645 | var lineEnd = params.lineEnd || lineStart; |
michael@0 | 3646 | if (count) { |
michael@0 | 3647 | lineStart = lineEnd; |
michael@0 | 3648 | lineEnd = lineStart + count - 1; |
michael@0 | 3649 | } |
michael@0 | 3650 | var startPos = clipCursorToContent(cm, Pos(lineStart, 0)); |
michael@0 | 3651 | var cursor = cm.getSearchCursor(query, startPos); |
michael@0 | 3652 | doReplace(cm, confirm, lineStart, lineEnd, cursor, query, replacePart); |
michael@0 | 3653 | }, |
michael@0 | 3654 | redo: CodeMirror.commands.redo, |
michael@0 | 3655 | undo: CodeMirror.commands.undo, |
michael@0 | 3656 | write: function(cm) { |
michael@0 | 3657 | if (CodeMirror.commands.save) { |
michael@0 | 3658 | // If a save command is defined, call it. |
michael@0 | 3659 | CodeMirror.commands.save(cm); |
michael@0 | 3660 | } else { |
michael@0 | 3661 | // Saves to text area if no save command is defined. |
michael@0 | 3662 | cm.save(); |
michael@0 | 3663 | } |
michael@0 | 3664 | }, |
michael@0 | 3665 | nohlsearch: function(cm) { |
michael@0 | 3666 | clearSearchHighlight(cm); |
michael@0 | 3667 | }, |
michael@0 | 3668 | delmarks: function(cm, params) { |
michael@0 | 3669 | if (!params.argString || !trim(params.argString)) { |
michael@0 | 3670 | showConfirm(cm, 'Argument required'); |
michael@0 | 3671 | return; |
michael@0 | 3672 | } |
michael@0 | 3673 | |
michael@0 | 3674 | var state = cm.state.vim; |
michael@0 | 3675 | var stream = new CodeMirror.StringStream(trim(params.argString)); |
michael@0 | 3676 | while (!stream.eol()) { |
michael@0 | 3677 | stream.eatSpace(); |
michael@0 | 3678 | |
michael@0 | 3679 | // Record the streams position at the beginning of the loop for use |
michael@0 | 3680 | // in error messages. |
michael@0 | 3681 | var count = stream.pos; |
michael@0 | 3682 | |
michael@0 | 3683 | if (!stream.match(/[a-zA-Z]/, false)) { |
michael@0 | 3684 | showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); |
michael@0 | 3685 | return; |
michael@0 | 3686 | } |
michael@0 | 3687 | |
michael@0 | 3688 | var sym = stream.next(); |
michael@0 | 3689 | // Check if this symbol is part of a range |
michael@0 | 3690 | if (stream.match('-', true)) { |
michael@0 | 3691 | // This symbol is part of a range. |
michael@0 | 3692 | |
michael@0 | 3693 | // The range must terminate at an alphabetic character. |
michael@0 | 3694 | if (!stream.match(/[a-zA-Z]/, false)) { |
michael@0 | 3695 | showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); |
michael@0 | 3696 | return; |
michael@0 | 3697 | } |
michael@0 | 3698 | |
michael@0 | 3699 | var startMark = sym; |
michael@0 | 3700 | var finishMark = stream.next(); |
michael@0 | 3701 | // The range must terminate at an alphabetic character which |
michael@0 | 3702 | // shares the same case as the start of the range. |
michael@0 | 3703 | if (isLowerCase(startMark) && isLowerCase(finishMark) || |
michael@0 | 3704 | isUpperCase(startMark) && isUpperCase(finishMark)) { |
michael@0 | 3705 | var start = startMark.charCodeAt(0); |
michael@0 | 3706 | var finish = finishMark.charCodeAt(0); |
michael@0 | 3707 | if (start >= finish) { |
michael@0 | 3708 | showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); |
michael@0 | 3709 | return; |
michael@0 | 3710 | } |
michael@0 | 3711 | |
michael@0 | 3712 | // Because marks are always ASCII values, and we have |
michael@0 | 3713 | // determined that they are the same case, we can use |
michael@0 | 3714 | // their char codes to iterate through the defined range. |
michael@0 | 3715 | for (var j = 0; j <= finish - start; j++) { |
michael@0 | 3716 | var mark = String.fromCharCode(start + j); |
michael@0 | 3717 | delete state.marks[mark]; |
michael@0 | 3718 | } |
michael@0 | 3719 | } else { |
michael@0 | 3720 | showConfirm(cm, 'Invalid argument: ' + startMark + '-'); |
michael@0 | 3721 | return; |
michael@0 | 3722 | } |
michael@0 | 3723 | } else { |
michael@0 | 3724 | // This symbol is a valid mark, and is not part of a range. |
michael@0 | 3725 | delete state.marks[sym]; |
michael@0 | 3726 | } |
michael@0 | 3727 | } |
michael@0 | 3728 | } |
michael@0 | 3729 | }; |
michael@0 | 3730 | |
michael@0 | 3731 | var exCommandDispatcher = new Vim.ExCommandDispatcher(); |
michael@0 | 3732 | |
michael@0 | 3733 | /** |
michael@0 | 3734 | * @param {CodeMirror} cm CodeMirror instance we are in. |
michael@0 | 3735 | * @param {boolean} confirm Whether to confirm each replace. |
michael@0 | 3736 | * @param {Cursor} lineStart Line to start replacing from. |
michael@0 | 3737 | * @param {Cursor} lineEnd Line to stop replacing at. |
michael@0 | 3738 | * @param {RegExp} query Query for performing matches with. |
michael@0 | 3739 | * @param {string} replaceWith Text to replace matches with. May contain $1, |
michael@0 | 3740 | * $2, etc for replacing captured groups using Javascript replace. |
michael@0 | 3741 | */ |
michael@0 | 3742 | function doReplace(cm, confirm, lineStart, lineEnd, searchCursor, query, |
michael@0 | 3743 | replaceWith) { |
michael@0 | 3744 | // Set up all the functions. |
michael@0 | 3745 | cm.state.vim.exMode = true; |
michael@0 | 3746 | var done = false; |
michael@0 | 3747 | var lastPos = searchCursor.from(); |
michael@0 | 3748 | function replaceAll() { |
michael@0 | 3749 | cm.operation(function() { |
michael@0 | 3750 | while (!done) { |
michael@0 | 3751 | replace(); |
michael@0 | 3752 | next(); |
michael@0 | 3753 | } |
michael@0 | 3754 | stop(); |
michael@0 | 3755 | }); |
michael@0 | 3756 | } |
michael@0 | 3757 | function replace() { |
michael@0 | 3758 | var text = cm.getRange(searchCursor.from(), searchCursor.to()); |
michael@0 | 3759 | var newText = text.replace(query, replaceWith); |
michael@0 | 3760 | searchCursor.replace(newText); |
michael@0 | 3761 | } |
michael@0 | 3762 | function next() { |
michael@0 | 3763 | var found = searchCursor.findNext(); |
michael@0 | 3764 | if (!found) { |
michael@0 | 3765 | done = true; |
michael@0 | 3766 | } else if (isInRange(searchCursor.from(), lineStart, lineEnd)) { |
michael@0 | 3767 | cm.scrollIntoView(searchCursor.from(), 30); |
michael@0 | 3768 | cm.setSelection(searchCursor.from(), searchCursor.to()); |
michael@0 | 3769 | lastPos = searchCursor.from(); |
michael@0 | 3770 | done = false; |
michael@0 | 3771 | } else { |
michael@0 | 3772 | done = true; |
michael@0 | 3773 | } |
michael@0 | 3774 | } |
michael@0 | 3775 | function stop(close) { |
michael@0 | 3776 | if (close) { close(); } |
michael@0 | 3777 | cm.focus(); |
michael@0 | 3778 | if (lastPos) { |
michael@0 | 3779 | cm.setCursor(lastPos); |
michael@0 | 3780 | var vim = cm.state.vim; |
michael@0 | 3781 | vim.exMode = false; |
michael@0 | 3782 | vim.lastHPos = vim.lastHSPos = lastPos.ch; |
michael@0 | 3783 | } |
michael@0 | 3784 | } |
michael@0 | 3785 | function onPromptKeyDown(e, _value, close) { |
michael@0 | 3786 | // Swallow all keys. |
michael@0 | 3787 | CodeMirror.e_stop(e); |
michael@0 | 3788 | var keyName = CodeMirror.keyName(e); |
michael@0 | 3789 | switch (keyName) { |
michael@0 | 3790 | case 'Y': |
michael@0 | 3791 | replace(); next(); break; |
michael@0 | 3792 | case 'N': |
michael@0 | 3793 | next(); break; |
michael@0 | 3794 | case 'A': |
michael@0 | 3795 | cm.operation(replaceAll); break; |
michael@0 | 3796 | case 'L': |
michael@0 | 3797 | replace(); |
michael@0 | 3798 | // fall through and exit. |
michael@0 | 3799 | case 'Q': |
michael@0 | 3800 | case 'Esc': |
michael@0 | 3801 | case 'Ctrl-C': |
michael@0 | 3802 | case 'Ctrl-[': |
michael@0 | 3803 | stop(close); |
michael@0 | 3804 | break; |
michael@0 | 3805 | } |
michael@0 | 3806 | if (done) { stop(close); } |
michael@0 | 3807 | } |
michael@0 | 3808 | |
michael@0 | 3809 | // Actually do replace. |
michael@0 | 3810 | next(); |
michael@0 | 3811 | if (done) { |
michael@0 | 3812 | showConfirm(cm, 'No matches for ' + query.source); |
michael@0 | 3813 | return; |
michael@0 | 3814 | } |
michael@0 | 3815 | if (!confirm) { |
michael@0 | 3816 | replaceAll(); |
michael@0 | 3817 | return; |
michael@0 | 3818 | } |
michael@0 | 3819 | showPrompt(cm, { |
michael@0 | 3820 | prefix: 'replace with <strong>' + replaceWith + '</strong> (y/n/a/q/l)', |
michael@0 | 3821 | onKeyDown: onPromptKeyDown |
michael@0 | 3822 | }); |
michael@0 | 3823 | } |
michael@0 | 3824 | |
michael@0 | 3825 | // Register Vim with CodeMirror |
michael@0 | 3826 | function buildVimKeyMap() { |
michael@0 | 3827 | /** |
michael@0 | 3828 | * Handle the raw key event from CodeMirror. Translate the |
michael@0 | 3829 | * Shift + key modifier to the resulting letter, while preserving other |
michael@0 | 3830 | * modifers. |
michael@0 | 3831 | */ |
michael@0 | 3832 | function cmKeyToVimKey(key, modifier) { |
michael@0 | 3833 | var vimKey = key; |
michael@0 | 3834 | if (isUpperCase(vimKey) && modifier == 'Ctrl') { |
michael@0 | 3835 | vimKey = vimKey.toLowerCase(); |
michael@0 | 3836 | } |
michael@0 | 3837 | if (modifier) { |
michael@0 | 3838 | // Vim will parse modifier+key combination as a single key. |
michael@0 | 3839 | vimKey = modifier.charAt(0) + '-' + vimKey; |
michael@0 | 3840 | } |
michael@0 | 3841 | var specialKey = ({Enter:'CR',Backspace:'BS',Delete:'Del'})[vimKey]; |
michael@0 | 3842 | vimKey = specialKey ? specialKey : vimKey; |
michael@0 | 3843 | vimKey = vimKey.length > 1 ? '<'+ vimKey + '>' : vimKey; |
michael@0 | 3844 | return vimKey; |
michael@0 | 3845 | } |
michael@0 | 3846 | |
michael@0 | 3847 | // Closure to bind CodeMirror, key, modifier. |
michael@0 | 3848 | function keyMapper(vimKey) { |
michael@0 | 3849 | return function(cm) { |
michael@0 | 3850 | CodeMirror.Vim.handleKey(cm, vimKey); |
michael@0 | 3851 | }; |
michael@0 | 3852 | } |
michael@0 | 3853 | |
michael@0 | 3854 | var cmToVimKeymap = { |
michael@0 | 3855 | 'nofallthrough': true, |
michael@0 | 3856 | 'style': 'fat-cursor' |
michael@0 | 3857 | }; |
michael@0 | 3858 | function bindKeys(keys, modifier) { |
michael@0 | 3859 | for (var i = 0; i < keys.length; i++) { |
michael@0 | 3860 | var key = keys[i]; |
michael@0 | 3861 | if (!modifier && key.length == 1) { |
michael@0 | 3862 | // Wrap all keys without modifiers with '' to identify them by their |
michael@0 | 3863 | // key characters instead of key identifiers. |
michael@0 | 3864 | key = "'" + key + "'"; |
michael@0 | 3865 | } |
michael@0 | 3866 | var vimKey = cmKeyToVimKey(keys[i], modifier); |
michael@0 | 3867 | var cmKey = modifier ? modifier + '-' + key : key; |
michael@0 | 3868 | cmToVimKeymap[cmKey] = keyMapper(vimKey); |
michael@0 | 3869 | } |
michael@0 | 3870 | } |
michael@0 | 3871 | bindKeys(upperCaseAlphabet); |
michael@0 | 3872 | bindKeys(lowerCaseAlphabet); |
michael@0 | 3873 | bindKeys(upperCaseAlphabet, 'Ctrl'); |
michael@0 | 3874 | bindKeys(specialSymbols); |
michael@0 | 3875 | bindKeys(specialSymbols, 'Ctrl'); |
michael@0 | 3876 | bindKeys(numbers); |
michael@0 | 3877 | bindKeys(numbers, 'Ctrl'); |
michael@0 | 3878 | bindKeys(specialKeys); |
michael@0 | 3879 | bindKeys(specialKeys, 'Ctrl'); |
michael@0 | 3880 | return cmToVimKeymap; |
michael@0 | 3881 | } |
michael@0 | 3882 | CodeMirror.keyMap.vim = buildVimKeyMap(); |
michael@0 | 3883 | |
michael@0 | 3884 | function exitInsertMode(cm) { |
michael@0 | 3885 | var vim = cm.state.vim; |
michael@0 | 3886 | var macroModeState = vimGlobalState.macroModeState; |
michael@0 | 3887 | var isPlaying = macroModeState.isPlaying; |
michael@0 | 3888 | if (!isPlaying) { |
michael@0 | 3889 | cm.off('change', onChange); |
michael@0 | 3890 | cm.off('cursorActivity', onCursorActivity); |
michael@0 | 3891 | CodeMirror.off(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown); |
michael@0 | 3892 | } |
michael@0 | 3893 | if (!isPlaying && vim.insertModeRepeat > 1) { |
michael@0 | 3894 | // Perform insert mode repeat for commands like 3,a and 3,o. |
michael@0 | 3895 | repeatLastEdit(cm, vim, vim.insertModeRepeat - 1, |
michael@0 | 3896 | true /** repeatForInsert */); |
michael@0 | 3897 | vim.lastEditInputState.repeatOverride = vim.insertModeRepeat; |
michael@0 | 3898 | } |
michael@0 | 3899 | delete vim.insertModeRepeat; |
michael@0 | 3900 | cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1); |
michael@0 | 3901 | vim.insertMode = false; |
michael@0 | 3902 | cm.setOption('keyMap', 'vim'); |
michael@0 | 3903 | cm.setOption('disableInput', true); |
michael@0 | 3904 | cm.toggleOverwrite(false); // exit replace mode if we were in it. |
michael@0 | 3905 | CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); |
michael@0 | 3906 | if (macroModeState.isRecording) { |
michael@0 | 3907 | logInsertModeChange(macroModeState); |
michael@0 | 3908 | } |
michael@0 | 3909 | } |
michael@0 | 3910 | |
michael@0 | 3911 | CodeMirror.keyMap['vim-insert'] = { |
michael@0 | 3912 | // TODO: override navigation keys so that Esc will cancel automatic |
michael@0 | 3913 | // indentation from o, O, i_<CR> |
michael@0 | 3914 | 'Esc': exitInsertMode, |
michael@0 | 3915 | 'Ctrl-[': exitInsertMode, |
michael@0 | 3916 | 'Ctrl-C': exitInsertMode, |
michael@0 | 3917 | 'Ctrl-N': 'autocomplete', |
michael@0 | 3918 | 'Ctrl-P': 'autocomplete', |
michael@0 | 3919 | 'Enter': function(cm) { |
michael@0 | 3920 | var fn = CodeMirror.commands.newlineAndIndentContinueComment || |
michael@0 | 3921 | CodeMirror.commands.newlineAndIndent; |
michael@0 | 3922 | fn(cm); |
michael@0 | 3923 | }, |
michael@0 | 3924 | fallthrough: ['default'] |
michael@0 | 3925 | }; |
michael@0 | 3926 | |
michael@0 | 3927 | CodeMirror.keyMap['vim-replace'] = { |
michael@0 | 3928 | 'Backspace': 'goCharLeft', |
michael@0 | 3929 | fallthrough: ['vim-insert'] |
michael@0 | 3930 | }; |
michael@0 | 3931 | |
michael@0 | 3932 | function executeMacroRegister(cm, vim, macroModeState, registerName) { |
michael@0 | 3933 | var register = vimGlobalState.registerController.getRegister(registerName); |
michael@0 | 3934 | var keyBuffer = register.keyBuffer; |
michael@0 | 3935 | var imc = 0; |
michael@0 | 3936 | macroModeState.isPlaying = true; |
michael@0 | 3937 | for (var i = 0; i < keyBuffer.length; i++) { |
michael@0 | 3938 | var text = keyBuffer[i]; |
michael@0 | 3939 | var match, key; |
michael@0 | 3940 | while (text) { |
michael@0 | 3941 | // Pull off one command key, which is either a single character |
michael@0 | 3942 | // or a special sequence wrapped in '<' and '>', e.g. '<Space>'. |
michael@0 | 3943 | match = (/<\w+-.+?>|<\w+>|./).exec(text); |
michael@0 | 3944 | key = match[0]; |
michael@0 | 3945 | text = text.substring(match.index + key.length); |
michael@0 | 3946 | CodeMirror.Vim.handleKey(cm, key); |
michael@0 | 3947 | if (vim.insertMode) { |
michael@0 | 3948 | repeatInsertModeChanges( |
michael@0 | 3949 | cm, register.insertModeChanges[imc++].changes, 1); |
michael@0 | 3950 | exitInsertMode(cm); |
michael@0 | 3951 | } |
michael@0 | 3952 | } |
michael@0 | 3953 | }; |
michael@0 | 3954 | macroModeState.isPlaying = false; |
michael@0 | 3955 | } |
michael@0 | 3956 | |
michael@0 | 3957 | function logKey(macroModeState, key) { |
michael@0 | 3958 | if (macroModeState.isPlaying) { return; } |
michael@0 | 3959 | var registerName = macroModeState.latestRegister; |
michael@0 | 3960 | var register = vimGlobalState.registerController.getRegister(registerName); |
michael@0 | 3961 | if (register) { |
michael@0 | 3962 | register.pushText(key); |
michael@0 | 3963 | } |
michael@0 | 3964 | } |
michael@0 | 3965 | |
michael@0 | 3966 | function logInsertModeChange(macroModeState) { |
michael@0 | 3967 | if (macroModeState.isPlaying) { return; } |
michael@0 | 3968 | var registerName = macroModeState.latestRegister; |
michael@0 | 3969 | var register = vimGlobalState.registerController.getRegister(registerName); |
michael@0 | 3970 | if (register) { |
michael@0 | 3971 | register.pushInsertModeChanges(macroModeState.lastInsertModeChanges); |
michael@0 | 3972 | } |
michael@0 | 3973 | } |
michael@0 | 3974 | |
michael@0 | 3975 | /** |
michael@0 | 3976 | * Listens for changes made in insert mode. |
michael@0 | 3977 | * Should only be active in insert mode. |
michael@0 | 3978 | */ |
michael@0 | 3979 | function onChange(_cm, changeObj) { |
michael@0 | 3980 | var macroModeState = vimGlobalState.macroModeState; |
michael@0 | 3981 | var lastChange = macroModeState.lastInsertModeChanges; |
michael@0 | 3982 | if (!macroModeState.isPlaying) { |
michael@0 | 3983 | while(changeObj) { |
michael@0 | 3984 | lastChange.expectCursorActivityForChange = true; |
michael@0 | 3985 | if (changeObj.origin == '+input' || changeObj.origin == 'paste' |
michael@0 | 3986 | || changeObj.origin === undefined /* only in testing */) { |
michael@0 | 3987 | var text = changeObj.text.join('\n'); |
michael@0 | 3988 | lastChange.changes.push(text); |
michael@0 | 3989 | } |
michael@0 | 3990 | // Change objects may be chained with next. |
michael@0 | 3991 | changeObj = changeObj.next; |
michael@0 | 3992 | } |
michael@0 | 3993 | } |
michael@0 | 3994 | } |
michael@0 | 3995 | |
michael@0 | 3996 | /** |
michael@0 | 3997 | * Listens for any kind of cursor activity on CodeMirror. |
michael@0 | 3998 | * - For tracking cursor activity in insert mode. |
michael@0 | 3999 | * - Should only be active in insert mode. |
michael@0 | 4000 | */ |
michael@0 | 4001 | function onCursorActivity() { |
michael@0 | 4002 | var macroModeState = vimGlobalState.macroModeState; |
michael@0 | 4003 | if (macroModeState.isPlaying) { return; } |
michael@0 | 4004 | var lastChange = macroModeState.lastInsertModeChanges; |
michael@0 | 4005 | if (lastChange.expectCursorActivityForChange) { |
michael@0 | 4006 | lastChange.expectCursorActivityForChange = false; |
michael@0 | 4007 | } else { |
michael@0 | 4008 | // Cursor moved outside the context of an edit. Reset the change. |
michael@0 | 4009 | lastChange.changes = []; |
michael@0 | 4010 | } |
michael@0 | 4011 | } |
michael@0 | 4012 | |
michael@0 | 4013 | /** Wrapper for special keys pressed in insert mode */ |
michael@0 | 4014 | function InsertModeKey(keyName) { |
michael@0 | 4015 | this.keyName = keyName; |
michael@0 | 4016 | } |
michael@0 | 4017 | |
michael@0 | 4018 | /** |
michael@0 | 4019 | * Handles raw key down events from the text area. |
michael@0 | 4020 | * - Should only be active in insert mode. |
michael@0 | 4021 | * - For recording deletes in insert mode. |
michael@0 | 4022 | */ |
michael@0 | 4023 | function onKeyEventTargetKeyDown(e) { |
michael@0 | 4024 | var macroModeState = vimGlobalState.macroModeState; |
michael@0 | 4025 | var lastChange = macroModeState.lastInsertModeChanges; |
michael@0 | 4026 | var keyName = CodeMirror.keyName(e); |
michael@0 | 4027 | function onKeyFound() { |
michael@0 | 4028 | lastChange.changes.push(new InsertModeKey(keyName)); |
michael@0 | 4029 | return true; |
michael@0 | 4030 | } |
michael@0 | 4031 | if (keyName.indexOf('Delete') != -1 || keyName.indexOf('Backspace') != -1) { |
michael@0 | 4032 | CodeMirror.lookupKey(keyName, ['vim-insert'], onKeyFound); |
michael@0 | 4033 | } |
michael@0 | 4034 | } |
michael@0 | 4035 | |
michael@0 | 4036 | /** |
michael@0 | 4037 | * Repeats the last edit, which includes exactly 1 command and at most 1 |
michael@0 | 4038 | * insert. Operator and motion commands are read from lastEditInputState, |
michael@0 | 4039 | * while action commands are read from lastEditActionCommand. |
michael@0 | 4040 | * |
michael@0 | 4041 | * If repeatForInsert is true, then the function was called by |
michael@0 | 4042 | * exitInsertMode to repeat the insert mode changes the user just made. The |
michael@0 | 4043 | * corresponding enterInsertMode call was made with a count. |
michael@0 | 4044 | */ |
michael@0 | 4045 | function repeatLastEdit(cm, vim, repeat, repeatForInsert) { |
michael@0 | 4046 | var macroModeState = vimGlobalState.macroModeState; |
michael@0 | 4047 | macroModeState.isPlaying = true; |
michael@0 | 4048 | var isAction = !!vim.lastEditActionCommand; |
michael@0 | 4049 | var cachedInputState = vim.inputState; |
michael@0 | 4050 | function repeatCommand() { |
michael@0 | 4051 | if (isAction) { |
michael@0 | 4052 | commandDispatcher.processAction(cm, vim, vim.lastEditActionCommand); |
michael@0 | 4053 | } else { |
michael@0 | 4054 | commandDispatcher.evalInput(cm, vim); |
michael@0 | 4055 | } |
michael@0 | 4056 | } |
michael@0 | 4057 | function repeatInsert(repeat) { |
michael@0 | 4058 | if (macroModeState.lastInsertModeChanges.changes.length > 0) { |
michael@0 | 4059 | // For some reason, repeat cw in desktop VIM does not repeat |
michael@0 | 4060 | // insert mode changes. Will conform to that behavior. |
michael@0 | 4061 | repeat = !vim.lastEditActionCommand ? 1 : repeat; |
michael@0 | 4062 | var changeObject = macroModeState.lastInsertModeChanges; |
michael@0 | 4063 | // This isn't strictly necessary, but since lastInsertModeChanges is |
michael@0 | 4064 | // supposed to be immutable during replay, this helps catch bugs. |
michael@0 | 4065 | macroModeState.lastInsertModeChanges = {}; |
michael@0 | 4066 | repeatInsertModeChanges(cm, changeObject.changes, repeat); |
michael@0 | 4067 | macroModeState.lastInsertModeChanges = changeObject; |
michael@0 | 4068 | } |
michael@0 | 4069 | } |
michael@0 | 4070 | vim.inputState = vim.lastEditInputState; |
michael@0 | 4071 | if (isAction && vim.lastEditActionCommand.interlaceInsertRepeat) { |
michael@0 | 4072 | // o and O repeat have to be interlaced with insert repeats so that the |
michael@0 | 4073 | // insertions appear on separate lines instead of the last line. |
michael@0 | 4074 | for (var i = 0; i < repeat; i++) { |
michael@0 | 4075 | repeatCommand(); |
michael@0 | 4076 | repeatInsert(1); |
michael@0 | 4077 | } |
michael@0 | 4078 | } else { |
michael@0 | 4079 | if (!repeatForInsert) { |
michael@0 | 4080 | // Hack to get the cursor to end up at the right place. If I is |
michael@0 | 4081 | // repeated in insert mode repeat, cursor will be 1 insert |
michael@0 | 4082 | // change set left of where it should be. |
michael@0 | 4083 | repeatCommand(); |
michael@0 | 4084 | } |
michael@0 | 4085 | repeatInsert(repeat); |
michael@0 | 4086 | } |
michael@0 | 4087 | vim.inputState = cachedInputState; |
michael@0 | 4088 | if (vim.insertMode && !repeatForInsert) { |
michael@0 | 4089 | // Don't exit insert mode twice. If repeatForInsert is set, then we |
michael@0 | 4090 | // were called by an exitInsertMode call lower on the stack. |
michael@0 | 4091 | exitInsertMode(cm); |
michael@0 | 4092 | } |
michael@0 | 4093 | macroModeState.isPlaying = false; |
michael@0 | 4094 | }; |
michael@0 | 4095 | |
michael@0 | 4096 | function repeatInsertModeChanges(cm, changes, repeat) { |
michael@0 | 4097 | function keyHandler(binding) { |
michael@0 | 4098 | if (typeof binding == 'string') { |
michael@0 | 4099 | CodeMirror.commands[binding](cm); |
michael@0 | 4100 | } else { |
michael@0 | 4101 | binding(cm); |
michael@0 | 4102 | } |
michael@0 | 4103 | return true; |
michael@0 | 4104 | } |
michael@0 | 4105 | for (var i = 0; i < repeat; i++) { |
michael@0 | 4106 | for (var j = 0; j < changes.length; j++) { |
michael@0 | 4107 | var change = changes[j]; |
michael@0 | 4108 | if (change instanceof InsertModeKey) { |
michael@0 | 4109 | CodeMirror.lookupKey(change.keyName, ['vim-insert'], keyHandler); |
michael@0 | 4110 | } else { |
michael@0 | 4111 | var cur = cm.getCursor(); |
michael@0 | 4112 | cm.replaceRange(change, cur, cur); |
michael@0 | 4113 | } |
michael@0 | 4114 | } |
michael@0 | 4115 | } |
michael@0 | 4116 | } |
michael@0 | 4117 | |
michael@0 | 4118 | resetVimGlobalState(); |
michael@0 | 4119 | return vimApi; |
michael@0 | 4120 | }; |
michael@0 | 4121 | // Initialize Vim and make it available as an API. |
michael@0 | 4122 | CodeMirror.Vim = Vim(); |
michael@0 | 4123 | }); |