You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

5977 lines
227 KiB

7 months ago
  1. (function(mod) {
  2. if (typeof exports == "object" && typeof module == "object") // CommonJS
  3. mod(require("../lib/codemirror"), require("../addon/search/searchcursor"), require("../addon/dialog/dialog"), require("../addon/edit/matchbrackets.js"));
  4. else if (typeof define == "function" && define.amd) // AMD
  5. define(["../lib/codemirror", "../addon/search/searchcursor", "../addon/dialog/dialog", "../addon/edit/matchbrackets"], mod);
  6. else // Plain browser env
  7. mod(CodeMirror);
  8. })(function(CodeMirror) {
  9. 'use strict';
  10. // CodeMirror, copyright (c) by Marijn Haverbeke and others
  11. // Distributed under an MIT license: https://codemirror.net/5/LICENSE
  12. /**
  13. * Supported keybindings:
  14. * Too many to list. Refer to defaultKeymap below.
  15. *
  16. * Supported Ex commands:
  17. * Refer to defaultExCommandMap below.
  18. *
  19. * Registers: unnamed, -, ., :, /, _, a-z, A-Z, 0-9
  20. * (Does not respect the special case for number registers when delete
  21. * operator is made with these commands: %, (, ), , /, ?, n, N, {, } )
  22. * TODO: Implement the remaining registers.
  23. *
  24. * Marks: a-z, A-Z, and 0-9
  25. * TODO: Implement the remaining special marks. They have more complex
  26. * behavior.
  27. *
  28. * Events:
  29. * 'vim-mode-change' - raised on the editor anytime the current mode changes,
  30. * Event object: {mode: "visual", subMode: "linewise"}
  31. *
  32. * Code structure:
  33. * 1. Default keymap
  34. * 2. Variable declarations and short basic helpers
  35. * 3. Instance (External API) implementation
  36. * 4. Internal state tracking objects (input state, counter) implementation
  37. * and instantiation
  38. * 5. Key handler (the main command dispatcher) implementation
  39. * 6. Motion, operator, and action implementations
  40. * 7. Helper functions for the key handler, motions, operators, and actions
  41. * 8. Set up Vim to work as a keymap for CodeMirror.
  42. * 9. Ex command implementations.
  43. */
  44. function initVim$1(CodeMirror) {
  45. var Pos = CodeMirror.Pos;
  46. function transformCursor(cm, range) {
  47. var vim = cm.state.vim;
  48. if (!vim || vim.insertMode) return range.head;
  49. var head = vim.sel.head;
  50. if (!head) return range.head;
  51. if (vim.visualBlock) {
  52. if (range.head.line != head.line) {
  53. return;
  54. }
  55. }
  56. if (range.from() == range.anchor && !range.empty()) {
  57. if (range.head.line == head.line && range.head.ch != head.ch)
  58. return new Pos(range.head.line, range.head.ch - 1);
  59. }
  60. return range.head;
  61. }
  62. var defaultKeymap = [
  63. // Key to key mapping. This goes first to make it possible to override
  64. // existing mappings.
  65. { keys: '<Left>', type: 'keyToKey', toKeys: 'h' },
  66. { keys: '<Right>', type: 'keyToKey', toKeys: 'l' },
  67. { keys: '<Up>', type: 'keyToKey', toKeys: 'k' },
  68. { keys: '<Down>', type: 'keyToKey', toKeys: 'j' },
  69. { keys: 'g<Up>', type: 'keyToKey', toKeys: 'gk' },
  70. { keys: 'g<Down>', type: 'keyToKey', toKeys: 'gj' },
  71. { keys: '<Space>', type: 'keyToKey', toKeys: 'l' },
  72. { keys: '<BS>', type: 'keyToKey', toKeys: 'h', context: 'normal'},
  73. { keys: '<Del>', type: 'keyToKey', toKeys: 'x', context: 'normal'},
  74. { keys: '<C-Space>', type: 'keyToKey', toKeys: 'W' },
  75. { keys: '<C-BS>', type: 'keyToKey', toKeys: 'B', context: 'normal' },
  76. { keys: '<S-Space>', type: 'keyToKey', toKeys: 'w' },
  77. { keys: '<S-BS>', type: 'keyToKey', toKeys: 'b', context: 'normal' },
  78. { keys: '<C-n>', type: 'keyToKey', toKeys: 'j' },
  79. { keys: '<C-p>', type: 'keyToKey', toKeys: 'k' },
  80. { keys: '<C-[>', type: 'keyToKey', toKeys: '<Esc>' },
  81. { keys: '<C-c>', type: 'keyToKey', toKeys: '<Esc>' },
  82. { keys: '<C-[>', type: 'keyToKey', toKeys: '<Esc>', context: 'insert' },
  83. { keys: '<C-c>', type: 'keyToKey', toKeys: '<Esc>', context: 'insert' },
  84. { keys: '<C-Esc>', type: 'keyToKey', toKeys: '<Esc>' }, // ipad keyboard sends C-Esc instead of C-[
  85. { keys: '<C-Esc>', type: 'keyToKey', toKeys: '<Esc>', context: 'insert' },
  86. { keys: 's', type: 'keyToKey', toKeys: 'cl', context: 'normal' },
  87. { keys: 's', type: 'keyToKey', toKeys: 'c', context: 'visual'},
  88. { keys: 'S', type: 'keyToKey', toKeys: 'cc', context: 'normal' },
  89. { keys: 'S', type: 'keyToKey', toKeys: 'VdO', context: 'visual' },
  90. { keys: '<Home>', type: 'keyToKey', toKeys: '0' },
  91. { keys: '<End>', type: 'keyToKey', toKeys: '$' },
  92. { keys: '<PageUp>', type: 'keyToKey', toKeys: '<C-b>' },
  93. { keys: '<PageDown>', type: 'keyToKey', toKeys: '<C-f>' },
  94. { keys: '<CR>', type: 'keyToKey', toKeys: 'j^', context: 'normal' },
  95. { keys: '<Ins>', type: 'keyToKey', toKeys: 'i', context: 'normal'},
  96. { keys: '<Ins>', type: 'action', action: 'toggleOverwrite', context: 'insert' },
  97. // Motions
  98. { keys: 'H', type: 'motion', motion: 'moveToTopLine', motionArgs: { linewise: true, toJumplist: true }},
  99. { keys: 'M', type: 'motion', motion: 'moveToMiddleLine', motionArgs: { linewise: true, toJumplist: true }},
  100. { keys: 'L', type: 'motion', motion: 'moveToBottomLine', motionArgs: { linewise: true, toJumplist: true }},
  101. { keys: 'h', type: 'motion', motion: 'moveByCharacters', motionArgs: { forward: false }},
  102. { keys: 'l', type: 'motion', motion: 'moveByCharacters', motionArgs: { forward: true }},
  103. { keys: 'j', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, linewise: true }},
  104. { keys: 'k', type: 'motion', motion: 'moveByLines', motionArgs: { forward: false, linewise: true }},
  105. { keys: 'gj', type: 'motion', motion: 'moveByDisplayLines', motionArgs: { forward: true }},
  106. { keys: 'gk', type: 'motion', motion: 'moveByDisplayLines', motionArgs: { forward: false }},
  107. { keys: 'w', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: false }},
  108. { keys: 'W', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: false, bigWord: true }},
  109. { keys: 'e', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: true, inclusive: true }},
  110. { keys: 'E', type: 'motion', motion: 'moveByWords', motionArgs: { forward: true, wordEnd: true, bigWord: true, inclusive: true }},
  111. { keys: 'b', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false }},
  112. { keys: 'B', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false, bigWord: true }},
  113. { keys: 'ge', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: true, inclusive: true }},
  114. { keys: 'gE', type: 'motion', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: true, bigWord: true, inclusive: true }},
  115. { keys: '{', type: 'motion', motion: 'moveByParagraph', motionArgs: { forward: false, toJumplist: true }},
  116. { keys: '}', type: 'motion', motion: 'moveByParagraph', motionArgs: { forward: true, toJumplist: true }},
  117. { keys: '(', type: 'motion', motion: 'moveBySentence', motionArgs: { forward: false }},
  118. { keys: ')', type: 'motion', motion: 'moveBySentence', motionArgs: { forward: true }},
  119. { keys: '<C-f>', type: 'motion', motion: 'moveByPage', motionArgs: { forward: true }},
  120. { keys: '<C-b>', type: 'motion', motion: 'moveByPage', motionArgs: { forward: false }},
  121. { keys: '<C-d>', type: 'motion', motion: 'moveByScroll', motionArgs: { forward: true, explicitRepeat: true }},
  122. { keys: '<C-u>', type: 'motion', motion: 'moveByScroll', motionArgs: { forward: false, explicitRepeat: true }},
  123. { keys: 'gg', type: 'motion', motion: 'moveToLineOrEdgeOfDocument', motionArgs: { forward: false, explicitRepeat: true, linewise: true, toJumplist: true }},
  124. { keys: 'G', type: 'motion', motion: 'moveToLineOrEdgeOfDocument', motionArgs: { forward: true, explicitRepeat: true, linewise: true, toJumplist: true }},
  125. {keys: "g$", type: "motion", motion: "moveToEndOfDisplayLine"},
  126. {keys: "g^", type: "motion", motion: "moveToStartOfDisplayLine"},
  127. {keys: "g0", type: "motion", motion: "moveToStartOfDisplayLine"},
  128. { keys: '0', type: 'motion', motion: 'moveToStartOfLine' },
  129. { keys: '^', type: 'motion', motion: 'moveToFirstNonWhiteSpaceCharacter' },
  130. { keys: '+', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, toFirstChar:true }},
  131. { keys: '-', type: 'motion', motion: 'moveByLines', motionArgs: { forward: false, toFirstChar:true }},
  132. { keys: '_', type: 'motion', motion: 'moveByLines', motionArgs: { forward: true, toFirstChar:true, repeatOffset:-1 }},
  133. { keys: '$', type: 'motion', motion: 'moveToEol', motionArgs: { inclusive: true }},
  134. { keys: '%', type: 'motion', motion: 'moveToMatchedSymbol', motionArgs: { inclusive: true, toJumplist: true }},
  135. { keys: 'f<character>', type: 'motion', motion: 'moveToCharacter', motionArgs: { forward: true , inclusive: true }},
  136. { keys: 'F<character>', type: 'motion', motion: 'moveToCharacter', motionArgs: { forward: false }},
  137. { keys: 't<character>', type: 'motion', motion: 'moveTillCharacter', motionArgs: { forward: true, inclusive: true }},
  138. { keys: 'T<character>', type: 'motion', motion: 'moveTillCharacter', motionArgs: { forward: false }},
  139. { keys: ';', type: 'motion', motion: 'repeatLastCharacterSearch', motionArgs: { forward: true }},
  140. { keys: ',', type: 'motion', motion: 'repeatLastCharacterSearch', motionArgs: { forward: false }},
  141. { keys: '\'<character>', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true, linewise: true}},
  142. { keys: '`<character>', type: 'motion', motion: 'goToMark', motionArgs: {toJumplist: true}},
  143. { keys: ']`', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true } },
  144. { keys: '[`', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false } },
  145. { keys: ']\'', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true, linewise: true } },
  146. { keys: '[\'', type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false, linewise: true } },
  147. // the next two aren't motions but must come before more general motion declarations
  148. { keys: ']p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: true, isEdit: true, matchIndent: true}},
  149. { keys: '[p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: false, isEdit: true, matchIndent: true}},
  150. { keys: ']<character>', type: 'motion', motion: 'moveToSymbol', motionArgs: { forward: true, toJumplist: true}},
  151. { keys: '[<character>', type: 'motion', motion: 'moveToSymbol', motionArgs: { forward: false, toJumplist: true}},
  152. { keys: '|', type: 'motion', motion: 'moveToColumn'},
  153. { keys: 'o', type: 'motion', motion: 'moveToOtherHighlightedEnd', context:'visual'},
  154. { keys: 'O', type: 'motion', motion: 'moveToOtherHighlightedEnd', motionArgs: {sameLine: true}, context:'visual'},
  155. // Operators
  156. { keys: 'd', type: 'operator', operator: 'delete' },
  157. { keys: 'y', type: 'operator', operator: 'yank' },
  158. { keys: 'c', type: 'operator', operator: 'change' },
  159. { keys: '=', type: 'operator', operator: 'indentAuto' },
  160. { keys: '>', type: 'operator', operator: 'indent', operatorArgs: { indentRight: true }},
  161. { keys: '<', type: 'operator', operator: 'indent', operatorArgs: { indentRight: false }},
  162. { keys: 'g~', type: 'operator', operator: 'changeCase' },
  163. { keys: 'gu', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: true}, isEdit: true },
  164. { keys: 'gU', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: false}, isEdit: true },
  165. { keys: 'n', type: 'motion', motion: 'findNext', motionArgs: { forward: true, toJumplist: true }},
  166. { keys: 'N', type: 'motion', motion: 'findNext', motionArgs: { forward: false, toJumplist: true }},
  167. { keys: 'gn', type: 'motion', motion: 'findAndSelectNextInclusive', motionArgs: { forward: true }},
  168. { keys: 'gN', type: 'motion', motion: 'findAndSelectNextInclusive', motionArgs: { forward: false }},
  169. // Operator-Motion dual commands
  170. { keys: 'x', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: true }, operatorMotionArgs: { visualLine: false }},
  171. { keys: 'X', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: false }, operatorMotionArgs: { visualLine: true }},
  172. { keys: 'D', type: 'operatorMotion', operator: 'delete', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'},
  173. { keys: 'D', type: 'operator', operator: 'delete', operatorArgs: { linewise: true }, context: 'visual'},
  174. { keys: 'Y', type: 'operatorMotion', operator: 'yank', motion: 'expandToLine', motionArgs: { linewise: true }, context: 'normal'},
  175. { keys: 'Y', type: 'operator', operator: 'yank', operatorArgs: { linewise: true }, context: 'visual'},
  176. { keys: 'C', type: 'operatorMotion', operator: 'change', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'},
  177. { keys: 'C', type: 'operator', operator: 'change', operatorArgs: { linewise: true }, context: 'visual'},
  178. { keys: '~', type: 'operatorMotion', operator: 'changeCase', motion: 'moveByCharacters', motionArgs: { forward: true }, operatorArgs: { shouldMoveCursor: true }, context: 'normal'},
  179. { keys: '~', type: 'operator', operator: 'changeCase', context: 'visual'},
  180. { keys: '<C-u>', type: 'operatorMotion', operator: 'delete', motion: 'moveToStartOfLine', context: 'insert' },
  181. { keys: '<C-w>', type: 'operatorMotion', operator: 'delete', motion: 'moveByWords', motionArgs: { forward: false, wordEnd: false }, context: 'insert' },
  182. //ignore C-w in normal mode
  183. { keys: '<C-w>', type: 'idle', context: 'normal' },
  184. // Actions
  185. { keys: '<C-i>', type: 'action', action: 'jumpListWalk', actionArgs: { forward: true }},
  186. { keys: '<C-o>', type: 'action', action: 'jumpListWalk', actionArgs: { forward: false }},
  187. { keys: '<C-e>', type: 'action', action: 'scroll', actionArgs: { forward: true, linewise: true }},
  188. { keys: '<C-y>', type: 'action', action: 'scroll', actionArgs: { forward: false, linewise: true }},
  189. { keys: 'a', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'charAfter' }, context: 'normal' },
  190. { keys: 'A', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'eol' }, context: 'normal' },
  191. { keys: 'A', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'endOfSelectedArea' }, context: 'visual' },
  192. { keys: 'i', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'inplace' }, context: 'normal' },
  193. { keys: 'gi', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'lastEdit' }, context: 'normal' },
  194. { keys: 'I', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'firstNonBlank'}, context: 'normal' },
  195. { keys: 'gI', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'bol'}, context: 'normal' },
  196. { keys: 'I', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { insertAt: 'startOfSelectedArea' }, context: 'visual' },
  197. { keys: 'o', type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: true }, context: 'normal' },
  198. { keys: 'O', type: 'action', action: 'newLineAndEnterInsertMode', isEdit: true, interlaceInsertRepeat: true, actionArgs: { after: false }, context: 'normal' },
  199. { keys: 'v', type: 'action', action: 'toggleVisualMode' },
  200. { keys: 'V', type: 'action', action: 'toggleVisualMode', actionArgs: { linewise: true }},
  201. { keys: '<C-v>', type: 'action', action: 'toggleVisualMode', actionArgs: { blockwise: true }},
  202. { keys: '<C-q>', type: 'action', action: 'toggleVisualMode', actionArgs: { blockwise: true }},
  203. { keys: 'gv', type: 'action', action: 'reselectLastSelection' },
  204. { keys: 'J', type: 'action', action: 'joinLines', isEdit: true },
  205. { keys: 'gJ', type: 'action', action: 'joinLines', actionArgs: { keepSpaces: true }, isEdit: true },
  206. { keys: 'p', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: true, isEdit: true }},
  207. { keys: 'P', type: 'action', action: 'paste', isEdit: true, actionArgs: { after: false, isEdit: true }},
  208. { keys: 'r<character>', type: 'action', action: 'replace', isEdit: true },
  209. { keys: '@<character>', type: 'action', action: 'replayMacro' },
  210. { keys: 'q<character>', type: 'action', action: 'enterMacroRecordMode' },
  211. // Handle Replace-mode as a special case of insert mode.
  212. { keys: 'R', type: 'action', action: 'enterInsertMode', isEdit: true, actionArgs: { replace: true }, context: 'normal'},
  213. { keys: 'R', type: 'operator', operator: 'change', operatorArgs: { linewise: true, fullLine: true }, context: 'visual', exitVisualBlock: true},
  214. { keys: 'u', type: 'action', action: 'undo', context: 'normal' },
  215. { keys: 'u', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: true}, context: 'visual', isEdit: true },
  216. { keys: 'U', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: false}, context: 'visual', isEdit: true },
  217. { keys: '<C-r>', type: 'action', action: 'redo' },
  218. { keys: 'm<character>', type: 'action', action: 'setMark' },
  219. { keys: '"<character>', type: 'action', action: 'setRegister' },
  220. { keys: 'zz', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }},
  221. { keys: 'z.', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'center' }, motion: 'moveToFirstNonWhiteSpaceCharacter' },
  222. { keys: 'zt', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'top' }},
  223. { keys: 'z<CR>', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'top' }, motion: 'moveToFirstNonWhiteSpaceCharacter' },
  224. { keys: 'zb', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }},
  225. { keys: 'z-', type: 'action', action: 'scrollToCursor', actionArgs: { position: 'bottom' }, motion: 'moveToFirstNonWhiteSpaceCharacter' },
  226. { keys: '.', type: 'action', action: 'repeatLastEdit' },
  227. { keys: '<C-a>', type: 'action', action: 'incrementNumberToken', isEdit: true, actionArgs: {increase: true, backtrack: false}},
  228. { keys: '<C-x>', type: 'action', action: 'incrementNumberToken', isEdit: true, actionArgs: {increase: false, backtrack: false}},
  229. { keys: '<C-t>', type: 'action', action: 'indent', actionArgs: { indentRight: true }, context: 'insert' },
  230. { keys: '<C-d>', type: 'action', action: 'indent', actionArgs: { indentRight: false }, context: 'insert' },
  231. // Text object motions
  232. { keys: 'a<character>', type: 'motion', motion: 'textObjectManipulation' },
  233. { keys: 'i<character>', type: 'motion', motion: 'textObjectManipulation', motionArgs: { textObjectInner: true }},
  234. // Search
  235. { keys: '/', type: 'search', searchArgs: { forward: true, querySrc: 'prompt', toJumplist: true }},
  236. { keys: '?', type: 'search', searchArgs: { forward: false, querySrc: 'prompt', toJumplist: true }},
  237. { keys: '*', type: 'search', searchArgs: { forward: true, querySrc: 'wordUnderCursor', wholeWordOnly: true, toJumplist: true }},
  238. { keys: '#', type: 'search', searchArgs: { forward: false, querySrc: 'wordUnderCursor', wholeWordOnly: true, toJumplist: true }},
  239. { keys: 'g*', type: 'search', searchArgs: { forward: true, querySrc: 'wordUnderCursor', toJumplist: true }},
  240. { keys: 'g#', type: 'search', searchArgs: { forward: false, querySrc: 'wordUnderCursor', toJumplist: true }},
  241. // Ex command
  242. { keys: ':', type: 'ex' }
  243. ];
  244. var defaultKeymapLength = defaultKeymap.length;
  245. /**
  246. * Ex commands
  247. * Care must be taken when adding to the default Ex command map. For any
  248. * pair of commands that have a shared prefix, at least one of their
  249. * shortNames must not match the prefix of the other command.
  250. */
  251. var defaultExCommandMap = [
  252. { name: 'colorscheme', shortName: 'colo' },
  253. { name: 'map' },
  254. { name: 'imap', shortName: 'im' },
  255. { name: 'nmap', shortName: 'nm' },
  256. { name: 'vmap', shortName: 'vm' },
  257. { name: 'unmap' },
  258. { name: 'write', shortName: 'w' },
  259. { name: 'undo', shortName: 'u' },
  260. { name: 'redo', shortName: 'red' },
  261. { name: 'set', shortName: 'se' },
  262. { name: 'setlocal', shortName: 'setl' },
  263. { name: 'setglobal', shortName: 'setg' },
  264. { name: 'sort', shortName: 'sor' },
  265. { name: 'substitute', shortName: 's', possiblyAsync: true },
  266. { name: 'nohlsearch', shortName: 'noh' },
  267. { name: 'yank', shortName: 'y' },
  268. { name: 'delmarks', shortName: 'delm' },
  269. { name: 'registers', shortName: 'reg', excludeFromCommandHistory: true },
  270. { name: 'vglobal', shortName: 'v' },
  271. { name: 'global', shortName: 'g' }
  272. ];
  273. function enterVimMode(cm) {
  274. cm.setOption('disableInput', true);
  275. cm.setOption('showCursorWhenSelecting', false);
  276. CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
  277. cm.on('cursorActivity', onCursorActivity);
  278. maybeInitVimState(cm);
  279. CodeMirror.on(cm.getInputField(), 'paste', getOnPasteFn(cm));
  280. }
  281. function leaveVimMode(cm) {
  282. cm.setOption('disableInput', false);
  283. cm.off('cursorActivity', onCursorActivity);
  284. CodeMirror.off(cm.getInputField(), 'paste', getOnPasteFn(cm));
  285. cm.state.vim = null;
  286. if (highlightTimeout) clearTimeout(highlightTimeout);
  287. }
  288. function detachVimMap(cm, next) {
  289. if (this == CodeMirror.keyMap.vim) {
  290. cm.options.$customCursor = null;
  291. CodeMirror.rmClass(cm.getWrapperElement(), "cm-fat-cursor");
  292. }
  293. if (!next || next.attach != attachVimMap)
  294. leaveVimMode(cm);
  295. }
  296. function attachVimMap(cm, prev) {
  297. if (this == CodeMirror.keyMap.vim) {
  298. if (cm.curOp) cm.curOp.selectionChanged = true;
  299. cm.options.$customCursor = transformCursor;
  300. CodeMirror.addClass(cm.getWrapperElement(), "cm-fat-cursor");
  301. }
  302. if (!prev || prev.attach != attachVimMap)
  303. enterVimMode(cm);
  304. }
  305. // Deprecated, simply setting the keymap works again.
  306. CodeMirror.defineOption('vimMode', false, function(cm, val, prev) {
  307. if (val && cm.getOption("keyMap") != "vim")
  308. cm.setOption("keyMap", "vim");
  309. else if (!val && prev != CodeMirror.Init && /^vim/.test(cm.getOption("keyMap")))
  310. cm.setOption("keyMap", "default");
  311. });
  312. function cmKey(key, cm) {
  313. if (!cm) { return undefined; }
  314. if (this[key]) { return this[key]; }
  315. var vimKey = cmKeyToVimKey(key);
  316. if (!vimKey) {
  317. return false;
  318. }
  319. var cmd = vimApi.findKey(cm, vimKey);
  320. if (typeof cmd == 'function') {
  321. CodeMirror.signal(cm, 'vim-keypress', vimKey);
  322. }
  323. return cmd;
  324. }
  325. var modifiers = {Shift:'S',Ctrl:'C',Alt:'A',Cmd:'D',Mod:'A',CapsLock:''};
  326. var specialKeys = {Enter:'CR',Backspace:'BS',Delete:'Del',Insert:'Ins'};
  327. function cmKeyToVimKey(key) {
  328. if (key.charAt(0) == '\'') {
  329. // Keypress character binding of format "'a'"
  330. return key.charAt(1);
  331. }
  332. var pieces = key.split(/-(?!$)/);
  333. var lastPiece = pieces[pieces.length - 1];
  334. if (pieces.length == 1 && pieces[0].length == 1) {
  335. // No-modifier bindings use literal character bindings above. Skip.
  336. return false;
  337. } else if (pieces.length == 2 && pieces[0] == 'Shift' && lastPiece.length == 1) {
  338. // Ignore Shift+char bindings as they should be handled by literal character.
  339. return false;
  340. }
  341. var hasCharacter = false;
  342. for (var i = 0; i < pieces.length; i++) {
  343. var piece = pieces[i];
  344. if (piece in modifiers) { pieces[i] = modifiers[piece]; }
  345. else { hasCharacter = true; }
  346. if (piece in specialKeys) { pieces[i] = specialKeys[piece]; }
  347. }
  348. if (!hasCharacter) {
  349. // Vim does not support modifier only keys.
  350. return false;
  351. }
  352. // TODO: Current bindings expect the character to be lower case, but
  353. // it looks like vim key notation uses upper case.
  354. if (isUpperCase(lastPiece)) {
  355. pieces[pieces.length - 1] = lastPiece.toLowerCase();
  356. }
  357. return '<' + pieces.join('-') + '>';
  358. }
  359. function getOnPasteFn(cm) {
  360. var vim = cm.state.vim;
  361. if (!vim.onPasteFn) {
  362. vim.onPasteFn = function() {
  363. if (!vim.insertMode) {
  364. cm.setCursor(offsetCursor(cm.getCursor(), 0, 1));
  365. actions.enterInsertMode(cm, {}, vim);
  366. }
  367. };
  368. }
  369. return vim.onPasteFn;
  370. }
  371. var numberRegex = /[\d]/;
  372. var wordCharTest = [CodeMirror.isWordChar, function(ch) {
  373. return ch && !CodeMirror.isWordChar(ch) && !/\s/.test(ch);
  374. }], bigWordCharTest = [function(ch) {
  375. return /\S/.test(ch);
  376. }];
  377. function makeKeyRange(start, size) {
  378. var keys = [];
  379. for (var i = start; i < start + size; i++) {
  380. keys.push(String.fromCharCode(i));
  381. }
  382. return keys;
  383. }
  384. var upperCaseAlphabet = makeKeyRange(65, 26);
  385. var lowerCaseAlphabet = makeKeyRange(97, 26);
  386. var numbers = makeKeyRange(48, 10);
  387. var validMarks = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['<', '>']);
  388. var validRegisters = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['-', '"', '.', ':', '_', '/']);
  389. var upperCaseChars;
  390. try { upperCaseChars = new RegExp("^[\\p{Lu}]$", "u"); }
  391. catch (_) { upperCaseChars = /^[A-Z]$/; }
  392. function isLine(cm, line) {
  393. return line >= cm.firstLine() && line <= cm.lastLine();
  394. }
  395. function isLowerCase(k) {
  396. return (/^[a-z]$/).test(k);
  397. }
  398. function isMatchableSymbol(k) {
  399. return '()[]{}'.indexOf(k) != -1;
  400. }
  401. function isNumber(k) {
  402. return numberRegex.test(k);
  403. }
  404. function isUpperCase(k) {
  405. return upperCaseChars.test(k);
  406. }
  407. function isWhiteSpaceString(k) {
  408. return (/^\s*$/).test(k);
  409. }
  410. function isEndOfSentenceSymbol(k) {
  411. return '.?!'.indexOf(k) != -1;
  412. }
  413. function inArray(val, arr) {
  414. for (var i = 0; i < arr.length; i++) {
  415. if (arr[i] == val) {
  416. return true;
  417. }
  418. }
  419. return false;
  420. }
  421. var options = {};
  422. function defineOption(name, defaultValue, type, aliases, callback) {
  423. if (defaultValue === undefined && !callback) {
  424. throw Error('defaultValue is required unless callback is provided');
  425. }
  426. if (!type) { type = 'string'; }
  427. options[name] = {
  428. type: type,
  429. defaultValue: defaultValue,
  430. callback: callback
  431. };
  432. if (aliases) {
  433. for (var i = 0; i < aliases.length; i++) {
  434. options[aliases[i]] = options[name];
  435. }
  436. }
  437. if (defaultValue) {
  438. setOption(name, defaultValue);
  439. }
  440. }
  441. function setOption(name, value, cm, cfg) {
  442. var option = options[name];
  443. cfg = cfg || {};
  444. var scope = cfg.scope;
  445. if (!option) {
  446. return new Error('Unknown option: ' + name);
  447. }
  448. if (option.type == 'boolean') {
  449. if (value && value !== true) {
  450. return new Error('Invalid argument: ' + name + '=' + value);
  451. } else if (value !== false) {
  452. // Boolean options are set to true if value is not defined.
  453. value = true;
  454. }
  455. }
  456. if (option.callback) {
  457. if (scope !== 'local') {
  458. option.callback(value, undefined);
  459. }
  460. if (scope !== 'global' && cm) {
  461. option.callback(value, cm);
  462. }
  463. } else {
  464. if (scope !== 'local') {
  465. option.value = option.type == 'boolean' ? !!value : value;
  466. }
  467. if (scope !== 'global' && cm) {
  468. cm.state.vim.options[name] = {value: value};
  469. }
  470. }
  471. }
  472. function getOption(name, cm, cfg) {
  473. var option = options[name];
  474. cfg = cfg || {};
  475. var scope = cfg.scope;
  476. if (!option) {
  477. return new Error('Unknown option: ' + name);
  478. }
  479. if (option.callback) {
  480. var local = cm && option.callback(undefined, cm);
  481. if (scope !== 'global' && local !== undefined) {
  482. return local;
  483. }
  484. if (scope !== 'local') {
  485. return option.callback();
  486. }
  487. return;
  488. } else {
  489. var local = (scope !== 'global') && (cm && cm.state.vim.options[name]);
  490. return (local || (scope !== 'local') && option || {}).value;
  491. }
  492. }
  493. defineOption('filetype', undefined, 'string', ['ft'], function(name, cm) {
  494. // Option is local. Do nothing for global.
  495. if (cm === undefined) {
  496. return;
  497. }
  498. // The 'filetype' option proxies to the CodeMirror 'mode' option.
  499. if (name === undefined) {
  500. var mode = cm.getOption('mode');
  501. return mode == 'null' ? '' : mode;
  502. } else {
  503. var mode = name == '' ? 'null' : name;
  504. cm.setOption('mode', mode);
  505. }
  506. });
  507. var createCircularJumpList = function() {
  508. var size = 100;
  509. var pointer = -1;
  510. var head = 0;
  511. var tail = 0;
  512. var buffer = new Array(size);
  513. function add(cm, oldCur, newCur) {
  514. var current = pointer % size;
  515. var curMark = buffer[current];
  516. function useNextSlot(cursor) {
  517. var next = ++pointer % size;
  518. var trashMark = buffer[next];
  519. if (trashMark) {
  520. trashMark.clear();
  521. }
  522. buffer[next] = cm.setBookmark(cursor);
  523. }
  524. if (curMark) {
  525. var markPos = curMark.find();
  526. // avoid recording redundant cursor position
  527. if (markPos && !cursorEqual(markPos, oldCur)) {
  528. useNextSlot(oldCur);
  529. }
  530. } else {
  531. useNextSlot(oldCur);
  532. }
  533. useNextSlot(newCur);
  534. head = pointer;
  535. tail = pointer - size + 1;
  536. if (tail < 0) {
  537. tail = 0;
  538. }
  539. }
  540. function move(cm, offset) {
  541. pointer += offset;
  542. if (pointer > head) {
  543. pointer = head;
  544. } else if (pointer < tail) {
  545. pointer = tail;
  546. }
  547. var mark = buffer[(size + pointer) % size];
  548. // skip marks that are temporarily removed from text buffer
  549. if (mark && !mark.find()) {
  550. var inc = offset > 0 ? 1 : -1;
  551. var newCur;
  552. var oldCur = cm.getCursor();
  553. do {
  554. pointer += inc;
  555. mark = buffer[(size + pointer) % size];
  556. // skip marks that are the same as current position
  557. if (mark &&
  558. (newCur = mark.find()) &&
  559. !cursorEqual(oldCur, newCur)) {
  560. break;
  561. }
  562. } while (pointer < head && pointer > tail);
  563. }
  564. return mark;
  565. }
  566. function find(cm, offset) {
  567. var oldPointer = pointer;
  568. var mark = move(cm, offset);
  569. pointer = oldPointer;
  570. return mark && mark.find();
  571. }
  572. return {
  573. cachedCursor: undefined, //used for # and * jumps
  574. add: add,
  575. find: find,
  576. move: move
  577. };
  578. };
  579. // Returns an object to track the changes associated insert mode. It
  580. // clones the object that is passed in, or creates an empty object one if
  581. // none is provided.
  582. var createInsertModeChanges = function(c) {
  583. if (c) {
  584. // Copy construction
  585. return {
  586. changes: c.changes,
  587. expectCursorActivityForChange: c.expectCursorActivityForChange
  588. };
  589. }
  590. return {
  591. // Change list
  592. changes: [],
  593. // Set to true on change, false on cursorActivity.
  594. expectCursorActivityForChange: false
  595. };
  596. };
  597. function MacroModeState() {
  598. this.latestRegister = undefined;
  599. this.isPlaying = false;
  600. this.isRecording = false;
  601. this.replaySearchQueries = [];
  602. this.onRecordingDone = undefined;
  603. this.lastInsertModeChanges = createInsertModeChanges();
  604. }
  605. MacroModeState.prototype = {
  606. exitMacroRecordMode: function() {
  607. var macroModeState = vimGlobalState.macroModeState;
  608. if (macroModeState.onRecordingDone) {
  609. macroModeState.onRecordingDone(); // close dialog
  610. }
  611. macroModeState.onRecordingDone = undefined;
  612. macroModeState.isRecording = false;
  613. },
  614. enterMacroRecordMode: function(cm, registerName) {
  615. var register =
  616. vimGlobalState.registerController.getRegister(registerName);
  617. if (register) {
  618. register.clear();
  619. this.latestRegister = registerName;
  620. if (cm.openDialog) {
  621. var template = dom('span', {class: 'cm-vim-message'}, 'recording @' + registerName);
  622. this.onRecordingDone = cm.openDialog(template, null, {bottom:true});
  623. }
  624. this.isRecording = true;
  625. }
  626. }
  627. };
  628. function maybeInitVimState(cm) {
  629. if (!cm.state.vim) {
  630. // Store instance state in the CodeMirror object.
  631. cm.state.vim = {
  632. inputState: new InputState(),
  633. // Vim's input state that triggered the last edit, used to repeat
  634. // motions and operators with '.'.
  635. lastEditInputState: undefined,
  636. // Vim's action command before the last edit, used to repeat actions
  637. // with '.' and insert mode repeat.
  638. lastEditActionCommand: undefined,
  639. // When using jk for navigation, if you move from a longer line to a
  640. // shorter line, the cursor may clip to the end of the shorter line.
  641. // If j is pressed again and cursor goes to the next line, the
  642. // cursor should go back to its horizontal position on the longer
  643. // line if it can. This is to keep track of the horizontal position.
  644. lastHPos: -1,
  645. // Doing the same with screen-position for gj/gk
  646. lastHSPos: -1,
  647. // The last motion command run. Cleared if a non-motion command gets
  648. // executed in between.
  649. lastMotion: null,
  650. marks: {},
  651. insertMode: false,
  652. // Repeat count for changes made in insert mode, triggered by key
  653. // sequences like 3,i. Only exists when insertMode is true.
  654. insertModeRepeat: undefined,
  655. visualMode: false,
  656. // If we are in visual line mode. No effect if visualMode is false.
  657. visualLine: false,
  658. visualBlock: false,
  659. lastSelection: null,
  660. lastPastedText: null,
  661. sel: {},
  662. // Buffer-local/window-local values of vim options.
  663. options: {}
  664. };
  665. }
  666. return cm.state.vim;
  667. }
  668. var vimGlobalState;
  669. function resetVimGlobalState() {
  670. vimGlobalState = {
  671. // The current search query.
  672. searchQuery: null,
  673. // Whether we are searching backwards.
  674. searchIsReversed: false,
  675. // Replace part of the last substituted pattern
  676. lastSubstituteReplacePart: undefined,
  677. jumpList: createCircularJumpList(),
  678. macroModeState: new MacroModeState,
  679. // Recording latest f, t, F or T motion command.
  680. lastCharacterSearch: {increment:0, forward:true, selectedCharacter:''},
  681. registerController: new RegisterController({}),
  682. // search history buffer
  683. searchHistoryController: new HistoryController(),
  684. // ex Command history buffer
  685. exCommandHistoryController : new HistoryController()
  686. };
  687. for (var optionName in options) {
  688. var option = options[optionName];
  689. option.value = option.defaultValue;
  690. }
  691. }
  692. var lastInsertModeKeyTimer;
  693. var vimApi = {
  694. enterVimMode: enterVimMode,
  695. buildKeyMap: function() {
  696. // TODO: Convert keymap into dictionary format for fast lookup.
  697. },
  698. // Testing hook, though it might be useful to expose the register
  699. // controller anyway.
  700. getRegisterController: function() {
  701. return vimGlobalState.registerController;
  702. },
  703. // Testing hook.
  704. resetVimGlobalState_: resetVimGlobalState,
  705. // Testing hook.
  706. getVimGlobalState_: function() {
  707. return vimGlobalState;
  708. },
  709. // Testing hook.
  710. maybeInitVimState_: maybeInitVimState,
  711. suppressErrorLogging: false,
  712. InsertModeKey: InsertModeKey,
  713. map: function(lhs, rhs, ctx) {
  714. // Add user defined key bindings.
  715. exCommandDispatcher.map(lhs, rhs, ctx);
  716. },
  717. unmap: function(lhs, ctx) {
  718. return exCommandDispatcher.unmap(lhs, ctx);
  719. },
  720. // Non-recursive map function.
  721. // NOTE: This will not create mappings to key maps that aren't present
  722. // in the default key map. See TODO at bottom of function.
  723. noremap: function(lhs, rhs, ctx) {
  724. function toCtxArray(ctx) {
  725. return ctx ? [ctx] : ['normal', 'insert', 'visual'];
  726. }
  727. var ctxsToMap = toCtxArray(ctx);
  728. // Look through all actual defaults to find a map candidate.
  729. var actualLength = defaultKeymap.length, origLength = defaultKeymapLength;
  730. for (var i = actualLength - origLength;
  731. i < actualLength && ctxsToMap.length;
  732. i++) {
  733. var mapping = defaultKeymap[i];
  734. // Omit mappings that operate in the wrong context(s) and those of invalid type.
  735. if (mapping.keys == rhs &&
  736. (!ctx || !mapping.context || mapping.context === ctx) &&
  737. mapping.type.substr(0, 2) !== 'ex' &&
  738. mapping.type.substr(0, 3) !== 'key') {
  739. // Make a shallow copy of the original keymap entry.
  740. var newMapping = {};
  741. for (var key in mapping) {
  742. newMapping[key] = mapping[key];
  743. }
  744. // Modify it point to the new mapping with the proper context.
  745. newMapping.keys = lhs;
  746. if (ctx && !newMapping.context) {
  747. newMapping.context = ctx;
  748. }
  749. // Add it to the keymap with a higher priority than the original.
  750. this._mapCommand(newMapping);
  751. // Record the mapped contexts as complete.
  752. var mappedCtxs = toCtxArray(mapping.context);
  753. ctxsToMap = ctxsToMap.filter(function(el) { return mappedCtxs.indexOf(el) === -1; });
  754. }
  755. }
  756. // TODO: Create non-recursive keyToKey mappings for the unmapped contexts once those exist.
  757. },
  758. // Remove all user-defined mappings for the provided context.
  759. mapclear: function(ctx) {
  760. // Partition the existing keymap into user-defined and true defaults.
  761. var actualLength = defaultKeymap.length,
  762. origLength = defaultKeymapLength;
  763. var userKeymap = defaultKeymap.slice(0, actualLength - origLength);
  764. defaultKeymap = defaultKeymap.slice(actualLength - origLength);
  765. if (ctx) {
  766. // If a specific context is being cleared, we need to keep mappings
  767. // from all other contexts.
  768. for (var i = userKeymap.length - 1; i >= 0; i--) {
  769. var mapping = userKeymap[i];
  770. if (ctx !== mapping.context) {
  771. if (mapping.context) {
  772. this._mapCommand(mapping);
  773. } else {
  774. // `mapping` applies to all contexts so create keymap copies
  775. // for each context except the one being cleared.
  776. var contexts = ['normal', 'insert', 'visual'];
  777. for (var j in contexts) {
  778. if (contexts[j] !== ctx) {
  779. var newMapping = {};
  780. for (var key in mapping) {
  781. newMapping[key] = mapping[key];
  782. }
  783. newMapping.context = contexts[j];
  784. this._mapCommand(newMapping);
  785. }
  786. }
  787. }
  788. }
  789. }
  790. }
  791. },
  792. // TODO: Expose setOption and getOption as instance methods. Need to decide how to namespace
  793. // them, or somehow make them work with the existing CodeMirror setOption/getOption API.
  794. setOption: setOption,
  795. getOption: getOption,
  796. defineOption: defineOption,
  797. defineEx: function(name, prefix, func){
  798. if (!prefix) {
  799. prefix = name;
  800. } else if (name.indexOf(prefix) !== 0) {
  801. throw new Error('(Vim.defineEx) "'+prefix+'" is not a prefix of "'+name+'", command not registered');
  802. }
  803. exCommands[name]=func;
  804. exCommandDispatcher.commandMap_[prefix]={name:name, shortName:prefix, type:'api'};
  805. },
  806. handleKey: function (cm, key, origin) {
  807. var command = this.findKey(cm, key, origin);
  808. if (typeof command === 'function') {
  809. return command();
  810. }
  811. },
  812. multiSelectHandleKey: multiSelectHandleKey,
  813. /**
  814. * This is the outermost function called by CodeMirror, after keys have
  815. * been mapped to their Vim equivalents.
  816. *
  817. * Finds a command based on the key (and cached keys if there is a
  818. * multi-key sequence). Returns `undefined` if no key is matched, a noop
  819. * function if a partial match is found (multi-key), and a function to
  820. * execute the bound command if a a key is matched. The function always
  821. * returns true.
  822. */
  823. findKey: function(cm, key, origin) {
  824. var vim = maybeInitVimState(cm);
  825. function handleMacroRecording() {
  826. var macroModeState = vimGlobalState.macroModeState;
  827. if (macroModeState.isRecording) {
  828. if (key == 'q') {
  829. macroModeState.exitMacroRecordMode();
  830. clearInputState(cm);
  831. return true;
  832. }
  833. if (origin != 'mapping') {
  834. logKey(macroModeState, key);
  835. }
  836. }
  837. }
  838. function handleEsc() {
  839. if (key == '<Esc>') {
  840. if (vim.visualMode) {
  841. // Get back to normal mode.
  842. exitVisualMode(cm);
  843. } else if (vim.insertMode) {
  844. // Get back to normal mode.
  845. exitInsertMode(cm);
  846. } else {
  847. // We're already in normal mode. Let '<Esc>' be handled normally.
  848. return;
  849. }
  850. clearInputState(cm);
  851. return true;
  852. }
  853. }
  854. function doKeyToKey(keys) {
  855. // TODO: prevent infinite recursion.
  856. var match;
  857. while (keys) {
  858. // Pull off one command key, which is either a single character
  859. // or a special sequence wrapped in '<' and '>', e.g. '<Space>'.
  860. match = (/<\w+-.+?>|<\w+>|./).exec(keys);
  861. key = match[0];
  862. keys = keys.substring(match.index + key.length);
  863. vimApi.handleKey(cm, key, 'mapping');
  864. }
  865. }
  866. function handleKeyInsertMode() {
  867. if (handleEsc()) { return true; }
  868. var keys = vim.inputState.keyBuffer = vim.inputState.keyBuffer + key;
  869. var keysAreChars = key.length == 1;
  870. var match = commandDispatcher.matchCommand(keys, defaultKeymap, vim.inputState, 'insert');
  871. // Need to check all key substrings in insert mode.
  872. while (keys.length > 1 && match.type != 'full') {
  873. var keys = vim.inputState.keyBuffer = keys.slice(1);
  874. var thisMatch = commandDispatcher.matchCommand(keys, defaultKeymap, vim.inputState, 'insert');
  875. if (thisMatch.type != 'none') { match = thisMatch; }
  876. }
  877. if (match.type == 'none') { clearInputState(cm); return false; }
  878. else if (match.type == 'partial') {
  879. if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); }
  880. lastInsertModeKeyTimer = window.setTimeout(
  881. function() { if (vim.insertMode && vim.inputState.keyBuffer) { clearInputState(cm); } },
  882. getOption('insertModeEscKeysTimeout'));
  883. return !keysAreChars;
  884. }
  885. if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); }
  886. if (keysAreChars) {
  887. var selections = cm.listSelections();
  888. for (var i = 0; i < selections.length; i++) {
  889. var here = selections[i].head;
  890. cm.replaceRange('', offsetCursor(here, 0, -(keys.length - 1)), here, '+input');
  891. }
  892. vimGlobalState.macroModeState.lastInsertModeChanges.changes.pop();
  893. }
  894. clearInputState(cm);
  895. return match.command;
  896. }
  897. function handleKeyNonInsertMode() {
  898. if (handleMacroRecording() || handleEsc()) { return true; }
  899. var keys = vim.inputState.keyBuffer = vim.inputState.keyBuffer + key;
  900. if (/^[1-9]\d*$/.test(keys)) { return true; }
  901. var keysMatcher = /^(\d*)(.*)$/.exec(keys);
  902. if (!keysMatcher) { clearInputState(cm); return false; }
  903. var context = vim.visualMode ? 'visual' :
  904. 'normal';
  905. var mainKey = keysMatcher[2] || keysMatcher[1];
  906. if (vim.inputState.operatorShortcut && vim.inputState.operatorShortcut.slice(-1) == mainKey) {
  907. // multikey operators act linewise by repeating only the last character
  908. mainKey = vim.inputState.operatorShortcut;
  909. }
  910. var match = commandDispatcher.matchCommand(mainKey, defaultKeymap, vim.inputState, context);
  911. if (match.type == 'none') { clearInputState(cm); return false; }
  912. else if (match.type == 'partial') { return true; }
  913. else if (match.type == 'clear') { clearInputState(cm); return true; }
  914. vim.inputState.keyBuffer = '';
  915. keysMatcher = /^(\d*)(.*)$/.exec(keys);
  916. if (keysMatcher[1] && keysMatcher[1] != '0') {
  917. vim.inputState.pushRepeatDigit(keysMatcher[1]);
  918. }
  919. return match.command;
  920. }
  921. var command;
  922. if (vim.insertMode) { command = handleKeyInsertMode(); }
  923. else { command = handleKeyNonInsertMode(); }
  924. if (command === false) {
  925. return !vim.insertMode && key.length === 1 ? function() { return true; } : undefined;
  926. } else if (command === true) {
  927. // TODO: Look into using CodeMirror's multi-key handling.
  928. // Return no-op since we are caching the key. Counts as handled, but
  929. // don't want act on it just yet.
  930. return function() { return true; };
  931. } else {
  932. return function() {
  933. return cm.operation(function() {
  934. cm.curOp.isVimOp = true;
  935. try {
  936. if (command.type == 'keyToKey') {
  937. doKeyToKey(command.toKeys);
  938. } else {
  939. commandDispatcher.processCommand(cm, vim, command);
  940. }
  941. } catch (e) {
  942. // clear VIM state in case it's in a bad state.
  943. cm.state.vim = undefined;
  944. maybeInitVimState(cm);
  945. if (!vimApi.suppressErrorLogging) {
  946. console['log'](e);
  947. }
  948. throw e;
  949. }
  950. return true;
  951. });
  952. };
  953. }
  954. },
  955. handleEx: function(cm, input) {
  956. exCommandDispatcher.processCommand(cm, input);
  957. },
  958. defineMotion: defineMotion,
  959. defineAction: defineAction,
  960. defineOperator: defineOperator,
  961. mapCommand: mapCommand,
  962. _mapCommand: _mapCommand,
  963. defineRegister: defineRegister,
  964. exitVisualMode: exitVisualMode,
  965. exitInsertMode: exitInsertMode
  966. };
  967. // Represents the current input state.
  968. function InputState() {
  969. this.prefixRepeat = [];
  970. this.motionRepeat = [];
  971. this.operator = null;
  972. this.operatorArgs = null;
  973. this.motion = null;
  974. this.motionArgs = null;
  975. this.keyBuffer = []; // For matching multi-key commands.
  976. this.registerName = null; // Defaults to the unnamed register.
  977. }
  978. InputState.prototype.pushRepeatDigit = function(n) {
  979. if (!this.operator) {
  980. this.prefixRepeat = this.prefixRepeat.concat(n);
  981. } else {
  982. this.motionRepeat = this.motionRepeat.concat(n);
  983. }
  984. };
  985. InputState.prototype.getRepeat = function() {
  986. var repeat = 0;
  987. if (this.prefixRepeat.length > 0 || this.motionRepeat.length > 0) {
  988. repeat = 1;
  989. if (this.prefixRepeat.length > 0) {
  990. repeat *= parseInt(this.prefixRepeat.join(''), 10);
  991. }
  992. if (this.motionRepeat.length > 0) {
  993. repeat *= parseInt(this.motionRepeat.join(''), 10);
  994. }
  995. }
  996. return repeat;
  997. };
  998. function clearInputState(cm, reason) {
  999. cm.state.vim.inputState = new InputState();
  1000. CodeMirror.signal(cm, 'vim-command-done', reason);
  1001. }
  1002. /*
  1003. * Register stores information about copy and paste registers. Besides
  1004. * text, a register must store whether it is linewise (i.e., when it is
  1005. * pasted, should it insert itself into a new line, or should the text be
  1006. * inserted at the cursor position.)
  1007. */
  1008. function Register(text, linewise, blockwise) {
  1009. this.clear();
  1010. this.keyBuffer = [text || ''];
  1011. this.insertModeChanges = [];
  1012. this.searchQueries = [];
  1013. this.linewise = !!linewise;
  1014. this.blockwise = !!blockwise;
  1015. }
  1016. Register.prototype = {
  1017. setText: function(text, linewise, blockwise) {
  1018. this.keyBuffer = [text || ''];
  1019. this.linewise = !!linewise;
  1020. this.blockwise = !!blockwise;
  1021. },
  1022. pushText: function(text, linewise) {
  1023. // if this register has ever been set to linewise, use linewise.
  1024. if (linewise) {
  1025. if (!this.linewise) {
  1026. this.keyBuffer.push('\n');
  1027. }
  1028. this.linewise = true;
  1029. }
  1030. this.keyBuffer.push(text);
  1031. },
  1032. pushInsertModeChanges: function(changes) {
  1033. this.insertModeChanges.push(createInsertModeChanges(changes));
  1034. },
  1035. pushSearchQuery: function(query) {
  1036. this.searchQueries.push(query);
  1037. },
  1038. clear: function() {
  1039. this.keyBuffer = [];
  1040. this.insertModeChanges = [];
  1041. this.searchQueries = [];
  1042. this.linewise = false;
  1043. },
  1044. toString: function() {
  1045. return this.keyBuffer.join('');
  1046. }
  1047. };
  1048. /**
  1049. * Defines an external register.
  1050. *
  1051. * The name should be a single character that will be used to reference the register.
  1052. * The register should support setText, pushText, clear, and toString(). See Register
  1053. * for a reference implementation.
  1054. */
  1055. function defineRegister(name, register) {
  1056. var registers = vimGlobalState.registerController.registers;
  1057. if (!name || name.length != 1) {
  1058. throw Error('Register name must be 1 character');
  1059. }
  1060. if (registers[name]) {
  1061. throw Error('Register already defined ' + name);
  1062. }
  1063. registers[name] = register;
  1064. validRegisters.push(name);
  1065. }
  1066. /*
  1067. * vim registers allow you to keep many independent copy and paste buffers.
  1068. * See http://usevim.com/2012/04/13/registers/ for an introduction.
  1069. *
  1070. * RegisterController keeps the state of all the registers. An initial
  1071. * state may be passed in. The unnamed register '"' will always be
  1072. * overridden.
  1073. */
  1074. function RegisterController(registers) {
  1075. this.registers = registers;
  1076. this.unnamedRegister = registers['"'] = new Register();
  1077. registers['.'] = new Register();
  1078. registers[':'] = new Register();
  1079. registers['/'] = new Register();
  1080. }
  1081. RegisterController.prototype = {
  1082. pushText: function(registerName, operator, text, linewise, blockwise) {
  1083. // The black hole register, "_, means delete/yank to nowhere.
  1084. if (registerName === '_') return;
  1085. if (linewise && text.charAt(text.length - 1) !== '\n'){
  1086. text += '\n';
  1087. }
  1088. // Lowercase and uppercase registers refer to the same register.
  1089. // Uppercase just means append.
  1090. var register = this.isValidRegister(registerName) ?
  1091. this.getRegister(registerName) : null;
  1092. // if no register/an invalid register was specified, things go to the
  1093. // default registers
  1094. if (!register) {
  1095. switch (operator) {
  1096. case 'yank':
  1097. // The 0 register contains the text from the most recent yank.
  1098. this.registers['0'] = new Register(text, linewise, blockwise);
  1099. break;
  1100. case 'delete':
  1101. case 'change':
  1102. if (text.indexOf('\n') == -1) {
  1103. // Delete less than 1 line. Update the small delete register.
  1104. this.registers['-'] = new Register(text, linewise);
  1105. } else {
  1106. // Shift down the contents of the numbered registers and put the
  1107. // deleted text into register 1.
  1108. this.shiftNumericRegisters_();
  1109. this.registers['1'] = new Register(text, linewise);
  1110. }
  1111. break;
  1112. }
  1113. // Make sure the unnamed register is set to what just happened
  1114. this.unnamedRegister.setText(text, linewise, blockwise);
  1115. return;
  1116. }
  1117. // If we've gotten to this point, we've actually specified a register
  1118. var append = isUpperCase(registerName);
  1119. if (append) {
  1120. register.pushText(text, linewise);
  1121. } else {
  1122. register.setText(text, linewise, blockwise);
  1123. }
  1124. // The unnamed register always has the same value as the last used
  1125. // register.
  1126. this.unnamedRegister.setText(register.toString(), linewise);
  1127. },
  1128. // Gets the register named @name. If one of @name doesn't already exist,
  1129. // create it. If @name is invalid, return the unnamedRegister.
  1130. getRegister: function(name) {
  1131. if (!this.isValidRegister(name)) {
  1132. return this.unnamedRegister;
  1133. }
  1134. name = name.toLowerCase();
  1135. if (!this.registers[name]) {
  1136. this.registers[name] = new Register();
  1137. }
  1138. return this.registers[name];
  1139. },
  1140. isValidRegister: function(name) {
  1141. return name && inArray(name, validRegisters);
  1142. },
  1143. shiftNumericRegisters_: function() {
  1144. for (var i = 9; i >= 2; i--) {
  1145. this.registers[i] = this.getRegister('' + (i - 1));
  1146. }
  1147. }
  1148. };
  1149. function HistoryController() {
  1150. this.historyBuffer = [];
  1151. this.iterator = 0;
  1152. this.initialPrefix = null;
  1153. }
  1154. HistoryController.prototype = {
  1155. // the input argument here acts a user entered prefix for a small time
  1156. // until we start autocompletion in which case it is the autocompleted.
  1157. nextMatch: function (input, up) {
  1158. var historyBuffer = this.historyBuffer;
  1159. var dir = up ? -1 : 1;
  1160. if (this.initialPrefix === null) this.initialPrefix = input;
  1161. for (var i = this.iterator + dir; up ? i >= 0 : i < historyBuffer.length; i+= dir) {
  1162. var element = historyBuffer[i];
  1163. for (var j = 0; j <= element.length; j++) {
  1164. if (this.initialPrefix == element.substring(0, j)) {
  1165. this.iterator = i;
  1166. return element;
  1167. }
  1168. }
  1169. }
  1170. // should return the user input in case we reach the end of buffer.
  1171. if (i >= historyBuffer.length) {
  1172. this.iterator = historyBuffer.length;
  1173. return this.initialPrefix;
  1174. }
  1175. // return the last autocompleted query or exCommand as it is.
  1176. if (i < 0 ) return input;
  1177. },
  1178. pushInput: function(input) {
  1179. var index = this.historyBuffer.indexOf(input);
  1180. if (index > -1) this.historyBuffer.splice(index, 1);
  1181. if (input.length) this.historyBuffer.push(input);
  1182. },
  1183. reset: function() {
  1184. this.initialPrefix = null;
  1185. this.iterator = this.historyBuffer.length;
  1186. }
  1187. };
  1188. var commandDispatcher = {
  1189. matchCommand: function(keys, keyMap, inputState, context) {
  1190. var matches = commandMatches(keys, keyMap, context, inputState);
  1191. if (!matches.full && !matches.partial) {
  1192. return {type: 'none'};
  1193. } else if (!matches.full && matches.partial) {
  1194. return {type: 'partial'};
  1195. }
  1196. var bestMatch;
  1197. for (var i = 0; i < matches.full.length; i++) {
  1198. var match = matches.full[i];
  1199. if (!bestMatch) {
  1200. bestMatch = match;
  1201. }
  1202. }
  1203. if (bestMatch.keys.slice(-11) == '<character>') {
  1204. var character = lastChar(keys);
  1205. if (!character || character.length > 1) return {type: 'clear'};
  1206. inputState.selectedCharacter = character;
  1207. }
  1208. return {type: 'full', command: bestMatch};
  1209. },
  1210. processCommand: function(cm, vim, command) {
  1211. vim.inputState.repeatOverride = command.repeatOverride;
  1212. switch (command.type) {
  1213. case 'motion':
  1214. this.processMotion(cm, vim, command);
  1215. break;
  1216. case 'operator':
  1217. this.processOperator(cm, vim, command);
  1218. break;
  1219. case 'operatorMotion':
  1220. this.processOperatorMotion(cm, vim, command);
  1221. break;
  1222. case 'action':
  1223. this.processAction(cm, vim, command);
  1224. break;
  1225. case 'search':
  1226. this.processSearch(cm, vim, command);
  1227. break;
  1228. case 'ex':
  1229. case 'keyToEx':
  1230. this.processEx(cm, vim, command);
  1231. break;
  1232. }
  1233. },
  1234. processMotion: function(cm, vim, command) {
  1235. vim.inputState.motion = command.motion;
  1236. vim.inputState.motionArgs = copyArgs(command.motionArgs);
  1237. this.evalInput(cm, vim);
  1238. },
  1239. processOperator: function(cm, vim, command) {
  1240. var inputState = vim.inputState;
  1241. if (inputState.operator) {
  1242. if (inputState.operator == command.operator) {
  1243. // Typing an operator twice like 'dd' makes the operator operate
  1244. // linewise
  1245. inputState.motion = 'expandToLine';
  1246. inputState.motionArgs = { linewise: true };
  1247. this.evalInput(cm, vim);
  1248. return;
  1249. } else {
  1250. // 2 different operators in a row doesn't make sense.
  1251. clearInputState(cm);
  1252. }
  1253. }
  1254. inputState.operator = command.operator;
  1255. inputState.operatorArgs = copyArgs(command.operatorArgs);
  1256. if (command.keys.length > 1) {
  1257. inputState.operatorShortcut = command.keys;
  1258. }
  1259. if (command.exitVisualBlock) {
  1260. vim.visualBlock = false;
  1261. updateCmSelection(cm);
  1262. }
  1263. if (vim.visualMode) {
  1264. // Operating on a selection in visual mode. We don't need a motion.
  1265. this.evalInput(cm, vim);
  1266. }
  1267. },
  1268. processOperatorMotion: function(cm, vim, command) {
  1269. var visualMode = vim.visualMode;
  1270. var operatorMotionArgs = copyArgs(command.operatorMotionArgs);
  1271. if (operatorMotionArgs) {
  1272. // Operator motions may have special behavior in visual mode.
  1273. if (visualMode && operatorMotionArgs.visualLine) {
  1274. vim.visualLine = true;
  1275. }
  1276. }
  1277. this.processOperator(cm, vim, command);
  1278. if (!visualMode) {
  1279. this.processMotion(cm, vim, command);
  1280. }
  1281. },
  1282. processAction: function(cm, vim, command) {
  1283. var inputState = vim.inputState;
  1284. var repeat = inputState.getRepeat();
  1285. var repeatIsExplicit = !!repeat;
  1286. var actionArgs = copyArgs(command.actionArgs) || {};
  1287. if (inputState.selectedCharacter) {
  1288. actionArgs.selectedCharacter = inputState.selectedCharacter;
  1289. }
  1290. // Actions may or may not have motions and operators. Do these first.
  1291. if (command.operator) {
  1292. this.processOperator(cm, vim, command);
  1293. }
  1294. if (command.motion) {
  1295. this.processMotion(cm, vim, command);
  1296. }
  1297. if (command.motion || command.operator) {
  1298. this.evalInput(cm, vim);
  1299. }
  1300. actionArgs.repeat = repeat || 1;
  1301. actionArgs.repeatIsExplicit = repeatIsExplicit;
  1302. actionArgs.registerName = inputState.registerName;
  1303. clearInputState(cm);
  1304. vim.lastMotion = null;
  1305. if (command.isEdit) {
  1306. this.recordLastEdit(vim, inputState, command);
  1307. }
  1308. actions[command.action](cm, actionArgs, vim);
  1309. },
  1310. processSearch: function(cm, vim, command) {
  1311. if (!cm.getSearchCursor) {
  1312. // Search depends on SearchCursor.
  1313. return;
  1314. }
  1315. var forward = command.searchArgs.forward;
  1316. var wholeWordOnly = command.searchArgs.wholeWordOnly;
  1317. getSearchState(cm).setReversed(!forward);
  1318. var promptPrefix = (forward) ? '/' : '?';
  1319. var originalQuery = getSearchState(cm).getQuery();
  1320. var originalScrollPos = cm.getScrollInfo();
  1321. function handleQuery(query, ignoreCase, smartCase) {
  1322. vimGlobalState.searchHistoryController.pushInput(query);
  1323. vimGlobalState.searchHistoryController.reset();
  1324. try {
  1325. updateSearchQuery(cm, query, ignoreCase, smartCase);
  1326. } catch (e) {
  1327. showConfirm(cm, 'Invalid regex: ' + query);
  1328. clearInputState(cm);
  1329. return;
  1330. }
  1331. commandDispatcher.processMotion(cm, vim, {
  1332. type: 'motion',
  1333. motion: 'findNext',
  1334. motionArgs: { forward: true, toJumplist: command.searchArgs.toJumplist }
  1335. });
  1336. }
  1337. function onPromptClose(query) {
  1338. cm.scrollTo(originalScrollPos.left, originalScrollPos.top);
  1339. handleQuery(query, true /** ignoreCase */, true /** smartCase */);
  1340. var macroModeState = vimGlobalState.macroModeState;
  1341. if (macroModeState.isRecording) {
  1342. logSearchQuery(macroModeState, query);
  1343. }
  1344. }
  1345. function onPromptKeyUp(e, query, close) {
  1346. var keyName = CodeMirror.keyName(e), up, offset;
  1347. if (keyName == 'Up' || keyName == 'Down') {
  1348. up = keyName == 'Up' ? true : false;
  1349. offset = e.target ? e.target.selectionEnd : 0;
  1350. query = vimGlobalState.searchHistoryController.nextMatch(query, up) || '';
  1351. close(query);
  1352. if (offset && e.target) e.target.selectionEnd = e.target.selectionStart = Math.min(offset, e.target.value.length);
  1353. } else {
  1354. if ( keyName != 'Left' && keyName != 'Right' && keyName != 'Ctrl' && keyName != 'Alt' && keyName != 'Shift')
  1355. vimGlobalState.searchHistoryController.reset();
  1356. }
  1357. var parsedQuery;
  1358. try {
  1359. parsedQuery = updateSearchQuery(cm, query,
  1360. true /** ignoreCase */, true /** smartCase */);
  1361. } catch (e) {
  1362. // Swallow bad regexes for incremental search.
  1363. }
  1364. if (parsedQuery) {
  1365. cm.scrollIntoView(findNext(cm, !forward, parsedQuery), 30);
  1366. } else {
  1367. clearSearchHighlight(cm);
  1368. cm.scrollTo(originalScrollPos.left, originalScrollPos.top);
  1369. }
  1370. }
  1371. function onPromptKeyDown(e, query, close) {
  1372. var keyName = CodeMirror.keyName(e);
  1373. if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[' ||
  1374. (keyName == 'Backspace' && query == '')) {
  1375. vimGlobalState.searchHistoryController.pushInput(query);
  1376. vimGlobalState.searchHistoryController.reset();
  1377. updateSearchQuery(cm, originalQuery);
  1378. clearSearchHighlight(cm);
  1379. cm.scrollTo(originalScrollPos.left, originalScrollPos.top);
  1380. CodeMirror.e_stop(e);
  1381. clearInputState(cm);
  1382. close();
  1383. cm.focus();
  1384. } else if (keyName == 'Up' || keyName == 'Down') {
  1385. CodeMirror.e_stop(e);
  1386. } else if (keyName == 'Ctrl-U') {
  1387. // Ctrl-U clears input.
  1388. CodeMirror.e_stop(e);
  1389. close('');
  1390. }
  1391. }
  1392. switch (command.searchArgs.querySrc) {
  1393. case 'prompt':
  1394. var macroModeState = vimGlobalState.macroModeState;
  1395. if (macroModeState.isPlaying) {
  1396. var query = macroModeState.replaySearchQueries.shift();
  1397. handleQuery(query, true /** ignoreCase */, false /** smartCase */);
  1398. } else {
  1399. showPrompt(cm, {
  1400. onClose: onPromptClose,
  1401. prefix: promptPrefix,
  1402. desc: '(JavaScript regexp)',
  1403. onKeyUp: onPromptKeyUp,
  1404. onKeyDown: onPromptKeyDown
  1405. });
  1406. }
  1407. break;
  1408. case 'wordUnderCursor':
  1409. var word = expandWordUnderCursor(cm, false /** inclusive */,
  1410. true /** forward */, false /** bigWord */,
  1411. true /** noSymbol */);
  1412. var isKeyword = true;
  1413. if (!word) {
  1414. word = expandWordUnderCursor(cm, false /** inclusive */,
  1415. true /** forward */, false /** bigWord */,
  1416. false /** noSymbol */);
  1417. isKeyword = false;
  1418. }
  1419. if (!word) {
  1420. return;
  1421. }
  1422. var query = cm.getLine(word.start.line).substring(word.start.ch,
  1423. word.end.ch);
  1424. if (isKeyword && wholeWordOnly) {
  1425. query = '\\b' + query + '\\b';
  1426. } else {
  1427. query = escapeRegex(query);
  1428. }
  1429. // cachedCursor is used to save the old position of the cursor
  1430. // when * or # causes vim to seek for the nearest word and shift
  1431. // the cursor before entering the motion.
  1432. vimGlobalState.jumpList.cachedCursor = cm.getCursor();
  1433. cm.setCursor(word.start);
  1434. handleQuery(query, true /** ignoreCase */, false /** smartCase */);
  1435. break;
  1436. }
  1437. },
  1438. processEx: function(cm, vim, command) {
  1439. function onPromptClose(input) {
  1440. // Give the prompt some time to close so that if processCommand shows
  1441. // an error, the elements don't overlap.
  1442. vimGlobalState.exCommandHistoryController.pushInput(input);
  1443. vimGlobalState.exCommandHistoryController.reset();
  1444. exCommandDispatcher.processCommand(cm, input);
  1445. clearInputState(cm);
  1446. }
  1447. function onPromptKeyDown(e, input, close) {
  1448. var keyName = CodeMirror.keyName(e), up, offset;
  1449. if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[' ||
  1450. (keyName == 'Backspace' && input == '')) {
  1451. vimGlobalState.exCommandHistoryController.pushInput(input);
  1452. vimGlobalState.exCommandHistoryController.reset();
  1453. CodeMirror.e_stop(e);
  1454. clearInputState(cm);
  1455. close();
  1456. cm.focus();
  1457. }
  1458. if (keyName == 'Up' || keyName == 'Down') {
  1459. CodeMirror.e_stop(e);
  1460. up = keyName == 'Up' ? true : false;
  1461. offset = e.target ? e.target.selectionEnd : 0;
  1462. input = vimGlobalState.exCommandHistoryController.nextMatch(input, up) || '';
  1463. close(input);
  1464. if (offset && e.target) e.target.selectionEnd = e.target.selectionStart = Math.min(offset, e.target.value.length);
  1465. } else if (keyName == 'Ctrl-U') {
  1466. // Ctrl-U clears input.
  1467. CodeMirror.e_stop(e);
  1468. close('');
  1469. } else {
  1470. if ( keyName != 'Left' && keyName != 'Right' && keyName != 'Ctrl' && keyName != 'Alt' && keyName != 'Shift')
  1471. vimGlobalState.exCommandHistoryController.reset();
  1472. }
  1473. }
  1474. if (command.type == 'keyToEx') {
  1475. // Handle user defined Ex to Ex mappings
  1476. exCommandDispatcher.processCommand(cm, command.exArgs.input);
  1477. } else {
  1478. if (vim.visualMode) {
  1479. showPrompt(cm, { onClose: onPromptClose, prefix: ':', value: '\'<,\'>',
  1480. onKeyDown: onPromptKeyDown, selectValueOnOpen: false});
  1481. } else {
  1482. showPrompt(cm, { onClose: onPromptClose, prefix: ':',
  1483. onKeyDown: onPromptKeyDown});
  1484. }
  1485. }
  1486. },
  1487. evalInput: function(cm, vim) {
  1488. // If the motion command is set, execute both the operator and motion.
  1489. // Otherwise return.
  1490. var inputState = vim.inputState;
  1491. var motion = inputState.motion;
  1492. var motionArgs = inputState.motionArgs || {};
  1493. var operator = inputState.operator;
  1494. var operatorArgs = inputState.operatorArgs || {};
  1495. var registerName = inputState.registerName;
  1496. var sel = vim.sel;
  1497. // TODO: Make sure cm and vim selections are identical outside visual mode.
  1498. var origHead = copyCursor(vim.visualMode ? clipCursorToContent(cm, sel.head): cm.getCursor('head'));
  1499. var origAnchor = copyCursor(vim.visualMode ? clipCursorToContent(cm, sel.anchor) : cm.getCursor('anchor'));
  1500. var oldHead = copyCursor(origHead);
  1501. var oldAnchor = copyCursor(origAnchor);
  1502. var newHead, newAnchor;
  1503. var repeat;
  1504. if (operator) {
  1505. this.recordLastEdit(vim, inputState);
  1506. }
  1507. if (inputState.repeatOverride !== undefined) {
  1508. // If repeatOverride is specified, that takes precedence over the
  1509. // input state's repeat. Used by Ex mode and can be user defined.
  1510. repeat = inputState.repeatOverride;
  1511. } else {
  1512. repeat = inputState.getRepeat();
  1513. }
  1514. if (repeat > 0 && motionArgs.explicitRepeat) {
  1515. motionArgs.repeatIsExplicit = true;
  1516. } else if (motionArgs.noRepeat ||
  1517. (!motionArgs.explicitRepeat && repeat === 0)) {
  1518. repeat = 1;
  1519. motionArgs.repeatIsExplicit = false;
  1520. }
  1521. if (inputState.selectedCharacter) {
  1522. // If there is a character input, stick it in all of the arg arrays.
  1523. motionArgs.selectedCharacter = operatorArgs.selectedCharacter =
  1524. inputState.selectedCharacter;
  1525. }
  1526. motionArgs.repeat = repeat;
  1527. clearInputState(cm);
  1528. if (motion) {
  1529. var motionResult = motions[motion](cm, origHead, motionArgs, vim, inputState);
  1530. vim.lastMotion = motions[motion];
  1531. if (!motionResult) {
  1532. return;
  1533. }
  1534. if (motionArgs.toJumplist) {
  1535. var jumpList = vimGlobalState.jumpList;
  1536. // if the current motion is # or *, use cachedCursor
  1537. var cachedCursor = jumpList.cachedCursor;
  1538. if (cachedCursor) {
  1539. recordJumpPosition(cm, cachedCursor, motionResult);
  1540. delete jumpList.cachedCursor;
  1541. } else {
  1542. recordJumpPosition(cm, origHead, motionResult);
  1543. }
  1544. }
  1545. if (motionResult instanceof Array) {
  1546. newAnchor = motionResult[0];
  1547. newHead = motionResult[1];
  1548. } else {
  1549. newHead = motionResult;
  1550. }
  1551. // TODO: Handle null returns from motion commands better.
  1552. if (!newHead) {
  1553. newHead = copyCursor(origHead);
  1554. }
  1555. if (vim.visualMode) {
  1556. if (!(vim.visualBlock && newHead.ch === Infinity)) {
  1557. newHead = clipCursorToContent(cm, newHead);
  1558. }
  1559. if (newAnchor) {
  1560. newAnchor = clipCursorToContent(cm, newAnchor);
  1561. }
  1562. newAnchor = newAnchor || oldAnchor;
  1563. sel.anchor = newAnchor;
  1564. sel.head = newHead;
  1565. updateCmSelection(cm);
  1566. updateMark(cm, vim, '<',
  1567. cursorIsBefore(newAnchor, newHead) ? newAnchor
  1568. : newHead);
  1569. updateMark(cm, vim, '>',
  1570. cursorIsBefore(newAnchor, newHead) ? newHead
  1571. : newAnchor);
  1572. } else if (!operator) {
  1573. newHead = clipCursorToContent(cm, newHead);
  1574. cm.setCursor(newHead.line, newHead.ch);
  1575. }
  1576. }
  1577. if (operator) {
  1578. if (operatorArgs.lastSel) {
  1579. // Replaying a visual mode operation
  1580. newAnchor = oldAnchor;
  1581. var lastSel = operatorArgs.lastSel;
  1582. var lineOffset = Math.abs(lastSel.head.line - lastSel.anchor.line);
  1583. var chOffset = Math.abs(lastSel.head.ch - lastSel.anchor.ch);
  1584. if (lastSel.visualLine) {
  1585. // Linewise Visual mode: The same number of lines.
  1586. newHead = new Pos(oldAnchor.line + lineOffset, oldAnchor.ch);
  1587. } else if (lastSel.visualBlock) {
  1588. // Blockwise Visual mode: The same number of lines and columns.
  1589. newHead = new Pos(oldAnchor.line + lineOffset, oldAnchor.ch + chOffset);
  1590. } else if (lastSel.head.line == lastSel.anchor.line) {
  1591. // Normal Visual mode within one line: The same number of characters.
  1592. newHead = new Pos(oldAnchor.line, oldAnchor.ch + chOffset);
  1593. } else {
  1594. // Normal Visual mode with several lines: The same number of lines, in the
  1595. // last line the same number of characters as in the last line the last time.
  1596. newHead = new Pos(oldAnchor.line + lineOffset, oldAnchor.ch);
  1597. }
  1598. vim.visualMode = true;
  1599. vim.visualLine = lastSel.visualLine;
  1600. vim.visualBlock = lastSel.visualBlock;
  1601. sel = vim.sel = {
  1602. anchor: newAnchor,
  1603. head: newHead
  1604. };
  1605. updateCmSelection(cm);
  1606. } else if (vim.visualMode) {
  1607. operatorArgs.lastSel = {
  1608. anchor: copyCursor(sel.anchor),
  1609. head: copyCursor(sel.head),
  1610. visualBlock: vim.visualBlock,
  1611. visualLine: vim.visualLine
  1612. };
  1613. }
  1614. var curStart, curEnd, linewise, mode;
  1615. var cmSel;
  1616. if (vim.visualMode) {
  1617. // Init visual op
  1618. curStart = cursorMin(sel.head, sel.anchor);
  1619. curEnd = cursorMax(sel.head, sel.anchor);
  1620. linewise = vim.visualLine || operatorArgs.linewise;
  1621. mode = vim.visualBlock ? 'block' :
  1622. linewise ? 'line' :
  1623. 'char';
  1624. cmSel = makeCmSelection(cm, {
  1625. anchor: curStart,
  1626. head: curEnd
  1627. }, mode);
  1628. if (linewise) {
  1629. var ranges = cmSel.ranges;
  1630. if (mode == 'block') {
  1631. // Linewise operators in visual block mode extend to end of line
  1632. for (var i = 0; i < ranges.length; i++) {
  1633. ranges[i].head.ch = lineLength(cm, ranges[i].head.line);
  1634. }
  1635. } else if (mode == 'line') {
  1636. ranges[0].head = new Pos(ranges[0].head.line + 1, 0);
  1637. }
  1638. }
  1639. } else {
  1640. // Init motion op
  1641. curStart = copyCursor(newAnchor || oldAnchor);
  1642. curEnd = copyCursor(newHead || oldHead);
  1643. if (cursorIsBefore(curEnd, curStart)) {
  1644. var tmp = curStart;
  1645. curStart = curEnd;
  1646. curEnd = tmp;
  1647. }
  1648. linewise = motionArgs.linewise || operatorArgs.linewise;
  1649. if (linewise) {
  1650. // Expand selection to entire line.
  1651. expandSelectionToLine(cm, curStart, curEnd);
  1652. } else if (motionArgs.forward) {
  1653. // Clip to trailing newlines only if the motion goes forward.
  1654. clipToLine(cm, curStart, curEnd);
  1655. }
  1656. mode = 'char';
  1657. var exclusive = !motionArgs.inclusive || linewise;
  1658. cmSel = makeCmSelection(cm, {
  1659. anchor: curStart,
  1660. head: curEnd
  1661. }, mode, exclusive);
  1662. }
  1663. cm.setSelections(cmSel.ranges, cmSel.primary);
  1664. vim.lastMotion = null;
  1665. operatorArgs.repeat = repeat; // For indent in visual mode.
  1666. operatorArgs.registerName = registerName;
  1667. // Keep track of linewise as it affects how paste and change behave.
  1668. operatorArgs.linewise = linewise;
  1669. var operatorMoveTo = operators[operator](
  1670. cm, operatorArgs, cmSel.ranges, oldAnchor, newHead);
  1671. if (vim.visualMode) {
  1672. exitVisualMode(cm, operatorMoveTo != null);
  1673. }
  1674. if (operatorMoveTo) {
  1675. cm.setCursor(operatorMoveTo);
  1676. }
  1677. }
  1678. },
  1679. recordLastEdit: function(vim, inputState, actionCommand) {
  1680. var macroModeState = vimGlobalState.macroModeState;
  1681. if (macroModeState.isPlaying) { return; }
  1682. vim.lastEditInputState = inputState;
  1683. vim.lastEditActionCommand = actionCommand;
  1684. macroModeState.lastInsertModeChanges.changes = [];
  1685. macroModeState.lastInsertModeChanges.expectCursorActivityForChange = false;
  1686. macroModeState.lastInsertModeChanges.visualBlock = vim.visualBlock ? vim.sel.head.line - vim.sel.anchor.line : 0;
  1687. }
  1688. };
  1689. /**
  1690. * typedef {Object{line:number,ch:number}} Cursor An object containing the
  1691. * position of the cursor.
  1692. */
  1693. // All of the functions below return Cursor objects.
  1694. var motions = {
  1695. moveToTopLine: function(cm, _head, motionArgs) {
  1696. var line = getUserVisibleLines(cm).top + motionArgs.repeat -1;
  1697. return new Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line)));
  1698. },
  1699. moveToMiddleLine: function(cm) {
  1700. var range = getUserVisibleLines(cm);
  1701. var line = Math.floor((range.top + range.bottom) * 0.5);
  1702. return new Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line)));
  1703. },
  1704. moveToBottomLine: function(cm, _head, motionArgs) {
  1705. var line = getUserVisibleLines(cm).bottom - motionArgs.repeat +1;
  1706. return new Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line)));
  1707. },
  1708. expandToLine: function(_cm, head, motionArgs) {
  1709. // Expands forward to end of line, and then to next line if repeat is
  1710. // >1. Does not handle backward motion!
  1711. var cur = head;
  1712. return new Pos(cur.line + motionArgs.repeat - 1, Infinity);
  1713. },
  1714. findNext: function(cm, _head, motionArgs) {
  1715. var state = getSearchState(cm);
  1716. var query = state.getQuery();
  1717. if (!query) {
  1718. return;
  1719. }
  1720. var prev = !motionArgs.forward;
  1721. // If search is initiated with ? instead of /, negate direction.
  1722. prev = (state.isReversed()) ? !prev : prev;
  1723. highlightSearchMatches(cm, query);
  1724. return findNext(cm, prev/** prev */, query, motionArgs.repeat);
  1725. },
  1726. /**
  1727. * Find and select the next occurrence of the search query. If the cursor is currently
  1728. * within a match, then find and select the current match. Otherwise, find the next occurrence in the
  1729. * appropriate direction.
  1730. *
  1731. * This differs from `findNext` in the following ways:
  1732. *
  1733. * 1. Instead of only returning the "from", this returns a "from", "to" range.
  1734. * 2. If the cursor is currently inside a search match, this selects the current match
  1735. * instead of the next match.
  1736. * 3. If there is no associated operator, this will turn on visual mode.
  1737. */
  1738. findAndSelectNextInclusive: function(cm, _head, motionArgs, vim, prevInputState) {
  1739. var state = getSearchState(cm);
  1740. var query = state.getQuery();
  1741. if (!query) {
  1742. return;
  1743. }
  1744. var prev = !motionArgs.forward;
  1745. prev = (state.isReversed()) ? !prev : prev;
  1746. // next: [from, to] | null
  1747. var next = findNextFromAndToInclusive(cm, prev, query, motionArgs.repeat, vim);
  1748. // No matches.
  1749. if (!next) {
  1750. return;
  1751. }
  1752. // If there's an operator that will be executed, return the selection.
  1753. if (prevInputState.operator) {
  1754. return next;
  1755. }
  1756. // At this point, we know that there is no accompanying operator -- let's
  1757. // deal with visual mode in order to select an appropriate match.
  1758. var from = next[0];
  1759. // For whatever reason, when we use the "to" as returned by searchcursor.js directly,
  1760. // the resulting selection is extended by 1 char. Let's shrink it so that only the
  1761. // match is selected.
  1762. var to = new Pos(next[1].line, next[1].ch - 1);
  1763. if (vim.visualMode) {
  1764. // If we were in visualLine or visualBlock mode, get out of it.
  1765. if (vim.visualLine || vim.visualBlock) {
  1766. vim.visualLine = false;
  1767. vim.visualBlock = false;
  1768. CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: ""});
  1769. }
  1770. // If we're currently in visual mode, we should extend the selection to include
  1771. // the search result.
  1772. var anchor = vim.sel.anchor;
  1773. if (anchor) {
  1774. if (state.isReversed()) {
  1775. if (motionArgs.forward) {
  1776. return [anchor, from];
  1777. }
  1778. return [anchor, to];
  1779. } else {
  1780. if (motionArgs.forward) {
  1781. return [anchor, to];
  1782. }
  1783. return [anchor, from];
  1784. }
  1785. }
  1786. } else {
  1787. // Let's turn visual mode on.
  1788. vim.visualMode = true;
  1789. vim.visualLine = false;
  1790. vim.visualBlock = false;
  1791. CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: ""});
  1792. }
  1793. return prev ? [to, from] : [from, to];
  1794. },
  1795. goToMark: function(cm, _head, motionArgs, vim) {
  1796. var pos = getMarkPos(cm, vim, motionArgs.selectedCharacter);
  1797. if (pos) {
  1798. return motionArgs.linewise ? { line: pos.line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(pos.line)) } : pos;
  1799. }
  1800. return null;
  1801. },
  1802. moveToOtherHighlightedEnd: function(cm, _head, motionArgs, vim) {
  1803. if (vim.visualBlock && motionArgs.sameLine) {
  1804. var sel = vim.sel;
  1805. return [
  1806. clipCursorToContent(cm, new Pos(sel.anchor.line, sel.head.ch)),
  1807. clipCursorToContent(cm, new Pos(sel.head.line, sel.anchor.ch))
  1808. ];
  1809. } else {
  1810. return ([vim.sel.head, vim.sel.anchor]);
  1811. }
  1812. },
  1813. jumpToMark: function(cm, head, motionArgs, vim) {
  1814. var best = head;
  1815. for (var i = 0; i < motionArgs.repeat; i++) {
  1816. var cursor = best;
  1817. for (var key in vim.marks) {
  1818. if (!isLowerCase(key)) {
  1819. continue;
  1820. }
  1821. var mark = vim.marks[key].find();
  1822. var isWrongDirection = (motionArgs.forward) ?
  1823. cursorIsBefore(mark, cursor) : cursorIsBefore(cursor, mark);
  1824. if (isWrongDirection) {
  1825. continue;
  1826. }
  1827. if (motionArgs.linewise && (mark.line == cursor.line)) {
  1828. continue;
  1829. }
  1830. var equal = cursorEqual(cursor, best);
  1831. var between = (motionArgs.forward) ?
  1832. cursorIsBetween(cursor, mark, best) :
  1833. cursorIsBetween(best, mark, cursor);
  1834. if (equal || between) {
  1835. best = mark;
  1836. }
  1837. }
  1838. }
  1839. if (motionArgs.linewise) {
  1840. // Vim places the cursor on the first non-whitespace character of
  1841. // the line if there is one, else it places the cursor at the end
  1842. // of the line, regardless of whether a mark was found.
  1843. best = new Pos(best.line, findFirstNonWhiteSpaceCharacter(cm.getLine(best.line)));
  1844. }
  1845. return best;
  1846. },
  1847. moveByCharacters: function(_cm, head, motionArgs) {
  1848. var cur = head;
  1849. var repeat = motionArgs.repeat;
  1850. var ch = motionArgs.forward ? cur.ch + repeat : cur.ch - repeat;
  1851. return new Pos(cur.line, ch);
  1852. },
  1853. moveByLines: function(cm, head, motionArgs, vim) {
  1854. var cur = head;
  1855. var endCh = cur.ch;
  1856. // Depending what our last motion was, we may want to do different
  1857. // things. If our last motion was moving vertically, we want to
  1858. // preserve the HPos from our last horizontal move. If our last motion
  1859. // was going to the end of a line, moving vertically we should go to
  1860. // the end of the line, etc.
  1861. switch (vim.lastMotion) {
  1862. case this.moveByLines:
  1863. case this.moveByDisplayLines:
  1864. case this.moveByScroll:
  1865. case this.moveToColumn:
  1866. case this.moveToEol:
  1867. endCh = vim.lastHPos;
  1868. break;
  1869. default:
  1870. vim.lastHPos = endCh;
  1871. }
  1872. var repeat = motionArgs.repeat+(motionArgs.repeatOffset||0);
  1873. var line = motionArgs.forward ? cur.line + repeat : cur.line - repeat;
  1874. var first = cm.firstLine();
  1875. var last = cm.lastLine();
  1876. var posV = cm.findPosV(cur, (motionArgs.forward ? repeat : -repeat), 'line', vim.lastHSPos);
  1877. var hasMarkedText = motionArgs.forward ? posV.line > line : posV.line < line;
  1878. if (hasMarkedText) {
  1879. line = posV.line;
  1880. endCh = posV.ch;
  1881. }
  1882. // Vim go to line begin or line end when cursor at first/last line and
  1883. // move to previous/next line is triggered.
  1884. if (line < first && cur.line == first){
  1885. return this.moveToStartOfLine(cm, head, motionArgs, vim);
  1886. } else if (line > last && cur.line == last){
  1887. return moveToEol(cm, head, motionArgs, vim, true);
  1888. }
  1889. if (motionArgs.toFirstChar){
  1890. endCh=findFirstNonWhiteSpaceCharacter(cm.getLine(line));
  1891. vim.lastHPos = endCh;
  1892. }
  1893. vim.lastHSPos = cm.charCoords(new Pos(line, endCh),'div').left;
  1894. return new Pos(line, endCh);
  1895. },
  1896. moveByDisplayLines: function(cm, head, motionArgs, vim) {
  1897. var cur = head;
  1898. switch (vim.lastMotion) {
  1899. case this.moveByDisplayLines:
  1900. case this.moveByScroll:
  1901. case this.moveByLines:
  1902. case this.moveToColumn:
  1903. case this.moveToEol:
  1904. break;
  1905. default:
  1906. vim.lastHSPos = cm.charCoords(cur,'div').left;
  1907. }
  1908. var repeat = motionArgs.repeat;
  1909. var res=cm.findPosV(cur,(motionArgs.forward ? repeat : -repeat),'line',vim.lastHSPos);
  1910. if (res.hitSide) {
  1911. if (motionArgs.forward) {
  1912. var lastCharCoords = cm.charCoords(res, 'div');
  1913. var goalCoords = { top: lastCharCoords.top + 8, left: vim.lastHSPos };
  1914. var res = cm.coordsChar(goalCoords, 'div');
  1915. } else {
  1916. var resCoords = cm.charCoords(new Pos(cm.firstLine(), 0), 'div');
  1917. resCoords.left = vim.lastHSPos;
  1918. res = cm.coordsChar(resCoords, 'div');
  1919. }
  1920. }
  1921. vim.lastHPos = res.ch;
  1922. return res;
  1923. },
  1924. moveByPage: function(cm, head, motionArgs) {
  1925. // CodeMirror only exposes functions that move the cursor page down, so
  1926. // doing this bad hack to move the cursor and move it back. evalInput
  1927. // will move the cursor to where it should be in the end.
  1928. var curStart = head;
  1929. var repeat = motionArgs.repeat;
  1930. return cm.findPosV(curStart, (motionArgs.forward ? repeat : -repeat), 'page');
  1931. },
  1932. moveByParagraph: function(cm, head, motionArgs) {
  1933. var dir = motionArgs.forward ? 1 : -1;
  1934. return findParagraph(cm, head, motionArgs.repeat, dir);
  1935. },
  1936. moveBySentence: function(cm, head, motionArgs) {
  1937. var dir = motionArgs.forward ? 1 : -1;
  1938. return findSentence(cm, head, motionArgs.repeat, dir);
  1939. },
  1940. moveByScroll: function(cm, head, motionArgs, vim) {
  1941. var scrollbox = cm.getScrollInfo();
  1942. var curEnd = null;
  1943. var repeat = motionArgs.repeat;
  1944. if (!repeat) {
  1945. repeat = scrollbox.clientHeight / (2 * cm.defaultTextHeight());
  1946. }
  1947. var orig = cm.charCoords(head, 'local');
  1948. motionArgs.repeat = repeat;
  1949. curEnd = motions.moveByDisplayLines(cm, head, motionArgs, vim);
  1950. if (!curEnd) {
  1951. return null;
  1952. }
  1953. var dest = cm.charCoords(curEnd, 'local');
  1954. cm.scrollTo(null, scrollbox.top + dest.top - orig.top);
  1955. return curEnd;
  1956. },
  1957. moveByWords: function(cm, head, motionArgs) {
  1958. return moveToWord(cm, head, motionArgs.repeat, !!motionArgs.forward,
  1959. !!motionArgs.wordEnd, !!motionArgs.bigWord);
  1960. },
  1961. moveTillCharacter: function(cm, _head, motionArgs) {
  1962. var repeat = motionArgs.repeat;
  1963. var curEnd = moveToCharacter(cm, repeat, motionArgs.forward,
  1964. motionArgs.selectedCharacter);
  1965. var increment = motionArgs.forward ? -1 : 1;
  1966. recordLastCharacterSearch(increment, motionArgs);
  1967. if (!curEnd) return null;
  1968. curEnd.ch += increment;
  1969. return curEnd;
  1970. },
  1971. moveToCharacter: function(cm, head, motionArgs) {
  1972. var repeat = motionArgs.repeat;
  1973. recordLastCharacterSearch(0, motionArgs);
  1974. return moveToCharacter(cm, repeat, motionArgs.forward,
  1975. motionArgs.selectedCharacter) || head;
  1976. },
  1977. moveToSymbol: function(cm, head, motionArgs) {
  1978. var repeat = motionArgs.repeat;
  1979. return findSymbol(cm, repeat, motionArgs.forward,
  1980. motionArgs.selectedCharacter) || head;
  1981. },
  1982. moveToColumn: function(cm, head, motionArgs, vim) {
  1983. var repeat = motionArgs.repeat;
  1984. // repeat is equivalent to which column we want to move to!
  1985. vim.lastHPos = repeat - 1;
  1986. vim.lastHSPos = cm.charCoords(head,'div').left;
  1987. return moveToColumn(cm, repeat);
  1988. },
  1989. moveToEol: function(cm, head, motionArgs, vim) {
  1990. return moveToEol(cm, head, motionArgs, vim, false);
  1991. },
  1992. moveToFirstNonWhiteSpaceCharacter: function(cm, head) {
  1993. // Go to the start of the line where the text begins, or the end for
  1994. // whitespace-only lines
  1995. var cursor = head;
  1996. return new Pos(cursor.line,
  1997. findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line)));
  1998. },
  1999. moveToMatchedSymbol: function(cm, head) {
  2000. var cursor = head;
  2001. var line = cursor.line;
  2002. var ch = cursor.ch;
  2003. var lineText = cm.getLine(line);
  2004. var symbol;
  2005. for (; ch < lineText.length; ch++) {
  2006. symbol = lineText.charAt(ch);
  2007. if (symbol && isMatchableSymbol(symbol)) {
  2008. var style = cm.getTokenTypeAt(new Pos(line, ch + 1));
  2009. if (style !== "string" && style !== "comment") {
  2010. break;
  2011. }
  2012. }
  2013. }
  2014. if (ch < lineText.length) {
  2015. // Only include angle brackets in analysis if they are being matched.
  2016. var re = (ch === '<' || ch === '>') ? /[(){}[\]<>]/ : /[(){}[\]]/;
  2017. var matched = cm.findMatchingBracket(new Pos(line, ch), {bracketRegex: re});
  2018. return matched.to;
  2019. } else {
  2020. return cursor;
  2021. }
  2022. },
  2023. moveToStartOfLine: function(_cm, head) {
  2024. return new Pos(head.line, 0);
  2025. },
  2026. moveToLineOrEdgeOfDocument: function(cm, _head, motionArgs) {
  2027. var lineNum = motionArgs.forward ? cm.lastLine() : cm.firstLine();
  2028. if (motionArgs.repeatIsExplicit) {
  2029. lineNum = motionArgs.repeat - cm.getOption('firstLineNumber');
  2030. }
  2031. return new Pos(lineNum,
  2032. findFirstNonWhiteSpaceCharacter(cm.getLine(lineNum)));
  2033. },
  2034. moveToStartOfDisplayLine: function(cm) {
  2035. cm.execCommand("goLineLeft");
  2036. return cm.getCursor();
  2037. },
  2038. moveToEndOfDisplayLine: function(cm) {
  2039. cm.execCommand("goLineRight");
  2040. var head = cm.getCursor();
  2041. if (head.sticky == "before") head.ch--;
  2042. return head;
  2043. },
  2044. textObjectManipulation: function(cm, head, motionArgs, vim) {
  2045. // TODO: lots of possible exceptions that can be thrown here. Try da(
  2046. // outside of a () block.
  2047. var mirroredPairs = {'(': ')', ')': '(',
  2048. '{': '}', '}': '{',
  2049. '[': ']', ']': '[',
  2050. '<': '>', '>': '<'};
  2051. var selfPaired = {'\'': true, '"': true, '`': true};
  2052. var character = motionArgs.selectedCharacter;
  2053. // 'b' refers to '()' block.
  2054. // 'B' refers to '{}' block.
  2055. if (character == 'b') {
  2056. character = '(';
  2057. } else if (character == 'B') {
  2058. character = '{';
  2059. }
  2060. // Inclusive is the difference between a and i
  2061. // TODO: Instead of using the additional text object map to perform text
  2062. // object operations, merge the map into the defaultKeyMap and use
  2063. // motionArgs to define behavior. Define separate entries for 'aw',
  2064. // 'iw', 'a[', 'i[', etc.
  2065. var inclusive = !motionArgs.textObjectInner;
  2066. var tmp;
  2067. if (mirroredPairs[character]) {
  2068. tmp = selectCompanionObject(cm, head, character, inclusive);
  2069. } else if (selfPaired[character]) {
  2070. tmp = findBeginningAndEnd(cm, head, character, inclusive);
  2071. } else if (character === 'W') {
  2072. tmp = expandWordUnderCursor(cm, inclusive, true /** forward */,
  2073. true /** bigWord */);
  2074. } else if (character === 'w') {
  2075. tmp = expandWordUnderCursor(cm, inclusive, true /** forward */,
  2076. false /** bigWord */);
  2077. } else if (character === 'p') {
  2078. tmp = findParagraph(cm, head, motionArgs.repeat, 0, inclusive);
  2079. motionArgs.linewise = true;
  2080. if (vim.visualMode) {
  2081. if (!vim.visualLine) { vim.visualLine = true; }
  2082. } else {
  2083. var operatorArgs = vim.inputState.operatorArgs;
  2084. if (operatorArgs) { operatorArgs.linewise = true; }
  2085. tmp.end.line--;
  2086. }
  2087. } else if (character === 't') {
  2088. tmp = expandTagUnderCursor(cm, head, inclusive);
  2089. } else if (character === 's') {
  2090. // account for cursor on end of sentence symbol
  2091. var content = cm.getLine(head.line);
  2092. if (head.ch > 0 && isEndOfSentenceSymbol(content[head.ch])) {
  2093. head.ch -= 1;
  2094. }
  2095. var end = getSentence(cm, head, motionArgs.repeat, 1, inclusive);
  2096. var start = getSentence(cm, head, motionArgs.repeat, -1, inclusive);
  2097. // closer vim behaviour, 'a' only takes the space after the sentence if there is one before and after
  2098. if (isWhiteSpaceString(cm.getLine(start.line)[start.ch])
  2099. && isWhiteSpaceString(cm.getLine(end.line)[end.ch -1])) {
  2100. start = {line: start.line, ch: start.ch + 1};
  2101. }
  2102. tmp = {start: start, end: end};
  2103. } else {
  2104. // No text object defined for this, don't move.
  2105. return null;
  2106. }
  2107. if (!cm.state.vim.visualMode) {
  2108. return [tmp.start, tmp.end];
  2109. } else {
  2110. return expandSelection(cm, tmp.start, tmp.end);
  2111. }
  2112. },
  2113. repeatLastCharacterSearch: function(cm, head, motionArgs) {
  2114. var lastSearch = vimGlobalState.lastCharacterSearch;
  2115. var repeat = motionArgs.repeat;
  2116. var forward = motionArgs.forward === lastSearch.forward;
  2117. var increment = (lastSearch.increment ? 1 : 0) * (forward ? -1 : 1);
  2118. cm.moveH(-increment, 'char');
  2119. motionArgs.inclusive = forward ? true : false;
  2120. var curEnd = moveToCharacter(cm, repeat, forward, lastSearch.selectedCharacter);
  2121. if (!curEnd) {
  2122. cm.moveH(increment, 'char');
  2123. return head;
  2124. }
  2125. curEnd.ch += increment;
  2126. return curEnd;
  2127. }
  2128. };
  2129. function defineMotion(name, fn) {
  2130. motions[name] = fn;
  2131. }
  2132. function fillArray(val, times) {
  2133. var arr = [];
  2134. for (var i = 0; i < times; i++) {
  2135. arr.push(val);
  2136. }
  2137. return arr;
  2138. }
  2139. /**
  2140. * An operator acts on a text selection. It receives the list of selections
  2141. * as input. The corresponding CodeMirror selection is guaranteed to
  2142. * match the input selection.
  2143. */
  2144. var operators = {
  2145. change: function(cm, args, ranges) {
  2146. var finalHead, text;
  2147. var vim = cm.state.vim;
  2148. var anchor = ranges[0].anchor,
  2149. head = ranges[0].head;
  2150. if (!vim.visualMode) {
  2151. text = cm.getRange(anchor, head);
  2152. var lastState = vim.lastEditInputState || {};
  2153. if (lastState.motion == "moveByWords" && !isWhiteSpaceString(text)) {
  2154. // Exclude trailing whitespace if the range is not all whitespace.
  2155. var match = (/\s+$/).exec(text);
  2156. if (match && lastState.motionArgs && lastState.motionArgs.forward) {
  2157. head = offsetCursor(head, 0, - match[0].length);
  2158. text = text.slice(0, - match[0].length);
  2159. }
  2160. }
  2161. var prevLineEnd = new Pos(anchor.line - 1, Number.MAX_VALUE);
  2162. var wasLastLine = cm.firstLine() == cm.lastLine();
  2163. if (head.line > cm.lastLine() && args.linewise && !wasLastLine) {
  2164. cm.replaceRange('', prevLineEnd, head);
  2165. } else {
  2166. cm.replaceRange('', anchor, head);
  2167. }
  2168. if (args.linewise) {
  2169. // Push the next line back down, if there is a next line.
  2170. if (!wasLastLine) {
  2171. cm.setCursor(prevLineEnd);
  2172. CodeMirror.commands.newlineAndIndent(cm);
  2173. }
  2174. // make sure cursor ends up at the end of the line.
  2175. anchor.ch = Number.MAX_VALUE;
  2176. }
  2177. finalHead = anchor;
  2178. } else if (args.fullLine) {
  2179. head.ch = Number.MAX_VALUE;
  2180. head.line--;
  2181. cm.setSelection(anchor, head);
  2182. text = cm.getSelection();
  2183. cm.replaceSelection("");
  2184. finalHead = anchor;
  2185. } else {
  2186. text = cm.getSelection();
  2187. var replacement = fillArray('', ranges.length);
  2188. cm.replaceSelections(replacement);
  2189. finalHead = cursorMin(ranges[0].head, ranges[0].anchor);
  2190. }
  2191. vimGlobalState.registerController.pushText(
  2192. args.registerName, 'change', text,
  2193. args.linewise, ranges.length > 1);
  2194. actions.enterInsertMode(cm, {head: finalHead}, cm.state.vim);
  2195. },
  2196. // delete is a javascript keyword.
  2197. 'delete': function(cm, args, ranges) {
  2198. var finalHead, text;
  2199. var vim = cm.state.vim;
  2200. if (!vim.visualBlock) {
  2201. var anchor = ranges[0].anchor,
  2202. head = ranges[0].head;
  2203. if (args.linewise &&
  2204. head.line != cm.firstLine() &&
  2205. anchor.line == cm.lastLine() &&
  2206. anchor.line == head.line - 1) {
  2207. // Special case for dd on last line (and first line).
  2208. if (anchor.line == cm.firstLine()) {
  2209. anchor.ch = 0;
  2210. } else {
  2211. anchor = new Pos(anchor.line - 1, lineLength(cm, anchor.line - 1));
  2212. }
  2213. }
  2214. text = cm.getRange(anchor, head);
  2215. cm.replaceRange('', anchor, head);
  2216. finalHead = anchor;
  2217. if (args.linewise) {
  2218. finalHead = motions.moveToFirstNonWhiteSpaceCharacter(cm, anchor);
  2219. }
  2220. } else {
  2221. text = cm.getSelection();
  2222. var replacement = fillArray('', ranges.length);
  2223. cm.replaceSelections(replacement);
  2224. finalHead = cursorMin(ranges[0].head, ranges[0].anchor);
  2225. }
  2226. vimGlobalState.registerController.pushText(
  2227. args.registerName, 'delete', text,
  2228. args.linewise, vim.visualBlock);
  2229. return clipCursorToContent(cm, finalHead);
  2230. },
  2231. indent: function(cm, args, ranges) {
  2232. var vim = cm.state.vim;
  2233. if (cm.indentMore) {
  2234. var repeat = (vim.visualMode) ? args.repeat : 1;
  2235. for (var j = 0; j < repeat; j++) {
  2236. if (args.indentRight) cm.indentMore();
  2237. else cm.indentLess();
  2238. }
  2239. } else {
  2240. var startLine = ranges[0].anchor.line;
  2241. var endLine = vim.visualBlock ?
  2242. ranges[ranges.length - 1].anchor.line :
  2243. ranges[0].head.line;
  2244. // In visual mode, n> shifts the selection right n times, instead of
  2245. // shifting n lines right once.
  2246. var repeat = (vim.visualMode) ? args.repeat : 1;
  2247. if (args.linewise) {
  2248. // The only way to delete a newline is to delete until the start of
  2249. // the next line, so in linewise mode evalInput will include the next
  2250. // line. We don't want this in indent, so we go back a line.
  2251. endLine--;
  2252. }
  2253. for (var i = startLine; i <= endLine; i++) {
  2254. for (var j = 0; j < repeat; j++) {
  2255. cm.indentLine(i, args.indentRight);
  2256. }
  2257. }
  2258. }
  2259. return motions.moveToFirstNonWhiteSpaceCharacter(cm, ranges[0].anchor);
  2260. },
  2261. indentAuto: function(cm, _args, ranges) {
  2262. cm.execCommand("indentAuto");
  2263. return motions.moveToFirstNonWhiteSpaceCharacter(cm, ranges[0].anchor);
  2264. },
  2265. changeCase: function(cm, args, ranges, oldAnchor, newHead) {
  2266. var selections = cm.getSelections();
  2267. var swapped = [];
  2268. var toLower = args.toLower;
  2269. for (var j = 0; j < selections.length; j++) {
  2270. var toSwap = selections[j];
  2271. var text = '';
  2272. if (toLower === true) {
  2273. text = toSwap.toLowerCase();
  2274. } else if (toLower === false) {
  2275. text = toSwap.toUpperCase();
  2276. } else {
  2277. for (var i = 0; i < toSwap.length; i++) {
  2278. var character = toSwap.charAt(i);
  2279. text += isUpperCase(character) ? character.toLowerCase() :
  2280. character.toUpperCase();
  2281. }
  2282. }
  2283. swapped.push(text);
  2284. }
  2285. cm.replaceSelections(swapped);
  2286. if (args.shouldMoveCursor){
  2287. return newHead;
  2288. } else if (!cm.state.vim.visualMode && args.linewise && ranges[0].anchor.line + 1 == ranges[0].head.line) {
  2289. return motions.moveToFirstNonWhiteSpaceCharacter(cm, oldAnchor);
  2290. } else if (args.linewise){
  2291. return oldAnchor;
  2292. } else {
  2293. return cursorMin(ranges[0].anchor, ranges[0].head);
  2294. }
  2295. },
  2296. yank: function(cm, args, ranges, oldAnchor) {
  2297. var vim = cm.state.vim;
  2298. var text = cm.getSelection();
  2299. var endPos = vim.visualMode
  2300. ? cursorMin(vim.sel.anchor, vim.sel.head, ranges[0].head, ranges[0].anchor)
  2301. : oldAnchor;
  2302. vimGlobalState.registerController.pushText(
  2303. args.registerName, 'yank',
  2304. text, args.linewise, vim.visualBlock);
  2305. return endPos;
  2306. }
  2307. };
  2308. function defineOperator(name, fn) {
  2309. operators[name] = fn;
  2310. }
  2311. var actions = {
  2312. jumpListWalk: function(cm, actionArgs, vim) {
  2313. if (vim.visualMode) {
  2314. return;
  2315. }
  2316. var repeat = actionArgs.repeat;
  2317. var forward = actionArgs.forward;
  2318. var jumpList = vimGlobalState.jumpList;
  2319. var mark = jumpList.move(cm, forward ? repeat : -repeat);
  2320. var markPos = mark ? mark.find() : undefined;
  2321. markPos = markPos ? markPos : cm.getCursor();
  2322. cm.setCursor(markPos);
  2323. },
  2324. scroll: function(cm, actionArgs, vim) {
  2325. if (vim.visualMode) {
  2326. return;
  2327. }
  2328. var repeat = actionArgs.repeat || 1;
  2329. var lineHeight = cm.defaultTextHeight();
  2330. var top = cm.getScrollInfo().top;
  2331. var delta = lineHeight * repeat;
  2332. var newPos = actionArgs.forward ? top + delta : top - delta;
  2333. var cursor = copyCursor(cm.getCursor());
  2334. var cursorCoords = cm.charCoords(cursor, 'local');
  2335. if (actionArgs.forward) {
  2336. if (newPos > cursorCoords.top) {
  2337. cursor.line += (newPos - cursorCoords.top) / lineHeight;
  2338. cursor.line = Math.ceil(cursor.line);
  2339. cm.setCursor(cursor);
  2340. cursorCoords = cm.charCoords(cursor, 'local');
  2341. cm.scrollTo(null, cursorCoords.top);
  2342. } else {
  2343. // Cursor stays within bounds. Just reposition the scroll window.
  2344. cm.scrollTo(null, newPos);
  2345. }
  2346. } else {
  2347. var newBottom = newPos + cm.getScrollInfo().clientHeight;
  2348. if (newBottom < cursorCoords.bottom) {
  2349. cursor.line -= (cursorCoords.bottom - newBottom) / lineHeight;
  2350. cursor.line = Math.floor(cursor.line);
  2351. cm.setCursor(cursor);
  2352. cursorCoords = cm.charCoords(cursor, 'local');
  2353. cm.scrollTo(
  2354. null, cursorCoords.bottom - cm.getScrollInfo().clientHeight);
  2355. } else {
  2356. // Cursor stays within bounds. Just reposition the scroll window.
  2357. cm.scrollTo(null, newPos);
  2358. }
  2359. }
  2360. },
  2361. scrollToCursor: function(cm, actionArgs) {
  2362. var lineNum = cm.getCursor().line;
  2363. var charCoords = cm.charCoords(new Pos(lineNum, 0), 'local');
  2364. var height = cm.getScrollInfo().clientHeight;
  2365. var y = charCoords.top;
  2366. switch (actionArgs.position) {
  2367. case 'center': y = charCoords.bottom - height / 2;
  2368. break;
  2369. case 'bottom':
  2370. var lineLastCharPos = new Pos(lineNum, cm.getLine(lineNum).length - 1);
  2371. var lineLastCharCoords = cm.charCoords(lineLastCharPos, 'local');
  2372. var lineHeight = lineLastCharCoords.bottom - y;
  2373. y = y - height + lineHeight;
  2374. break;
  2375. }
  2376. cm.scrollTo(null, y);
  2377. },
  2378. replayMacro: function(cm, actionArgs, vim) {
  2379. var registerName = actionArgs.selectedCharacter;
  2380. var repeat = actionArgs.repeat;
  2381. var macroModeState = vimGlobalState.macroModeState;
  2382. if (registerName == '@') {
  2383. registerName = macroModeState.latestRegister;
  2384. } else {
  2385. macroModeState.latestRegister = registerName;
  2386. }
  2387. while(repeat--){
  2388. executeMacroRegister(cm, vim, macroModeState, registerName);
  2389. }
  2390. },
  2391. enterMacroRecordMode: function(cm, actionArgs) {
  2392. var macroModeState = vimGlobalState.macroModeState;
  2393. var registerName = actionArgs.selectedCharacter;
  2394. if (vimGlobalState.registerController.isValidRegister(registerName)) {
  2395. macroModeState.enterMacroRecordMode(cm, registerName);
  2396. }
  2397. },
  2398. toggleOverwrite: function(cm) {
  2399. if (!cm.state.overwrite) {
  2400. cm.toggleOverwrite(true);
  2401. cm.setOption('keyMap', 'vim-replace');
  2402. CodeMirror.signal(cm, "vim-mode-change", {mode: "replace"});
  2403. } else {
  2404. cm.toggleOverwrite(false);
  2405. cm.setOption('keyMap', 'vim-insert');
  2406. CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"});
  2407. }
  2408. },
  2409. enterInsertMode: function(cm, actionArgs, vim) {
  2410. if (cm.getOption('readOnly')) { return; }
  2411. vim.insertMode = true;
  2412. vim.insertModeRepeat = actionArgs && actionArgs.repeat || 1;
  2413. var insertAt = (actionArgs) ? actionArgs.insertAt : null;
  2414. var sel = vim.sel;
  2415. var head = actionArgs.head || cm.getCursor('head');
  2416. var height = cm.listSelections().length;
  2417. if (insertAt == 'eol') {
  2418. head = new Pos(head.line, lineLength(cm, head.line));
  2419. } else if (insertAt == 'bol') {
  2420. head = new Pos(head.line, 0);
  2421. } else if (insertAt == 'charAfter') {
  2422. head = offsetCursor(head, 0, 1);
  2423. } else if (insertAt == 'firstNonBlank') {
  2424. head = motions.moveToFirstNonWhiteSpaceCharacter(cm, head);
  2425. } else if (insertAt == 'startOfSelectedArea') {
  2426. if (!vim.visualMode)
  2427. return;
  2428. if (!vim.visualBlock) {
  2429. if (sel.head.line < sel.anchor.line) {
  2430. head = sel.head;
  2431. } else {
  2432. head = new Pos(sel.anchor.line, 0);
  2433. }
  2434. } else {
  2435. head = new Pos(
  2436. Math.min(sel.head.line, sel.anchor.line),
  2437. Math.min(sel.head.ch, sel.anchor.ch));
  2438. height = Math.abs(sel.head.line - sel.anchor.line) + 1;
  2439. }
  2440. } else if (insertAt == 'endOfSelectedArea') {
  2441. if (!vim.visualMode)
  2442. return;
  2443. if (!vim.visualBlock) {
  2444. if (sel.head.line >= sel.anchor.line) {
  2445. head = offsetCursor(sel.head, 0, 1);
  2446. } else {
  2447. head = new Pos(sel.anchor.line, 0);
  2448. }
  2449. } else {
  2450. head = new Pos(
  2451. Math.min(sel.head.line, sel.anchor.line),
  2452. Math.max(sel.head.ch, sel.anchor.ch) + 1);
  2453. height = Math.abs(sel.head.line - sel.anchor.line) + 1;
  2454. }
  2455. } else if (insertAt == 'inplace') {
  2456. if (vim.visualMode){
  2457. return;
  2458. }
  2459. } else if (insertAt == 'lastEdit') {
  2460. head = getLastEditPos(cm) || head;
  2461. }
  2462. cm.setOption('disableInput', false);
  2463. if (actionArgs && actionArgs.replace) {
  2464. // Handle Replace-mode as a special case of insert mode.
  2465. cm.toggleOverwrite(true);
  2466. cm.setOption('keyMap', 'vim-replace');
  2467. CodeMirror.signal(cm, "vim-mode-change", {mode: "replace"});
  2468. } else {
  2469. cm.toggleOverwrite(false);
  2470. cm.setOption('keyMap', 'vim-insert');
  2471. CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"});
  2472. }
  2473. if (!vimGlobalState.macroModeState.isPlaying) {
  2474. // Only record if not replaying.
  2475. cm.on('change', onChange);
  2476. CodeMirror.on(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown);
  2477. }
  2478. if (vim.visualMode) {
  2479. exitVisualMode(cm);
  2480. }
  2481. selectForInsert(cm, head, height);
  2482. },
  2483. toggleVisualMode: function(cm, actionArgs, vim) {
  2484. var repeat = actionArgs.repeat;
  2485. var anchor = cm.getCursor();
  2486. var head;
  2487. // TODO: The repeat should actually select number of characters/lines
  2488. // equal to the repeat times the size of the previous visual
  2489. // operation.
  2490. if (!vim.visualMode) {
  2491. // Entering visual mode
  2492. vim.visualMode = true;
  2493. vim.visualLine = !!actionArgs.linewise;
  2494. vim.visualBlock = !!actionArgs.blockwise;
  2495. head = clipCursorToContent(
  2496. cm, new Pos(anchor.line, anchor.ch + repeat - 1));
  2497. vim.sel = {
  2498. anchor: anchor,
  2499. head: head
  2500. };
  2501. CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : vim.visualBlock ? "blockwise" : ""});
  2502. updateCmSelection(cm);
  2503. updateMark(cm, vim, '<', cursorMin(anchor, head));
  2504. updateMark(cm, vim, '>', cursorMax(anchor, head));
  2505. } else if (vim.visualLine ^ actionArgs.linewise ||
  2506. vim.visualBlock ^ actionArgs.blockwise) {
  2507. // Toggling between modes
  2508. vim.visualLine = !!actionArgs.linewise;
  2509. vim.visualBlock = !!actionArgs.blockwise;
  2510. CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : vim.visualBlock ? "blockwise" : ""});
  2511. updateCmSelection(cm);
  2512. } else {
  2513. exitVisualMode(cm);
  2514. }
  2515. },
  2516. reselectLastSelection: function(cm, _actionArgs, vim) {
  2517. var lastSelection = vim.lastSelection;
  2518. if (vim.visualMode) {
  2519. updateLastSelection(cm, vim);
  2520. }
  2521. if (lastSelection) {
  2522. var anchor = lastSelection.anchorMark.find();
  2523. var head = lastSelection.headMark.find();
  2524. if (!anchor || !head) {
  2525. // If the marks have been destroyed due to edits, do nothing.
  2526. return;
  2527. }
  2528. vim.sel = {
  2529. anchor: anchor,
  2530. head: head
  2531. };
  2532. vim.visualMode = true;
  2533. vim.visualLine = lastSelection.visualLine;
  2534. vim.visualBlock = lastSelection.visualBlock;
  2535. updateCmSelection(cm);
  2536. updateMark(cm, vim, '<', cursorMin(anchor, head));
  2537. updateMark(cm, vim, '>', cursorMax(anchor, head));
  2538. CodeMirror.signal(cm, 'vim-mode-change', {
  2539. mode: 'visual',
  2540. subMode: vim.visualLine ? 'linewise' :
  2541. vim.visualBlock ? 'blockwise' : ''});
  2542. }
  2543. },
  2544. joinLines: function(cm, actionArgs, vim) {
  2545. var curStart, curEnd;
  2546. if (vim.visualMode) {
  2547. curStart = cm.getCursor('anchor');
  2548. curEnd = cm.getCursor('head');
  2549. if (cursorIsBefore(curEnd, curStart)) {
  2550. var tmp = curEnd;
  2551. curEnd = curStart;
  2552. curStart = tmp;
  2553. }
  2554. curEnd.ch = lineLength(cm, curEnd.line) - 1;
  2555. } else {
  2556. // Repeat is the number of lines to join. Minimum 2 lines.
  2557. var repeat = Math.max(actionArgs.repeat, 2);
  2558. curStart = cm.getCursor();
  2559. curEnd = clipCursorToContent(cm, new Pos(curStart.line + repeat - 1,
  2560. Infinity));
  2561. }
  2562. var finalCh = 0;
  2563. for (var i = curStart.line; i < curEnd.line; i++) {
  2564. finalCh = lineLength(cm, curStart.line);
  2565. var tmp = new Pos(curStart.line + 1,
  2566. lineLength(cm, curStart.line + 1));
  2567. var text = cm.getRange(curStart, tmp);
  2568. text = actionArgs.keepSpaces
  2569. ? text.replace(/\n\r?/g, '')
  2570. : text.replace(/\n\s*/g, ' ');
  2571. cm.replaceRange(text, curStart, tmp);
  2572. }
  2573. var curFinalPos = new Pos(curStart.line, finalCh);
  2574. if (vim.visualMode) {
  2575. exitVisualMode(cm, false);
  2576. }
  2577. cm.setCursor(curFinalPos);
  2578. },
  2579. newLineAndEnterInsertMode: function(cm, actionArgs, vim) {
  2580. vim.insertMode = true;
  2581. var insertAt = copyCursor(cm.getCursor());
  2582. if (insertAt.line === cm.firstLine() && !actionArgs.after) {
  2583. // Special case for inserting newline before start of document.
  2584. cm.replaceRange('\n', new Pos(cm.firstLine(), 0));
  2585. cm.setCursor(cm.firstLine(), 0);
  2586. } else {
  2587. insertAt.line = (actionArgs.after) ? insertAt.line :
  2588. insertAt.line - 1;
  2589. insertAt.ch = lineLength(cm, insertAt.line);
  2590. cm.setCursor(insertAt);
  2591. var newlineFn = CodeMirror.commands.newlineAndIndentContinueComment ||
  2592. CodeMirror.commands.newlineAndIndent;
  2593. newlineFn(cm);
  2594. }
  2595. this.enterInsertMode(cm, { repeat: actionArgs.repeat }, vim);
  2596. },
  2597. paste: function(cm, actionArgs, vim) {
  2598. var cur = copyCursor(cm.getCursor());
  2599. var register = vimGlobalState.registerController.getRegister(
  2600. actionArgs.registerName);
  2601. var text = register.toString();
  2602. if (!text) {
  2603. return;
  2604. }
  2605. if (actionArgs.matchIndent) {
  2606. var tabSize = cm.getOption("tabSize");
  2607. // length that considers tabs and tabSize
  2608. var whitespaceLength = function(str) {
  2609. var tabs = (str.split("\t").length - 1);
  2610. var spaces = (str.split(" ").length - 1);
  2611. return tabs * tabSize + spaces * 1;
  2612. };
  2613. var currentLine = cm.getLine(cm.getCursor().line);
  2614. var indent = whitespaceLength(currentLine.match(/^\s*/)[0]);
  2615. // chomp last newline b/c don't want it to match /^\s*/gm
  2616. var chompedText = text.replace(/\n$/, '');
  2617. var wasChomped = text !== chompedText;
  2618. var firstIndent = whitespaceLength(text.match(/^\s*/)[0]);
  2619. var text = chompedText.replace(/^\s*/gm, function(wspace) {
  2620. var newIndent = indent + (whitespaceLength(wspace) - firstIndent);
  2621. if (newIndent < 0) {
  2622. return "";
  2623. }
  2624. else if (cm.getOption("indentWithTabs")) {
  2625. var quotient = Math.floor(newIndent / tabSize);
  2626. return Array(quotient + 1).join('\t');
  2627. }
  2628. else {
  2629. return Array(newIndent + 1).join(' ');
  2630. }
  2631. });
  2632. text += wasChomped ? "\n" : "";
  2633. }
  2634. if (actionArgs.repeat > 1) {
  2635. var text = Array(actionArgs.repeat + 1).join(text);
  2636. }
  2637. var linewise = register.linewise;
  2638. var blockwise = register.blockwise;
  2639. if (blockwise) {
  2640. text = text.split('\n');
  2641. if (linewise) {
  2642. text.pop();
  2643. }
  2644. for (var i = 0; i < text.length; i++) {
  2645. text[i] = (text[i] == '') ? ' ' : text[i];
  2646. }
  2647. cur.ch += actionArgs.after ? 1 : 0;
  2648. cur.ch = Math.min(lineLength(cm, cur.line), cur.ch);
  2649. } else if (linewise) {
  2650. if(vim.visualMode) {
  2651. text = vim.visualLine ? text.slice(0, -1) : '\n' + text.slice(0, text.length - 1) + '\n';
  2652. } else if (actionArgs.after) {
  2653. // Move the newline at the end to the start instead, and paste just
  2654. // before the newline character of the line we are on right now.
  2655. text = '\n' + text.slice(0, text.length - 1);
  2656. cur.ch = lineLength(cm, cur.line);
  2657. } else {
  2658. cur.ch = 0;
  2659. }
  2660. } else {
  2661. cur.ch += actionArgs.after ? 1 : 0;
  2662. }
  2663. var curPosFinal;
  2664. var idx;
  2665. if (vim.visualMode) {
  2666. // save the pasted text for reselection if the need arises
  2667. vim.lastPastedText = text;
  2668. var lastSelectionCurEnd;
  2669. var selectedArea = getSelectedAreaRange(cm, vim);
  2670. var selectionStart = selectedArea[0];
  2671. var selectionEnd = selectedArea[1];
  2672. var selectedText = cm.getSelection();
  2673. var selections = cm.listSelections();
  2674. var emptyStrings = new Array(selections.length).join('1').split('1');
  2675. // save the curEnd marker before it get cleared due to cm.replaceRange.
  2676. if (vim.lastSelection) {
  2677. lastSelectionCurEnd = vim.lastSelection.headMark.find();
  2678. }
  2679. // push the previously selected text to unnamed register
  2680. vimGlobalState.registerController.unnamedRegister.setText(selectedText);
  2681. if (blockwise) {
  2682. // first delete the selected text
  2683. cm.replaceSelections(emptyStrings);
  2684. // Set new selections as per the block length of the yanked text
  2685. selectionEnd = new Pos(selectionStart.line + text.length-1, selectionStart.ch);
  2686. cm.setCursor(selectionStart);
  2687. selectBlock(cm, selectionEnd);
  2688. cm.replaceSelections(text);
  2689. curPosFinal = selectionStart;
  2690. } else if (vim.visualBlock) {
  2691. cm.replaceSelections(emptyStrings);
  2692. cm.setCursor(selectionStart);
  2693. cm.replaceRange(text, selectionStart, selectionStart);
  2694. curPosFinal = selectionStart;
  2695. } else {
  2696. cm.replaceRange(text, selectionStart, selectionEnd);
  2697. curPosFinal = cm.posFromIndex(cm.indexFromPos(selectionStart) + text.length - 1);
  2698. }
  2699. // restore the the curEnd marker
  2700. if(lastSelectionCurEnd) {
  2701. vim.lastSelection.headMark = cm.setBookmark(lastSelectionCurEnd);
  2702. }
  2703. if (linewise) {
  2704. curPosFinal.ch=0;
  2705. }
  2706. } else {
  2707. if (blockwise) {
  2708. cm.setCursor(cur);
  2709. for (var i = 0; i < text.length; i++) {
  2710. var line = cur.line+i;
  2711. if (line > cm.lastLine()) {
  2712. cm.replaceRange('\n', new Pos(line, 0));
  2713. }
  2714. var lastCh = lineLength(cm, line);
  2715. if (lastCh < cur.ch) {
  2716. extendLineToColumn(cm, line, cur.ch);
  2717. }
  2718. }
  2719. cm.setCursor(cur);
  2720. selectBlock(cm, new Pos(cur.line + text.length-1, cur.ch));
  2721. cm.replaceSelections(text);
  2722. curPosFinal = cur;
  2723. } else {
  2724. cm.replaceRange(text, cur);
  2725. // Now fine tune the cursor to where we want it.
  2726. if (linewise && actionArgs.after) {
  2727. curPosFinal = new Pos(
  2728. cur.line + 1,
  2729. findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line + 1)));
  2730. } else if (linewise && !actionArgs.after) {
  2731. curPosFinal = new Pos(
  2732. cur.line,
  2733. findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line)));
  2734. } else if (!linewise && actionArgs.after) {
  2735. idx = cm.indexFromPos(cur);
  2736. curPosFinal = cm.posFromIndex(idx + text.length - 1);
  2737. } else {
  2738. idx = cm.indexFromPos(cur);
  2739. curPosFinal = cm.posFromIndex(idx + text.length);
  2740. }
  2741. }
  2742. }
  2743. if (vim.visualMode) {
  2744. exitVisualMode(cm, false);
  2745. }
  2746. cm.setCursor(curPosFinal);
  2747. },
  2748. undo: function(cm, actionArgs) {
  2749. cm.operation(function() {
  2750. repeatFn(cm, CodeMirror.commands.undo, actionArgs.repeat)();
  2751. cm.setCursor(cm.getCursor('anchor'));
  2752. });
  2753. },
  2754. redo: function(cm, actionArgs) {
  2755. repeatFn(cm, CodeMirror.commands.redo, actionArgs.repeat)();
  2756. },
  2757. setRegister: function(_cm, actionArgs, vim) {
  2758. vim.inputState.registerName = actionArgs.selectedCharacter;
  2759. },
  2760. setMark: function(cm, actionArgs, vim) {
  2761. var markName = actionArgs.selectedCharacter;
  2762. updateMark(cm, vim, markName, cm.getCursor());
  2763. },
  2764. replace: function(cm, actionArgs, vim) {
  2765. var replaceWith = actionArgs.selectedCharacter;
  2766. var curStart = cm.getCursor();
  2767. var replaceTo;
  2768. var curEnd;
  2769. var selections = cm.listSelections();
  2770. if (vim.visualMode) {
  2771. curStart = cm.getCursor('start');
  2772. curEnd = cm.getCursor('end');
  2773. } else {
  2774. var line = cm.getLine(curStart.line);
  2775. replaceTo = curStart.ch + actionArgs.repeat;
  2776. if (replaceTo > line.length) {
  2777. replaceTo=line.length;
  2778. }
  2779. curEnd = new Pos(curStart.line, replaceTo);
  2780. }
  2781. if (replaceWith=='\n') {
  2782. if (!vim.visualMode) cm.replaceRange('', curStart, curEnd);
  2783. // special case, where vim help says to replace by just one line-break
  2784. (CodeMirror.commands.newlineAndIndentContinueComment || CodeMirror.commands.newlineAndIndent)(cm);
  2785. } else {
  2786. var replaceWithStr = cm.getRange(curStart, curEnd);
  2787. //replace all characters in range by selected, but keep linebreaks
  2788. replaceWithStr = replaceWithStr.replace(/[^\n]/g, replaceWith);
  2789. if (vim.visualBlock) {
  2790. // Tabs are split in visua block before replacing
  2791. var spaces = new Array(cm.getOption("tabSize")+1).join(' ');
  2792. replaceWithStr = cm.getSelection();
  2793. replaceWithStr = replaceWithStr.replace(/\t/g, spaces).replace(/[^\n]/g, replaceWith).split('\n');
  2794. cm.replaceSelections(replaceWithStr);
  2795. } else {
  2796. cm.replaceRange(replaceWithStr, curStart, curEnd);
  2797. }
  2798. if (vim.visualMode) {
  2799. curStart = cursorIsBefore(selections[0].anchor, selections[0].head) ?
  2800. selections[0].anchor : selections[0].head;
  2801. cm.setCursor(curStart);
  2802. exitVisualMode(cm, false);
  2803. } else {
  2804. cm.setCursor(offsetCursor(curEnd, 0, -1));
  2805. }
  2806. }
  2807. },
  2808. incrementNumberToken: function(cm, actionArgs) {
  2809. var cur = cm.getCursor();
  2810. var lineStr = cm.getLine(cur.line);
  2811. var re = /(-?)(?:(0x)([\da-f]+)|(0b|0|)(\d+))/gi;
  2812. var match;
  2813. var start;
  2814. var end;
  2815. var numberStr;
  2816. while ((match = re.exec(lineStr)) !== null) {
  2817. start = match.index;
  2818. end = start + match[0].length;
  2819. if (cur.ch < end)break;
  2820. }
  2821. if (!actionArgs.backtrack && (end <= cur.ch))return;
  2822. if (match) {
  2823. var baseStr = match[2] || match[4];
  2824. var digits = match[3] || match[5];
  2825. var increment = actionArgs.increase ? 1 : -1;
  2826. var base = {'0b': 2, '0': 8, '': 10, '0x': 16}[baseStr.toLowerCase()];
  2827. var number = parseInt(match[1] + digits, base) + (increment * actionArgs.repeat);
  2828. numberStr = number.toString(base);
  2829. var zeroPadding = baseStr ? new Array(digits.length - numberStr.length + 1 + match[1].length).join('0') : '';
  2830. if (numberStr.charAt(0) === '-') {
  2831. numberStr = '-' + baseStr + zeroPadding + numberStr.substr(1);
  2832. } else {
  2833. numberStr = baseStr + zeroPadding + numberStr;
  2834. }
  2835. var from = new Pos(cur.line, start);
  2836. var to = new Pos(cur.line, end);
  2837. cm.replaceRange(numberStr, from, to);
  2838. } else {
  2839. return;
  2840. }
  2841. cm.setCursor(new Pos(cur.line, start + numberStr.length - 1));
  2842. },
  2843. repeatLastEdit: function(cm, actionArgs, vim) {
  2844. var lastEditInputState = vim.lastEditInputState;
  2845. if (!lastEditInputState) { return; }
  2846. var repeat = actionArgs.repeat;
  2847. if (repeat && actionArgs.repeatIsExplicit) {
  2848. vim.lastEditInputState.repeatOverride = repeat;
  2849. } else {
  2850. repeat = vim.lastEditInputState.repeatOverride || repeat;
  2851. }
  2852. repeatLastEdit(cm, vim, repeat, false /** repeatForInsert */);
  2853. },
  2854. indent: function(cm, actionArgs) {
  2855. cm.indentLine(cm.getCursor().line, actionArgs.indentRight);
  2856. },
  2857. exitInsertMode: exitInsertMode
  2858. };
  2859. function defineAction(name, fn) {
  2860. actions[name] = fn;
  2861. }
  2862. /*
  2863. * Below are miscellaneous utility functions used by vim.js
  2864. */
  2865. /**
  2866. * Clips cursor to ensure that line is within the buffer's range
  2867. * If includeLineBreak is true, then allow cur.ch == lineLength.
  2868. */
  2869. function clipCursorToContent(cm, cur) {
  2870. var vim = cm.state.vim;
  2871. var includeLineBreak = vim.insertMode || vim.visualMode;
  2872. var line = Math.min(Math.max(cm.firstLine(), cur.line), cm.lastLine() );
  2873. var maxCh = lineLength(cm, line) - 1 + !!includeLineBreak;
  2874. var ch = Math.min(Math.max(0, cur.ch), maxCh);
  2875. return new Pos(line, ch);
  2876. }
  2877. function copyArgs(args) {
  2878. var ret = {};
  2879. for (var prop in args) {
  2880. if (args.hasOwnProperty(prop)) {
  2881. ret[prop] = args[prop];
  2882. }
  2883. }
  2884. return ret;
  2885. }
  2886. function offsetCursor(cur, offsetLine, offsetCh) {
  2887. if (typeof offsetLine === 'object') {
  2888. offsetCh = offsetLine.ch;
  2889. offsetLine = offsetLine.line;
  2890. }
  2891. return new Pos(cur.line + offsetLine, cur.ch + offsetCh);
  2892. }
  2893. function commandMatches(keys, keyMap, context, inputState) {
  2894. // Partial matches are not applied. They inform the key handler
  2895. // that the current key sequence is a subsequence of a valid key
  2896. // sequence, so that the key buffer is not cleared.
  2897. var match, partial = [], full = [];
  2898. for (var i = 0; i < keyMap.length; i++) {
  2899. var command = keyMap[i];
  2900. if (context == 'insert' && command.context != 'insert' ||
  2901. command.context && command.context != context ||
  2902. inputState.operator && command.type == 'action' ||
  2903. !(match = commandMatch(keys, command.keys))) { continue; }
  2904. if (match == 'partial') { partial.push(command); }
  2905. if (match == 'full') { full.push(command); }
  2906. }
  2907. return {
  2908. partial: partial.length && partial,
  2909. full: full.length && full
  2910. };
  2911. }
  2912. function commandMatch(pressed, mapped) {
  2913. if (mapped.slice(-11) == '<character>') {
  2914. // Last character matches anything.
  2915. var prefixLen = mapped.length - 11;
  2916. var pressedPrefix = pressed.slice(0, prefixLen);
  2917. var mappedPrefix = mapped.slice(0, prefixLen);
  2918. return pressedPrefix == mappedPrefix && pressed.length > prefixLen ? 'full' :
  2919. mappedPrefix.indexOf(pressedPrefix) == 0 ? 'partial' : false;
  2920. } else {
  2921. return pressed == mapped ? 'full' :
  2922. mapped.indexOf(pressed) == 0 ? 'partial' : false;
  2923. }
  2924. }
  2925. function lastChar(keys) {
  2926. var match = /^.*(<[^>]+>)$/.exec(keys);
  2927. var selectedCharacter = match ? match[1] : keys.slice(-1);
  2928. if (selectedCharacter.length > 1){
  2929. switch(selectedCharacter){
  2930. case '<CR>':
  2931. selectedCharacter='\n';
  2932. break;
  2933. case '<Space>':
  2934. selectedCharacter=' ';
  2935. break;
  2936. default:
  2937. selectedCharacter='';
  2938. break;
  2939. }
  2940. }
  2941. return selectedCharacter;
  2942. }
  2943. function repeatFn(cm, fn, repeat) {
  2944. return function() {
  2945. for (var i = 0; i < repeat; i++) {
  2946. fn(cm);
  2947. }
  2948. };
  2949. }
  2950. function copyCursor(cur) {
  2951. return new Pos(cur.line, cur.ch);
  2952. }
  2953. function cursorEqual(cur1, cur2) {
  2954. return cur1.ch == cur2.ch && cur1.line == cur2.line;
  2955. }
  2956. function cursorIsBefore(cur1, cur2) {
  2957. if (cur1.line < cur2.line) {
  2958. return true;
  2959. }
  2960. if (cur1.line == cur2.line && cur1.ch < cur2.ch) {
  2961. return true;
  2962. }
  2963. return false;
  2964. }
  2965. function cursorMin(cur1, cur2) {
  2966. if (arguments.length > 2) {
  2967. cur2 = cursorMin.apply(undefined, Array.prototype.slice.call(arguments, 1));
  2968. }
  2969. return cursorIsBefore(cur1, cur2) ? cur1 : cur2;
  2970. }
  2971. function cursorMax(cur1, cur2) {
  2972. if (arguments.length > 2) {
  2973. cur2 = cursorMax.apply(undefined, Array.prototype.slice.call(arguments, 1));
  2974. }
  2975. return cursorIsBefore(cur1, cur2) ? cur2 : cur1;
  2976. }
  2977. function cursorIsBetween(cur1, cur2, cur3) {
  2978. // returns true if cur2 is between cur1 and cur3.
  2979. var cur1before2 = cursorIsBefore(cur1, cur2);
  2980. var cur2before3 = cursorIsBefore(cur2, cur3);
  2981. return cur1before2 && cur2before3;
  2982. }
  2983. function lineLength(cm, lineNum) {
  2984. return cm.getLine(lineNum).length;
  2985. }
  2986. function trim(s) {
  2987. if (s.trim) {
  2988. return s.trim();
  2989. }
  2990. return s.replace(/^\s+|\s+$/g, '');
  2991. }
  2992. function escapeRegex(s) {
  2993. return s.replace(/([.?*+$\[\]\/\\(){}|\-])/g, '\\$1');
  2994. }
  2995. function extendLineToColumn(cm, lineNum, column) {
  2996. var endCh = lineLength(cm, lineNum);
  2997. var spaces = new Array(column-endCh+1).join(' ');
  2998. cm.setCursor(new Pos(lineNum, endCh));
  2999. cm.replaceRange(spaces, cm.getCursor());
  3000. }
  3001. // This functions selects a rectangular block
  3002. // of text with selectionEnd as any of its corner
  3003. // Height of block:
  3004. // Difference in selectionEnd.line and first/last selection.line
  3005. // Width of the block:
  3006. // Distance between selectionEnd.ch and any(first considered here) selection.ch
  3007. function selectBlock(cm, selectionEnd) {
  3008. var selections = [], ranges = cm.listSelections();
  3009. var head = copyCursor(cm.clipPos(selectionEnd));
  3010. var isClipped = !cursorEqual(selectionEnd, head);
  3011. var curHead = cm.getCursor('head');
  3012. var primIndex = getIndex(ranges, curHead);
  3013. var wasClipped = cursorEqual(ranges[primIndex].head, ranges[primIndex].anchor);
  3014. var max = ranges.length - 1;
  3015. var index = max - primIndex > primIndex ? max : 0;
  3016. var base = ranges[index].anchor;
  3017. var firstLine = Math.min(base.line, head.line);
  3018. var lastLine = Math.max(base.line, head.line);
  3019. var baseCh = base.ch, headCh = head.ch;
  3020. var dir = ranges[index].head.ch - baseCh;
  3021. var newDir = headCh - baseCh;
  3022. if (dir > 0 && newDir <= 0) {
  3023. baseCh++;
  3024. if (!isClipped) { headCh--; }
  3025. } else if (dir < 0 && newDir >= 0) {
  3026. baseCh--;
  3027. if (!wasClipped) { headCh++; }
  3028. } else if (dir < 0 && newDir == -1) {
  3029. baseCh--;
  3030. headCh++;
  3031. }
  3032. for (var line = firstLine; line <= lastLine; line++) {
  3033. var range = {anchor: new Pos(line, baseCh), head: new Pos(line, headCh)};
  3034. selections.push(range);
  3035. }
  3036. cm.setSelections(selections);
  3037. selectionEnd.ch = headCh;
  3038. base.ch = baseCh;
  3039. return base;
  3040. }
  3041. function selectForInsert(cm, head, height) {
  3042. var sel = [];
  3043. for (var i = 0; i < height; i++) {
  3044. var lineHead = offsetCursor(head, i, 0);
  3045. sel.push({anchor: lineHead, head: lineHead});
  3046. }
  3047. cm.setSelections(sel, 0);
  3048. }
  3049. // getIndex returns the index of the cursor in the selections.
  3050. function getIndex(ranges, cursor, end) {
  3051. for (var i = 0; i < ranges.length; i++) {
  3052. var atAnchor = end != 'head' && cursorEqual(ranges[i].anchor, cursor);
  3053. var atHead = end != 'anchor' && cursorEqual(ranges[i].head, cursor);
  3054. if (atAnchor || atHead) {
  3055. return i;
  3056. }
  3057. }
  3058. return -1;
  3059. }
  3060. function getSelectedAreaRange(cm, vim) {
  3061. var lastSelection = vim.lastSelection;
  3062. var getCurrentSelectedAreaRange = function() {
  3063. var selections = cm.listSelections();
  3064. var start = selections[0];
  3065. var end = selections[selections.length-1];
  3066. var selectionStart = cursorIsBefore(start.anchor, start.head) ? start.anchor : start.head;
  3067. var selectionEnd = cursorIsBefore(end.anchor, end.head) ? end.head : end.anchor;
  3068. return [selectionStart, selectionEnd];
  3069. };
  3070. var getLastSelectedAreaRange = function() {
  3071. var selectionStart = cm.getCursor();
  3072. var selectionEnd = cm.getCursor();
  3073. var block = lastSelection.visualBlock;
  3074. if (block) {
  3075. var width = block.width;
  3076. var height = block.height;
  3077. selectionEnd = new Pos(selectionStart.line + height, selectionStart.ch + width);
  3078. var selections = [];
  3079. // selectBlock creates a 'proper' rectangular block.
  3080. // We do not want that in all cases, so we manually set selections.
  3081. for (var i = selectionStart.line; i < selectionEnd.line; i++) {
  3082. var anchor = new Pos(i, selectionStart.ch);
  3083. var head = new Pos(i, selectionEnd.ch);
  3084. var range = {anchor: anchor, head: head};
  3085. selections.push(range);
  3086. }
  3087. cm.setSelections(selections);
  3088. } else {
  3089. var start = lastSelection.anchorMark.find();
  3090. var end = lastSelection.headMark.find();
  3091. var line = end.line - start.line;
  3092. var ch = end.ch - start.ch;
  3093. selectionEnd = {line: selectionEnd.line + line, ch: line ? selectionEnd.ch : ch + selectionEnd.ch};
  3094. if (lastSelection.visualLine) {
  3095. selectionStart = new Pos(selectionStart.line, 0);
  3096. selectionEnd = new Pos(selectionEnd.line, lineLength(cm, selectionEnd.line));
  3097. }
  3098. cm.setSelection(selectionStart, selectionEnd);
  3099. }
  3100. return [selectionStart, selectionEnd];
  3101. };
  3102. if (!vim.visualMode) {
  3103. // In case of replaying the action.
  3104. return getLastSelectedAreaRange();
  3105. } else {
  3106. return getCurrentSelectedAreaRange();
  3107. }
  3108. }
  3109. // Updates the previous selection with the current selection's values. This
  3110. // should only be called in visual mode.
  3111. function updateLastSelection(cm, vim) {
  3112. var anchor = vim.sel.anchor;
  3113. var head = vim.sel.head;
  3114. // To accommodate the effect of lastPastedText in the last selection
  3115. if (vim.lastPastedText) {
  3116. head = cm.posFromIndex(cm.indexFromPos(anchor) + vim.lastPastedText.length);
  3117. vim.lastPastedText = null;
  3118. }
  3119. vim.lastSelection = {'anchorMark': cm.setBookmark(anchor),
  3120. 'headMark': cm.setBookmark(head),
  3121. 'anchor': copyCursor(anchor),
  3122. 'head': copyCursor(head),
  3123. 'visualMode': vim.visualMode,
  3124. 'visualLine': vim.visualLine,
  3125. 'visualBlock': vim.visualBlock};
  3126. }
  3127. function expandSelection(cm, start, end) {
  3128. var sel = cm.state.vim.sel;
  3129. var head = sel.head;
  3130. var anchor = sel.anchor;
  3131. var tmp;
  3132. if (cursorIsBefore(end, start)) {
  3133. tmp = end;
  3134. end = start;
  3135. start = tmp;
  3136. }
  3137. if (cursorIsBefore(head, anchor)) {
  3138. head = cursorMin(start, head);
  3139. anchor = cursorMax(anchor, end);
  3140. } else {
  3141. anchor = cursorMin(start, anchor);
  3142. head = cursorMax(head, end);
  3143. head = offsetCursor(head, 0, -1);
  3144. if (head.ch == -1 && head.line != cm.firstLine()) {
  3145. head = new Pos(head.line - 1, lineLength(cm, head.line - 1));
  3146. }
  3147. }
  3148. return [anchor, head];
  3149. }
  3150. /**
  3151. * Updates the CodeMirror selection to match the provided vim selection.
  3152. * If no arguments are given, it uses the current vim selection state.
  3153. */
  3154. function updateCmSelection(cm, sel, mode) {
  3155. var vim = cm.state.vim;
  3156. sel = sel || vim.sel;
  3157. var mode = mode ||
  3158. vim.visualLine ? 'line' : vim.visualBlock ? 'block' : 'char';
  3159. var cmSel = makeCmSelection(cm, sel, mode);
  3160. cm.setSelections(cmSel.ranges, cmSel.primary);
  3161. }
  3162. function makeCmSelection(cm, sel, mode, exclusive) {
  3163. var head = copyCursor(sel.head);
  3164. var anchor = copyCursor(sel.anchor);
  3165. if (mode == 'char') {
  3166. var headOffset = !exclusive && !cursorIsBefore(sel.head, sel.anchor) ? 1 : 0;
  3167. var anchorOffset = cursorIsBefore(sel.head, sel.anchor) ? 1 : 0;
  3168. head = offsetCursor(sel.head, 0, headOffset);
  3169. anchor = offsetCursor(sel.anchor, 0, anchorOffset);
  3170. return {
  3171. ranges: [{anchor: anchor, head: head}],
  3172. primary: 0
  3173. };
  3174. } else if (mode == 'line') {
  3175. if (!cursorIsBefore(sel.head, sel.anchor)) {
  3176. anchor.ch = 0;
  3177. var lastLine = cm.lastLine();
  3178. if (head.line > lastLine) {
  3179. head.line = lastLine;
  3180. }
  3181. head.ch = lineLength(cm, head.line);
  3182. } else {
  3183. head.ch = 0;
  3184. anchor.ch = lineLength(cm, anchor.line);
  3185. }
  3186. return {
  3187. ranges: [{anchor: anchor, head: head}],
  3188. primary: 0
  3189. };
  3190. } else if (mode == 'block') {
  3191. var top = Math.min(anchor.line, head.line),
  3192. fromCh = anchor.ch,
  3193. bottom = Math.max(anchor.line, head.line),
  3194. toCh = head.ch;
  3195. if (fromCh < toCh) { toCh += 1; }
  3196. else { fromCh += 1; } var height = bottom - top + 1;
  3197. var primary = head.line == top ? 0 : height - 1;
  3198. var ranges = [];
  3199. for (var i = 0; i < height; i++) {
  3200. ranges.push({
  3201. anchor: new Pos(top + i, fromCh),
  3202. head: new Pos(top + i, toCh)
  3203. });
  3204. }
  3205. return {
  3206. ranges: ranges,
  3207. primary: primary
  3208. };
  3209. }
  3210. }
  3211. function getHead(cm) {
  3212. var cur = cm.getCursor('head');
  3213. if (cm.getSelection().length == 1) {
  3214. // Small corner case when only 1 character is selected. The "real"
  3215. // head is the left of head and anchor.
  3216. cur = cursorMin(cur, cm.getCursor('anchor'));
  3217. }
  3218. return cur;
  3219. }
  3220. /**
  3221. * If moveHead is set to false, the CodeMirror selection will not be
  3222. * touched. The caller assumes the responsibility of putting the cursor
  3223. * in the right place.
  3224. */
  3225. function exitVisualMode(cm, moveHead) {
  3226. var vim = cm.state.vim;
  3227. if (moveHead !== false) {
  3228. cm.setCursor(clipCursorToContent(cm, vim.sel.head));
  3229. }
  3230. updateLastSelection(cm, vim);
  3231. vim.visualMode = false;
  3232. vim.visualLine = false;
  3233. vim.visualBlock = false;
  3234. if (!vim.insertMode) CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
  3235. }
  3236. // Remove any trailing newlines from the selection. For
  3237. // example, with the caret at the start of the last word on the line,
  3238. // 'dw' should word, but not the newline, while 'w' should advance the
  3239. // caret to the first character of the next line.
  3240. function clipToLine(cm, curStart, curEnd) {
  3241. var selection = cm.getRange(curStart, curEnd);
  3242. // Only clip if the selection ends with trailing newline + whitespace
  3243. if (/\n\s*$/.test(selection)) {
  3244. var lines = selection.split('\n');
  3245. // We know this is all whitespace.
  3246. lines.pop();
  3247. // Cases:
  3248. // 1. Last word is an empty line - do not clip the trailing '\n'
  3249. // 2. Last word is not an empty line - clip the trailing '\n'
  3250. var line;
  3251. // Find the line containing the last word, and clip all whitespace up
  3252. // to it.
  3253. for (var line = lines.pop(); lines.length > 0 && line && isWhiteSpaceString(line); line = lines.pop()) {
  3254. curEnd.line--;
  3255. curEnd.ch = 0;
  3256. }
  3257. // If the last word is not an empty line, clip an additional newline
  3258. if (line) {
  3259. curEnd.line--;
  3260. curEnd.ch = lineLength(cm, curEnd.line);
  3261. } else {
  3262. curEnd.ch = 0;
  3263. }
  3264. }
  3265. }
  3266. // Expand the selection to line ends.
  3267. function expandSelectionToLine(_cm, curStart, curEnd) {
  3268. curStart.ch = 0;
  3269. curEnd.ch = 0;
  3270. curEnd.line++;
  3271. }
  3272. function findFirstNonWhiteSpaceCharacter(text) {
  3273. if (!text) {
  3274. return 0;
  3275. }
  3276. var firstNonWS = text.search(/\S/);
  3277. return firstNonWS == -1 ? text.length : firstNonWS;
  3278. }
  3279. function expandWordUnderCursor(cm, inclusive, _forward, bigWord, noSymbol) {
  3280. var cur = getHead(cm);
  3281. var line = cm.getLine(cur.line);
  3282. var idx = cur.ch;
  3283. // Seek to first word or non-whitespace character, depending on if
  3284. // noSymbol is true.
  3285. var test = noSymbol ? wordCharTest[0] : bigWordCharTest [0];
  3286. while (!test(line.charAt(idx))) {
  3287. idx++;
  3288. if (idx >= line.length) { return null; }
  3289. }
  3290. if (bigWord) {
  3291. test = bigWordCharTest[0];
  3292. } else {
  3293. test = wordCharTest[0];
  3294. if (!test(line.charAt(idx))) {
  3295. test = wordCharTest[1];
  3296. }
  3297. }
  3298. var end = idx, start = idx;
  3299. while (test(line.charAt(end)) && end < line.length) { end++; }
  3300. while (test(line.charAt(start)) && start >= 0) { start--; }
  3301. start++;
  3302. if (inclusive) {
  3303. // If present, include all whitespace after word.
  3304. // Otherwise, include all whitespace before word, except indentation.
  3305. var wordEnd = end;
  3306. while (/\s/.test(line.charAt(end)) && end < line.length) { end++; }
  3307. if (wordEnd == end) {
  3308. var wordStart = start;
  3309. while (/\s/.test(line.charAt(start - 1)) && start > 0) { start--; }
  3310. if (!start) { start = wordStart; }
  3311. }
  3312. }
  3313. return { start: new Pos(cur.line, start), end: new Pos(cur.line, end) };
  3314. }
  3315. /**
  3316. * Depends on the following:
  3317. *
  3318. * - editor mode should be htmlmixedmode / xml
  3319. * - mode/xml/xml.js should be loaded
  3320. * - addon/fold/xml-fold.js should be loaded
  3321. *
  3322. * If any of the above requirements are not true, this function noops.
  3323. *
  3324. * This is _NOT_ a 100% accurate implementation of vim tag text objects.
  3325. * The following caveats apply (based off cursory testing, I'm sure there
  3326. * are other discrepancies):
  3327. *
  3328. * - Does not work inside comments:
  3329. * ```
  3330. * <!-- <div>broken</div> -->
  3331. * ```
  3332. * - Does not work when tags have different cases:
  3333. * ```
  3334. * <div>broken</DIV>
  3335. * ```
  3336. * - Does not work when cursor is inside a broken tag:
  3337. * ```
  3338. * <div><brok><en></div>
  3339. * ```
  3340. */
  3341. function expandTagUnderCursor(cm, head, inclusive) {
  3342. var cur = head;
  3343. if (!CodeMirror.findMatchingTag || !CodeMirror.findEnclosingTag) {
  3344. return { start: cur, end: cur };
  3345. }
  3346. var tags = CodeMirror.findMatchingTag(cm, head) || CodeMirror.findEnclosingTag(cm, head);
  3347. if (!tags || !tags.open || !tags.close) {
  3348. return { start: cur, end: cur };
  3349. }
  3350. if (inclusive) {
  3351. return { start: tags.open.from, end: tags.close.to };
  3352. }
  3353. return { start: tags.open.to, end: tags.close.from };
  3354. }
  3355. function recordJumpPosition(cm, oldCur, newCur) {
  3356. if (!cursorEqual(oldCur, newCur)) {
  3357. vimGlobalState.jumpList.add(cm, oldCur, newCur);
  3358. }
  3359. }
  3360. function recordLastCharacterSearch(increment, args) {
  3361. vimGlobalState.lastCharacterSearch.increment = increment;
  3362. vimGlobalState.lastCharacterSearch.forward = args.forward;
  3363. vimGlobalState.lastCharacterSearch.selectedCharacter = args.selectedCharacter;
  3364. }
  3365. var symbolToMode = {
  3366. '(': 'bracket', ')': 'bracket', '{': 'bracket', '}': 'bracket',
  3367. '[': 'section', ']': 'section',
  3368. '*': 'comment', '/': 'comment',
  3369. 'm': 'method', 'M': 'method',
  3370. '#': 'preprocess'
  3371. };
  3372. var findSymbolModes = {
  3373. bracket: {
  3374. isComplete: function(state) {
  3375. if (state.nextCh === state.symb) {
  3376. state.depth++;
  3377. if (state.depth >= 1)return true;
  3378. } else if (state.nextCh === state.reverseSymb) {
  3379. state.depth--;
  3380. }
  3381. return false;
  3382. }
  3383. },
  3384. section: {
  3385. init: function(state) {
  3386. state.curMoveThrough = true;
  3387. state.symb = (state.forward ? ']' : '[') === state.symb ? '{' : '}';
  3388. },
  3389. isComplete: function(state) {
  3390. return state.index === 0 && state.nextCh === state.symb;
  3391. }
  3392. },
  3393. comment: {
  3394. isComplete: function(state) {
  3395. var found = state.lastCh === '*' && state.nextCh === '/';
  3396. state.lastCh = state.nextCh;
  3397. return found;
  3398. }
  3399. },
  3400. // TODO: The original Vim implementation only operates on level 1 and 2.
  3401. // The current implementation doesn't check for code block level and
  3402. // therefore it operates on any levels.
  3403. method: {
  3404. init: function(state) {
  3405. state.symb = (state.symb === 'm' ? '{' : '}');
  3406. state.reverseSymb = state.symb === '{' ? '}' : '{';
  3407. },
  3408. isComplete: function(state) {
  3409. if (state.nextCh === state.symb)return true;
  3410. return false;
  3411. }
  3412. },
  3413. preprocess: {
  3414. init: function(state) {
  3415. state.index = 0;
  3416. },
  3417. isComplete: function(state) {
  3418. if (state.nextCh === '#') {
  3419. var token = state.lineText.match(/^#(\w+)/)[1];
  3420. if (token === 'endif') {
  3421. if (state.forward && state.depth === 0) {
  3422. return true;
  3423. }
  3424. state.depth++;
  3425. } else if (token === 'if') {
  3426. if (!state.forward && state.depth === 0) {
  3427. return true;
  3428. }
  3429. state.depth--;
  3430. }
  3431. if (token === 'else' && state.depth === 0)return true;
  3432. }
  3433. return false;
  3434. }
  3435. }
  3436. };
  3437. function findSymbol(cm, repeat, forward, symb) {
  3438. var cur = copyCursor(cm.getCursor());
  3439. var increment = forward ? 1 : -1;
  3440. var endLine = forward ? cm.lineCount() : -1;
  3441. var curCh = cur.ch;
  3442. var line = cur.line;
  3443. var lineText = cm.getLine(line);
  3444. var state = {
  3445. lineText: lineText,
  3446. nextCh: lineText.charAt(curCh),
  3447. lastCh: null,
  3448. index: curCh,
  3449. symb: symb,
  3450. reverseSymb: (forward ? { ')': '(', '}': '{' } : { '(': ')', '{': '}' })[symb],
  3451. forward: forward,
  3452. depth: 0,
  3453. curMoveThrough: false
  3454. };
  3455. var mode = symbolToMode[symb];
  3456. if (!mode)return cur;
  3457. var init = findSymbolModes[mode].init;
  3458. var isComplete = findSymbolModes[mode].isComplete;
  3459. if (init) { init(state); }
  3460. while (line !== endLine && repeat) {
  3461. state.index += increment;
  3462. state.nextCh = state.lineText.charAt(state.index);
  3463. if (!state.nextCh) {
  3464. line += increment;
  3465. state.lineText = cm.getLine(line) || '';
  3466. if (increment > 0) {
  3467. state.index = 0;
  3468. } else {
  3469. var lineLen = state.lineText.length;
  3470. state.index = (lineLen > 0) ? (lineLen-1) : 0;
  3471. }
  3472. state.nextCh = state.lineText.charAt(state.index);
  3473. }
  3474. if (isComplete(state)) {
  3475. cur.line = line;
  3476. cur.ch = state.index;
  3477. repeat--;
  3478. }
  3479. }
  3480. if (state.nextCh || state.curMoveThrough) {
  3481. return new Pos(line, state.index);
  3482. }
  3483. return cur;
  3484. }
  3485. /*
  3486. * Returns the boundaries of the next word. If the cursor in the middle of
  3487. * the word, then returns the boundaries of the current word, starting at
  3488. * the cursor. If the cursor is at the start/end of a word, and we are going
  3489. * forward/backward, respectively, find the boundaries of the next word.
  3490. *
  3491. * @param {CodeMirror} cm CodeMirror object.
  3492. * @param {Cursor} cur The cursor position.
  3493. * @param {boolean} forward True to search forward. False to search
  3494. * backward.
  3495. * @param {boolean} bigWord True if punctuation count as part of the word.
  3496. * False if only [a-zA-Z0-9] characters count as part of the word.
  3497. * @param {boolean} emptyLineIsWord True if empty lines should be treated
  3498. * as words.
  3499. * @return {Object{from:number, to:number, line: number}} The boundaries of
  3500. * the word, or null if there are no more words.
  3501. */
  3502. function findWord(cm, cur, forward, bigWord, emptyLineIsWord) {
  3503. var lineNum = cur.line;
  3504. var pos = cur.ch;
  3505. var line = cm.getLine(lineNum);
  3506. var dir = forward ? 1 : -1;
  3507. var charTests = bigWord ? bigWordCharTest: wordCharTest;
  3508. if (emptyLineIsWord && line == '') {
  3509. lineNum += dir;
  3510. line = cm.getLine(lineNum);
  3511. if (!isLine(cm, lineNum)) {
  3512. return null;
  3513. }
  3514. pos = (forward) ? 0 : line.length;
  3515. }
  3516. while (true) {
  3517. if (emptyLineIsWord && line == '') {
  3518. return { from: 0, to: 0, line: lineNum };
  3519. }
  3520. var stop = (dir > 0) ? line.length : -1;
  3521. var wordStart = stop, wordEnd = stop;
  3522. // Find bounds of next word.
  3523. while (pos != stop) {
  3524. var foundWord = false;
  3525. for (var i = 0; i < charTests.length && !foundWord; ++i) {
  3526. if (charTests[i](line.charAt(pos))) {
  3527. wordStart = pos;
  3528. // Advance to end of word.
  3529. while (pos != stop && charTests[i](line.charAt(pos))) {
  3530. pos += dir;
  3531. }
  3532. wordEnd = pos;
  3533. foundWord = wordStart != wordEnd;
  3534. if (wordStart == cur.ch && lineNum == cur.line &&
  3535. wordEnd == wordStart + dir) {
  3536. // We started at the end of a word. Find the next one.
  3537. continue;
  3538. } else {
  3539. return {
  3540. from: Math.min(wordStart, wordEnd + 1),
  3541. to: Math.max(wordStart, wordEnd),
  3542. line: lineNum };
  3543. }
  3544. }
  3545. }
  3546. if (!foundWord) {
  3547. pos += dir;
  3548. }
  3549. }
  3550. // Advance to next/prev line.
  3551. lineNum += dir;
  3552. if (!isLine(cm, lineNum)) {
  3553. return null;
  3554. }
  3555. line = cm.getLine(lineNum);
  3556. pos = (dir > 0) ? 0 : line.length;
  3557. }
  3558. }
  3559. /**
  3560. * @param {CodeMirror} cm CodeMirror object.
  3561. * @param {Pos} cur The position to start from.
  3562. * @param {int} repeat Number of words to move past.
  3563. * @param {boolean} forward True to search forward. False to search
  3564. * backward.
  3565. * @param {boolean} wordEnd True to move to end of word. False to move to
  3566. * beginning of word.
  3567. * @param {boolean} bigWord True if punctuation count as part of the word.
  3568. * False if only alphabet characters count as part of the word.
  3569. * @return {Cursor} The position the cursor should move to.
  3570. */
  3571. function moveToWord(cm, cur, repeat, forward, wordEnd, bigWord) {
  3572. var curStart = copyCursor(cur);
  3573. var words = [];
  3574. if (forward && !wordEnd || !forward && wordEnd) {
  3575. repeat++;
  3576. }
  3577. // For 'e', empty lines are not considered words, go figure.
  3578. var emptyLineIsWord = !(forward && wordEnd);
  3579. for (var i = 0; i < repeat; i++) {
  3580. var word = findWord(cm, cur, forward, bigWord, emptyLineIsWord);
  3581. if (!word) {
  3582. var eodCh = lineLength(cm, cm.lastLine());
  3583. words.push(forward
  3584. ? {line: cm.lastLine(), from: eodCh, to: eodCh}
  3585. : {line: 0, from: 0, to: 0});
  3586. break;
  3587. }
  3588. words.push(word);
  3589. cur = new Pos(word.line, forward ? (word.to - 1) : word.from);
  3590. }
  3591. var shortCircuit = words.length != repeat;
  3592. var firstWord = words[0];
  3593. var lastWord = words.pop();
  3594. if (forward && !wordEnd) {
  3595. // w
  3596. if (!shortCircuit && (firstWord.from != curStart.ch || firstWord.line != curStart.line)) {
  3597. // We did not start in the middle of a word. Discard the extra word at the end.
  3598. lastWord = words.pop();
  3599. }
  3600. return new Pos(lastWord.line, lastWord.from);
  3601. } else if (forward && wordEnd) {
  3602. return new Pos(lastWord.line, lastWord.to - 1);
  3603. } else if (!forward && wordEnd) {
  3604. // ge
  3605. if (!shortCircuit && (firstWord.to != curStart.ch || firstWord.line != curStart.line)) {
  3606. // We did not start in the middle of a word. Discard the extra word at the end.
  3607. lastWord = words.pop();
  3608. }
  3609. return new Pos(lastWord.line, lastWord.to);
  3610. } else {
  3611. // b
  3612. return new Pos(lastWord.line, lastWord.from);
  3613. }
  3614. }
  3615. function moveToEol(cm, head, motionArgs, vim, keepHPos) {
  3616. var cur = head;
  3617. var retval= new Pos(cur.line + motionArgs.repeat - 1, Infinity);
  3618. var end=cm.clipPos(retval);
  3619. end.ch--;
  3620. if (!keepHPos) {
  3621. vim.lastHPos = Infinity;
  3622. vim.lastHSPos = cm.charCoords(end,'div').left;
  3623. }
  3624. return retval;
  3625. }
  3626. function moveToCharacter(cm, repeat, forward, character) {
  3627. var cur = cm.getCursor();
  3628. var start = cur.ch;
  3629. var idx;
  3630. for (var i = 0; i < repeat; i ++) {
  3631. var line = cm.getLine(cur.line);
  3632. idx = charIdxInLine(start, line, character, forward, true);
  3633. if (idx == -1) {
  3634. return null;
  3635. }
  3636. start = idx;
  3637. }
  3638. return new Pos(cm.getCursor().line, idx);
  3639. }
  3640. function moveToColumn(cm, repeat) {
  3641. // repeat is always >= 1, so repeat - 1 always corresponds
  3642. // to the column we want to go to.
  3643. var line = cm.getCursor().line;
  3644. return clipCursorToContent(cm, new Pos(line, repeat - 1));
  3645. }
  3646. function updateMark(cm, vim, markName, pos) {
  3647. if (!inArray(markName, validMarks)) {
  3648. return;
  3649. }
  3650. if (vim.marks[markName]) {
  3651. vim.marks[markName].clear();
  3652. }
  3653. vim.marks[markName] = cm.setBookmark(pos);
  3654. }
  3655. function charIdxInLine(start, line, character, forward, includeChar) {
  3656. // Search for char in line.
  3657. // motion_options: {forward, includeChar}
  3658. // If includeChar = true, include it too.
  3659. // If forward = true, search forward, else search backwards.
  3660. // If char is not found on this line, do nothing
  3661. var idx;
  3662. if (forward) {
  3663. idx = line.indexOf(character, start + 1);
  3664. if (idx != -1 && !includeChar) {
  3665. idx -= 1;
  3666. }
  3667. } else {
  3668. idx = line.lastIndexOf(character, start - 1);
  3669. if (idx != -1 && !includeChar) {
  3670. idx += 1;
  3671. }
  3672. }
  3673. return idx;
  3674. }
  3675. function findParagraph(cm, head, repeat, dir, inclusive) {
  3676. var line = head.line;
  3677. var min = cm.firstLine();
  3678. var max = cm.lastLine();
  3679. var start, end, i = line;
  3680. function isEmpty(i) { return !cm.getLine(i); }
  3681. function isBoundary(i, dir, any) {
  3682. if (any) { return isEmpty(i) != isEmpty(i + dir); }
  3683. return !isEmpty(i) && isEmpty(i + dir);
  3684. }
  3685. if (dir) {
  3686. while (min <= i && i <= max && repeat > 0) {
  3687. if (isBoundary(i, dir)) { repeat--; }
  3688. i += dir;
  3689. }
  3690. return new Pos(i, 0);
  3691. }
  3692. var vim = cm.state.vim;
  3693. if (vim.visualLine && isBoundary(line, 1, true)) {
  3694. var anchor = vim.sel.anchor;
  3695. if (isBoundary(anchor.line, -1, true)) {
  3696. if (!inclusive || anchor.line != line) {
  3697. line += 1;
  3698. }
  3699. }
  3700. }
  3701. var startState = isEmpty(line);
  3702. for (i = line; i <= max && repeat; i++) {
  3703. if (isBoundary(i, 1, true)) {
  3704. if (!inclusive || isEmpty(i) != startState) {
  3705. repeat--;
  3706. }
  3707. }
  3708. }
  3709. end = new Pos(i, 0);
  3710. // select boundary before paragraph for the last one
  3711. if (i > max && !startState) { startState = true; }
  3712. else { inclusive = false; }
  3713. for (i = line; i > min; i--) {
  3714. if (!inclusive || isEmpty(i) == startState || i == line) {
  3715. if (isBoundary(i, -1, true)) { break; }
  3716. }
  3717. }
  3718. start = new Pos(i, 0);
  3719. return { start: start, end: end };
  3720. }
  3721. function getSentence(cm, cur, repeat, dir, inclusive /*includes whitespace*/) {
  3722. /*
  3723. Takes an index object
  3724. {
  3725. line: the line string,
  3726. ln: line number,
  3727. pos: index in line,
  3728. dir: direction of traversal (-1 or 1)
  3729. }
  3730. and modifies the pos member to represent the
  3731. next valid position or sets the line to null if there are
  3732. no more valid positions.
  3733. */
  3734. function nextChar(curr) {
  3735. if (curr.pos + curr.dir < 0 || curr.pos + curr.dir >= curr.line.length) {
  3736. curr.line = null;
  3737. }
  3738. else {
  3739. curr.pos += curr.dir;
  3740. }
  3741. }
  3742. /*
  3743. Performs one iteration of traversal in forward direction
  3744. Returns an index object of the new location
  3745. */
  3746. function forward(cm, ln, pos, dir) {
  3747. var line = cm.getLine(ln);
  3748. var curr = {
  3749. line: line,
  3750. ln: ln,
  3751. pos: pos,
  3752. dir: dir,
  3753. };
  3754. if (curr.line === "") {
  3755. return { ln: curr.ln, pos: curr.pos };
  3756. }
  3757. var lastSentencePos = curr.pos;
  3758. // Move one step to skip character we start on
  3759. nextChar(curr);
  3760. while (curr.line !== null) {
  3761. lastSentencePos = curr.pos;
  3762. if (isEndOfSentenceSymbol(curr.line[curr.pos])) {
  3763. if (!inclusive) {
  3764. return { ln: curr.ln, pos: curr.pos + 1 };
  3765. } else {
  3766. nextChar(curr);
  3767. while (curr.line !== null ) {
  3768. if (isWhiteSpaceString(curr.line[curr.pos])) {
  3769. lastSentencePos = curr.pos;
  3770. nextChar(curr);
  3771. } else {
  3772. break;
  3773. }
  3774. }
  3775. return { ln: curr.ln, pos: lastSentencePos + 1, };
  3776. }
  3777. }
  3778. nextChar(curr);
  3779. }
  3780. return { ln: curr.ln, pos: lastSentencePos + 1 };
  3781. }
  3782. /*
  3783. Performs one iteration of traversal in reverse direction
  3784. Returns an index object of the new location
  3785. */
  3786. function reverse(cm, ln, pos, dir) {
  3787. var line = cm.getLine(ln);
  3788. var curr = {
  3789. line: line,
  3790. ln: ln,
  3791. pos: pos,
  3792. dir: dir,
  3793. };
  3794. if (curr.line === "") {
  3795. return { ln: curr.ln, pos: curr.pos };
  3796. }
  3797. var lastSentencePos = curr.pos;
  3798. // Move one step to skip character we start on
  3799. nextChar(curr);
  3800. while (curr.line !== null) {
  3801. if (!isWhiteSpaceString(curr.line[curr.pos]) && !isEndOfSentenceSymbol(curr.line[curr.pos])) {
  3802. lastSentencePos = curr.pos;
  3803. }
  3804. else if (isEndOfSentenceSymbol(curr.line[curr.pos]) ) {
  3805. if (!inclusive) {
  3806. return { ln: curr.ln, pos: lastSentencePos };
  3807. } else {
  3808. if (isWhiteSpaceString(curr.line[curr.pos + 1])) {
  3809. return { ln: curr.ln, pos: curr.pos + 1, };
  3810. } else {
  3811. return {ln: curr.ln, pos: lastSentencePos};
  3812. }
  3813. }
  3814. }
  3815. nextChar(curr);
  3816. }
  3817. curr.line = line;
  3818. if (inclusive && isWhiteSpaceString(curr.line[curr.pos])) {
  3819. return { ln: curr.ln, pos: curr.pos };
  3820. } else {
  3821. return { ln: curr.ln, pos: lastSentencePos };
  3822. }
  3823. }
  3824. var curr_index = {
  3825. ln: cur.line,
  3826. pos: cur.ch,
  3827. };
  3828. while (repeat > 0) {
  3829. if (dir < 0) {
  3830. curr_index = reverse(cm, curr_index.ln, curr_index.pos, dir);
  3831. }
  3832. else {
  3833. curr_index = forward(cm, curr_index.ln, curr_index.pos, dir);
  3834. }
  3835. repeat--;
  3836. }
  3837. return new Pos(curr_index.ln, curr_index.pos);
  3838. }
  3839. function findSentence(cm, cur, repeat, dir) {
  3840. /*
  3841. Takes an index object
  3842. {
  3843. line: the line string,
  3844. ln: line number,
  3845. pos: index in line,
  3846. dir: direction of traversal (-1 or 1)
  3847. }
  3848. and modifies the line, ln, and pos members to represent the
  3849. next valid position or sets them to null if there are
  3850. no more valid positions.
  3851. */
  3852. function nextChar(cm, idx) {
  3853. if (idx.pos + idx.dir < 0 || idx.pos + idx.dir >= idx.line.length) {
  3854. idx.ln += idx.dir;
  3855. if (!isLine(cm, idx.ln)) {
  3856. idx.line = null;
  3857. idx.ln = null;
  3858. idx.pos = null;
  3859. return;
  3860. }
  3861. idx.line = cm.getLine(idx.ln);
  3862. idx.pos = (idx.dir > 0) ? 0 : idx.line.length - 1;
  3863. }
  3864. else {
  3865. idx.pos += idx.dir;
  3866. }
  3867. }
  3868. /*
  3869. Performs one iteration of traversal in forward direction
  3870. Returns an index object of the new location
  3871. */
  3872. function forward(cm, ln, pos, dir) {
  3873. var line = cm.getLine(ln);
  3874. var stop = (line === "");
  3875. var curr = {
  3876. line: line,
  3877. ln: ln,
  3878. pos: pos,
  3879. dir: dir,
  3880. };
  3881. var last_valid = {
  3882. ln: curr.ln,
  3883. pos: curr.pos,
  3884. };
  3885. var skip_empty_lines = (curr.line === "");
  3886. // Move one step to skip character we start on
  3887. nextChar(cm, curr);
  3888. while (curr.line !== null) {
  3889. last_valid.ln = curr.ln;
  3890. last_valid.pos = curr.pos;
  3891. if (curr.line === "" && !skip_empty_lines) {
  3892. return { ln: curr.ln, pos: curr.pos, };
  3893. }
  3894. else if (stop && curr.line !== "" && !isWhiteSpaceString(curr.line[curr.pos])) {
  3895. return { ln: curr.ln, pos: curr.pos, };
  3896. }
  3897. else if (isEndOfSentenceSymbol(curr.line[curr.pos])
  3898. && !stop
  3899. && (curr.pos === curr.line.length - 1
  3900. || isWhiteSpaceString(curr.line[curr.pos + 1]))) {
  3901. stop = true;
  3902. }
  3903. nextChar(cm, curr);
  3904. }
  3905. /*
  3906. Set the position to the last non whitespace character on the last
  3907. valid line in the case that we reach the end of the document.
  3908. */
  3909. var line = cm.getLine(last_valid.ln);
  3910. last_valid.pos = 0;
  3911. for(var i = line.length - 1; i >= 0; --i) {
  3912. if (!isWhiteSpaceString(line[i])) {
  3913. last_valid.pos = i;
  3914. break;
  3915. }
  3916. }
  3917. return last_valid;
  3918. }
  3919. /*
  3920. Performs one iteration of traversal in reverse direction
  3921. Returns an index object of the new location
  3922. */
  3923. function reverse(cm, ln, pos, dir) {
  3924. var line = cm.getLine(ln);
  3925. var curr = {
  3926. line: line,
  3927. ln: ln,
  3928. pos: pos,
  3929. dir: dir,
  3930. };
  3931. var last_valid = {
  3932. ln: curr.ln,
  3933. pos: null,
  3934. };
  3935. var skip_empty_lines = (curr.line === "");
  3936. // Move one step to skip character we start on
  3937. nextChar(cm, curr);
  3938. while (curr.line !== null) {
  3939. if (curr.line === "" && !skip_empty_lines) {
  3940. if (last_valid.pos !== null) {
  3941. return last_valid;
  3942. }
  3943. else {
  3944. return { ln: curr.ln, pos: curr.pos };
  3945. }
  3946. }
  3947. else if (isEndOfSentenceSymbol(curr.line[curr.pos])
  3948. && last_valid.pos !== null
  3949. && !(curr.ln === last_valid.ln && curr.pos + 1 === last_valid.pos)) {
  3950. return last_valid;
  3951. }
  3952. else if (curr.line !== "" && !isWhiteSpaceString(curr.line[curr.pos])) {
  3953. skip_empty_lines = false;
  3954. last_valid = { ln: curr.ln, pos: curr.pos };
  3955. }
  3956. nextChar(cm, curr);
  3957. }
  3958. /*
  3959. Set the position to the first non whitespace character on the last
  3960. valid line in the case that we reach the beginning of the document.
  3961. */
  3962. var line = cm.getLine(last_valid.ln);
  3963. last_valid.pos = 0;
  3964. for(var i = 0; i < line.length; ++i) {
  3965. if (!isWhiteSpaceString(line[i])) {
  3966. last_valid.pos = i;
  3967. break;
  3968. }
  3969. }
  3970. return last_valid;
  3971. }
  3972. var curr_index = {
  3973. ln: cur.line,
  3974. pos: cur.ch,
  3975. };
  3976. while (repeat > 0) {
  3977. if (dir < 0) {
  3978. curr_index = reverse(cm, curr_index.ln, curr_index.pos, dir);
  3979. }
  3980. else {
  3981. curr_index = forward(cm, curr_index.ln, curr_index.pos, dir);
  3982. }
  3983. repeat--;
  3984. }
  3985. return new Pos(curr_index.ln, curr_index.pos);
  3986. }
  3987. // TODO: perhaps this finagling of start and end positions belongs
  3988. // in codemirror/replaceRange?
  3989. function selectCompanionObject(cm, head, symb, inclusive) {
  3990. var cur = head, start, end;
  3991. var bracketRegexp = ({
  3992. '(': /[()]/, ')': /[()]/,
  3993. '[': /[[\]]/, ']': /[[\]]/,
  3994. '{': /[{}]/, '}': /[{}]/,
  3995. '<': /[<>]/, '>': /[<>]/})[symb];
  3996. var openSym = ({
  3997. '(': '(', ')': '(',
  3998. '[': '[', ']': '[',
  3999. '{': '{', '}': '{',
  4000. '<': '<', '>': '<'})[symb];
  4001. var curChar = cm.getLine(cur.line).charAt(cur.ch);
  4002. // Due to the behavior of scanForBracket, we need to add an offset if the
  4003. // cursor is on a matching open bracket.
  4004. var offset = curChar === openSym ? 1 : 0;
  4005. start = cm.scanForBracket(new Pos(cur.line, cur.ch + offset), -1, undefined, {'bracketRegex': bracketRegexp});
  4006. end = cm.scanForBracket(new Pos(cur.line, cur.ch + offset), 1, undefined, {'bracketRegex': bracketRegexp});
  4007. if (!start || !end) {
  4008. return { start: cur, end: cur };
  4009. }
  4010. start = start.pos;
  4011. end = end.pos;
  4012. if ((start.line == end.line && start.ch > end.ch)
  4013. || (start.line > end.line)) {
  4014. var tmp = start;
  4015. start = end;
  4016. end = tmp;
  4017. }
  4018. if (inclusive) {
  4019. end.ch += 1;
  4020. } else {
  4021. start.ch += 1;
  4022. }
  4023. return { start: start, end: end };
  4024. }
  4025. // Takes in a symbol and a cursor and tries to simulate text objects that
  4026. // have identical opening and closing symbols
  4027. // TODO support across multiple lines
  4028. function findBeginningAndEnd(cm, head, symb, inclusive) {
  4029. var cur = copyCursor(head);
  4030. var line = cm.getLine(cur.line);
  4031. var chars = line.split('');
  4032. var start, end, i, len;
  4033. var firstIndex = chars.indexOf(symb);
  4034. // the decision tree is to always look backwards for the beginning first,
  4035. // but if the cursor is in front of the first instance of the symb,
  4036. // then move the cursor forward
  4037. if (cur.ch < firstIndex) {
  4038. cur.ch = firstIndex;
  4039. // Why is this line even here???
  4040. // cm.setCursor(cur.line, firstIndex+1);
  4041. }
  4042. // otherwise if the cursor is currently on the closing symbol
  4043. else if (firstIndex < cur.ch && chars[cur.ch] == symb) {
  4044. end = cur.ch; // assign end to the current cursor
  4045. --cur.ch; // make sure to look backwards
  4046. }
  4047. // if we're currently on the symbol, we've got a start
  4048. if (chars[cur.ch] == symb && !end) {
  4049. start = cur.ch + 1; // assign start to ahead of the cursor
  4050. } else {
  4051. // go backwards to find the start
  4052. for (i = cur.ch; i > -1 && !start; i--) {
  4053. if (chars[i] == symb) {
  4054. start = i + 1;
  4055. }
  4056. }
  4057. }
  4058. // look forwards for the end symbol
  4059. if (start && !end) {
  4060. for (i = start, len = chars.length; i < len && !end; i++) {
  4061. if (chars[i] == symb) {
  4062. end = i;
  4063. }
  4064. }
  4065. }
  4066. // nothing found
  4067. if (!start || !end) {
  4068. return { start: cur, end: cur };
  4069. }
  4070. // include the symbols
  4071. if (inclusive) {
  4072. --start; ++end;
  4073. }
  4074. return {
  4075. start: new Pos(cur.line, start),
  4076. end: new Pos(cur.line, end)
  4077. };
  4078. }
  4079. // Search functions
  4080. defineOption('pcre', true, 'boolean');
  4081. function SearchState() {}
  4082. SearchState.prototype = {
  4083. getQuery: function() {
  4084. return vimGlobalState.query;
  4085. },
  4086. setQuery: function(query) {
  4087. vimGlobalState.query = query;
  4088. },
  4089. getOverlay: function() {
  4090. return this.searchOverlay;
  4091. },
  4092. setOverlay: function(overlay) {
  4093. this.searchOverlay = overlay;
  4094. },
  4095. isReversed: function() {
  4096. return vimGlobalState.isReversed;
  4097. },
  4098. setReversed: function(reversed) {
  4099. vimGlobalState.isReversed = reversed;
  4100. },
  4101. getScrollbarAnnotate: function() {
  4102. return this.annotate;
  4103. },
  4104. setScrollbarAnnotate: function(annotate) {
  4105. this.annotate = annotate;
  4106. }
  4107. };
  4108. function getSearchState(cm) {
  4109. var vim = cm.state.vim;
  4110. return vim.searchState_ || (vim.searchState_ = new SearchState());
  4111. }
  4112. function splitBySlash(argString) {
  4113. return splitBySeparator(argString, '/');
  4114. }
  4115. function findUnescapedSlashes(argString) {
  4116. return findUnescapedSeparators(argString, '/');
  4117. }
  4118. function splitBySeparator(argString, separator) {
  4119. var slashes = findUnescapedSeparators(argString, separator) || [];
  4120. if (!slashes.length) return [];
  4121. var tokens = [];
  4122. // in case of strings like foo/bar
  4123. if (slashes[0] !== 0) return;
  4124. for (var i = 0; i < slashes.length; i++) {
  4125. if (typeof slashes[i] == 'number')
  4126. tokens.push(argString.substring(slashes[i] + 1, slashes[i+1]));
  4127. }
  4128. return tokens;
  4129. }
  4130. function findUnescapedSeparators(str, separator) {
  4131. if (!separator)
  4132. separator = '/';
  4133. var escapeNextChar = false;
  4134. var slashes = [];
  4135. for (var i = 0; i < str.length; i++) {
  4136. var c = str.charAt(i);
  4137. if (!escapeNextChar && c == separator) {
  4138. slashes.push(i);
  4139. }
  4140. escapeNextChar = !escapeNextChar && (c == '\\');
  4141. }
  4142. return slashes;
  4143. }
  4144. // Translates a search string from ex (vim) syntax into javascript form.
  4145. function translateRegex(str) {
  4146. // When these match, add a '\' if unescaped or remove one if escaped.
  4147. var specials = '|(){';
  4148. // Remove, but never add, a '\' for these.
  4149. var unescape = '}';
  4150. var escapeNextChar = false;
  4151. var out = [];
  4152. for (var i = -1; i < str.length; i++) {
  4153. var c = str.charAt(i) || '';
  4154. var n = str.charAt(i+1) || '';
  4155. var specialComesNext = (n && specials.indexOf(n) != -1);
  4156. if (escapeNextChar) {
  4157. if (c !== '\\' || !specialComesNext) {
  4158. out.push(c);
  4159. }
  4160. escapeNextChar = false;
  4161. } else {
  4162. if (c === '\\') {
  4163. escapeNextChar = true;
  4164. // Treat the unescape list as special for removing, but not adding '\'.
  4165. if (n && unescape.indexOf(n) != -1) {
  4166. specialComesNext = true;
  4167. }
  4168. // Not passing this test means removing a '\'.
  4169. if (!specialComesNext || n === '\\') {
  4170. out.push(c);
  4171. }
  4172. } else {
  4173. out.push(c);
  4174. if (specialComesNext && n !== '\\') {
  4175. out.push('\\');
  4176. }
  4177. }
  4178. }
  4179. }
  4180. return out.join('');
  4181. }
  4182. // Translates the replace part of a search and replace from ex (vim) syntax into
  4183. // javascript form. Similar to translateRegex, but additionally fixes back references
  4184. // (translates '\[0..9]' to '$[0..9]') and follows different rules for escaping '$'.
  4185. var charUnescapes = {'\\n': '\n', '\\r': '\r', '\\t': '\t'};
  4186. function translateRegexReplace(str) {
  4187. var escapeNextChar = false;
  4188. var out = [];
  4189. for (var i = -1; i < str.length; i++) {
  4190. var c = str.charAt(i) || '';
  4191. var n = str.charAt(i+1) || '';
  4192. if (charUnescapes[c + n]) {
  4193. out.push(charUnescapes[c+n]);
  4194. i++;
  4195. } else if (escapeNextChar) {
  4196. // At any point in the loop, escapeNextChar is true if the previous
  4197. // character was a '\' and was not escaped.
  4198. out.push(c);
  4199. escapeNextChar = false;
  4200. } else {
  4201. if (c === '\\') {
  4202. escapeNextChar = true;
  4203. if ((isNumber(n) || n === '$')) {
  4204. out.push('$');
  4205. } else if (n !== '/' && n !== '\\') {
  4206. out.push('\\');
  4207. }
  4208. } else {
  4209. if (c === '$') {
  4210. out.push('$');
  4211. }
  4212. out.push(c);
  4213. if (n === '/') {
  4214. out.push('\\');
  4215. }
  4216. }
  4217. }
  4218. }
  4219. return out.join('');
  4220. }
  4221. // Unescape \ and / in the replace part, for PCRE mode.
  4222. var unescapes = {'\\/': '/', '\\\\': '\\', '\\n': '\n', '\\r': '\r', '\\t': '\t', '\\&':'&'};
  4223. function unescapeRegexReplace(str) {
  4224. var stream = new CodeMirror.StringStream(str);
  4225. var output = [];
  4226. while (!stream.eol()) {
  4227. // Search for \.
  4228. while (stream.peek() && stream.peek() != '\\') {
  4229. output.push(stream.next());
  4230. }
  4231. var matched = false;
  4232. for (var matcher in unescapes) {
  4233. if (stream.match(matcher, true)) {
  4234. matched = true;
  4235. output.push(unescapes[matcher]);
  4236. break;
  4237. }
  4238. }
  4239. if (!matched) {
  4240. // Don't change anything
  4241. output.push(stream.next());
  4242. }
  4243. }
  4244. return output.join('');
  4245. }
  4246. /**
  4247. * Extract the regular expression from the query and return a Regexp object.
  4248. * Returns null if the query is blank.
  4249. * If ignoreCase is passed in, the Regexp object will have the 'i' flag set.
  4250. * If smartCase is passed in, and the query contains upper case letters,
  4251. * then ignoreCase is overridden, and the 'i' flag will not be set.
  4252. * If the query contains the /i in the flag part of the regular expression,
  4253. * then both ignoreCase and smartCase are ignored, and 'i' will be passed
  4254. * through to the Regex object.
  4255. */
  4256. function parseQuery(query, ignoreCase, smartCase) {
  4257. // First update the last search register
  4258. var lastSearchRegister = vimGlobalState.registerController.getRegister('/');
  4259. lastSearchRegister.setText(query);
  4260. // Check if the query is already a regex.
  4261. if (query instanceof RegExp) { return query; }
  4262. // First try to extract regex + flags from the input. If no flags found,
  4263. // extract just the regex. IE does not accept flags directly defined in
  4264. // the regex string in the form /regex/flags
  4265. var slashes = findUnescapedSlashes(query);
  4266. var regexPart;
  4267. var forceIgnoreCase;
  4268. if (!slashes.length) {
  4269. // Query looks like 'regexp'
  4270. regexPart = query;
  4271. } else {
  4272. // Query looks like 'regexp/...'
  4273. regexPart = query.substring(0, slashes[0]);
  4274. var flagsPart = query.substring(slashes[0]);
  4275. forceIgnoreCase = (flagsPart.indexOf('i') != -1);
  4276. }
  4277. if (!regexPart) {
  4278. return null;
  4279. }
  4280. if (!getOption('pcre')) {
  4281. regexPart = translateRegex(regexPart);
  4282. }
  4283. if (smartCase) {
  4284. ignoreCase = (/^[^A-Z]*$/).test(regexPart);
  4285. }
  4286. var regexp = new RegExp(regexPart,
  4287. (ignoreCase || forceIgnoreCase) ? 'im' : 'm');
  4288. return regexp;
  4289. }
  4290. /**
  4291. * dom - Document Object Manipulator
  4292. * Usage:
  4293. * dom('<tag>'|<node>[, ...{<attributes>|<$styles>}|<child-node>|'<text>'])
  4294. * Examples:
  4295. * dom('div', {id:'xyz'}, dom('p', 'CM rocks!', {$color:'red'}))
  4296. * dom(document.head, dom('script', 'alert("hello!")'))
  4297. * Not supported:
  4298. * dom('p', ['arrays are objects'], Error('objects specify attributes'))
  4299. */
  4300. function dom(n) {
  4301. if (typeof n === 'string') n = document.createElement(n);
  4302. for (var a, i = 1; i < arguments.length; i++) {
  4303. if (!(a = arguments[i])) continue;
  4304. if (typeof a !== 'object') a = document.createTextNode(a);
  4305. if (a.nodeType) n.appendChild(a);
  4306. else for (var key in a) {
  4307. if (!Object.prototype.hasOwnProperty.call(a, key)) continue;
  4308. if (key[0] === '$') n.style[key.slice(1)] = a[key];
  4309. else n.setAttribute(key, a[key]);
  4310. }
  4311. }
  4312. return n;
  4313. }
  4314. function showConfirm(cm, template) {
  4315. var pre = dom('div', {$color: 'red', $whiteSpace: 'pre', class: 'cm-vim-message'}, template);
  4316. if (cm.openNotification) {
  4317. cm.openNotification(pre, {bottom: true, duration: 5000});
  4318. } else {
  4319. alert(pre.innerText);
  4320. }
  4321. }
  4322. function makePrompt(prefix, desc) {
  4323. return dom(document.createDocumentFragment(),
  4324. dom('span', {$fontFamily: 'monospace', $whiteSpace: 'pre'},
  4325. prefix,
  4326. dom('input', {type: 'text', autocorrect: 'off',
  4327. autocapitalize: 'off', spellcheck: 'false'})),
  4328. desc && dom('span', {$color: '#888'}, desc));
  4329. }
  4330. function showPrompt(cm, options) {
  4331. var template = makePrompt(options.prefix, options.desc);
  4332. if (cm.openDialog) {
  4333. cm.openDialog(template, options.onClose, {
  4334. onKeyDown: options.onKeyDown, onKeyUp: options.onKeyUp,
  4335. bottom: true, selectValueOnOpen: false, value: options.value
  4336. });
  4337. }
  4338. else {
  4339. var shortText = '';
  4340. if (typeof options.prefix != "string" && options.prefix) shortText += options.prefix.textContent;
  4341. if (options.desc) shortText += " " + options.desc;
  4342. options.onClose(prompt(shortText, ''));
  4343. }
  4344. }
  4345. function regexEqual(r1, r2) {
  4346. if (r1 instanceof RegExp && r2 instanceof RegExp) {
  4347. var props = ['global', 'multiline', 'ignoreCase', 'source'];
  4348. for (var i = 0; i < props.length; i++) {
  4349. var prop = props[i];
  4350. if (r1[prop] !== r2[prop]) {
  4351. return false;
  4352. }
  4353. }
  4354. return true;
  4355. }
  4356. return false;
  4357. }
  4358. // Returns true if the query is valid.
  4359. function updateSearchQuery(cm, rawQuery, ignoreCase, smartCase) {
  4360. if (!rawQuery) {
  4361. return;
  4362. }
  4363. var state = getSearchState(cm);
  4364. var query = parseQuery(rawQuery, !!ignoreCase, !!smartCase);
  4365. if (!query) {
  4366. return;
  4367. }
  4368. highlightSearchMatches(cm, query);
  4369. if (regexEqual(query, state.getQuery())) {
  4370. return query;
  4371. }
  4372. state.setQuery(query);
  4373. return query;
  4374. }
  4375. function searchOverlay(query) {
  4376. if (query.source.charAt(0) == '^') {
  4377. var matchSol = true;
  4378. }
  4379. return {
  4380. token: function(stream) {
  4381. if (matchSol && !stream.sol()) {
  4382. stream.skipToEnd();
  4383. return;
  4384. }
  4385. var match = stream.match(query, false);
  4386. if (match) {
  4387. if (match[0].length == 0) {
  4388. // Matched empty string, skip to next.
  4389. stream.next();
  4390. return 'searching';
  4391. }
  4392. if (!stream.sol()) {
  4393. // Backtrack 1 to match \b
  4394. stream.backUp(1);
  4395. if (!query.exec(stream.next() + match[0])) {
  4396. stream.next();
  4397. return null;
  4398. }
  4399. }
  4400. stream.match(query);
  4401. return 'searching';
  4402. }
  4403. while (!stream.eol()) {
  4404. stream.next();
  4405. if (stream.match(query, false)) break;
  4406. }
  4407. },
  4408. query: query
  4409. };
  4410. }
  4411. var highlightTimeout = 0;
  4412. function highlightSearchMatches(cm, query) {
  4413. clearTimeout(highlightTimeout);
  4414. highlightTimeout = setTimeout(function() {
  4415. if (!cm.state.vim) return;
  4416. var searchState = getSearchState(cm);
  4417. var overlay = searchState.getOverlay();
  4418. if (!overlay || query != overlay.query) {
  4419. if (overlay) {
  4420. cm.removeOverlay(overlay);
  4421. }
  4422. overlay = searchOverlay(query);
  4423. cm.addOverlay(overlay);
  4424. if (cm.showMatchesOnScrollbar) {
  4425. if (searchState.getScrollbarAnnotate()) {
  4426. searchState.getScrollbarAnnotate().clear();
  4427. }
  4428. searchState.setScrollbarAnnotate(cm.showMatchesOnScrollbar(query));
  4429. }
  4430. searchState.setOverlay(overlay);
  4431. }
  4432. }, 50);
  4433. }
  4434. function findNext(cm, prev, query, repeat) {
  4435. if (repeat === undefined) { repeat = 1; }
  4436. return cm.operation(function() {
  4437. var pos = cm.getCursor();
  4438. var cursor = cm.getSearchCursor(query, pos);
  4439. for (var i = 0; i < repeat; i++) {
  4440. var found = cursor.find(prev);
  4441. if (i == 0 && found && cursorEqual(cursor.from(), pos)) {
  4442. var lastEndPos = prev ? cursor.from() : cursor.to();
  4443. found = cursor.find(prev);
  4444. if (found && !found[0] && cursorEqual(cursor.from(), lastEndPos)) {
  4445. if (cm.getLine(lastEndPos.line).length == lastEndPos.ch)
  4446. found = cursor.find(prev);
  4447. }
  4448. }
  4449. if (!found) {
  4450. // SearchCursor may have returned null because it hit EOF, wrap
  4451. // around and try again.
  4452. cursor = cm.getSearchCursor(query,
  4453. (prev) ? new Pos(cm.lastLine()) : new Pos(cm.firstLine(), 0) );
  4454. if (!cursor.find(prev)) {
  4455. return;
  4456. }
  4457. }
  4458. }
  4459. return cursor.from();
  4460. });
  4461. }
  4462. /**
  4463. * Pretty much the same as `findNext`, except for the following differences:
  4464. *
  4465. * 1. Before starting the search, move to the previous search. This way if our cursor is
  4466. * already inside a match, we should return the current match.
  4467. * 2. Rather than only returning the cursor's from, we return the cursor's from and to as a tuple.
  4468. */
  4469. function findNextFromAndToInclusive(cm, prev, query, repeat, vim) {
  4470. if (repeat === undefined) { repeat = 1; }
  4471. return cm.operation(function() {
  4472. var pos = cm.getCursor();
  4473. var cursor = cm.getSearchCursor(query, pos);
  4474. // Go back one result to ensure that if the cursor is currently a match, we keep it.
  4475. var found = cursor.find(!prev);
  4476. // If we haven't moved, go back one more (similar to if i==0 logic in findNext).
  4477. if (!vim.visualMode && found && cursorEqual(cursor.from(), pos)) {
  4478. cursor.find(!prev);
  4479. }
  4480. for (var i = 0; i < repeat; i++) {
  4481. found = cursor.find(prev);
  4482. if (!found) {
  4483. // SearchCursor may have returned null because it hit EOF, wrap
  4484. // around and try again.
  4485. cursor = cm.getSearchCursor(query,
  4486. (prev) ? new Pos(cm.lastLine()) : new Pos(cm.firstLine(), 0) );
  4487. if (!cursor.find(prev)) {
  4488. return;
  4489. }
  4490. }
  4491. }
  4492. return [cursor.from(), cursor.to()];
  4493. });
  4494. }
  4495. function clearSearchHighlight(cm) {
  4496. var state = getSearchState(cm);
  4497. cm.removeOverlay(getSearchState(cm).getOverlay());
  4498. state.setOverlay(null);
  4499. if (state.getScrollbarAnnotate()) {
  4500. state.getScrollbarAnnotate().clear();
  4501. state.setScrollbarAnnotate(null);
  4502. }
  4503. }
  4504. /**
  4505. * Check if pos is in the specified range, INCLUSIVE.
  4506. * Range can be specified with 1 or 2 arguments.
  4507. * If the first range argument is an array, treat it as an array of line
  4508. * numbers. Match pos against any of the lines.
  4509. * If the first range argument is a number,
  4510. * if there is only 1 range argument, check if pos has the same line
  4511. * number
  4512. * if there are 2 range arguments, then check if pos is in between the two
  4513. * range arguments.
  4514. */
  4515. function isInRange(pos, start, end) {
  4516. if (typeof pos != 'number') {
  4517. // Assume it is a cursor position. Get the line number.
  4518. pos = pos.line;
  4519. }
  4520. if (start instanceof Array) {
  4521. return inArray(pos, start);
  4522. } else {
  4523. if (typeof end == 'number') {
  4524. return (pos >= start && pos <= end);
  4525. } else {
  4526. return pos == start;
  4527. }
  4528. }
  4529. }
  4530. function getUserVisibleLines(cm) {
  4531. var scrollInfo = cm.getScrollInfo();
  4532. var occludeToleranceTop = 6;
  4533. var occludeToleranceBottom = 10;
  4534. var from = cm.coordsChar({left:0, top: occludeToleranceTop + scrollInfo.top}, 'local');
  4535. var bottomY = scrollInfo.clientHeight - occludeToleranceBottom + scrollInfo.top;
  4536. var to = cm.coordsChar({left:0, top: bottomY}, 'local');
  4537. return {top: from.line, bottom: to.line};
  4538. }
  4539. function getMarkPos(cm, vim, markName) {
  4540. if (markName == '\'' || markName == '`') {
  4541. return vimGlobalState.jumpList.find(cm, -1) || new Pos(0, 0);
  4542. } else if (markName == '.') {
  4543. return getLastEditPos(cm);
  4544. }
  4545. var mark = vim.marks[markName];
  4546. return mark && mark.find();
  4547. }
  4548. function getLastEditPos(cm) {
  4549. var done = cm.doc.history.done;
  4550. for (var i = done.length; i--;) {
  4551. if (done[i].changes) {
  4552. return copyCursor(done[i].changes[0].to);
  4553. }
  4554. }
  4555. }
  4556. var ExCommandDispatcher = function() {
  4557. this.buildCommandMap_();
  4558. };
  4559. ExCommandDispatcher.prototype = {
  4560. processCommand: function(cm, input, opt_params) {
  4561. var that = this;
  4562. cm.operation(function () {
  4563. cm.curOp.isVimOp = true;
  4564. that._processCommand(cm, input, opt_params);
  4565. });
  4566. },
  4567. _processCommand: function(cm, input, opt_params) {
  4568. var vim = cm.state.vim;
  4569. var commandHistoryRegister = vimGlobalState.registerController.getRegister(':');
  4570. var previousCommand = commandHistoryRegister.toString();
  4571. if (vim.visualMode) {
  4572. exitVisualMode(cm);
  4573. }
  4574. var inputStream = new CodeMirror.StringStream(input);
  4575. // update ": with the latest command whether valid or invalid
  4576. commandHistoryRegister.setText(input);
  4577. var params = opt_params || {};
  4578. params.input = input;
  4579. try {
  4580. this.parseInput_(cm, inputStream, params);
  4581. } catch(e) {
  4582. showConfirm(cm, e.toString());
  4583. throw e;
  4584. }
  4585. var command;
  4586. var commandName;
  4587. if (!params.commandName) {
  4588. // If only a line range is defined, move to the line.
  4589. if (params.line !== undefined) {
  4590. commandName = 'move';
  4591. }
  4592. } else {
  4593. command = this.matchCommand_(params.commandName);
  4594. if (command) {
  4595. commandName = command.name;
  4596. if (command.excludeFromCommandHistory) {
  4597. commandHistoryRegister.setText(previousCommand);
  4598. }
  4599. this.parseCommandArgs_(inputStream, params, command);
  4600. if (command.type == 'exToKey') {
  4601. // Handle Ex to Key mapping.
  4602. for (var i = 0; i < command.toKeys.length; i++) {
  4603. vimApi.handleKey(cm, command.toKeys[i], 'mapping');
  4604. }
  4605. return;
  4606. } else if (command.type == 'exToEx') {
  4607. // Handle Ex to Ex mapping.
  4608. this.processCommand(cm, command.toInput);
  4609. return;
  4610. }
  4611. }
  4612. }
  4613. if (!commandName) {
  4614. showConfirm(cm, 'Not an editor command ":' + input + '"');
  4615. return;
  4616. }
  4617. try {
  4618. exCommands[commandName](cm, params);
  4619. // Possibly asynchronous commands (e.g. substitute, which might have a
  4620. // user confirmation), are responsible for calling the callback when
  4621. // done. All others have it taken care of for them here.
  4622. if ((!command || !command.possiblyAsync) && params.callback) {
  4623. params.callback();
  4624. }
  4625. } catch(e) {
  4626. showConfirm(cm, e.toString());
  4627. throw e;
  4628. }
  4629. },
  4630. parseInput_: function(cm, inputStream, result) {
  4631. inputStream.eatWhile(':');
  4632. // Parse range.
  4633. if (inputStream.eat('%')) {
  4634. result.line = cm.firstLine();
  4635. result.lineEnd = cm.lastLine();
  4636. } else {
  4637. result.line = this.parseLineSpec_(cm, inputStream);
  4638. if (result.line !== undefined && inputStream.eat(',')) {
  4639. result.lineEnd = this.parseLineSpec_(cm, inputStream);
  4640. }
  4641. }
  4642. // Parse command name.
  4643. var commandMatch = inputStream.match(/^(\w+|!!|@@|[!#&*<=>@~])/);
  4644. if (commandMatch) {
  4645. result.commandName = commandMatch[1];
  4646. } else {
  4647. result.commandName = inputStream.match(/.*/)[0];
  4648. }
  4649. return result;
  4650. },
  4651. parseLineSpec_: function(cm, inputStream) {
  4652. var numberMatch = inputStream.match(/^(\d+)/);
  4653. if (numberMatch) {
  4654. // Absolute line number plus offset (N+M or N-M) is probably a typo,
  4655. // not something the user actually wanted. (NB: vim does allow this.)
  4656. return parseInt(numberMatch[1], 10) - 1;
  4657. }
  4658. switch (inputStream.next()) {
  4659. case '.':
  4660. return this.parseLineSpecOffset_(inputStream, cm.getCursor().line);
  4661. case '$':
  4662. return this.parseLineSpecOffset_(inputStream, cm.lastLine());
  4663. case '\'':
  4664. var markName = inputStream.next();
  4665. var markPos = getMarkPos(cm, cm.state.vim, markName);
  4666. if (!markPos) throw new Error('Mark not set');
  4667. return this.parseLineSpecOffset_(inputStream, markPos.line);
  4668. case '-':
  4669. case '+':
  4670. inputStream.backUp(1);
  4671. // Offset is relative to current line if not otherwise specified.
  4672. return this.parseLineSpecOffset_(inputStream, cm.getCursor().line);
  4673. default:
  4674. inputStream.backUp(1);
  4675. return undefined;
  4676. }
  4677. },
  4678. parseLineSpecOffset_: function(inputStream, line) {
  4679. var offsetMatch = inputStream.match(/^([+-])?(\d+)/);
  4680. if (offsetMatch) {
  4681. var offset = parseInt(offsetMatch[2], 10);
  4682. if (offsetMatch[1] == "-") {
  4683. line -= offset;
  4684. } else {
  4685. line += offset;
  4686. }
  4687. }
  4688. return line;
  4689. },
  4690. parseCommandArgs_: function(inputStream, params, command) {
  4691. if (inputStream.eol()) {
  4692. return;
  4693. }
  4694. params.argString = inputStream.match(/.*/)[0];
  4695. // Parse command-line arguments
  4696. var delim = command.argDelimiter || /\s+/;
  4697. var args = trim(params.argString).split(delim);
  4698. if (args.length && args[0]) {
  4699. params.args = args;
  4700. }
  4701. },
  4702. matchCommand_: function(commandName) {
  4703. // Return the command in the command map that matches the shortest
  4704. // prefix of the passed in command name. The match is guaranteed to be
  4705. // unambiguous if the defaultExCommandMap's shortNames are set up
  4706. // correctly. (see @code{defaultExCommandMap}).
  4707. for (var i = commandName.length; i > 0; i--) {
  4708. var prefix = commandName.substring(0, i);
  4709. if (this.commandMap_[prefix]) {
  4710. var command = this.commandMap_[prefix];
  4711. if (command.name.indexOf(commandName) === 0) {
  4712. return command;
  4713. }
  4714. }
  4715. }
  4716. return null;
  4717. },
  4718. buildCommandMap_: function() {
  4719. this.commandMap_ = {};
  4720. for (var i = 0; i < defaultExCommandMap.length; i++) {
  4721. var command = defaultExCommandMap[i];
  4722. var key = command.shortName || command.name;
  4723. this.commandMap_[key] = command;
  4724. }
  4725. },
  4726. map: function(lhs, rhs, ctx) {
  4727. if (lhs != ':' && lhs.charAt(0) == ':') {
  4728. if (ctx) { throw Error('Mode not supported for ex mappings'); }
  4729. var commandName = lhs.substring(1);
  4730. if (rhs != ':' && rhs.charAt(0) == ':') {
  4731. // Ex to Ex mapping
  4732. this.commandMap_[commandName] = {
  4733. name: commandName,
  4734. type: 'exToEx',
  4735. toInput: rhs.substring(1),
  4736. user: true
  4737. };
  4738. } else {
  4739. // Ex to key mapping
  4740. this.commandMap_[commandName] = {
  4741. name: commandName,
  4742. type: 'exToKey',
  4743. toKeys: rhs,
  4744. user: true
  4745. };
  4746. }
  4747. } else {
  4748. if (rhs != ':' && rhs.charAt(0) == ':') {
  4749. // Key to Ex mapping.
  4750. var mapping = {
  4751. keys: lhs,
  4752. type: 'keyToEx',
  4753. exArgs: { input: rhs.substring(1) }
  4754. };
  4755. if (ctx) { mapping.context = ctx; }
  4756. defaultKeymap.unshift(mapping);
  4757. } else {
  4758. // Key to key mapping
  4759. var mapping = {
  4760. keys: lhs,
  4761. type: 'keyToKey',
  4762. toKeys: rhs
  4763. };
  4764. if (ctx) { mapping.context = ctx; }
  4765. defaultKeymap.unshift(mapping);
  4766. }
  4767. }
  4768. },
  4769. unmap: function(lhs, ctx) {
  4770. if (lhs != ':' && lhs.charAt(0) == ':') {
  4771. // Ex to Ex or Ex to key mapping
  4772. if (ctx) { throw Error('Mode not supported for ex mappings'); }
  4773. var commandName = lhs.substring(1);
  4774. if (this.commandMap_[commandName] && this.commandMap_[commandName].user) {
  4775. delete this.commandMap_[commandName];
  4776. return true;
  4777. }
  4778. } else {
  4779. // Key to Ex or key to key mapping
  4780. var keys = lhs;
  4781. for (var i = 0; i < defaultKeymap.length; i++) {
  4782. if (keys == defaultKeymap[i].keys
  4783. && defaultKeymap[i].context === ctx) {
  4784. defaultKeymap.splice(i, 1);
  4785. return true;
  4786. }
  4787. }
  4788. }
  4789. }
  4790. };
  4791. var exCommands = {
  4792. colorscheme: function(cm, params) {
  4793. if (!params.args || params.args.length < 1) {
  4794. showConfirm(cm, cm.getOption('theme'));
  4795. return;
  4796. }
  4797. cm.setOption('theme', params.args[0]);
  4798. },
  4799. map: function(cm, params, ctx) {
  4800. var mapArgs = params.args;
  4801. if (!mapArgs || mapArgs.length < 2) {
  4802. if (cm) {
  4803. showConfirm(cm, 'Invalid mapping: ' + params.input);
  4804. }
  4805. return;
  4806. }
  4807. exCommandDispatcher.map(mapArgs[0], mapArgs[1], ctx);
  4808. },
  4809. imap: function(cm, params) { this.map(cm, params, 'insert'); },
  4810. nmap: function(cm, params) { this.map(cm, params, 'normal'); },
  4811. vmap: function(cm, params) { this.map(cm, params, 'visual'); },
  4812. unmap: function(cm, params, ctx) {
  4813. var mapArgs = params.args;
  4814. if (!mapArgs || mapArgs.length < 1 || !exCommandDispatcher.unmap(mapArgs[0], ctx)) {
  4815. if (cm) {
  4816. showConfirm(cm, 'No such mapping: ' + params.input);
  4817. }
  4818. }
  4819. },
  4820. move: function(cm, params) {
  4821. commandDispatcher.processCommand(cm, cm.state.vim, {
  4822. type: 'motion',
  4823. motion: 'moveToLineOrEdgeOfDocument',
  4824. motionArgs: { forward: false, explicitRepeat: true,
  4825. linewise: true },
  4826. repeatOverride: params.line+1});
  4827. },
  4828. set: function(cm, params) {
  4829. var setArgs = params.args;
  4830. // Options passed through to the setOption/getOption calls. May be passed in by the
  4831. // local/global versions of the set command
  4832. var setCfg = params.setCfg || {};
  4833. if (!setArgs || setArgs.length < 1) {
  4834. if (cm) {
  4835. showConfirm(cm, 'Invalid mapping: ' + params.input);
  4836. }
  4837. return;
  4838. }
  4839. var expr = setArgs[0].split('=');
  4840. var optionName = expr[0];
  4841. var value = expr[1];
  4842. var forceGet = false;
  4843. if (optionName.charAt(optionName.length - 1) == '?') {
  4844. // If post-fixed with ?, then the set is actually a get.
  4845. if (value) { throw Error('Trailing characters: ' + params.argString); }
  4846. optionName = optionName.substring(0, optionName.length - 1);
  4847. forceGet = true;
  4848. }
  4849. if (value === undefined && optionName.substring(0, 2) == 'no') {
  4850. // To set boolean options to false, the option name is prefixed with
  4851. // 'no'.
  4852. optionName = optionName.substring(2);
  4853. value = false;
  4854. }
  4855. var optionIsBoolean = options[optionName] && options[optionName].type == 'boolean';
  4856. if (optionIsBoolean && value == undefined) {
  4857. // Calling set with a boolean option sets it to true.
  4858. value = true;
  4859. }
  4860. // If no value is provided, then we assume this is a get.
  4861. if (!optionIsBoolean && value === undefined || forceGet) {
  4862. var oldValue = getOption(optionName, cm, setCfg);
  4863. if (oldValue instanceof Error) {
  4864. showConfirm(cm, oldValue.message);
  4865. } else if (oldValue === true || oldValue === false) {
  4866. showConfirm(cm, ' ' + (oldValue ? '' : 'no') + optionName);
  4867. } else {
  4868. showConfirm(cm, ' ' + optionName + '=' + oldValue);
  4869. }
  4870. } else {
  4871. var setOptionReturn = setOption(optionName, value, cm, setCfg);
  4872. if (setOptionReturn instanceof Error) {
  4873. showConfirm(cm, setOptionReturn.message);
  4874. }
  4875. }
  4876. },
  4877. setlocal: function (cm, params) {
  4878. // setCfg is passed through to setOption
  4879. params.setCfg = {scope: 'local'};
  4880. this.set(cm, params);
  4881. },
  4882. setglobal: function (cm, params) {
  4883. // setCfg is passed through to setOption
  4884. params.setCfg = {scope: 'global'};
  4885. this.set(cm, params);
  4886. },
  4887. registers: function(cm, params) {
  4888. var regArgs = params.args;
  4889. var registers = vimGlobalState.registerController.registers;
  4890. var regInfo = '----------Registers----------\n\n';
  4891. if (!regArgs) {
  4892. for (var registerName in registers) {
  4893. var text = registers[registerName].toString();
  4894. if (text.length) {
  4895. regInfo += '"' + registerName + ' ' + text + '\n';
  4896. }
  4897. }
  4898. } else {
  4899. var registerName;
  4900. regArgs = regArgs.join('');
  4901. for (var i = 0; i < regArgs.length; i++) {
  4902. registerName = regArgs.charAt(i);
  4903. if (!vimGlobalState.registerController.isValidRegister(registerName)) {
  4904. continue;
  4905. }
  4906. var register = registers[registerName] || new Register();
  4907. regInfo += '"' + registerName + ' ' + register.toString() + '\n';
  4908. }
  4909. }
  4910. showConfirm(cm, regInfo);
  4911. },
  4912. sort: function(cm, params) {
  4913. var reverse, ignoreCase, unique, number, pattern;
  4914. function parseArgs() {
  4915. if (params.argString) {
  4916. var args = new CodeMirror.StringStream(params.argString);
  4917. if (args.eat('!')) { reverse = true; }
  4918. if (args.eol()) { return; }
  4919. if (!args.eatSpace()) { return 'Invalid arguments'; }
  4920. var opts = args.match(/([dinuox]+)?\s*(\/.+\/)?\s*/);
  4921. if (!opts && !args.eol()) { return 'Invalid arguments'; }
  4922. if (opts[1]) {
  4923. ignoreCase = opts[1].indexOf('i') != -1;
  4924. unique = opts[1].indexOf('u') != -1;
  4925. var decimal = opts[1].indexOf('d') != -1 || opts[1].indexOf('n') != -1 && 1;
  4926. var hex = opts[1].indexOf('x') != -1 && 1;
  4927. var octal = opts[1].indexOf('o') != -1 && 1;
  4928. if (decimal + hex + octal > 1) { return 'Invalid arguments'; }
  4929. number = decimal && 'decimal' || hex && 'hex' || octal && 'octal';
  4930. }
  4931. if (opts[2]) {
  4932. pattern = new RegExp(opts[2].substr(1, opts[2].length - 2), ignoreCase ? 'i' : '');
  4933. }
  4934. }
  4935. }
  4936. var err = parseArgs();
  4937. if (err) {
  4938. showConfirm(cm, err + ': ' + params.argString);
  4939. return;
  4940. }
  4941. var lineStart = params.line || cm.firstLine();
  4942. var lineEnd = params.lineEnd || params.line || cm.lastLine();
  4943. if (lineStart == lineEnd) { return; }
  4944. var curStart = new Pos(lineStart, 0);
  4945. var curEnd = new Pos(lineEnd, lineLength(cm, lineEnd));
  4946. var text = cm.getRange(curStart, curEnd).split('\n');
  4947. var numberRegex = pattern ? pattern :
  4948. (number == 'decimal') ? /(-?)([\d]+)/ :
  4949. (number == 'hex') ? /(-?)(?:0x)?([0-9a-f]+)/i :
  4950. (number == 'octal') ? /([0-7]+)/ : null;
  4951. var radix = (number == 'decimal') ? 10 : (number == 'hex') ? 16 : (number == 'octal') ? 8 : null;
  4952. var numPart = [], textPart = [];
  4953. if (number || pattern) {
  4954. for (var i = 0; i < text.length; i++) {
  4955. var matchPart = pattern ? text[i].match(pattern) : null;
  4956. if (matchPart && matchPart[0] != '') {
  4957. numPart.push(matchPart);
  4958. } else if (!pattern && numberRegex.exec(text[i])) {
  4959. numPart.push(text[i]);
  4960. } else {
  4961. textPart.push(text[i]);
  4962. }
  4963. }
  4964. } else {
  4965. textPart = text;
  4966. }
  4967. function compareFn(a, b) {
  4968. if (reverse) { var tmp; tmp = a; a = b; b = tmp; }
  4969. if (ignoreCase) { a = a.toLowerCase(); b = b.toLowerCase(); }
  4970. var anum = number && numberRegex.exec(a);
  4971. var bnum = number && numberRegex.exec(b);
  4972. if (!anum) { return a < b ? -1 : 1; }
  4973. anum = parseInt((anum[1] + anum[2]).toLowerCase(), radix);
  4974. bnum = parseInt((bnum[1] + bnum[2]).toLowerCase(), radix);
  4975. return anum - bnum;
  4976. }
  4977. function comparePatternFn(a, b) {
  4978. if (reverse) { var tmp; tmp = a; a = b; b = tmp; }
  4979. if (ignoreCase) { a[0] = a[0].toLowerCase(); b[0] = b[0].toLowerCase(); }
  4980. return (a[0] < b[0]) ? -1 : 1;
  4981. }
  4982. numPart.sort(pattern ? comparePatternFn : compareFn);
  4983. if (pattern) {
  4984. for (var i = 0; i < numPart.length; i++) {
  4985. numPart[i] = numPart[i].input;
  4986. }
  4987. } else if (!number) { textPart.sort(compareFn); }
  4988. text = (!reverse) ? textPart.concat(numPart) : numPart.concat(textPart);
  4989. if (unique) { // Remove duplicate lines
  4990. var textOld = text;
  4991. var lastLine;
  4992. text = [];
  4993. for (var i = 0; i < textOld.length; i++) {
  4994. if (textOld[i] != lastLine) {
  4995. text.push(textOld[i]);
  4996. }
  4997. lastLine = textOld[i];
  4998. }
  4999. }
  5000. cm.replaceRange(text.join('\n'), curStart, curEnd);
  5001. },
  5002. vglobal: function(cm, params) {
  5003. // global inspects params.commandName
  5004. this.global(cm, params);
  5005. },
  5006. global: function(cm, params) {
  5007. // a global command is of the form
  5008. // :[range]g/pattern/[cmd]
  5009. // argString holds the string /pattern/[cmd]
  5010. var argString = params.argString;
  5011. if (!argString) {
  5012. showConfirm(cm, 'Regular Expression missing from global');
  5013. return;
  5014. }
  5015. var inverted = params.commandName[0] === 'v';
  5016. // range is specified here
  5017. var lineStart = (params.line !== undefined) ? params.line : cm.firstLine();
  5018. var lineEnd = params.lineEnd || params.line || cm.lastLine();
  5019. // get the tokens from argString
  5020. var tokens = splitBySlash(argString);
  5021. var regexPart = argString, cmd;
  5022. if (tokens.length) {
  5023. regexPart = tokens[0];
  5024. cmd = tokens.slice(1, tokens.length).join('/');
  5025. }
  5026. if (regexPart) {
  5027. // If regex part is empty, then use the previous query. Otherwise
  5028. // use the regex part as the new query.
  5029. try {
  5030. updateSearchQuery(cm, regexPart, true /** ignoreCase */,
  5031. true /** smartCase */);
  5032. } catch (e) {
  5033. showConfirm(cm, 'Invalid regex: ' + regexPart);
  5034. return;
  5035. }
  5036. }
  5037. // now that we have the regexPart, search for regex matches in the
  5038. // specified range of lines
  5039. var query = getSearchState(cm).getQuery();
  5040. var matchedLines = [];
  5041. for (var i = lineStart; i <= lineEnd; i++) {
  5042. var line = cm.getLineHandle(i);
  5043. var matched = query.test(line.text);
  5044. if (matched !== inverted) {
  5045. matchedLines.push(cmd ? line : line.text);
  5046. }
  5047. }
  5048. // if there is no [cmd], just display the list of matched lines
  5049. if (!cmd) {
  5050. showConfirm(cm, matchedLines.join('\n'));
  5051. return;
  5052. }
  5053. var index = 0;
  5054. var nextCommand = function() {
  5055. if (index < matchedLines.length) {
  5056. var line = matchedLines[index++];
  5057. var lineNum = cm.getLineNumber(line);
  5058. if (lineNum == null) {
  5059. nextCommand();
  5060. return;
  5061. }
  5062. var command = (lineNum + 1) + cmd;
  5063. exCommandDispatcher.processCommand(cm, command, {
  5064. callback: nextCommand
  5065. });
  5066. }
  5067. };
  5068. nextCommand();
  5069. },
  5070. substitute: function(cm, params) {
  5071. if (!cm.getSearchCursor) {
  5072. throw new Error('Search feature not available. Requires searchcursor.js or ' +
  5073. 'any other getSearchCursor implementation.');
  5074. }
  5075. var argString = params.argString;
  5076. var tokens = argString ? splitBySeparator(argString, argString[0]) : [];
  5077. var regexPart, replacePart = '', trailing, flagsPart, count;
  5078. var confirm = false; // Whether to confirm each replace.
  5079. var global = false; // True to replace all instances on a line, false to replace only 1.
  5080. if (tokens.length) {
  5081. regexPart = tokens[0];
  5082. if (getOption('pcre') && regexPart !== '') {
  5083. regexPart = new RegExp(regexPart).source; //normalize not escaped characters
  5084. }
  5085. replacePart = tokens[1];
  5086. if (replacePart !== undefined) {
  5087. if (getOption('pcre')) {
  5088. replacePart = unescapeRegexReplace(replacePart.replace(/([^\\])&/g,"$1$$&"));
  5089. } else {
  5090. replacePart = translateRegexReplace(replacePart);
  5091. }
  5092. vimGlobalState.lastSubstituteReplacePart = replacePart;
  5093. }
  5094. trailing = tokens[2] ? tokens[2].split(' ') : [];
  5095. } else {
  5096. // either the argString is empty or its of the form ' hello/world'
  5097. // actually splitBySlash returns a list of tokens
  5098. // only if the string starts with a '/'
  5099. if (argString && argString.length) {
  5100. showConfirm(cm, 'Substitutions should be of the form ' +
  5101. ':s/pattern/replace/');
  5102. return;
  5103. }
  5104. }
  5105. // After the 3rd slash, we can have flags followed by a space followed
  5106. // by count.
  5107. if (trailing) {
  5108. flagsPart = trailing[0];
  5109. count = parseInt(trailing[1]);
  5110. if (flagsPart) {
  5111. if (flagsPart.indexOf('c') != -1) {
  5112. confirm = true;
  5113. }
  5114. if (flagsPart.indexOf('g') != -1) {
  5115. global = true;
  5116. }
  5117. if (getOption('pcre')) {
  5118. regexPart = regexPart + '/' + flagsPart;
  5119. } else {
  5120. regexPart = regexPart.replace(/\//g, "\\/") + '/' + flagsPart;
  5121. }
  5122. }
  5123. }
  5124. if (regexPart) {
  5125. // If regex part is empty, then use the previous query. Otherwise use
  5126. // the regex part as the new query.
  5127. try {
  5128. updateSearchQuery(cm, regexPart, true /** ignoreCase */,
  5129. true /** smartCase */);
  5130. } catch (e) {
  5131. showConfirm(cm, 'Invalid regex: ' + regexPart);
  5132. return;
  5133. }
  5134. }
  5135. replacePart = replacePart || vimGlobalState.lastSubstituteReplacePart;
  5136. if (replacePart === undefined) {
  5137. showConfirm(cm, 'No previous substitute regular expression');
  5138. return;
  5139. }
  5140. var state = getSearchState(cm);
  5141. var query = state.getQuery();
  5142. var lineStart = (params.line !== undefined) ? params.line : cm.getCursor().line;
  5143. var lineEnd = params.lineEnd || lineStart;
  5144. if (lineStart == cm.firstLine() && lineEnd == cm.lastLine()) {
  5145. lineEnd = Infinity;
  5146. }
  5147. if (count) {
  5148. lineStart = lineEnd;
  5149. lineEnd = lineStart + count - 1;
  5150. }
  5151. var startPos = clipCursorToContent(cm, new Pos(lineStart, 0));
  5152. var cursor = cm.getSearchCursor(query, startPos);
  5153. doReplace(cm, confirm, global, lineStart, lineEnd, cursor, query, replacePart, params.callback);
  5154. },
  5155. redo: CodeMirror.commands.redo,
  5156. undo: CodeMirror.commands.undo,
  5157. write: function(cm) {
  5158. if (CodeMirror.commands.save) {
  5159. // If a save command is defined, call it.
  5160. CodeMirror.commands.save(cm);
  5161. } else if (cm.save) {
  5162. // Saves to text area if no save command is defined and cm.save() is available.
  5163. cm.save();
  5164. }
  5165. },
  5166. nohlsearch: function(cm) {
  5167. clearSearchHighlight(cm);
  5168. },
  5169. yank: function (cm) {
  5170. var cur = copyCursor(cm.getCursor());
  5171. var line = cur.line;
  5172. var lineText = cm.getLine(line);
  5173. vimGlobalState.registerController.pushText(
  5174. '0', 'yank', lineText, true, true);
  5175. },
  5176. delmarks: function(cm, params) {
  5177. if (!params.argString || !trim(params.argString)) {
  5178. showConfirm(cm, 'Argument required');
  5179. return;
  5180. }
  5181. var state = cm.state.vim;
  5182. var stream = new CodeMirror.StringStream(trim(params.argString));
  5183. while (!stream.eol()) {
  5184. stream.eatSpace();
  5185. // Record the streams position at the beginning of the loop for use
  5186. // in error messages.
  5187. var count = stream.pos;
  5188. if (!stream.match(/[a-zA-Z]/, false)) {
  5189. showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));
  5190. return;
  5191. }
  5192. var sym = stream.next();
  5193. // Check if this symbol is part of a range
  5194. if (stream.match('-', true)) {
  5195. // This symbol is part of a range.
  5196. // The range must terminate at an alphabetic character.
  5197. if (!stream.match(/[a-zA-Z]/, false)) {
  5198. showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));
  5199. return;
  5200. }
  5201. var startMark = sym;
  5202. var finishMark = stream.next();
  5203. // The range must terminate at an alphabetic character which
  5204. // shares the same case as the start of the range.
  5205. if (isLowerCase(startMark) && isLowerCase(finishMark) ||
  5206. isUpperCase(startMark) && isUpperCase(finishMark)) {
  5207. var start = startMark.charCodeAt(0);
  5208. var finish = finishMark.charCodeAt(0);
  5209. if (start >= finish) {
  5210. showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));
  5211. return;
  5212. }
  5213. // Because marks are always ASCII values, and we have
  5214. // determined that they are the same case, we can use
  5215. // their char codes to iterate through the defined range.
  5216. for (var j = 0; j <= finish - start; j++) {
  5217. var mark = String.fromCharCode(start + j);
  5218. delete state.marks[mark];
  5219. }
  5220. } else {
  5221. showConfirm(cm, 'Invalid argument: ' + startMark + '-');
  5222. return;
  5223. }
  5224. } else {
  5225. // This symbol is a valid mark, and is not part of a range.
  5226. delete state.marks[sym];
  5227. }
  5228. }
  5229. }
  5230. };
  5231. var exCommandDispatcher = new ExCommandDispatcher();
  5232. /**
  5233. * @param {CodeMirror} cm CodeMirror instance we are in.
  5234. * @param {boolean} confirm Whether to confirm each replace.
  5235. * @param {Cursor} lineStart Line to start replacing from.
  5236. * @param {Cursor} lineEnd Line to stop replacing at.
  5237. * @param {RegExp} query Query for performing matches with.
  5238. * @param {string} replaceWith Text to replace matches with. May contain $1,
  5239. * $2, etc for replacing captured groups using JavaScript replace.
  5240. * @param {function()} callback A callback for when the replace is done.
  5241. */
  5242. function doReplace(cm, confirm, global, lineStart, lineEnd, searchCursor, query,
  5243. replaceWith, callback) {
  5244. // Set up all the functions.
  5245. cm.state.vim.exMode = true;
  5246. var done = false;
  5247. var lastPos, modifiedLineNumber, joined;
  5248. function replaceAll() {
  5249. cm.operation(function() {
  5250. while (!done) {
  5251. replace();
  5252. next();
  5253. }
  5254. stop();
  5255. });
  5256. }
  5257. function replace() {
  5258. var text = cm.getRange(searchCursor.from(), searchCursor.to());
  5259. var newText = text.replace(query, replaceWith);
  5260. var unmodifiedLineNumber = searchCursor.to().line;
  5261. searchCursor.replace(newText);
  5262. modifiedLineNumber = searchCursor.to().line;
  5263. lineEnd += modifiedLineNumber - unmodifiedLineNumber;
  5264. joined = modifiedLineNumber < unmodifiedLineNumber;
  5265. }
  5266. function findNextValidMatch() {
  5267. var lastMatchTo = lastPos && copyCursor(searchCursor.to());
  5268. var match = searchCursor.findNext();
  5269. if (match && !match[0] && lastMatchTo && cursorEqual(searchCursor.from(), lastMatchTo)) {
  5270. match = searchCursor.findNext();
  5271. }
  5272. return match;
  5273. }
  5274. function next() {
  5275. // The below only loops to skip over multiple occurrences on the same
  5276. // line when 'global' is not true.
  5277. while(findNextValidMatch() &&
  5278. isInRange(searchCursor.from(), lineStart, lineEnd)) {
  5279. if (!global && searchCursor.from().line == modifiedLineNumber && !joined) {
  5280. continue;
  5281. }
  5282. cm.scrollIntoView(searchCursor.from(), 30);
  5283. cm.setSelection(searchCursor.from(), searchCursor.to());
  5284. lastPos = searchCursor.from();
  5285. done = false;
  5286. return;
  5287. }
  5288. done = true;
  5289. }
  5290. function stop(close) {
  5291. if (close) { close(); }
  5292. cm.focus();
  5293. if (lastPos) {
  5294. cm.setCursor(lastPos);
  5295. var vim = cm.state.vim;
  5296. vim.exMode = false;
  5297. vim.lastHPos = vim.lastHSPos = lastPos.ch;
  5298. }
  5299. if (callback) { callback(); }
  5300. }
  5301. function onPromptKeyDown(e, _value, close) {
  5302. // Swallow all keys.
  5303. CodeMirror.e_stop(e);
  5304. var keyName = CodeMirror.keyName(e);
  5305. switch (keyName) {
  5306. case 'Y':
  5307. replace(); next(); break;
  5308. case 'N':
  5309. next(); break;
  5310. case 'A':
  5311. // replaceAll contains a call to close of its own. We don't want it
  5312. // to fire too early or multiple times.
  5313. var savedCallback = callback;
  5314. callback = undefined;
  5315. cm.operation(replaceAll);
  5316. callback = savedCallback;
  5317. break;
  5318. case 'L':
  5319. replace();
  5320. // fall through and exit.
  5321. case 'Q':
  5322. case 'Esc':
  5323. case 'Ctrl-C':
  5324. case 'Ctrl-[':
  5325. stop(close);
  5326. break;
  5327. }
  5328. if (done) { stop(close); }
  5329. return true;
  5330. }
  5331. // Actually do replace.
  5332. next();
  5333. if (done) {
  5334. showConfirm(cm, 'No matches for ' + query.source);
  5335. return;
  5336. }
  5337. if (!confirm) {
  5338. replaceAll();
  5339. if (callback) { callback(); }
  5340. return;
  5341. }
  5342. showPrompt(cm, {
  5343. prefix: dom('span', 'replace with ', dom('strong', replaceWith), ' (y/n/a/q/l)'),
  5344. onKeyDown: onPromptKeyDown
  5345. });
  5346. }
  5347. CodeMirror.keyMap.vim = {
  5348. attach: attachVimMap,
  5349. detach: detachVimMap,
  5350. call: cmKey
  5351. };
  5352. function exitInsertMode(cm) {
  5353. var vim = cm.state.vim;
  5354. var macroModeState = vimGlobalState.macroModeState;
  5355. var insertModeChangeRegister = vimGlobalState.registerController.getRegister('.');
  5356. var isPlaying = macroModeState.isPlaying;
  5357. var lastChange = macroModeState.lastInsertModeChanges;
  5358. if (!isPlaying) {
  5359. cm.off('change', onChange);
  5360. CodeMirror.off(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown);
  5361. }
  5362. if (!isPlaying && vim.insertModeRepeat > 1) {
  5363. // Perform insert mode repeat for commands like 3,a and 3,o.
  5364. repeatLastEdit(cm, vim, vim.insertModeRepeat - 1,
  5365. true /** repeatForInsert */);
  5366. vim.lastEditInputState.repeatOverride = vim.insertModeRepeat;
  5367. }
  5368. delete vim.insertModeRepeat;
  5369. vim.insertMode = false;
  5370. cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1);
  5371. cm.setOption('keyMap', 'vim');
  5372. cm.setOption('disableInput', true);
  5373. cm.toggleOverwrite(false); // exit replace mode if we were in it.
  5374. // update the ". register before exiting insert mode
  5375. insertModeChangeRegister.setText(lastChange.changes.join(''));
  5376. CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
  5377. if (macroModeState.isRecording) {
  5378. logInsertModeChange(macroModeState);
  5379. }
  5380. }
  5381. function _mapCommand(command) {
  5382. defaultKeymap.unshift(command);
  5383. }
  5384. function mapCommand(keys, type, name, args, extra) {
  5385. var command = {keys: keys, type: type};
  5386. command[type] = name;
  5387. command[type + "Args"] = args;
  5388. for (var key in extra)
  5389. command[key] = extra[key];
  5390. _mapCommand(command);
  5391. }
  5392. // The timeout in milliseconds for the two-character ESC keymap should be
  5393. // adjusted according to your typing speed to prevent false positives.
  5394. defineOption('insertModeEscKeysTimeout', 200, 'number');
  5395. CodeMirror.keyMap['vim-insert'] = {
  5396. // TODO: override navigation keys so that Esc will cancel automatic
  5397. // indentation from o, O, i_<CR>
  5398. fallthrough: ['default'],
  5399. attach: attachVimMap,
  5400. detach: detachVimMap,
  5401. call: cmKey
  5402. };
  5403. CodeMirror.keyMap['vim-replace'] = {
  5404. 'Backspace': 'goCharLeft',
  5405. fallthrough: ['vim-insert'],
  5406. attach: attachVimMap,
  5407. detach: detachVimMap,
  5408. call: cmKey
  5409. };
  5410. function executeMacroRegister(cm, vim, macroModeState, registerName) {
  5411. var register = vimGlobalState.registerController.getRegister(registerName);
  5412. if (registerName == ':') {
  5413. // Read-only register containing last Ex command.
  5414. if (register.keyBuffer[0]) {
  5415. exCommandDispatcher.processCommand(cm, register.keyBuffer[0]);
  5416. }
  5417. macroModeState.isPlaying = false;
  5418. return;
  5419. }
  5420. var keyBuffer = register.keyBuffer;
  5421. var imc = 0;
  5422. macroModeState.isPlaying = true;
  5423. macroModeState.replaySearchQueries = register.searchQueries.slice(0);
  5424. for (var i = 0; i < keyBuffer.length; i++) {
  5425. var text = keyBuffer[i];
  5426. var match, key;
  5427. while (text) {
  5428. // Pull off one command key, which is either a single character
  5429. // or a special sequence wrapped in '<' and '>', e.g. '<Space>'.
  5430. match = (/<\w+-.+?>|<\w+>|./).exec(text);
  5431. key = match[0];
  5432. text = text.substring(match.index + key.length);
  5433. vimApi.handleKey(cm, key, 'macro');
  5434. if (vim.insertMode) {
  5435. var changes = register.insertModeChanges[imc++].changes;
  5436. vimGlobalState.macroModeState.lastInsertModeChanges.changes =
  5437. changes;
  5438. repeatInsertModeChanges(cm, changes, 1);
  5439. exitInsertMode(cm);
  5440. }
  5441. }
  5442. }
  5443. macroModeState.isPlaying = false;
  5444. }
  5445. function logKey(macroModeState, key) {
  5446. if (macroModeState.isPlaying) { return; }
  5447. var registerName = macroModeState.latestRegister;
  5448. var register = vimGlobalState.registerController.getRegister(registerName);
  5449. if (register) {
  5450. register.pushText(key);
  5451. }
  5452. }
  5453. function logInsertModeChange(macroModeState) {
  5454. if (macroModeState.isPlaying) { return; }
  5455. var registerName = macroModeState.latestRegister;
  5456. var register = vimGlobalState.registerController.getRegister(registerName);
  5457. if (register && register.pushInsertModeChanges) {
  5458. register.pushInsertModeChanges(macroModeState.lastInsertModeChanges);
  5459. }
  5460. }
  5461. function logSearchQuery(macroModeState, query) {
  5462. if (macroModeState.isPlaying) { return; }
  5463. var registerName = macroModeState.latestRegister;
  5464. var register = vimGlobalState.registerController.getRegister(registerName);
  5465. if (register && register.pushSearchQuery) {
  5466. register.pushSearchQuery(query);
  5467. }
  5468. }
  5469. /**
  5470. * Listens for changes made in insert mode.
  5471. * Should only be active in insert mode.
  5472. */
  5473. function onChange(cm, changeObj) {
  5474. var macroModeState = vimGlobalState.macroModeState;
  5475. var lastChange = macroModeState.lastInsertModeChanges;
  5476. if (!macroModeState.isPlaying) {
  5477. while(changeObj) {
  5478. lastChange.expectCursorActivityForChange = true;
  5479. if (lastChange.ignoreCount > 1) {
  5480. lastChange.ignoreCount--;
  5481. } else if (changeObj.origin == '+input' || changeObj.origin == 'paste'
  5482. || changeObj.origin === undefined /* only in testing */) {
  5483. var selectionCount = cm.listSelections().length;
  5484. if (selectionCount > 1)
  5485. lastChange.ignoreCount = selectionCount;
  5486. var text = changeObj.text.join('\n');
  5487. if (lastChange.maybeReset) {
  5488. lastChange.changes = [];
  5489. lastChange.maybeReset = false;
  5490. }
  5491. if (text) {
  5492. if (cm.state.overwrite && !/\n/.test(text)) {
  5493. lastChange.changes.push([text]);
  5494. } else {
  5495. lastChange.changes.push(text);
  5496. }
  5497. }
  5498. }
  5499. // Change objects may be chained with next.
  5500. changeObj = changeObj.next;
  5501. }
  5502. }
  5503. }
  5504. /**
  5505. * Listens for any kind of cursor activity on CodeMirror.
  5506. */
  5507. function onCursorActivity(cm) {
  5508. var vim = cm.state.vim;
  5509. if (vim.insertMode) {
  5510. // Tracking cursor activity in insert mode (for macro support).
  5511. var macroModeState = vimGlobalState.macroModeState;
  5512. if (macroModeState.isPlaying) { return; }
  5513. var lastChange = macroModeState.lastInsertModeChanges;
  5514. if (lastChange.expectCursorActivityForChange) {
  5515. lastChange.expectCursorActivityForChange = false;
  5516. } else {
  5517. // Cursor moved outside the context of an edit. Reset the change.
  5518. lastChange.maybeReset = true;
  5519. }
  5520. } else if (!cm.curOp.isVimOp) {
  5521. handleExternalSelection(cm, vim);
  5522. }
  5523. }
  5524. function handleExternalSelection(cm, vim) {
  5525. var anchor = cm.getCursor('anchor');
  5526. var head = cm.getCursor('head');
  5527. // Enter or exit visual mode to match mouse selection.
  5528. if (vim.visualMode && !cm.somethingSelected()) {
  5529. exitVisualMode(cm, false);
  5530. } else if (!vim.visualMode && !vim.insertMode && cm.somethingSelected()) {
  5531. vim.visualMode = true;
  5532. vim.visualLine = false;
  5533. CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"});
  5534. }
  5535. if (vim.visualMode) {
  5536. // Bind CodeMirror selection model to vim selection model.
  5537. // Mouse selections are considered visual characterwise.
  5538. var headOffset = !cursorIsBefore(head, anchor) ? -1 : 0;
  5539. var anchorOffset = cursorIsBefore(head, anchor) ? -1 : 0;
  5540. head = offsetCursor(head, 0, headOffset);
  5541. anchor = offsetCursor(anchor, 0, anchorOffset);
  5542. vim.sel = {
  5543. anchor: anchor,
  5544. head: head
  5545. };
  5546. updateMark(cm, vim, '<', cursorMin(head, anchor));
  5547. updateMark(cm, vim, '>', cursorMax(head, anchor));
  5548. } else if (!vim.insertMode) {
  5549. // Reset lastHPos if selection was modified by something outside of vim mode e.g. by mouse.
  5550. vim.lastHPos = cm.getCursor().ch;
  5551. }
  5552. }
  5553. /** Wrapper for special keys pressed in insert mode */
  5554. function InsertModeKey(keyName) {
  5555. this.keyName = keyName;
  5556. }
  5557. /**
  5558. * Handles raw key down events from the text area.
  5559. * - Should only be active in insert mode.
  5560. * - For recording deletes in insert mode.
  5561. */
  5562. function onKeyEventTargetKeyDown(e) {
  5563. var macroModeState = vimGlobalState.macroModeState;
  5564. var lastChange = macroModeState.lastInsertModeChanges;
  5565. var keyName = CodeMirror.keyName(e);
  5566. if (!keyName) { return; }
  5567. function onKeyFound() {
  5568. if (lastChange.maybeReset) {
  5569. lastChange.changes = [];
  5570. lastChange.maybeReset = false;
  5571. }
  5572. lastChange.changes.push(new InsertModeKey(keyName));
  5573. return true;
  5574. }
  5575. if (keyName.indexOf('Delete') != -1 || keyName.indexOf('Backspace') != -1) {
  5576. CodeMirror.lookupKey(keyName, 'vim-insert', onKeyFound);
  5577. }
  5578. }
  5579. /**
  5580. * Repeats the last edit, which includes exactly 1 command and at most 1
  5581. * insert. Operator and motion commands are read from lastEditInputState,
  5582. * while action commands are read from lastEditActionCommand.
  5583. *
  5584. * If repeatForInsert is true, then the function was called by
  5585. * exitInsertMode to repeat the insert mode changes the user just made. The
  5586. * corresponding enterInsertMode call was made with a count.
  5587. */
  5588. function repeatLastEdit(cm, vim, repeat, repeatForInsert) {
  5589. var macroModeState = vimGlobalState.macroModeState;
  5590. macroModeState.isPlaying = true;
  5591. var isAction = !!vim.lastEditActionCommand;
  5592. var cachedInputState = vim.inputState;
  5593. function repeatCommand() {
  5594. if (isAction) {
  5595. commandDispatcher.processAction(cm, vim, vim.lastEditActionCommand);
  5596. } else {
  5597. commandDispatcher.evalInput(cm, vim);
  5598. }
  5599. }
  5600. function repeatInsert(repeat) {
  5601. if (macroModeState.lastInsertModeChanges.changes.length > 0) {
  5602. // For some reason, repeat cw in desktop VIM does not repeat
  5603. // insert mode changes. Will conform to that behavior.
  5604. repeat = !vim.lastEditActionCommand ? 1 : repeat;
  5605. var changeObject = macroModeState.lastInsertModeChanges;
  5606. repeatInsertModeChanges(cm, changeObject.changes, repeat);
  5607. }
  5608. }
  5609. vim.inputState = vim.lastEditInputState;
  5610. if (isAction && vim.lastEditActionCommand.interlaceInsertRepeat) {
  5611. // o and O repeat have to be interlaced with insert repeats so that the
  5612. // insertions appear on separate lines instead of the last line.
  5613. for (var i = 0; i < repeat; i++) {
  5614. repeatCommand();
  5615. repeatInsert(1);
  5616. }
  5617. } else {
  5618. if (!repeatForInsert) {
  5619. // Hack to get the cursor to end up at the right place. If I is
  5620. // repeated in insert mode repeat, cursor will be 1 insert
  5621. // change set left of where it should be.
  5622. repeatCommand();
  5623. }
  5624. repeatInsert(repeat);
  5625. }
  5626. vim.inputState = cachedInputState;
  5627. if (vim.insertMode && !repeatForInsert) {
  5628. // Don't exit insert mode twice. If repeatForInsert is set, then we
  5629. // were called by an exitInsertMode call lower on the stack.
  5630. exitInsertMode(cm);
  5631. }
  5632. macroModeState.isPlaying = false;
  5633. }
  5634. function repeatInsertModeChanges(cm, changes, repeat) {
  5635. function keyHandler(binding) {
  5636. if (typeof binding == 'string') {
  5637. CodeMirror.commands[binding](cm);
  5638. } else {
  5639. binding(cm);
  5640. }
  5641. return true;
  5642. }
  5643. var head = cm.getCursor('head');
  5644. var visualBlock = vimGlobalState.macroModeState.lastInsertModeChanges.visualBlock;
  5645. if (visualBlock) {
  5646. // Set up block selection again for repeating the changes.
  5647. selectForInsert(cm, head, visualBlock + 1);
  5648. repeat = cm.listSelections().length;
  5649. cm.setCursor(head);
  5650. }
  5651. for (var i = 0; i < repeat; i++) {
  5652. if (visualBlock) {
  5653. cm.setCursor(offsetCursor(head, i, 0));
  5654. }
  5655. for (var j = 0; j < changes.length; j++) {
  5656. var change = changes[j];
  5657. if (change instanceof InsertModeKey) {
  5658. CodeMirror.lookupKey(change.keyName, 'vim-insert', keyHandler);
  5659. } else if (typeof change == "string") {
  5660. cm.replaceSelection(change);
  5661. } else {
  5662. var start = cm.getCursor();
  5663. var end = offsetCursor(start, 0, change[0].length);
  5664. cm.replaceRange(change[0], start, end);
  5665. cm.setCursor(end);
  5666. }
  5667. }
  5668. }
  5669. if (visualBlock) {
  5670. cm.setCursor(offsetCursor(head, 0, 1));
  5671. }
  5672. }
  5673. // multiselect support
  5674. function cloneVimState(state) {
  5675. var n = new state.constructor();
  5676. Object.keys(state).forEach(function(key) {
  5677. var o = state[key];
  5678. if (Array.isArray(o))
  5679. o = o.slice();
  5680. else if (o && typeof o == "object" && o.constructor != Object)
  5681. o = cloneVimState(o);
  5682. n[key] = o;
  5683. });
  5684. if (state.sel) {
  5685. n.sel = {
  5686. head: state.sel.head && copyCursor(state.sel.head),
  5687. anchor: state.sel.anchor && copyCursor(state.sel.anchor)
  5688. };
  5689. }
  5690. return n;
  5691. }
  5692. function multiSelectHandleKey(cm, key, origin) {
  5693. var isHandled = false;
  5694. var vim = vimApi.maybeInitVimState_(cm);
  5695. var visualBlock = vim.visualBlock || vim.wasInVisualBlock;
  5696. var wasMultiselect = cm.isInMultiSelectMode();
  5697. if (vim.wasInVisualBlock && !wasMultiselect) {
  5698. vim.wasInVisualBlock = false;
  5699. } else if (wasMultiselect && vim.visualBlock) {
  5700. vim.wasInVisualBlock = true;
  5701. }
  5702. if (key == '<Esc>' && !vim.insertMode && !vim.visualMode && wasMultiselect && vim.status == "<Esc>") {
  5703. // allow editor to exit multiselect
  5704. clearInputState(cm);
  5705. } else if (visualBlock || !wasMultiselect || cm.inVirtualSelectionMode) {
  5706. isHandled = vimApi.handleKey(cm, key, origin);
  5707. } else {
  5708. var old = cloneVimState(vim);
  5709. cm.operation(function() {
  5710. cm.curOp.isVimOp = true;
  5711. cm.forEachSelection(function() {
  5712. var head = cm.getCursor("head");
  5713. var anchor = cm.getCursor("anchor");
  5714. var headOffset = !cursorIsBefore(head, anchor) ? -1 : 0;
  5715. var anchorOffset = cursorIsBefore(head, anchor) ? -1 : 0;
  5716. head = offsetCursor(head, 0, headOffset);
  5717. anchor = offsetCursor(anchor, 0, anchorOffset);
  5718. cm.state.vim.sel.head = head;
  5719. cm.state.vim.sel.anchor = anchor;
  5720. isHandled = vimApi.handleKey(cm, key, origin);
  5721. if (cm.virtualSelection) {
  5722. cm.state.vim = cloneVimState(old);
  5723. }
  5724. });
  5725. if (cm.curOp.cursorActivity && !isHandled)
  5726. cm.curOp.cursorActivity = false;
  5727. cm.state.vim = vim;
  5728. }, true);
  5729. }
  5730. // some commands may bring visualMode and selection out of sync
  5731. if (isHandled && !vim.visualMode && !vim.insert && vim.visualMode != cm.somethingSelected()) {
  5732. handleExternalSelection(cm, vim);
  5733. }
  5734. return isHandled;
  5735. }
  5736. resetVimGlobalState();
  5737. return vimApi;
  5738. }
  5739. function initVim(CodeMirror5) {
  5740. CodeMirror5.Vim = initVim$1(CodeMirror5);
  5741. return CodeMirror5.Vim;
  5742. }
  5743. CodeMirror.Vim = initVim(CodeMirror);
  5744. });