codemirror-rails 2.36 → 3.00

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. data/lib/codemirror/rails/version.rb +2 -2
  2. data/vendor/assets/javascripts/codemirror.js +3727 -2345
  3. data/vendor/assets/javascripts/codemirror/keymaps/vim.js +2226 -825
  4. data/vendor/assets/javascripts/codemirror/modes/clike.js +23 -8
  5. data/vendor/assets/javascripts/codemirror/modes/clojure.js +4 -4
  6. data/vendor/assets/javascripts/codemirror/modes/coffeescript.js +1 -1
  7. data/vendor/assets/javascripts/codemirror/modes/commonlisp.js +1 -1
  8. data/vendor/assets/javascripts/codemirror/modes/css.js +20 -3
  9. data/vendor/assets/javascripts/codemirror/modes/diff.js +2 -2
  10. data/vendor/assets/javascripts/codemirror/modes/ecl.js +192 -203
  11. data/vendor/assets/javascripts/codemirror/modes/erlang.js +1 -1
  12. data/vendor/assets/javascripts/codemirror/modes/gfm.js +1 -1
  13. data/vendor/assets/javascripts/codemirror/modes/go.js +1 -6
  14. data/vendor/assets/javascripts/codemirror/modes/groovy.js +1 -1
  15. data/vendor/assets/javascripts/codemirror/modes/haskell.js +1 -1
  16. data/vendor/assets/javascripts/codemirror/modes/haxe.js +13 -13
  17. data/vendor/assets/javascripts/codemirror/modes/htmlembedded.js +3 -3
  18. data/vendor/assets/javascripts/codemirror/modes/htmlmixed.js +1 -1
  19. data/vendor/assets/javascripts/codemirror/modes/http.js +98 -0
  20. data/vendor/assets/javascripts/codemirror/modes/javascript.js +1 -1
  21. data/vendor/assets/javascripts/codemirror/modes/jinja2.js +1 -1
  22. data/vendor/assets/javascripts/codemirror/modes/markdown.js +7 -14
  23. data/vendor/assets/javascripts/codemirror/modes/mysql.js +2 -2
  24. data/vendor/assets/javascripts/codemirror/modes/ntriples.js +0 -2
  25. data/vendor/assets/javascripts/codemirror/modes/ocaml.js +1 -2
  26. data/vendor/assets/javascripts/codemirror/modes/pascal.js +2 -2
  27. data/vendor/assets/javascripts/codemirror/modes/perl.js +1 -1
  28. data/vendor/assets/javascripts/codemirror/modes/php.js +5 -4
  29. data/vendor/assets/javascripts/codemirror/modes/pig.js +3 -4
  30. data/vendor/assets/javascripts/codemirror/modes/plsql.js +3 -4
  31. data/vendor/assets/javascripts/codemirror/modes/python.js +1 -1
  32. data/vendor/assets/javascripts/codemirror/modes/r.js +1 -1
  33. data/vendor/assets/javascripts/codemirror/modes/rpm-changes.js +1 -1
  34. data/vendor/assets/javascripts/codemirror/modes/rpm-spec.js +1 -1
  35. data/vendor/assets/javascripts/codemirror/modes/rst.js +1 -13
  36. data/vendor/assets/javascripts/codemirror/modes/ruby.js +1 -1
  37. data/vendor/assets/javascripts/codemirror/modes/rust.js +3 -3
  38. data/vendor/assets/javascripts/codemirror/modes/scheme.js +4 -4
  39. data/vendor/assets/javascripts/codemirror/modes/shell.js +1 -1
  40. data/vendor/assets/javascripts/codemirror/modes/sieve.js +1 -1
  41. data/vendor/assets/javascripts/codemirror/modes/smalltalk.js +2 -2
  42. data/vendor/assets/javascripts/codemirror/modes/smarty.js +1 -1
  43. data/vendor/assets/javascripts/codemirror/modes/sparql.js +2 -2
  44. data/vendor/assets/javascripts/codemirror/modes/stex.js +6 -13
  45. data/vendor/assets/javascripts/codemirror/modes/tiddlywiki.js +6 -37
  46. data/vendor/assets/javascripts/codemirror/modes/tiki.js +3 -3
  47. data/vendor/assets/javascripts/codemirror/modes/vb.js +3 -3
  48. data/vendor/assets/javascripts/codemirror/modes/velocity.js +2 -4
  49. data/vendor/assets/javascripts/codemirror/modes/verilog.js +182 -194
  50. data/vendor/assets/javascripts/codemirror/modes/xml.js +9 -5
  51. data/vendor/assets/javascripts/codemirror/modes/xquery.js +3 -4
  52. data/vendor/assets/javascripts/codemirror/utils/closetag.js +85 -164
  53. data/vendor/assets/javascripts/codemirror/utils/colorize.js +29 -0
  54. data/vendor/assets/javascripts/codemirror/utils/continuecomment.js +1 -1
  55. data/vendor/assets/javascripts/codemirror/utils/continuelist.js +28 -0
  56. data/vendor/assets/javascripts/codemirror/utils/dialog.js +21 -16
  57. data/vendor/assets/javascripts/codemirror/utils/foldcode.js +59 -73
  58. data/vendor/assets/javascripts/codemirror/utils/formatting.js +43 -131
  59. data/vendor/assets/javascripts/codemirror/utils/javascript-hint.js +22 -19
  60. data/vendor/assets/javascripts/codemirror/utils/match-highlighter.js +5 -3
  61. data/vendor/assets/javascripts/codemirror/utils/matchbrackets.js +63 -0
  62. data/vendor/assets/javascripts/codemirror/utils/multiplex.js +18 -0
  63. data/vendor/assets/javascripts/codemirror/utils/pig-hint.js +3 -9
  64. data/vendor/assets/javascripts/codemirror/utils/runmode.js +19 -20
  65. data/vendor/assets/javascripts/codemirror/utils/search.js +6 -5
  66. data/vendor/assets/javascripts/codemirror/utils/searchcursor.js +1 -1
  67. data/vendor/assets/javascripts/codemirror/utils/simple-hint.js +10 -10
  68. data/vendor/assets/javascripts/codemirror/utils/xml-hint.js +2 -2
  69. data/vendor/assets/stylesheets/codemirror.css +169 -104
  70. data/vendor/assets/stylesheets/codemirror/themes/ambiance-mobile.css +1 -1
  71. data/vendor/assets/stylesheets/codemirror/themes/ambiance.css +7 -12
  72. data/vendor/assets/stylesheets/codemirror/themes/blackboard.css +3 -3
  73. data/vendor/assets/stylesheets/codemirror/themes/cobalt.css +3 -3
  74. data/vendor/assets/stylesheets/codemirror/themes/erlang-dark.css +3 -3
  75. data/vendor/assets/stylesheets/codemirror/themes/lesser-dark.css +4 -4
  76. data/vendor/assets/stylesheets/codemirror/themes/monokai.css +3 -3
  77. data/vendor/assets/stylesheets/codemirror/themes/night.css +3 -3
  78. data/vendor/assets/stylesheets/codemirror/themes/rubyblue.css +3 -3
  79. data/vendor/assets/stylesheets/codemirror/themes/solarized.css +207 -0
  80. data/vendor/assets/stylesheets/codemirror/themes/twilight.css +4 -4
  81. data/vendor/assets/stylesheets/codemirror/themes/vibrant-ink.css +3 -3
  82. data/vendor/assets/stylesheets/codemirror/themes/xq-dark.css +3 -3
  83. data/vendor/assets/stylesheets/codemirror/utils/dialog.css +12 -7
  84. metadata +16 -11
@@ -1,897 +1,2298 @@
1
- // Supported keybindings:
2
- //
3
- // Cursor movement:
4
- // h, j, k, l
5
- // e, E, w, W, b, B
6
- // Ctrl-f, Ctrl-b
7
- // Ctrl-n, Ctrl-p
8
- // $, ^, 0
9
- // G
10
- // ge, gE
11
- // gg
12
- // f<char>, F<char>, t<char>, T<char>
13
- // Ctrl-o, Ctrl-i TODO (FIXME - Ctrl-O wont work in Chrome)
14
- // /, ?, n, N TODO (does not work)
15
- // #, * TODO
16
- //
17
- // Entering insert mode:
18
- // i, I, a, A, o, O
19
- // s
20
- // ce, cb
21
- // cc
22
- // S, C TODO
23
- // cf<char>, cF<char>, ct<char>, cT<char>
24
- //
25
- // Deleting text:
26
- // x, X
27
- // J
28
- // dd, D
29
- // de, db
30
- // df<char>, dF<char>, dt<char>, dT<char>
31
- //
32
- // Yanking and pasting:
33
- // yy, Y
34
- // p, P
35
- // p'<char> TODO - test
36
- // y'<char> TODO - test
37
- // m<char> TODO - test
38
- //
39
- // Changing text in place:
40
- // ~
41
- // r<char>
42
- //
43
- // Visual mode:
44
- // v, V TODO
45
- //
46
- // Misc:
47
- // . TODO
48
- //
1
+ /**
2
+ * Supported keybindings:
3
+ *
4
+ * Motion:
5
+ * h, j, k, l
6
+ * e, E, w, W, b, B, ge, gE
7
+ * f<character>, F<character>, t<character>, T<character>
8
+ * $, ^, 0
9
+ * gg, G
10
+ * %
11
+ * '<character>, `<character>
12
+ *
13
+ * Operator:
14
+ * d, y, c
15
+ * dd, yy, cc
16
+ * g~, g~g~
17
+ * >, <, >>, <<
18
+ *
19
+ * Operator-Motion:
20
+ * x, X, D, Y, C, ~
21
+ *
22
+ * Action:
23
+ * a, i, s, A, I, S, o, O
24
+ * J
25
+ * u, Ctrl-r
26
+ * m<character>
27
+ * r<character>
28
+ *
29
+ * Modes:
30
+ * ESC - leave insert mode, visual mode, and clear input state.
31
+ * Ctrl-[, Ctrl-c - same as ESC.
32
+ *
33
+ * Registers: unamed, -, a-z, A-Z, 0-9
34
+ * (Does not respect the special case for number registers when delete
35
+ * operator is made with these commands: %, (, ), , /, ?, n, N, {, } )
36
+ * TODO: Implement the remaining registers.
37
+ * Marks: a-z, A-Z, and 0-9
38
+ * TODO: Implement the remaining special marks. They have more complex
39
+ * behavior.
40
+ *
41
+ * Code structure:
42
+ * 1. Default keymap
43
+ * 2. Variable declarations and short basic helpers
44
+ * 3. Instance (External API) implementation
45
+ * 4. Internal state tracking objects (input state, counter) implementation
46
+ * and instanstiation
47
+ * 5. Key handler (the main command dispatcher) implementation
48
+ * 6. Motion, operator, and action implementations
49
+ * 7. Helper functions for the key handler, motions, operators, and actions
50
+ * 8. Set up Vim to work as a keymap for CodeMirror.
51
+ */
49
52
 
50
53
  (function() {
51
- var sdir = "f";
52
- var buf = "";
53
- var mark = {};
54
- var repeatCount = 0;
55
- function isLine(cm, line) { return line >= 0 && line < cm.lineCount(); }
56
- function emptyBuffer() { buf = ""; }
57
- function pushInBuffer(str) { buf += str; }
58
- function pushRepeatCountDigit(digit) {return function(cm) {repeatCount = (repeatCount * 10) + digit}; }
59
- function getCountOrOne() {
60
- var i = repeatCount;
61
- return i || 1;
62
- }
63
- function clearCount() {
64
- repeatCount = 0;
65
- }
66
- function iterTimes(func) {
67
- for (var i = 0, c = getCountOrOne(); i < c; ++i) func(i, i == c - 1);
68
- clearCount();
69
- }
70
- function countTimes(func) {
71
- if (typeof func == "string") func = CodeMirror.commands[func];
72
- return function(cm) { iterTimes(function (i, last) { func(cm, i, last); }); };
73
- }
74
-
75
- function iterObj(o, f) {
76
- for (var prop in o) if (o.hasOwnProperty(prop)) f(prop, o[prop]);
77
- }
78
- function iterList(l, f) {
79
- for (var i = 0; i < l.length; ++i) f(l[i]);
80
- }
81
- function toLetter(ch) {
82
- // T -> t, Shift-T -> T, '*' -> *, "Space" -> " "
83
- if (ch.slice(0, 6) == "Shift-") {
84
- return ch.slice(0, 1);
85
- } else {
86
- if (ch == "Space") return " ";
87
- if (ch.length == 3 && ch[0] == "'" && ch[2] == "'") return ch[1];
88
- return ch.toLowerCase();
89
- }
90
- }
91
- var SPECIAL_SYMBOLS = "~`!@#$%^&*()_-+=[{}]\\|/?.,<>:;\"\'1234567890";
92
- function toCombo(ch) {
93
- // t -> T, T -> Shift-T, * -> '*', " " -> "Space"
94
- if (ch == " ") return "Space";
95
- var specialIdx = SPECIAL_SYMBOLS.indexOf(ch);
96
- if (specialIdx != -1) return "'" + ch + "'";
97
- if (ch.toLowerCase() == ch) return ch.toUpperCase();
98
- return "Shift-" + ch.toUpperCase();
99
- }
100
-
101
- var word = [/\w/, /[^\w\s]/], bigWord = [/\S/];
102
- // Finds a word on the given line, and continue searching the next line if it can't find one.
103
- function findWord(cm, lineNum, pos, dir, regexps) {
104
- var line = cm.getLine(lineNum);
105
- while (true) {
106
- var stop = (dir > 0) ? line.length : -1;
107
- var wordStart = stop, wordEnd = stop;
108
- // Find bounds of next word.
109
- for (; pos != stop; pos += dir) {
110
- for (var i = 0; i < regexps.length; ++i) {
111
- if (regexps[i].test(line.charAt(pos))) {
112
- wordStart = pos;
113
- // Advance to end of word.
114
- for (; pos != stop && regexps[i].test(line.charAt(pos)); pos += dir) {}
115
- wordEnd = (dir > 0) ? pos : pos + 1;
116
- return {
117
- from: Math.min(wordStart, wordEnd),
118
- to: Math.max(wordStart, wordEnd),
119
- line: lineNum};
120
- }
121
- }
122
- }
123
- // Advance to next/prev line.
124
- lineNum += dir;
125
- if (!isLine(cm, lineNum)) return null;
126
- line = cm.getLine(lineNum);
127
- pos = (dir > 0) ? 0 : line.length;
128
- }
129
- }
130
- /**
131
- * @param {boolean} cm CodeMirror object.
132
- * @param {regexp} regexps Regular expressions for word characters.
133
- * @param {number} dir Direction, +/- 1.
134
- * @param {number} times Number of times to advance word.
135
- * @param {string} where Go to "start" or "end" of word, 'e' vs 'w'.
136
- * @param {boolean} yank Whether we are finding words to yank. If true,
137
- * do not go to the next line to look for the last word. This is to
138
- * prevent deleting new line on 'dw' at the end of a line.
139
- */
140
- function moveToWord(cm, regexps, dir, times, where, yank) {
141
- var cur = cm.getCursor();
142
- if (yank) {
143
- where = 'start';
144
- }
145
- for (var i = 0; i < times; i++) {
146
- var startCh = cur.ch, startLine = cur.line, word;
147
- while (true) {
148
- // Search and advance.
149
- word = findWord(cm, cur.line, cur.ch, dir, regexps);
150
- if (word) {
151
- if (yank && times == 1 && dir == 1 && cur.line != word.line) {
152
- // Stop at end of line of last word. Don't want to delete line return
153
- // for dw if the last deleted word is at the end of a line.
154
- cur.ch = cm.getLine(cur.line).length;
155
- break;
156
- } else {
157
- // Move to the word we just found. If by moving to the word we end up
158
- // in the same spot, then move an extra character and search again.
159
- cur.line = word.line;
160
- if (dir > 0 && where == 'end') {
161
- // 'e'
162
- if (startCh != word.to - 1 || startLine != word.line) {
163
- cur.ch = word.to - 1;
164
- break;
54
+ 'use strict';
55
+
56
+ var defaultKeymap = [
57
+ // Key to key mapping. This goes first to make it possible to override
58
+ // existing mappings.
59
+ { keys: ['Left'], type: 'keyToKey', toKeys: ['h'] },
60
+ { keys: ['Right'], type: 'keyToKey', toKeys: ['l'] },
61
+ { keys: ['Up'], type: 'keyToKey', toKeys: ['k'] },
62
+ { keys: ['Down'], type: 'keyToKey', toKeys: ['j'] },
63
+ { keys: ['Space'], type: 'keyToKey', toKeys: ['l'] },
64
+ { keys: ['Backspace'], type: 'keyToKey', toKeys: ['h'] },
65
+ { keys: ['Ctrl-Space'], type: 'keyToKey', toKeys: ['W'] },
66
+ { keys: ['Ctrl-Backspace'], type: 'keyToKey', toKeys: ['B'] },
67
+ { keys: ['Shift-Space'], type: 'keyToKey', toKeys: ['w'] },
68
+ { keys: ['Shift-Backspace'], type: 'keyToKey', toKeys: ['b'] },
69
+ { keys: ['Ctrl-n'], type: 'keyToKey', toKeys: ['j'] },
70
+ { keys: ['Ctrl-p'], type: 'keyToKey', toKeys: ['k'] },
71
+ { keys: ['Ctrl-['], type: 'keyToKey', toKeys: ['Esc'] },
72
+ { keys: ['Ctrl-c'], type: 'keyToKey', toKeys: ['Esc'] },
73
+ { keys: ['s'], type: 'keyToKey', toKeys: ['c', 'l'] },
74
+ { keys: ['S'], type: 'keyToKey', toKeys: ['c', 'c'] },
75
+ { keys: ['Home'], type: 'keyToKey', toKeys: ['0'] },
76
+ { keys: ['End'], type: 'keyToKey', toKeys: ['$'] },
77
+ { keys: ['PageUp'], type: 'keyToKey', toKeys: ['Ctrl-b'] },
78
+ { keys: ['PageDown'], type: 'keyToKey', toKeys: ['Ctrl-f'] },
79
+ // Motions
80
+ { keys: ['h'], type: 'motion',
81
+ motion: 'moveByCharacters',
82
+ motionArgs: { forward: false }},
83
+ { keys: ['l'], type: 'motion',
84
+ motion: 'moveByCharacters',
85
+ motionArgs: { forward: true }},
86
+ { keys: ['j'], type: 'motion',
87
+ motion: 'moveByLines',
88
+ motionArgs: { forward: true, linewise: true }},
89
+ { keys: ['k'], type: 'motion',
90
+ motion: 'moveByLines',
91
+ motionArgs: { forward: false, linewise: true }},
92
+ { keys: ['w'], type: 'motion',
93
+ motion: 'moveByWords',
94
+ motionArgs: { forward: true, wordEnd: false }},
95
+ { keys: ['W'], type: 'motion',
96
+ motion: 'moveByWords',
97
+ motionArgs: { forward: true, wordEnd: false, bigWord: true }},
98
+ { keys: ['e'], type: 'motion',
99
+ motion: 'moveByWords',
100
+ motionArgs: { forward: true, wordEnd: true, inclusive: true }},
101
+ { keys: ['E'], type: 'motion',
102
+ motion: 'moveByWords',
103
+ motionArgs: { forward: true, wordEnd: true, bigWord: true,
104
+ inclusive: true }},
105
+ { keys: ['b'], type: 'motion',
106
+ motion: 'moveByWords',
107
+ motionArgs: { forward: false, wordEnd: false }},
108
+ { keys: ['B'], type: 'motion',
109
+ motion: 'moveByWords',
110
+ motionArgs: { forward: false, wordEnd: false, bigWord: true }},
111
+ { keys: ['g', 'e'], type: 'motion',
112
+ motion: 'moveByWords',
113
+ motionArgs: { forward: false, wordEnd: true, inclusive: true }},
114
+ { keys: ['g', 'E'], type: 'motion',
115
+ motion: 'moveByWords',
116
+ motionArgs: { forward: false, wordEnd: true, bigWord: true,
117
+ inclusive: true }},
118
+ { keys: ['Ctrl-f'], type: 'motion',
119
+ motion: 'moveByPage', motionArgs: { forward: true }},
120
+ { keys: ['Ctrl-b'], type: 'motion',
121
+ motion: 'moveByPage', motionArgs: { forward: false }},
122
+ { keys: ['g', 'g'], type: 'motion',
123
+ motion: 'moveToLineOrEdgeOfDocument',
124
+ motionArgs: { forward: false, explicitRepeat: true, linewise: true }},
125
+ { keys: ['G'], type: 'motion',
126
+ motion: 'moveToLineOrEdgeOfDocument',
127
+ motionArgs: { forward: true, explicitRepeat: true, linewise: true }},
128
+ { keys: ['0'], type: 'motion', motion: 'moveToStartOfLine' },
129
+ { keys: ['^'], type: 'motion',
130
+ motion: 'moveToFirstNonWhiteSpaceCharacter' },
131
+ { keys: ['$'], type: 'motion',
132
+ motion: 'moveToEol',
133
+ motionArgs: { inclusive: true }},
134
+ { keys: ['%'], type: 'motion',
135
+ motion: 'moveToMatchedSymbol',
136
+ motionArgs: { inclusive: true }},
137
+ { keys: ['f', 'character'], type: 'motion',
138
+ motion: 'moveToCharacter',
139
+ motionArgs: { forward: true , inclusive: true }},
140
+ { keys: ['F', 'character'], type: 'motion',
141
+ motion: 'moveToCharacter',
142
+ motionArgs: { forward: false }},
143
+ { keys: ['t', 'character'], type: 'motion',
144
+ motion: 'moveTillCharacter',
145
+ motionArgs: { forward: true, inclusive: true }},
146
+ { keys: ['T', 'character'], type: 'motion',
147
+ motion: 'moveTillCharacter',
148
+ motionArgs: { forward: false }},
149
+ { keys: ['\'', 'character'], type: 'motion', motion: 'goToMark' },
150
+ { keys: ['`', 'character'], type: 'motion', motion: 'goToMark' },
151
+ { keys: ['|'], type: 'motion',
152
+ motion: 'moveToColumn',
153
+ motionArgs: { }},
154
+ // Operators
155
+ { keys: ['d'], type: 'operator', operator: 'delete' },
156
+ { keys: ['y'], type: 'operator', operator: 'yank' },
157
+ { keys: ['c'], type: 'operator', operator: 'change',
158
+ operatorArgs: { enterInsertMode: true } },
159
+ { keys: ['>'], type: 'operator', operator: 'indent',
160
+ operatorArgs: { indentRight: true }},
161
+ { keys: ['<'], type: 'operator', operator: 'indent',
162
+ operatorArgs: { indentRight: false }},
163
+ { keys: ['g', '~'], type: 'operator', operator: 'swapcase' },
164
+ { keys: ['n'], type: 'motion', motion: 'findNext' },
165
+ { keys: ['N'], type: 'motion', motion: 'findPrev' },
166
+ // Operator-Motion dual commands
167
+ { keys: ['x'], type: 'operatorMotion', operator: 'delete',
168
+ motion: 'moveByCharacters', motionArgs: { forward: true },
169
+ operatorMotionArgs: { visualLine: false }},
170
+ { keys: ['X'], type: 'operatorMotion', operator: 'delete',
171
+ motion: 'moveByCharacters', motionArgs: { forward: false },
172
+ operatorMotionArgs: { visualLine: true }},
173
+ { keys: ['D'], type: 'operatorMotion', operator: 'delete',
174
+ motion: 'moveToEol', motionArgs: { inclusive: true },
175
+ operatorMotionArgs: { visualLine: true }},
176
+ { keys: ['Y'], type: 'operatorMotion', operator: 'yank',
177
+ motion: 'moveToEol', motionArgs: { inclusive: true },
178
+ operatorMotionArgs: { visualLine: true }},
179
+ { keys: ['C'], type: 'operatorMotion',
180
+ operator: 'change', operatorArgs: { enterInsertMode: true },
181
+ motion: 'moveToEol', motionArgs: { inclusive: true },
182
+ operatorMotionArgs: { visualLine: true }},
183
+ { keys: ['~'], type: 'operatorMotion', operator: 'swapcase',
184
+ motion: 'moveByCharacters', motionArgs: { forward: true }},
185
+ // Actions
186
+ { keys: ['a'], type: 'action', action: 'enterInsertMode',
187
+ actionArgs: { insertAt: 'charAfter' }},
188
+ { keys: ['A'], type: 'action', action: 'enterInsertMode',
189
+ actionArgs: { insertAt: 'eol' }},
190
+ { keys: ['i'], type: 'action', action: 'enterInsertMode' },
191
+ { keys: ['I'], type: 'action', action: 'enterInsertMode',
192
+ motion: 'moveToFirstNonWhiteSpaceCharacter' },
193
+ { keys: ['o'], type: 'action', action: 'newLineAndEnterInsertMode',
194
+ actionArgs: { after: true }},
195
+ { keys: ['O'], type: 'action', action: 'newLineAndEnterInsertMode',
196
+ actionArgs: { after: false }},
197
+ { keys: ['v'], type: 'action', action: 'toggleVisualMode' },
198
+ { keys: ['V'], type: 'action', action: 'toggleVisualMode',
199
+ actionArgs: { linewise: true }},
200
+ { keys: ['J'], type: 'action', action: 'joinLines' },
201
+ { keys: ['p'], type: 'action', action: 'paste',
202
+ actionArgs: { after: true }},
203
+ { keys: ['P'], type: 'action', action: 'paste',
204
+ actionArgs: { after: false }},
205
+ { keys: ['r', 'character'], type: 'action', action: 'replace' },
206
+ { keys: ['u'], type: 'action', action: 'undo' },
207
+ { keys: ['Ctrl-r'], type: 'action', action: 'redo' },
208
+ { keys: ['m', 'character'], type: 'action', action: 'setMark' },
209
+ { keys: ['\"', 'character'], type: 'action', action: 'setRegister' },
210
+ { keys: [',', '/'], type: 'action', action: 'clearSearchHighlight' },
211
+ // Text object motions
212
+ { keys: ['a', 'character'], type: 'motion',
213
+ motion: 'textObjectManipulation' },
214
+ { keys: ['i', 'character'], type: 'motion',
215
+ motion: 'textObjectManipulation',
216
+ motionArgs: { textObjectInner: true }},
217
+ // Search
218
+ { keys: ['/'], type: 'search',
219
+ searchArgs: { forward: true, querySrc: 'prompt' }},
220
+ { keys: ['?'], type: 'search',
221
+ searchArgs: { forward: false, querySrc: 'prompt' }},
222
+ { keys: ['*'], type: 'search',
223
+ searchArgs: { forward: true, querySrc: 'wordUnderCursor' }},
224
+ { keys: ['#'], type: 'search',
225
+ searchArgs: { forward: false, querySrc: 'wordUnderCursor' }},
226
+ // Ex command
227
+ { keys: [':'], type: 'ex' }
228
+ ];
229
+
230
+ var Vim = function() {
231
+ var alphabetRegex = /[A-Za-z]/;
232
+ var numberRegex = /[\d]/;
233
+ var whiteSpaceRegex = /\s/;
234
+ var wordRegexp = [(/\w/), (/[^\w\s]/)], bigWordRegexp = [(/\S/)];
235
+ function makeKeyRange(start, size) {
236
+ var keys = [];
237
+ for (var i = start; i < start + size; i++) {
238
+ keys.push(String.fromCharCode(i));
239
+ }
240
+ return keys;
241
+ }
242
+ var upperCaseAlphabet = makeKeyRange(65, 26);
243
+ var lowerCaseAlphabet = makeKeyRange(97, 26);
244
+ var numbers = makeKeyRange(48, 10);
245
+ var SPECIAL_SYMBOLS = '~`!@#$%^&*()_-+=[{}]\\|/?.,<>:;\"\'';
246
+ var specialSymbols = SPECIAL_SYMBOLS.split('');
247
+ var specialKeys = ['Left', 'Right', 'Up', 'Down', 'Space', 'Backspace',
248
+ 'Esc', 'Home', 'End', 'PageUp', 'PageDown'];
249
+ var validMarks = upperCaseAlphabet.concat(lowerCaseAlphabet).concat(
250
+ numbers);
251
+ var validRegisters = upperCaseAlphabet.concat(lowerCaseAlphabet).concat(
252
+ numbers).concat('-\"'.split(''));
253
+
254
+ function isAlphabet(k) {
255
+ return alphabetRegex.test(k);
256
+ }
257
+ function isLine(cm, line) {
258
+ return line >= 0 && line < cm.lineCount();
259
+ }
260
+ function isLowerCase(k) {
261
+ return (/^[a-z]$/).test(k);
262
+ }
263
+ function isMatchableSymbol(k) {
264
+ return '()[]{}'.indexOf(k) != -1;
265
+ }
266
+ function isNumber(k) {
267
+ return numberRegex.test(k);
268
+ }
269
+ function isUpperCase(k) {
270
+ return (/^[A-Z]$/).test(k);
271
+ }
272
+ function isAlphanumeric(k) {
273
+ return (/^[\w]$/).test(k);
274
+ }
275
+ function isWhiteSpace(k) {
276
+ return whiteSpaceRegex.test(k);
277
+ }
278
+ function isWhiteSpaceString(k) {
279
+ return (/^\s*$/).test(k);
280
+ }
281
+ function inRangeInclusive(x, start, end) {
282
+ return x >= start && x <= end;
283
+ }
284
+ function inArray(val, arr) {
285
+ for (var i = 0; i < arr.length; i++) {
286
+ if (arr[i] == val) {
287
+ return true;
288
+ }
289
+ }
290
+ return false;
291
+ }
292
+
293
+ // Global Vim state. Call getVimGlobalState to get and initialize.
294
+ var vimGlobalState;
295
+ function getVimGlobalState() {
296
+ if (!vimGlobalState) {
297
+ vimGlobalState = {
298
+ // The current search query.
299
+ searchQuery: null,
300
+ // Whether we are searching backwards.
301
+ searchIsReversed: false,
302
+ registerController: new RegisterController({})
303
+ };
304
+ }
305
+ return vimGlobalState;
306
+ }
307
+ function getVimState(cm) {
308
+ if (!cm.vimState) {
309
+ // Store instance state in the CodeMirror object.
310
+ cm.vimState = {
311
+ inputState: new InputState(),
312
+ // When using jk for navigation, if you move from a longer line to a
313
+ // shorter line, the cursor may clip to the end of the shorter line.
314
+ // If j is pressed again and cursor goes to the next line, the
315
+ // cursor should go back to its horizontal position on the longer
316
+ // line if it can. This is to keep track of the horizontal position.
317
+ lastHPos: -1,
318
+ // The last motion command run. Cleared if a non-motion command gets
319
+ // executed in between.
320
+ lastMotion: null,
321
+ marks: {},
322
+ visualMode: false,
323
+ // If we are in visual line mode. No effect if visualMode is false.
324
+ visualLine: false
325
+ };
326
+ }
327
+ return cm.vimState;
328
+ }
329
+
330
+ var vimApi= {
331
+ buildKeyMap: function() {
332
+ // TODO: Convert keymap into dictionary format for fast lookup.
333
+ },
334
+ // Testing hook, though it might be useful to expose the register
335
+ // controller anyways.
336
+ getRegisterController: function() {
337
+ return getVimGlobalState().registerController;
338
+ },
339
+ // Testing hook.
340
+ clearVimGlobalState_: function() {
341
+ vimGlobalState = null;
342
+ },
343
+ map: function(lhs, rhs) {
344
+ // Add user defined key bindings.
345
+ exCommandDispatcher.map(lhs, rhs);
346
+ },
347
+ // Initializes vim state variable on the CodeMirror object. Should only be
348
+ // called lazily by handleKey or for testing.
349
+ maybeInitState: function(cm) {
350
+ getVimState(cm);
351
+ },
352
+ // This is the outermost function called by CodeMirror, after keys have
353
+ // been mapped to their Vim equivalents.
354
+ handleKey: function(cm, key) {
355
+ var command;
356
+ var vim = getVimState(cm);
357
+ if (key == 'Esc') {
358
+ // Clear input state and get back to normal mode.
359
+ vim.inputState.reset();
360
+ if (vim.visualMode) {
361
+ exitVisualMode(cm, vim);
362
+ }
363
+ return;
364
+ }
365
+ if (vim.visualMode &&
366
+ cursorEqual(cm.getCursor('head'), cm.getCursor('anchor'))) {
367
+ // The selection was cleared. Exit visual mode.
368
+ exitVisualMode(cm, vim);
369
+ }
370
+ if (!vim.visualMode &&
371
+ !cursorEqual(cm.getCursor('head'), cm.getCursor('anchor'))) {
372
+ vim.visualMode = true;
373
+ vim.visualLine = false;
374
+ }
375
+ if (key != '0' || (key == '0' && vim.inputState.getRepeat() === 0)) {
376
+ // Have to special case 0 since it's both a motion and a number.
377
+ command = commandDispatcher.matchCommand(key, defaultKeymap, vim);
378
+ }
379
+ if (!command) {
380
+ if (isNumber(key)) {
381
+ // Increment count unless count is 0 and key is 0.
382
+ vim.inputState.pushRepeatDigit(key);
383
+ }
384
+ return;
385
+ }
386
+ if (command.type == 'keyToKey') {
387
+ // TODO: prevent infinite recursion.
388
+ for (var i = 0; i < command.toKeys.length; i++) {
389
+ this.handleKey(cm, command.toKeys[i]);
390
+ }
391
+ } else {
392
+ commandDispatcher.processCommand(cm, vim, command);
393
+ }
394
+ }
395
+ };
396
+
397
+ // Represents the current input state.
398
+ function InputState() {
399
+ this.reset();
400
+ }
401
+ InputState.prototype.reset = function() {
402
+ this.prefixRepeat = [];
403
+ this.motionRepeat = [];
404
+
405
+ this.operator = null;
406
+ this.operatorArgs = null;
407
+ this.motion = null;
408
+ this.motionArgs = null;
409
+ this.keyBuffer = []; // For matching multi-key commands.
410
+ this.registerName = null; // Defaults to the unamed register.
411
+ };
412
+ InputState.prototype.pushRepeatDigit = function(n) {
413
+ if (!this.operator) {
414
+ this.prefixRepeat = this.prefixRepeat.concat(n);
415
+ } else {
416
+ this.motionRepeat = this.motionRepeat.concat(n);
417
+ }
418
+ };
419
+ InputState.prototype.getRepeat = function() {
420
+ var repeat = 0;
421
+ if (this.prefixRepeat.length > 0 || this.motionRepeat.length > 0) {
422
+ repeat = 1;
423
+ if (this.prefixRepeat.length > 0) {
424
+ repeat *= parseInt(this.prefixRepeat.join(''), 10);
425
+ }
426
+ if (this.motionRepeat.length > 0) {
427
+ repeat *= parseInt(this.motionRepeat.join(''), 10);
428
+ }
429
+ }
430
+ return repeat;
431
+ };
432
+
433
+ /*
434
+ * Register stores information about copy and paste registers. Besides
435
+ * text, a register must store whether it is linewise (i.e., when it is
436
+ * pasted, should it insert itself into a new line, or should the text be
437
+ * inserted at the cursor position.)
438
+ */
439
+ function Register(text, linewise) {
440
+ this.clear();
441
+ if (text) {
442
+ this.set(text, linewise);
443
+ }
444
+ }
445
+ Register.prototype = {
446
+ set: function(text, linewise) {
447
+ this.text = text;
448
+ this.linewise = !!linewise;
449
+ },
450
+ append: function(text, linewise) {
451
+ // if this register has ever been set to linewise, use linewise.
452
+ if (linewise || this.linewise) {
453
+ this.text += '\n' + text;
454
+ this.linewise = true;
455
+ } else {
456
+ this.text += text;
457
+ }
458
+ },
459
+ clear: function() {
460
+ this.text = '';
461
+ this.linewise = false;
462
+ },
463
+ toString: function() { return this.text; }
464
+ };
465
+
466
+ /*
467
+ * vim registers allow you to keep many independent copy and paste buffers.
468
+ * See http://usevim.com/2012/04/13/registers/ for an introduction.
469
+ *
470
+ * RegisterController keeps the state of all the registers. An initial
471
+ * state may be passed in. The unnamed register '"' will always be
472
+ * overridden.
473
+ */
474
+ function RegisterController(registers) {
475
+ this.registers = registers;
476
+ this.unamedRegister = registers['\"'] = new Register();
477
+ }
478
+ RegisterController.prototype = {
479
+ pushText: function(registerName, operator, text, linewise) {
480
+ // Lowercase and uppercase registers refer to the same register.
481
+ // Uppercase just means append.
482
+ var register = this.isValidRegister(registerName) ?
483
+ this.getRegister(registerName) : null;
484
+ // if no register/an invalid register was specified, things go to the
485
+ // default registers
486
+ if (!register) {
487
+ switch (operator) {
488
+ case 'yank':
489
+ // The 0 register contains the text from the most recent yank.
490
+ this.registers['0'] = new Register(text, linewise);
491
+ break;
492
+ case 'delete':
493
+ case 'change':
494
+ if (text.indexOf('\n') == -1) {
495
+ // Delete less than 1 line. Update the small delete register.
496
+ this.registers['-'] = new Register(text, linewise);
165
497
  } else {
166
- cur.ch = word.to;
498
+ // Shift down the contents of the numbered registers and put the
499
+ // deleted text into register 1.
500
+ this.shiftNumericRegisters_();
501
+ this.registers['1'] = new Register(text, linewise);
167
502
  }
168
- } else if (dir > 0 && where == 'start') {
169
- // 'w'
170
- if (startCh != word.from || startLine != word.line) {
171
- cur.ch = word.from;
172
- break;
173
- } else {
174
- cur.ch = word.to;
503
+ break;
504
+ }
505
+ // Make sure the unnamed register is set to what just happened
506
+ this.unamedRegister.set(text, linewise);
507
+ return;
508
+ }
509
+
510
+ // If we've gotten to this point, we've actually specified a register
511
+ var append = isUpperCase(registerName);
512
+ if (append) {
513
+ register.append(text, linewise);
514
+ // The unamed register always has the same value as the last used
515
+ // register.
516
+ this.unamedRegister.append(text, linewise);
517
+ } else {
518
+ register.set(text, linewise);
519
+ this.unamedRegister.set(text, linewise);
520
+ }
521
+ },
522
+ // Gets the register named @name. If one of @name doesn't already exist,
523
+ // create it. If @name is invalid, return the unamedRegister.
524
+ getRegister: function(name) {
525
+ if (!this.isValidRegister(name)) {
526
+ return this.unamedRegister;
527
+ }
528
+ name = name.toLowerCase();
529
+ if (!this.registers[name]) {
530
+ this.registers[name] = new Register();
531
+ }
532
+ return this.registers[name];
533
+ },
534
+ isValidRegister: function(name) {
535
+ return name && inArray(name, validRegisters);
536
+ },
537
+ shiftNumericRegisters_: function() {
538
+ for (var i = 9; i >= 2; i--) {
539
+ this.registers[i] = this.getRegister('' + (i - 1));
540
+ }
541
+ }
542
+ };
543
+
544
+ var commandDispatcher = {
545
+ matchCommand: function(key, keyMap, vim) {
546
+ var inputState = vim.inputState;
547
+ var keys = inputState.keyBuffer.concat(key);
548
+ for (var i = 0; i < keyMap.length; i++) {
549
+ var command = keyMap[i];
550
+ if (matchKeysPartial(keys, command.keys)) {
551
+ if (keys.length < command.keys.length) {
552
+ // Matches part of a multi-key command. Buffer and wait for next
553
+ // stroke.
554
+ inputState.keyBuffer.push(key);
555
+ return null;
556
+ } else {
557
+ if (inputState.operator && command.type == 'action') {
558
+ // Ignore matched action commands after an operator. Operators
559
+ // only operate on motions. This check is really for text
560
+ // objects since aW, a[ etcs conflicts with a.
561
+ continue;
175
562
  }
176
- } else if (dir < 0 && where == 'end') {
177
- // 'ge'
178
- if (startCh != word.to || startLine != word.line) {
179
- cur.ch = word.to;
180
- break;
181
- } else {
182
- cur.ch = word.from - 1;
563
+ // Matches whole comand. Return the command.
564
+ if (command.keys[keys.length - 1] == 'character') {
565
+ inputState.selectedCharacter = keys[keys.length - 1];
183
566
  }
184
- } else if (dir < 0 && where == 'start') {
185
- // 'b'
186
- if (startCh != word.from || startLine != word.line) {
187
- cur.ch = word.from;
188
- break;
567
+ inputState.keyBuffer = [];
568
+ return command;
569
+ }
570
+ }
571
+ }
572
+ // Clear the buffer since there are no partial matches.
573
+ inputState.keyBuffer = [];
574
+ return null;
575
+ },
576
+ processCommand: function(cm, vim, command) {
577
+ switch (command.type) {
578
+ case 'motion':
579
+ this.processMotion(cm, vim, command);
580
+ break;
581
+ case 'operator':
582
+ this.processOperator(cm, vim, command);
583
+ break;
584
+ case 'operatorMotion':
585
+ this.processOperatorMotion(cm, vim, command);
586
+ break;
587
+ case 'action':
588
+ this.processAction(cm, vim, command);
589
+ break;
590
+ case 'search':
591
+ this.processSearch(cm, vim, command);
592
+ break;
593
+ case 'ex':
594
+ case 'keyToEx':
595
+ this.processEx(cm, vim, command);
596
+ break;
597
+ default:
598
+ break;
599
+ }
600
+ },
601
+ processMotion: function(cm, vim, command) {
602
+ vim.inputState.motion = command.motion;
603
+ vim.inputState.motionArgs = copyArgs(command.motionArgs);
604
+ this.evalInput(cm, vim);
605
+ },
606
+ processOperator: function(cm, vim, command) {
607
+ var inputState = vim.inputState;
608
+ if (inputState.operator) {
609
+ if (inputState.operator == command.operator) {
610
+ // Typing an operator twice like 'dd' makes the operator operate
611
+ // linewise
612
+ inputState.motion = 'expandToLine';
613
+ inputState.motionArgs = { linewise: true };
614
+ this.evalInput(cm, vim);
615
+ return;
616
+ } else {
617
+ // 2 different operators in a row doesn't make sense.
618
+ inputState.reset();
619
+ }
620
+ }
621
+ inputState.operator = command.operator;
622
+ inputState.operatorArgs = copyArgs(command.operatorArgs);
623
+ if (vim.visualMode) {
624
+ // Operating on a selection in visual mode. We don't need a motion.
625
+ this.evalInput(cm, vim);
626
+ }
627
+ },
628
+ processOperatorMotion: function(cm, vim, command) {
629
+ var visualMode = vim.visualMode;
630
+ var operatorMotionArgs = copyArgs(command.operatorMotionArgs);
631
+ if (operatorMotionArgs) {
632
+ // Operator motions may have special behavior in visual mode.
633
+ if (visualMode && operatorMotionArgs.visualLine) {
634
+ vim.visualLine = true;
635
+ }
636
+ }
637
+ this.processOperator(cm, vim, command);
638
+ if (!visualMode) {
639
+ this.processMotion(cm, vim, command);
640
+ }
641
+ },
642
+ processAction: function(cm, vim, command) {
643
+ var inputState = vim.inputState;
644
+ var repeat = inputState.getRepeat();
645
+ var repeatIsExplicit = !!repeat;
646
+ var actionArgs = copyArgs(command.actionArgs) || {};
647
+ if (inputState.selectedCharacter) {
648
+ actionArgs.selectedCharacter = inputState.selectedCharacter;
649
+ }
650
+ // Actions may or may not have motions and operators. Do these first.
651
+ if (command.operator) {
652
+ this.processOperator(cm, vim, command);
653
+ }
654
+ if (command.motion) {
655
+ this.processMotion(cm, vim, command);
656
+ }
657
+ if (command.motion || command.operator) {
658
+ this.evalInput(cm, vim);
659
+ }
660
+ actionArgs.repeat = repeat || 1;
661
+ actionArgs.repeatIsExplicit = repeatIsExplicit;
662
+ actionArgs.registerName = inputState.registerName;
663
+ inputState.reset();
664
+ vim.lastMotion = null,
665
+ actions[command.action](cm, actionArgs, vim);
666
+ },
667
+ processSearch: function(cm, vim, command) {
668
+ if (!cm.getSearchCursor) {
669
+ // Search depends on SearchCursor.
670
+ return;
671
+ }
672
+ var forward = command.searchArgs.forward;
673
+ getSearchState(cm).setReversed(!forward);
674
+ var promptPrefix = (forward) ? '/' : '?';
675
+ function handleQuery(query, ignoreCase, smartCase) {
676
+ updateSearchQuery(cm, query, ignoreCase, smartCase);
677
+ commandDispatcher.processMotion(cm, vim, {
678
+ type: 'motion',
679
+ motion: 'findNext'
680
+ });
681
+ }
682
+ function onPromptClose(query) {
683
+ handleQuery(query, true /** ignoreCase */, true /** smartCase */);
684
+ }
685
+ switch (command.searchArgs.querySrc) {
686
+ case 'prompt':
687
+ showPrompt(cm, onPromptClose, promptPrefix, searchPromptDesc);
688
+ break;
689
+ case 'wordUnderCursor':
690
+ var word = expandWordUnderCursor(cm, false /** inclusive */,
691
+ true /** forward */, false /** bigWord */,
692
+ true /** noSymbol */);
693
+ var isKeyword = true;
694
+ if (!word) {
695
+ word = expandWordUnderCursor(cm, false /** inclusive */,
696
+ true /** forward */, false /** bigWord */,
697
+ false /** noSymbol */);
698
+ isKeyword = false;
699
+ }
700
+ if (!word) {
701
+ return;
702
+ }
703
+ var query = cm.getLine(word.start.line).substring(word.start.ch,
704
+ word.end.ch + 1);
705
+ if (isKeyword) {
706
+ query = '\\b' + query + '\\b';
707
+ } else {
708
+ query = escapeRegex(query);
709
+ }
710
+ cm.setCursor(word.start);
711
+ handleQuery(query, true /** ignoreCase */, false /** smartCase */);
712
+ break;
713
+ }
714
+ },
715
+ processEx: function(cm, vim, command) {
716
+ function onPromptClose(input) {
717
+ exCommandDispatcher.processCommand(cm, input);
718
+ }
719
+ if (command.type == 'keyToEx') {
720
+ // Handle user defined Ex to Ex mappings
721
+ exCommandDispatcher.processCommand(cm, command.exArgs.input);
722
+ } else {
723
+ showPrompt(cm, onPromptClose, ':');
724
+ }
725
+ },
726
+ evalInput: function(cm, vim) {
727
+ // If the motion comand is set, execute both the operator and motion.
728
+ // Otherwise return.
729
+ var inputState = vim.inputState;
730
+ var motion = inputState.motion;
731
+ var motionArgs = inputState.motionArgs || {};
732
+ var operator = inputState.operator;
733
+ var operatorArgs = inputState.operatorArgs || {};
734
+ var registerName = inputState.registerName;
735
+ var selectionEnd = cm.getCursor('head');
736
+ var selectionStart = cm.getCursor('anchor');
737
+ // The difference between cur and selection cursors are that cur is
738
+ // being operated on and ignores that there is a selection.
739
+ var curStart = copyCursor(selectionEnd);
740
+ var curOriginal = copyCursor(curStart);
741
+ var curEnd;
742
+ var repeat;
743
+ if (motionArgs.repeat !== undefined) {
744
+ // If motionArgs specifies a repeat, that takes precedence over the
745
+ // input state's repeat. Used by Ex mode and can be user defined.
746
+ repeat = inputState.motionArgs.repeat;
747
+ } else {
748
+ repeat = inputState.getRepeat();
749
+ }
750
+ if (repeat > 0 && motionArgs.explicitRepeat) {
751
+ motionArgs.repeatIsExplicit = true;
752
+ } else if (motionArgs.noRepeat ||
753
+ (!motionArgs.explicitRepeat && repeat === 0)) {
754
+ repeat = 1;
755
+ motionArgs.repeatIsExplicit = false;
756
+ }
757
+ if (inputState.selectedCharacter) {
758
+ // If there is a character input, stick it in all of the arg arrays.
759
+ motionArgs.selectedCharacter = operatorArgs.selectedCharacter =
760
+ inputState.selectedCharacter;
761
+ }
762
+ motionArgs.repeat = repeat;
763
+ inputState.reset();
764
+ if (motion) {
765
+ var motionResult = motions[motion](cm, motionArgs, vim);
766
+ vim.lastMotion = motions[motion];
767
+ if (!motionResult) {
768
+ return;
769
+ }
770
+ if (motionResult instanceof Array) {
771
+ curStart = motionResult[0];
772
+ curEnd = motionResult[1];
773
+ } else {
774
+ curEnd = motionResult;
775
+ }
776
+ // TODO: Handle null returns from motion commands better.
777
+ if (!curEnd) {
778
+ curEnd = { ch: curStart.ch, line: curStart.line };
779
+ }
780
+ if (vim.visualMode) {
781
+ // Check if the selection crossed over itself. Will need to shift
782
+ // the start point if that happened.
783
+ if (cursorIsBefore(selectionStart, selectionEnd) &&
784
+ (cursorEqual(selectionStart, curEnd) ||
785
+ cursorIsBefore(curEnd, selectionStart))) {
786
+ // The end of the selection has moved from after the start to
787
+ // before the start. We will shift the start right by 1.
788
+ selectionStart.ch += 1;
789
+ } else if (cursorIsBefore(selectionEnd, selectionStart) &&
790
+ (cursorEqual(selectionStart, curEnd) ||
791
+ cursorIsBefore(selectionStart, curEnd))) {
792
+ // The opposite happened. We will shift the start left by 1.
793
+ selectionStart.ch -= 1;
794
+ }
795
+ selectionEnd = curEnd;
796
+ if (vim.visualLine) {
797
+ if (cursorIsBefore(selectionStart, selectionEnd)) {
798
+ selectionStart.ch = 0;
799
+ selectionEnd.ch = lineLength(cm, selectionEnd.line);
189
800
  } else {
190
- cur.ch = word.from - 1;
801
+ selectionEnd.ch = 0;
802
+ selectionStart.ch = lineLength(cm, selectionStart.line);
191
803
  }
192
804
  }
805
+ // Need to set the cursor to clear the selection. Otherwise,
806
+ // CodeMirror can't figure out that we changed directions...
807
+ cm.setCursor(selectionStart);
808
+ cm.setSelection(selectionStart, selectionEnd);
809
+ } else if (!operator) {
810
+ curEnd = clipCursorToContent(cm, curEnd);
811
+ cm.setCursor(curEnd.line, curEnd.ch);
193
812
  }
194
- } else {
195
- // No more words to be found. Move to end of document.
196
- for (; isLine(cm, cur.line + dir); cur.line += dir) {}
197
- cur.ch = (dir > 0) ? cm.getLine(cur.line).length : 0;
198
- break;
199
- }
200
- }
201
- }
202
- if (where == 'end' && yank) {
203
- // Include the last character of the word for actions.
204
- cur.ch++;
205
- }
206
- return cur;
207
- }
208
- function joinLineNext(cm) {
209
- var cur = cm.getCursor(), ch = cur.ch, line = cm.getLine(cur.line);
210
- CodeMirror.commands.goLineEnd(cm);
211
- if (cur.line != cm.lineCount()) {
212
- CodeMirror.commands.goLineEnd(cm);
213
- cm.replaceSelection(" ", "end");
214
- CodeMirror.commands.delCharRight(cm);
215
- }
216
- }
217
- function delTillMark(cm, cHar) {
218
- var i = mark[cHar];
219
- if (i === undefined) {
220
- // console.log("Mark not set"); // TODO - show in status bar
221
- return;
222
- }
223
- var l = cm.getCursor().line, start = i > l ? l : i, end = i > l ? i : l;
224
- cm.setCursor(start);
225
- for (var c = start; c <= end; c++) {
226
- pushInBuffer("\n" + cm.getLine(start));
227
- cm.removeLine(start);
228
- }
229
- }
230
- function yankTillMark(cm, cHar) {
231
- var i = mark[cHar];
232
- if (i === undefined) {
233
- // console.log("Mark not set"); // TODO - show in status bar
234
- return;
235
- }
236
- var l = cm.getCursor().line, start = i > l ? l : i, end = i > l ? i : l;
237
- for (var c = start; c <= end; c++) {
238
- pushInBuffer("\n" + cm.getLine(c));
239
- }
240
- cm.setCursor(start);
241
- }
242
- function goLineStartText(cm) {
243
- // Go to the start of the line where the text begins, or the end for whitespace-only lines
244
- var cur = cm.getCursor(), firstNonWS = cm.getLine(cur.line).search(/\S/);
245
- cm.setCursor(cur.line, firstNonWS == -1 ? line.length : firstNonWS, true);
246
- }
247
-
248
- function charIdxInLine(cm, cHar, motion_options) {
249
- // Search for cHar in line.
250
- // motion_options: {forward, inclusive}
251
- // If inclusive = true, include it too.
252
- // If forward = true, search forward, else search backwards.
253
- // If char is not found on this line, do nothing
254
- var cur = cm.getCursor(), line = cm.getLine(cur.line), idx;
255
- var ch = toLetter(cHar), mo = motion_options;
256
- if (mo.forward) {
257
- idx = line.indexOf(ch, cur.ch + 1);
258
- if (idx != -1 && mo.inclusive) idx += 1;
259
- } else {
260
- idx = line.lastIndexOf(ch, cur.ch);
261
- if (idx != -1 && !mo.inclusive) idx += 1;
262
- }
263
- return idx;
264
- }
265
-
266
- function moveTillChar(cm, cHar, motion_options) {
267
- // Move to cHar in line, as found by charIdxInLine.
268
- var idx = charIdxInLine(cm, cHar, motion_options), cur = cm.getCursor();
269
- if (idx != -1) cm.setCursor({line: cur.line, ch: idx});
270
- }
271
-
272
- function delTillChar(cm, cHar, motion_options) {
273
- // delete text in this line, untill cHar is met,
274
- // as found by charIdxInLine.
275
- // If char is not found on this line, do nothing
276
- var idx = charIdxInLine(cm, cHar, motion_options);
277
- var cur = cm.getCursor();
278
- if (idx !== -1) {
279
- if (motion_options.forward) {
280
- cm.replaceRange("", {line: cur.line, ch: cur.ch}, {line: cur.line, ch: idx});
281
- } else {
282
- cm.replaceRange("", {line: cur.line, ch: idx}, {line: cur.line, ch: cur.ch});
283
- }
284
- }
285
- }
286
-
287
- function enterInsertMode(cm) {
288
- // enter insert mode: switch mode and cursor
289
- clearCount();
290
- cm.setOption("keyMap", "vim-insert");
291
- }
292
-
293
- function dialog(cm, text, shortText, f) {
294
- if (cm.openDialog) cm.openDialog(text, f);
295
- else f(prompt(shortText, ""));
296
- }
297
- function showAlert(cm, text) {
298
- var esc = text.replace(/[<&]/, function(ch) { return ch == "<" ? "&lt;" : "&amp;"; });
299
- if (cm.openDialog) cm.openDialog(esc + " <button type=button>OK</button>");
300
- else alert(text);
301
- }
302
-
303
- // main keymap
304
- var map = CodeMirror.keyMap.vim = {
305
- // Pipe (|); TODO: should be *screen* chars, so need a util function to turn tabs into spaces?
306
- "'|'": function(cm) {
307
- cm.setCursor(cm.getCursor().line, getCountOrOne() - 1, true);
308
- clearCount();
309
- },
310
- "A": function(cm) {
311
- cm.setCursor(cm.getCursor().line, cm.getCursor().ch+1, true);
312
- enterInsertMode(cm);
313
- },
314
- "Shift-A": function(cm) { CodeMirror.commands.goLineEnd(cm); enterInsertMode(cm);},
315
- "I": function(cm) { enterInsertMode(cm);},
316
- "Shift-I": function(cm) { goLineStartText(cm); enterInsertMode(cm);},
317
- "O": function(cm) {
318
- CodeMirror.commands.goLineEnd(cm);
319
- CodeMirror.commands.newlineAndIndent(cm);
320
- enterInsertMode(cm);
321
- },
322
- "Shift-O": function(cm) {
323
- CodeMirror.commands.goLineStart(cm);
324
- cm.replaceSelection("\n", "start");
325
- cm.indentLine(cm.getCursor().line);
326
- enterInsertMode(cm);
327
- },
328
- "G": function(cm) { cm.setOption("keyMap", "vim-prefix-g");},
329
- "Shift-D": function(cm) {
330
- var cursor = cm.getCursor();
331
- var lineN = cursor.line;
332
- var line = cm.getLine(lineN);
333
- cm.setLine(lineN, line.slice(0, cursor.ch));
334
-
335
- emptyBuffer();
336
- pushInBuffer(line.slice(cursor.ch));
337
-
338
- if (repeatCount > 1) {
339
- // we've already done it once
340
- --repeatCount;
341
- // the lines dissapear (ie, cursor stays on the same lineN),
342
- // so only incremenet once
343
- ++lineN;
344
-
345
- iterTimes(function() {
346
- pushInBuffer(cm.getLine(lineN));
347
- cm.removeLine(lineN);
348
- });
813
+ }
814
+
815
+ if (operator) {
816
+ var inverted = false;
817
+ vim.lastMotion = null;
818
+ operatorArgs.repeat = repeat; // Indent in visual mode needs this.
819
+ if (vim.visualMode) {
820
+ curStart = selectionStart;
821
+ curEnd = selectionEnd;
822
+ motionArgs.inclusive = true;
823
+ }
824
+ // Swap start and end if motion was backward.
825
+ if (cursorIsBefore(curEnd, curStart)) {
826
+ var tmp = curStart;
827
+ curStart = curEnd;
828
+ curEnd = tmp;
829
+ inverted = true;
830
+ }
831
+ if (motionArgs.inclusive && !(vim.visualMode && inverted)) {
832
+ // Move the selection end one to the right to include the last
833
+ // character.
834
+ curEnd.ch++;
835
+ }
836
+ var linewise = motionArgs.linewise ||
837
+ (vim.visualMode && vim.visualLine);
838
+ if (linewise) {
839
+ // Expand selection to entire line.
840
+ expandSelectionToLine(cm, curStart, curEnd);
841
+ } else if (motionArgs.forward) {
842
+ // Clip to trailing newlines only if we the motion goes forward.
843
+ clipToLine(cm, curStart, curEnd);
844
+ }
845
+ operatorArgs.registerName = registerName;
846
+ // Keep track of linewise as it affects how paste and change behave.
847
+ operatorArgs.linewise = linewise;
848
+ operators[operator](cm, operatorArgs, vim, curStart,
849
+ curEnd, curOriginal);
850
+ if (vim.visualMode) {
851
+ exitVisualMode(cm, vim);
852
+ }
853
+ if (operatorArgs.enterInsertMode) {
854
+ actions.enterInsertMode(cm);
855
+ }
856
+ }
349
857
  }
350
- },
351
-
352
- "S": function (cm) {
353
- countTimes(function (_cm) {
354
- CodeMirror.commands.delCharRight(_cm);
355
- })(cm);
356
- enterInsertMode(cm);
357
- },
358
- "M": function(cm) {cm.setOption("keyMap", "vim-prefix-m"); mark = {};},
359
- "Y": function(cm) {cm.setOption("keyMap", "vim-prefix-y"); emptyBuffer();},
360
- "Shift-Y": function(cm) {
361
- emptyBuffer();
362
- iterTimes(function(i) { pushInBuffer("\n" + cm.getLine(cm.getCursor().line + i)); });
363
- },
364
- "/": function(cm) {var f = CodeMirror.commands.find; f && f(cm); sdir = "f";},
365
- "'?'": function(cm) {
366
- var f = CodeMirror.commands.find;
367
- if (f) { f(cm); CodeMirror.commands.findPrev(cm); sdir = "r"; }
368
- },
369
- "N": function(cm) {
370
- var fn = CodeMirror.commands.findNext;
371
- if (fn) sdir != "r" ? fn(cm) : CodeMirror.commands.findPrev(cm);
372
- },
373
- "Shift-N": function(cm) {
374
- var fn = CodeMirror.commands.findNext;
375
- if (fn) sdir != "r" ? CodeMirror.commands.findPrev(cm) : fn.findNext(cm);
376
- },
377
- "Shift-G": function(cm) {
378
- (repeatCount == 0) ? cm.setCursor(cm.lineCount()) : cm.setCursor(repeatCount - 1);
379
- clearCount();
380
- CodeMirror.commands.goLineStart(cm);
381
- },
382
- "':'": function(cm) {
383
- var exModeDialog = ': <input type="text" style="width: 90%"/>';
384
- dialog(cm, exModeDialog, ':', function(command) {
385
- if (command.match(/^\d+$/)) {
386
- cm.setCursor(command - 1, cm.getCursor().ch);
858
+ };
859
+
860
+ /**
861
+ * typedef {Object{line:number,ch:number}} Cursor An object containing the
862
+ * position of the cursor.
863
+ */
864
+ // All of the functions below return Cursor objects.
865
+ var motions = {
866
+ expandToLine: function(cm, motionArgs) {
867
+ // Expands forward to end of line, and then to next line if repeat is
868
+ // >1. Does not handle backward motion!
869
+ var cur = cm.getCursor();
870
+ return { line: cur.line + motionArgs.repeat - 1, ch: Infinity };
871
+ },
872
+ findNext: function(cm, motionArgs, vim) {
873
+ return findNext(cm, false /** prev */, motionArgs.repeat);
874
+ },
875
+ findPrev: function(cm, motionArgs, vim) {
876
+ return findNext(cm, true /** prev */, motionArgs.repeat);
877
+ },
878
+ goToMark: function(cm, motionArgs, vim) {
879
+ var mark = vim.marks[motionArgs.selectedCharacter];
880
+ if (mark) {
881
+ return mark.find();
882
+ }
883
+ return null;
884
+ },
885
+ moveByCharacters: function(cm, motionArgs) {
886
+ var cur = cm.getCursor();
887
+ var repeat = motionArgs.repeat;
888
+ var ch = motionArgs.forward ? cur.ch + repeat : cur.ch - repeat;
889
+ return { line: cur.line, ch: ch };
890
+ },
891
+ moveByLines: function(cm, motionArgs, vim) {
892
+ var endCh = cm.getCursor().ch;
893
+ // Depending what our last motion was, we may want to do different
894
+ // things. If our last motion was moving vertically, we want to
895
+ // preserve the HPos from our last horizontal move. If our last motion
896
+ // was going to the end of a line, moving vertically we should go to
897
+ // the end of the line, etc.
898
+ switch (vim.lastMotion) {
899
+ case this.moveByLines:
900
+ case this.moveToColumn:
901
+ case this.moveToEol:
902
+ endCh = vim.lastHPos;
903
+ break;
904
+ default:
905
+ vim.lastHPos = endCh;
906
+ }
907
+ var cur = cm.getCursor();
908
+ var repeat = motionArgs.repeat;
909
+ var line = motionArgs.forward ? cur.line + repeat : cur.line - repeat;
910
+ if (line < 0 || line > cm.lineCount() - 1) {
911
+ return null;
912
+ }
913
+ return { line: line, ch: endCh };
914
+ },
915
+ moveByPage: function(cm, motionArgs) {
916
+ // CodeMirror only exposes functions that move the cursor page down, so
917
+ // doing this bad hack to move the cursor and move it back. evalInput
918
+ // will move the cursor to where it should be in the end.
919
+ var curStart = cm.getCursor();
920
+ var repeat = motionArgs.repeat;
921
+ cm.moveV((motionArgs.forward ? repeat : -repeat), 'page');
922
+ var curEnd = cm.getCursor();
923
+ cm.setCursor(curStart);
924
+ return curEnd;
925
+ },
926
+ moveByWords: function(cm, motionArgs) {
927
+ return moveToWord(cm, motionArgs.repeat, !!motionArgs.forward,
928
+ !!motionArgs.wordEnd, !!motionArgs.bigWord);
929
+ },
930
+ moveTillCharacter: function(cm, motionArgs) {
931
+ var repeat = motionArgs.repeat;
932
+ var curEnd = moveToCharacter(cm, repeat, motionArgs.forward,
933
+ motionArgs.selectedCharacter);
934
+ var increment = motionArgs.forward ? -1 : 1;
935
+ curEnd.ch += increment;
936
+ return curEnd;
937
+ },
938
+ moveToCharacter: function(cm, motionArgs) {
939
+ var repeat = motionArgs.repeat;
940
+ return moveToCharacter(cm, repeat, motionArgs.forward,
941
+ motionArgs.selectedCharacter);
942
+ },
943
+ moveToColumn: function(cm, motionArgs, vim) {
944
+ var repeat = motionArgs.repeat;
945
+ // repeat is equivalent to which column we want to move to!
946
+ vim.lastHPos = repeat - 1;
947
+ return moveToColumn(cm, repeat);
948
+ },
949
+ moveToEol: function(cm, motionArgs, vim) {
950
+ var cur = cm.getCursor();
951
+ vim.lastHPos = Infinity;
952
+ return { line: cur.line + motionArgs.repeat - 1, ch: Infinity };
953
+ },
954
+ moveToFirstNonWhiteSpaceCharacter: function(cm) {
955
+ // Go to the start of the line where the text begins, or the end for
956
+ // whitespace-only lines
957
+ var cursor = cm.getCursor();
958
+ var line = cm.getLine(cursor.line);
959
+ return { line: cursor.line,
960
+ ch: findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line)) };
961
+ },
962
+ moveToMatchedSymbol: function(cm, motionArgs) {
963
+ var cursor = cm.getCursor();
964
+ var symbol = cm.getLine(cursor.line).charAt(cursor.ch);
965
+ if (isMatchableSymbol(symbol)) {
966
+ return findMatchedSymbol(cm, cm.getCursor(), motionArgs.symbol);
387
967
  } else {
388
- showAlert(cm, "Bad command: " + command);
968
+ return cursor;
389
969
  }
390
- });
391
- },
392
- nofallthrough: true, style: "fat-cursor"
393
- };
394
-
395
- // standard mode switching
396
- iterList(["d", "t", "T", "f", "F", "c", "r"], function (ch) {
397
- CodeMirror.keyMap.vim[toCombo(ch)] = function (cm) {
398
- cm.setOption("keyMap", "vim-prefix-" + ch);
399
- emptyBuffer();
970
+ },
971
+ moveToStartOfLine: function(cm) {
972
+ var cursor = cm.getCursor();
973
+ return { line: cursor.line, ch: 0 };
974
+ },
975
+ moveToLineOrEdgeOfDocument: function(cm, motionArgs) {
976
+ var lineNum = motionArgs.forward ? cm.lineCount() - 1 : 0;
977
+ if (motionArgs.repeatIsExplicit) {
978
+ lineNum = motionArgs.repeat - 1;
979
+ }
980
+ return { line: lineNum,
981
+ ch: findFirstNonWhiteSpaceCharacter(cm.getLine(lineNum)) };
982
+ },
983
+ textObjectManipulation: function(cm, motionArgs) {
984
+ var character = motionArgs.selectedCharacter;
985
+ // Inclusive is the difference between a and i
986
+ // TODO: Instead of using the additional text object map to perform text
987
+ // object operations, merge the map into the defaultKeyMap and use
988
+ // motionArgs to define behavior. Define separate entries for 'aw',
989
+ // 'iw', 'a[', 'i[', etc.
990
+ var inclusive = !motionArgs.textObjectInner;
991
+ if (!textObjects[character]) {
992
+ // No text object defined for this, don't move.
993
+ return null;
994
+ }
995
+ var tmp = textObjects[character](cm, inclusive);
996
+ var start = tmp.start;
997
+ var end = tmp.end;
998
+ return [start, end];
999
+ }
400
1000
  };
401
- });
402
-
403
- // main num keymap
404
- // Add bindings that are influenced by number keys
405
- iterObj({
406
- "X": function(cm) {CodeMirror.commands.delCharRight(cm);},
407
- "P": function(cm) {
408
- var cur = cm.getCursor().line;
409
- if (buf!= "") {
410
- if (buf[0] == "\n") CodeMirror.commands.goLineEnd(cm);
411
- cm.replaceRange(buf, cm.getCursor());
412
- }
413
- },
414
- "Shift-X": function(cm) {CodeMirror.commands.delCharLeft(cm);},
415
- "Shift-J": function(cm) {joinLineNext(cm);},
416
- "Shift-P": function(cm) {
417
- var cur = cm.getCursor().line;
418
- if (buf!= "") {
419
- CodeMirror.commands.goLineUp(cm);
420
- CodeMirror.commands.goLineEnd(cm);
421
- cm.replaceSelection(buf, "end");
422
- }
423
- cm.setCursor(cur+1);
424
- },
425
- "'~'": function(cm) {
426
- var cur = cm.getCursor(), cHar = cm.getRange({line: cur.line, ch: cur.ch}, {line: cur.line, ch: cur.ch+1});
427
- cHar = cHar != cHar.toLowerCase() ? cHar.toLowerCase() : cHar.toUpperCase();
428
- cm.replaceRange(cHar, {line: cur.line, ch: cur.ch}, {line: cur.line, ch: cur.ch+1});
429
- cm.setCursor(cur.line, cur.ch+1);
430
- },
431
- "Ctrl-B": function(cm) {CodeMirror.commands.goPageUp(cm);},
432
- "Ctrl-F": function(cm) {CodeMirror.commands.goPageDown(cm);},
433
- "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown",
434
- "U": "undo", "Ctrl-R": "redo"
435
- }, function(key, cmd) { map[key] = countTimes(cmd); });
436
-
437
- // empty key maps
438
- iterList([
439
- "vim-prefix-d'",
440
- "vim-prefix-y'",
441
- "vim-prefix-df",
442
- "vim-prefix-dF",
443
- "vim-prefix-dt",
444
- "vim-prefix-dT",
445
- "vim-prefix-c",
446
- "vim-prefix-cf",
447
- "vim-prefix-cF",
448
- "vim-prefix-ct",
449
- "vim-prefix-cT",
450
- "vim-prefix-",
451
- "vim-prefix-f",
452
- "vim-prefix-F",
453
- "vim-prefix-t",
454
- "vim-prefix-T",
455
- "vim-prefix-r",
456
- "vim-prefix-m"
457
- ],
458
- function (prefix) {
459
- CodeMirror.keyMap[prefix] = {
460
- auto: "vim",
461
- nofallthrough: true,
462
- style: "fat-cursor"
463
- };
464
- });
465
1001
 
466
- CodeMirror.keyMap["vim-prefix-g"] = {
467
- "E": countTimes(function(cm) { cm.setCursor(moveToWord(cm, word, -1, 1, "end"));}),
468
- "Shift-E": countTimes(function(cm) { cm.setCursor(moveToWord(cm, bigWord, -1, 1, "end"));}),
469
- "G": function (cm) {
470
- cm.setCursor({line: repeatCount - 1, ch: cm.getCursor().ch});
471
- clearCount();
472
- },
473
- auto: "vim", nofallthrough: true, style: "fat-cursor"
474
- };
1002
+ var operators = {
1003
+ change: function(cm, operatorArgs, vim, curStart, curEnd) {
1004
+ getVimGlobalState().registerController.pushText(
1005
+ operatorArgs.registerName, 'change', cm.getRange(curStart, curEnd),
1006
+ operatorArgs.linewise);
1007
+ if (operatorArgs.linewise) {
1008
+ // Delete starting at the first nonwhitespace character of the first
1009
+ // line, instead of from the start of the first line. This way we get
1010
+ // an indent when we get into insert mode. This behavior isn't quite
1011
+ // correct because we should treat this as a completely new line, and
1012
+ // indent should be whatever codemirror thinks is the right indent.
1013
+ // But cm.indentLine doesn't seem work on empty lines.
1014
+ // TODO: Fix the above.
1015
+ curStart.ch =
1016
+ findFirstNonWhiteSpaceCharacter(cm.getLine(curStart.line));
1017
+ // Insert an additional newline so that insert mode can start there.
1018
+ // curEnd should be on the first character of the new line.
1019
+ cm.replaceRange('\n', curStart, curEnd);
1020
+ } else {
1021
+ cm.replaceRange('', curStart, curEnd);
1022
+ }
1023
+ cm.setCursor(curStart);
1024
+ },
1025
+ // delete is a javascript keyword.
1026
+ 'delete': function(cm, operatorArgs, vim, curStart, curEnd) {
1027
+ getVimGlobalState().registerController.pushText(
1028
+ operatorArgs.registerName, 'delete', cm.getRange(curStart, curEnd),
1029
+ operatorArgs.linewise);
1030
+ cm.replaceRange('', curStart, curEnd);
1031
+ if (operatorArgs.linewise) {
1032
+ cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm));
1033
+ } else {
1034
+ cm.setCursor(curStart);
1035
+ }
1036
+ },
1037
+ indent: function(cm, operatorArgs, vim, curStart, curEnd) {
1038
+ var startLine = curStart.line;
1039
+ var endLine = curEnd.line;
1040
+ // In visual mode, n> shifts the selection right n times, instead of
1041
+ // shifting n lines right once.
1042
+ var repeat = (vim.visualMode) ? operatorArgs.repeat : 1;
1043
+ if (operatorArgs.linewise) {
1044
+ // The only way to delete a newline is to delete until the start of
1045
+ // the next line, so in linewise mode evalInput will include the next
1046
+ // line. We don't want this in indent, so we go back a line.
1047
+ endLine--;
1048
+ }
1049
+ for (var i = startLine; i <= endLine; i++) {
1050
+ for (var j = 0; j < repeat; j++) {
1051
+ cm.indentLine(i, operatorArgs.indentRight);
1052
+ }
1053
+ }
1054
+ cm.setCursor(curStart);
1055
+ cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm));
1056
+ },
1057
+ swapcase: function(cm, operatorArgs, vim, curStart, curEnd, curOriginal) {
1058
+ var toSwap = cm.getRange(curStart, curEnd);
1059
+ var swapped = '';
1060
+ for (var i = 0; i < toSwap.length; i++) {
1061
+ var character = toSwap.charAt(i);
1062
+ swapped += isUpperCase(character) ? character.toLowerCase() :
1063
+ character.toUpperCase();
1064
+ }
1065
+ cm.replaceRange(swapped, curStart, curEnd);
1066
+ cm.setCursor(curOriginal);
1067
+ },
1068
+ yank: function(cm, operatorArgs, vim, curStart, curEnd, curOriginal) {
1069
+ getVimGlobalState().registerController.pushText(
1070
+ operatorArgs.registerName, 'yank',
1071
+ cm.getRange(curStart, curEnd), operatorArgs.linewise);
1072
+ cm.setCursor(curOriginal);
1073
+ }
1074
+ };
475
1075
 
476
- CodeMirror.keyMap["vim-prefix-d"] = {
477
- "D": countTimes(function(cm) {
478
- pushInBuffer("\n" + cm.getLine(cm.getCursor().line));
479
- cm.removeLine(cm.getCursor().line);
480
- cm.setOption("keyMap", "vim");
481
- }),
482
- "'": function(cm) {
483
- cm.setOption("keyMap", "vim-prefix-d'");
484
- emptyBuffer();
485
- },
486
- "B": function(cm) {
487
- var cur = cm.getCursor();
488
- var line = cm.getLine(cur.line);
489
- var index = line.lastIndexOf(" ", cur.ch);
1076
+ var actions = {
1077
+ clearSearchHighlight: clearSearchHighlight,
1078
+ enterInsertMode: function(cm, actionArgs) {
1079
+ var insertAt = (actionArgs) ? actionArgs.insertAt : null;
1080
+ if (insertAt == 'eol') {
1081
+ var cursor = cm.getCursor();
1082
+ cursor = { line: cursor.line, ch: lineLength(cm, cursor.line) };
1083
+ cm.setCursor(cursor);
1084
+ } else if (insertAt == 'charAfter') {
1085
+ cm.setCursor(offsetCursor(cm.getCursor(), 0, 1));
1086
+ }
1087
+ cm.setOption('keyMap', 'vim-insert');
1088
+ },
1089
+ toggleVisualMode: function(cm, actionArgs, vim) {
1090
+ var repeat = actionArgs.repeat;
1091
+ var curStart = cm.getCursor();
1092
+ var curEnd;
1093
+ // TODO: The repeat should actually select number of characters/lines
1094
+ // equal to the repeat times the size of the previous visual
1095
+ // operation.
1096
+ if (!vim.visualMode) {
1097
+ vim.visualMode = true;
1098
+ vim.visualLine = !!actionArgs.linewise;
1099
+ if (vim.visualLine) {
1100
+ curStart.ch = 0;
1101
+ curEnd = clipCursorToContent(cm, {
1102
+ line: curStart.line + repeat - 1,
1103
+ ch: lineLength(cm, curStart.line)
1104
+ }, true /** includeLineBreak */);
1105
+ } else {
1106
+ curEnd = clipCursorToContent(cm, {
1107
+ line: curStart.line,
1108
+ ch: curStart.ch + repeat
1109
+ }, true /** includeLineBreak */);
1110
+ }
1111
+ // Make the initial selection.
1112
+ if (!actionArgs.repeatIsExplicit && !vim.visualLine) {
1113
+ // This is a strange case. Here the implicit repeat is 1. The
1114
+ // following commands lets the cursor hover over the 1 character
1115
+ // selection.
1116
+ cm.setCursor(curEnd);
1117
+ cm.setSelection(curEnd, curStart);
1118
+ } else {
1119
+ cm.setSelection(curStart, curEnd);
1120
+ }
1121
+ } else {
1122
+ if (!vim.visualLine && actionArgs.linewise) {
1123
+ // Shift-V pressed in characterwise visual mode. Switch to linewise
1124
+ // visual mode instead of exiting visual mode.
1125
+ vim.visualLine = true;
1126
+ curStart = cm.getCursor('anchor');
1127
+ curEnd = cm.getCursor('head');
1128
+ curStart.ch = cursorIsBefore(curStart, curEnd) ? 0 :
1129
+ lineLength(cm, curStart.line);
1130
+ curEnd.ch = cursorIsBefore(curStart, curEnd) ?
1131
+ lineLength(cm, curEnd.line) : 0;
1132
+ cm.setSelection(curStart, curEnd);
1133
+ } else {
1134
+ exitVisualMode(cm, vim);
1135
+ }
1136
+ }
1137
+ },
1138
+ joinLines: function(cm, actionArgs, vim) {
1139
+ var curStart, curEnd;
1140
+ if (vim.visualMode) {
1141
+ curStart = cm.getCursor('anchor');
1142
+ curEnd = cm.getCursor('head');
1143
+ curEnd.ch = lineLength(cm, curEnd.line) - 1;
1144
+ } else {
1145
+ // Repeat is the number of lines to join. Minimum 2 lines.
1146
+ var repeat = Math.max(actionArgs.repeat, 2);
1147
+ curStart = cm.getCursor();
1148
+ curEnd = clipCursorToContent(cm, { line: curStart.line + repeat - 1,
1149
+ ch: Infinity });
1150
+ }
1151
+ var finalCh = 0;
1152
+ cm.operation(function() {
1153
+ for (var i = curStart.line; i < curEnd.line; i++) {
1154
+ finalCh = lineLength(cm, curStart.line);
1155
+ var tmp = { line: curStart.line + 1,
1156
+ ch: lineLength(cm, curStart.line + 1) };
1157
+ var text = cm.getRange(curStart, tmp);
1158
+ text = text.replace(/\n\s*/g, ' ');
1159
+ cm.replaceRange(text, curStart, tmp);
1160
+ }
1161
+ var curFinalPos = { line: curStart.line, ch: finalCh };
1162
+ cm.setCursor(curFinalPos);
1163
+ });
1164
+ },
1165
+ newLineAndEnterInsertMode: function(cm, actionArgs) {
1166
+ var insertAt = cm.getCursor();
1167
+ if (insertAt.line === 0 && !actionArgs.after) {
1168
+ // Special case for inserting newline before start of document.
1169
+ cm.replaceRange('\n', { line: 0, ch: 0 });
1170
+ cm.setCursor(0, 0);
1171
+ } else {
1172
+ insertAt.line = (actionArgs.after) ? insertAt.line :
1173
+ insertAt.line - 1;
1174
+ insertAt.ch = lineLength(cm, insertAt.line);
1175
+ cm.setCursor(insertAt);
1176
+ var newlineFn = CodeMirror.commands.newlineAndIndentContinueComment ||
1177
+ CodeMirror.commands.newlineAndIndent;
1178
+ newlineFn(cm);
1179
+ }
1180
+ this.enterInsertMode(cm);
1181
+ },
1182
+ paste: function(cm, actionArgs, vim) {
1183
+ var cur = cm.getCursor();
1184
+ var register = getVimGlobalState().registerController.getRegister(
1185
+ actionArgs.registerName);
1186
+ if (!register.text) {
1187
+ return;
1188
+ }
1189
+ for (var text = '', i = 0; i < actionArgs.repeat; i++) {
1190
+ text += register.text;
1191
+ }
1192
+ var linewise = register.linewise;
1193
+ if (linewise) {
1194
+ if (actionArgs.after) {
1195
+ // Move the newline at the end to the start instead, and paste just
1196
+ // before the newline character of the line we are on right now.
1197
+ text = '\n' + text.slice(0, text.length - 1);
1198
+ cur.ch = lineLength(cm, cur.line);
1199
+ } else {
1200
+ cur.ch = 0;
1201
+ }
1202
+ } else {
1203
+ cur.ch += actionArgs.after ? 1 : 0;
1204
+ }
1205
+ cm.replaceRange(text, cur);
1206
+ // Now fine tune the cursor to where we want it.
1207
+ var curPosFinal;
1208
+ var idx;
1209
+ if (linewise && actionArgs.after) {
1210
+ curPosFinal = { line: cur.line + 1,
1211
+ ch: findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line + 1)) };
1212
+ } else if (linewise && !actionArgs.after) {
1213
+ curPosFinal = { line: cur.line,
1214
+ ch: findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line)) };
1215
+ } else if (!linewise && actionArgs.after) {
1216
+ idx = cm.indexFromPos(cur);
1217
+ curPosFinal = cm.posFromIndex(idx + text.length - 1);
1218
+ } else {
1219
+ idx = cm.indexFromPos(cur);
1220
+ curPosFinal = cm.posFromIndex(idx + text.length);
1221
+ }
1222
+ cm.setCursor(curPosFinal);
1223
+ },
1224
+ undo: function(cm, actionArgs) {
1225
+ repeatFn(cm, CodeMirror.commands.undo, actionArgs.repeat)();
1226
+ },
1227
+ redo: function(cm, actionArgs) {
1228
+ repeatFn(cm, CodeMirror.commands.redo, actionArgs.repeat)();
1229
+ },
1230
+ setRegister: function(cm, actionArgs, vim) {
1231
+ vim.inputState.registerName = actionArgs.selectedCharacter;
1232
+ },
1233
+ setMark: function(cm, actionArgs, vim) {
1234
+ var markName = actionArgs.selectedCharacter;
1235
+ if (!inArray(markName, validMarks)) {
1236
+ return;
1237
+ }
1238
+ if (vim.marks[markName]) {
1239
+ vim.marks[markName].clear();
1240
+ }
1241
+ vim.marks[markName] = cm.setBookmark(cm.getCursor());
1242
+ },
1243
+ replace: function(cm, actionArgs) {
1244
+ var replaceWith = actionArgs.selectedCharacter;
1245
+ var curStart = cm.getCursor();
1246
+ var line = cm.getLine(curStart.line);
1247
+ var replaceTo = curStart.ch + actionArgs.repeat;
1248
+ if (replaceTo > line.length) {
1249
+ return;
1250
+ }
1251
+ var curEnd = { line: curStart.line, ch: replaceTo };
1252
+ var replaceWithStr = '';
1253
+ for (var i = 0; i < curEnd.ch - curStart.ch; i++) {
1254
+ replaceWithStr += replaceWith;
1255
+ }
1256
+ cm.replaceRange(replaceWithStr, curStart, curEnd);
1257
+ cm.setCursor(offsetCursor(curEnd, 0, -1));
1258
+ }
1259
+ };
490
1260
 
491
- pushInBuffer(line.substring(index, cur.ch));
492
- cm.replaceRange("", {line: cur.line, ch: index}, cur);
493
- cm.setOption("keyMap", "vim");
494
- },
495
- nofallthrough: true, style: "fat-cursor"
496
- };
1261
+ var textObjects = {
1262
+ // TODO: lots of possible exceptions that can be thrown here. Try da(
1263
+ // outside of a () block.
1264
+ // TODO: implement text objects for the reverse like }. Should just be
1265
+ // an additional mapping after moving to the defaultKeyMap.
1266
+ 'w': function(cm, inclusive) {
1267
+ return expandWordUnderCursor(cm, inclusive, true /** forward */,
1268
+ false /** bigWord */);
1269
+ },
1270
+ 'W': function(cm, inclusive) {
1271
+ return expandWordUnderCursor(cm, inclusive,
1272
+ true /** forward */, true /** bigWord */);
1273
+ },
1274
+ '{': function(cm, inclusive) {
1275
+ return selectCompanionObject(cm, '}', inclusive);
1276
+ },
1277
+ '(': function(cm, inclusive) {
1278
+ return selectCompanionObject(cm, ')', inclusive);
1279
+ },
1280
+ '[': function(cm, inclusive) {
1281
+ return selectCompanionObject(cm, ']', inclusive);
1282
+ },
1283
+ '\'': function(cm, inclusive) {
1284
+ return findBeginningAndEnd(cm, "'", inclusive);
1285
+ },
1286
+ '\"': function(cm, inclusive) {
1287
+ return findBeginningAndEnd(cm, '"', inclusive);
1288
+ }
1289
+ };
497
1290
 
498
- CodeMirror.keyMap["vim-prefix-c"] = {
499
- "B": function (cm) {
500
- countTimes("delWordLeft")(cm);
501
- enterInsertMode(cm);
502
- },
503
- "C": function (cm) {
504
- iterTimes(function (i, last) {
505
- CodeMirror.commands.deleteLine(cm);
506
- if (i) {
507
- CodeMirror.commands.delCharRight(cm);
508
- if (last) CodeMirror.commands.deleteLine(cm);
1291
+ /*
1292
+ * Below are miscellaneous utility functions used by vim.js
1293
+ */
1294
+
1295
+ /**
1296
+ * Clips cursor to ensure that:
1297
+ * 0 <= cur.ch < lineLength
1298
+ * AND
1299
+ * 0 <= cur.line < lineCount
1300
+ * If includeLineBreak is true, then allow cur.ch == lineLength.
1301
+ */
1302
+ function clipCursorToContent(cm, cur, includeLineBreak) {
1303
+ var line = Math.min(Math.max(0, cur.line), cm.lineCount() - 1);
1304
+ var maxCh = lineLength(cm, line) - 1;
1305
+ maxCh = (includeLineBreak) ? maxCh + 1 : maxCh;
1306
+ var ch = Math.min(Math.max(0, cur.ch), maxCh);
1307
+ return { line: line, ch: ch };
1308
+ }
1309
+ // Merge arguments in place, for overriding arguments.
1310
+ function mergeArgs(to, from) {
1311
+ for (var prop in from) {
1312
+ if (from.hasOwnProperty(prop)) {
1313
+ to[prop] = from[prop];
509
1314
  }
510
- });
511
- enterInsertMode(cm);
512
- },
513
- nofallthrough: true, style: "fat-cursor"
514
- };
1315
+ }
1316
+ }
1317
+ function copyArgs(args) {
1318
+ var ret = {};
1319
+ for (var prop in args) {
1320
+ if (args.hasOwnProperty(prop)) {
1321
+ ret[prop] = args[prop];
1322
+ }
1323
+ }
1324
+ return ret;
1325
+ }
1326
+ function offsetCursor(cur, offsetLine, offsetCh) {
1327
+ return { line: cur.line + offsetLine, ch: cur.ch + offsetCh };
1328
+ }
1329
+ function arrayEq(a1, a2) {
1330
+ if (a1.length != a2.length) {
1331
+ return false;
1332
+ }
1333
+ for (var i = 0; i < a1.length; i++) {
1334
+ if (a1[i] != a2[i]) {
1335
+ return false;
1336
+ }
1337
+ }
1338
+ return true;
1339
+ }
1340
+ function matchKeysPartial(pressed, mapped) {
1341
+ for (var i = 0; i < pressed.length; i++) {
1342
+ // 'character' means any character. For mark, register commads, etc.
1343
+ if (pressed[i] != mapped[i] && mapped[i] != 'character') {
1344
+ return false;
1345
+ }
1346
+ }
1347
+ return true;
1348
+ }
1349
+ function arrayIsSubsetFromBeginning(small, big) {
1350
+ for (var i = 0; i < small.length; i++) {
1351
+ if (small[i] != big[i]) {
1352
+ return false;
1353
+ }
1354
+ }
1355
+ return true;
1356
+ }
1357
+ function repeatFn(cm, fn, repeat) {
1358
+ return function() {
1359
+ for (var i = 0; i < repeat; i++) {
1360
+ fn(cm);
1361
+ }
1362
+ };
1363
+ }
1364
+ function copyCursor(cur) {
1365
+ return { line: cur.line, ch: cur.ch };
1366
+ }
1367
+ function cursorEqual(cur1, cur2) {
1368
+ return cur1.ch == cur2.ch && cur1.line == cur2.line;
1369
+ }
1370
+ function cursorIsBefore(cur1, cur2) {
1371
+ if (cur1.line < cur2.line) {
1372
+ return true;
1373
+ } else if (cur1.line == cur2.line && cur1.ch < cur2.ch) {
1374
+ return true;
1375
+ }
1376
+ return false;
1377
+ }
1378
+ function lineLength(cm, lineNum) {
1379
+ return cm.getLine(lineNum).length;
1380
+ }
1381
+ function reverse(s){
1382
+ return s.split("").reverse().join("");
1383
+ }
1384
+ function trim(s) {
1385
+ if (s.trim) {
1386
+ return s.trim();
1387
+ } else {
1388
+ return s.replace(/^\s+|\s+$/g, '');
1389
+ }
1390
+ }
1391
+ function escapeRegex(s) {
1392
+ return s.replace(/([.?*+$\[\]\/\\(){}|\-])/g, "\\$1");
1393
+ }
515
1394
 
516
- iterList(["vim-prefix-d", "vim-prefix-c", "vim-prefix-"], function (prefix) {
517
- iterList(["f", "F", "T", "t"],
518
- function (ch) {
519
- CodeMirror.keyMap[prefix][toCombo(ch)] = function (cm) {
520
- cm.setOption("keyMap", prefix + ch);
521
- emptyBuffer();
522
- };
523
- });
524
- });
1395
+ function exitVisualMode(cm, vim) {
1396
+ vim.visualMode = false;
1397
+ vim.visualLine = false;
1398
+ var selectionStart = cm.getCursor('anchor');
1399
+ var selectionEnd = cm.getCursor('head');
1400
+ if (!cursorEqual(selectionStart, selectionEnd)) {
1401
+ // Clear the selection and set the cursor only if the selection has not
1402
+ // already been cleared. Otherwise we risk moving the cursor somewhere
1403
+ // it's not supposed to be.
1404
+ cm.setCursor(clipCursorToContent(cm, selectionEnd));
1405
+ }
1406
+ }
525
1407
 
526
- var MOTION_OPTIONS = {
527
- "t": {inclusive: false, forward: true},
528
- "f": {inclusive: true, forward: true},
529
- "T": {inclusive: false, forward: false},
530
- "F": {inclusive: true, forward: false}
531
- };
1408
+ // Remove any trailing newlines from the selection. For
1409
+ // example, with the caret at the start of the last word on the line,
1410
+ // 'dw' should word, but not the newline, while 'w' should advance the
1411
+ // caret to the first character of the next line.
1412
+ function clipToLine(cm, curStart, curEnd) {
1413
+ var selection = cm.getRange(curStart, curEnd);
1414
+ var lines = selection.split('\n');
1415
+ if (lines.length > 1 && isWhiteSpaceString(lines.pop())) {
1416
+ curEnd.line--;
1417
+ curEnd.ch = lineLength(cm, curEnd.line);
1418
+ }
1419
+ }
532
1420
 
533
- function setupPrefixBindingForKey(m) {
534
- CodeMirror.keyMap["vim-prefix-m"][m] = function(cm) {
535
- mark[m] = cm.getCursor().line;
536
- };
537
- CodeMirror.keyMap["vim-prefix-d'"][m] = function(cm) {
538
- delTillMark(cm, m);
539
- };
540
- CodeMirror.keyMap["vim-prefix-y'"][m] = function(cm) {
541
- yankTillMark(cm, m);
542
- };
543
- CodeMirror.keyMap["vim-prefix-r"][m] = function (cm) {
544
- var cur = cm.getCursor();
545
- cm.replaceRange(toLetter(m),
546
- {line: cur.line, ch: cur.ch},
547
- {line: cur.line, ch: cur.ch + 1});
548
- CodeMirror.commands.goColumnLeft(cm);
549
- };
550
- // all commands, related to motions till char in line
551
- iterObj(MOTION_OPTIONS, function (ch, options) {
552
- CodeMirror.keyMap["vim-prefix-" + ch][m] = function(cm) {
553
- moveTillChar(cm, m, options);
554
- };
555
- CodeMirror.keyMap["vim-prefix-d" + ch][m] = function(cm) {
556
- delTillChar(cm, m, options);
557
- };
558
- CodeMirror.keyMap["vim-prefix-c" + ch][m] = function(cm) {
559
- delTillChar(cm, m, options);
560
- enterInsertMode(cm);
561
- };
562
- });
563
- }
564
- for (var i = 65; i < 65 + 26; i++) { // uppercase alphabet char codes
565
- var ch = String.fromCharCode(i);
566
- setupPrefixBindingForKey(toCombo(ch));
567
- setupPrefixBindingForKey(toCombo(ch.toLowerCase()));
568
- }
569
- for (var i = 0; i < SPECIAL_SYMBOLS.length; ++i) {
570
- setupPrefixBindingForKey(toCombo(SPECIAL_SYMBOLS.charAt(i)));
571
- }
572
- setupPrefixBindingForKey("Space");
573
-
574
- CodeMirror.keyMap["vim-prefix-y"] = {
575
- "Y": countTimes(function(cm, i, last) {
576
- pushInBuffer("\n" + cm.getLine(cm.getCursor().line + i));
577
- cm.setOption("keyMap", "vim");
578
- }),
579
- "'": function(cm) {cm.setOption("keyMap", "vim-prefix-y'"); emptyBuffer();},
580
- nofallthrough: true, style: "fat-cursor"
581
- };
1421
+ // Expand the selection to line ends.
1422
+ function expandSelectionToLine(cm, curStart, curEnd) {
1423
+ curStart.ch = 0;
1424
+ curEnd.ch = 0;
1425
+ curEnd.line++;
1426
+ }
582
1427
 
583
- CodeMirror.keyMap["vim-insert"] = {
584
- // TODO: override navigation keys so that Esc will cancel automatic indentation from o, O, i_<CR>
585
- "Esc": function(cm) {
586
- cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1, true);
587
- cm.setOption("keyMap", "vim");
588
- },
589
- "Ctrl-N": "autocomplete",
590
- "Ctrl-P": "autocomplete",
591
- fallthrough: ["default"]
592
- };
1428
+ function findFirstNonWhiteSpaceCharacter(text) {
1429
+ if (!text) {
1430
+ return 0;
1431
+ }
1432
+ var firstNonWS = text.search(/\S/);
1433
+ return firstNonWS == -1 ? text.length : firstNonWS;
1434
+ }
593
1435
 
594
- function findMatchedSymbol(cm, cur, symb) {
595
- var line = cur.line;
596
- var symb = symb ? symb : cm.getLine(line)[cur.ch];
1436
+ function expandWordUnderCursor(cm, inclusive, forward, bigWord, noSymbol) {
1437
+ var cur = cm.getCursor();
1438
+ var line = cm.getLine(cur.line);
1439
+ var idx = cur.ch;
1440
+
1441
+ // Seek to first word or non-whitespace character, depending on if
1442
+ // noSymbol is true.
1443
+ var textAfterIdx = line.substring(idx);
1444
+ var firstMatchedChar;
1445
+ if (noSymbol) {
1446
+ firstMatchedChar = textAfterIdx.search(/\w/);
1447
+ } else {
1448
+ firstMatchedChar = textAfterIdx.search(/\S/);
1449
+ }
1450
+ if (firstMatchedChar == -1) {
1451
+ return null;
1452
+ }
1453
+ idx += firstMatchedChar;
1454
+ textAfterIdx = line.substring(idx);
1455
+ var textBeforeIdx = line.substring(0, idx);
1456
+
1457
+ var matchRegex;
1458
+ // Greedy matchers for the "word" we are trying to expand.
1459
+ if (bigWord) {
1460
+ matchRegex = /^\S+/;
1461
+ } else {
1462
+ if ((/\w/).test(line.charAt(idx))) {
1463
+ matchRegex = /^\w+/;
1464
+ } else {
1465
+ matchRegex = /^[^\w\s]+/;
1466
+ }
1467
+ }
597
1468
 
598
- // Are we at the opening or closing char
599
- var forwards = ['(', '[', '{'].indexOf(symb) != -1;
1469
+ var wordAfterRegex = matchRegex.exec(textAfterIdx);
1470
+ var wordStart = idx;
1471
+ var wordEnd = idx + wordAfterRegex[0].length - 1;
1472
+ // TODO: Find a better way to do this. It will be slow on very long lines.
1473
+ var wordBeforeRegex = matchRegex.exec(reverse(textBeforeIdx));
1474
+ if (wordBeforeRegex) {
1475
+ wordStart -= wordBeforeRegex[0].length;
1476
+ }
600
1477
 
601
- var reverseSymb = (function(sym) {
602
- switch (sym) {
603
- case '(' : return ')';
604
- case '[' : return ']';
605
- case '{' : return '}';
606
- case ')' : return '(';
607
- case ']' : return '[';
608
- case '}' : return '{';
609
- default : return null;
1478
+ if (inclusive) {
1479
+ wordEnd++;
610
1480
  }
611
- })(symb);
612
1481
 
613
- // Couldn't find a matching symbol, abort
614
- if (reverseSymb == null) return cur;
1482
+ return { start: { line: cur.line, ch: wordStart },
1483
+ end: { line: cur.line, ch: wordEnd }};
1484
+ }
615
1485
 
616
- // Tracking our imbalance in open/closing symbols. An opening symbol wii be
617
- // the first thing we pick up if moving forward, this isn't true moving backwards
618
- var disBal = forwards ? 0 : 1;
1486
+ /*
1487
+ * Returns the boundaries of the next word. If the cursor in the middle of
1488
+ * the word, then returns the boundaries of the current word, starting at
1489
+ * the cursor. If the cursor is at the start/end of a word, and we are going
1490
+ * forward/backward, respectively, find the boundaries of the next word.
1491
+ *
1492
+ * @param {CodeMirror} cm CodeMirror object.
1493
+ * @param {Cursor} cur The cursor position.
1494
+ * @param {boolean} forward True to search forward. False to search
1495
+ * backward.
1496
+ * @param {boolean} bigWord True if punctuation count as part of the word.
1497
+ * False if only [a-zA-Z0-9] characters count as part of the word.
1498
+ * @return {Object{from:number, to:number, line: number}} The boundaries of
1499
+ * the word, or null if there are no more words.
1500
+ */
1501
+ // TODO: Treat empty lines (with no whitespace) as words.
1502
+ function findWord(cm, cur, forward, bigWord) {
1503
+ var lineNum = cur.line;
1504
+ var pos = cur.ch;
1505
+ var line = cm.getLine(lineNum);
1506
+ var dir = forward ? 1 : -1;
1507
+ var regexps = bigWord ? bigWordRegexp : wordRegexp;
619
1508
 
620
- while (true) {
621
- if (line == cur.line) {
622
- // First pass, do some special stuff
623
- var currLine = forwards ? cm.getLine(line).substr(cur.ch).split('') : cm.getLine(line).substr(0,cur.ch).split('').reverse();
624
- } else {
625
- var currLine = forwards ? cm.getLine(line).split('') : cm.getLine(line).split('').reverse();
1509
+ while (true) {
1510
+ var stop = (dir > 0) ? line.length : -1;
1511
+ var wordStart = stop, wordEnd = stop;
1512
+ // Find bounds of next word.
1513
+ while (pos != stop) {
1514
+ var foundWord = false;
1515
+ for (var i = 0; i < regexps.length && !foundWord; ++i) {
1516
+ if (regexps[i].test(line.charAt(pos))) {
1517
+ wordStart = pos;
1518
+ // Advance to end of word.
1519
+ while (pos != stop && regexps[i].test(line.charAt(pos))) {
1520
+ pos += dir;
1521
+ }
1522
+ wordEnd = pos;
1523
+ foundWord = wordStart != wordEnd;
1524
+ if (wordStart == cur.ch && lineNum == cur.line &&
1525
+ wordEnd == wordStart + dir) {
1526
+ // We started at the end of a word. Find the next one.
1527
+ continue;
1528
+ } else {
1529
+ return {
1530
+ from: Math.min(wordStart, wordEnd + 1),
1531
+ to: Math.max(wordStart, wordEnd),
1532
+ line: lineNum };
1533
+ }
1534
+ }
1535
+ }
1536
+ if (!foundWord) {
1537
+ pos += dir;
1538
+ }
1539
+ }
1540
+ // Advance to next/prev line.
1541
+ lineNum += dir;
1542
+ if (!isLine(cm, lineNum)) {
1543
+ return null;
1544
+ }
1545
+ line = cm.getLine(lineNum);
1546
+ pos = (dir > 0) ? 0 : line.length;
626
1547
  }
1548
+ // Should never get here.
1549
+ throw 'The impossible happened.';
1550
+ }
627
1551
 
628
- for (var index = 0; index < currLine.length; index++) {
629
- if (currLine[index] == symb) disBal++;
630
- else if (currLine[index] == reverseSymb) disBal--;
1552
+ /**
1553
+ * @param {CodeMirror} cm CodeMirror object.
1554
+ * @param {int} repeat Number of words to move past.
1555
+ * @param {boolean} forward True to search forward. False to search
1556
+ * backward.
1557
+ * @param {boolean} wordEnd True to move to end of word. False to move to
1558
+ * beginning of word.
1559
+ * @param {boolean} bigWord True if punctuation count as part of the word.
1560
+ * False if only alphabet characters count as part of the word.
1561
+ * @return {Cursor} The position the cursor should move to.
1562
+ */
1563
+ function moveToWord(cm, repeat, forward, wordEnd, bigWord) {
1564
+ var cur = cm.getCursor();
1565
+ for (var i = 0; i < repeat; i++) {
1566
+ var startCh = cur.ch, startLine = cur.line, word;
1567
+ var movedToNextWord = false;
1568
+ while (!movedToNextWord) {
1569
+ // Search and advance.
1570
+ word = findWord(cm, cur, forward, bigWord);
1571
+ movedToNextWord = true;
1572
+ if (word) {
1573
+ // Move to the word we just found. If by moving to the word we end
1574
+ // up in the same spot, then move an extra character and search
1575
+ // again.
1576
+ cur.line = word.line;
1577
+ if (forward && wordEnd) {
1578
+ // 'e'
1579
+ cur.ch = word.to - 1;
1580
+ } else if (forward && !wordEnd) {
1581
+ // 'w'
1582
+ if (inRangeInclusive(cur.ch, word.from, word.to) &&
1583
+ word.line == startLine) {
1584
+ // Still on the same word. Go to the next one.
1585
+ movedToNextWord = false;
1586
+ cur.ch = word.to - 1;
1587
+ } else {
1588
+ cur.ch = word.from;
1589
+ }
1590
+ } else if (!forward && wordEnd) {
1591
+ // 'ge'
1592
+ if (inRangeInclusive(cur.ch, word.from, word.to) &&
1593
+ word.line == startLine) {
1594
+ // still on the same word. Go to the next one.
1595
+ movedToNextWord = false;
1596
+ cur.ch = word.from;
1597
+ } else {
1598
+ cur.ch = word.to;
1599
+ }
1600
+ } else if (!forward && !wordEnd) {
1601
+ // 'b'
1602
+ cur.ch = word.from;
1603
+ }
1604
+ } else {
1605
+ // No more words to be found. Move to the end.
1606
+ if (forward) {
1607
+ return { line: cur.line, ch: lineLength(cm, cur.line) };
1608
+ } else {
1609
+ return { line: cur.line, ch: 0 };
1610
+ }
1611
+ }
1612
+ }
1613
+ }
1614
+ return cur;
1615
+ }
631
1616
 
632
- if (disBal == 0) {
633
- if (forwards && cur.line == line) return {line: line, ch: index + cur.ch};
634
- else if (forwards) return {line: line, ch: index};
635
- else return {line: line, ch: currLine.length - index - 1 };
1617
+ function moveToCharacter(cm, repeat, forward, character) {
1618
+ var cur = cm.getCursor();
1619
+ var start = cur.ch;
1620
+ var idx;
1621
+ for (var i = 0; i < repeat; i ++) {
1622
+ var line = cm.getLine(cur.line);
1623
+ idx = charIdxInLine(start, line, character, forward, true);
1624
+ if (idx == -1) {
1625
+ return cur;
636
1626
  }
1627
+ start = idx;
637
1628
  }
1629
+ return { line: cm.getCursor().line, ch: idx };
1630
+ }
638
1631
 
639
- if (forwards) line++;
640
- else line--;
1632
+ function moveToColumn(cm, repeat) {
1633
+ // repeat is always >= 1, so repeat - 1 always corresponds
1634
+ // to the column we want to go to.
1635
+ var line = cm.getCursor().line;
1636
+ return clipCursorToContent(cm, { line: line, ch: repeat - 1 });
641
1637
  }
642
- }
643
1638
 
644
- function selectCompanionObject(cm, revSymb, inclusive) {
645
- var cur = cm.getCursor();
1639
+ function charIdxInLine(start, line, character, forward, includeChar) {
1640
+ // Search for char in line.
1641
+ // motion_options: {forward, includeChar}
1642
+ // If includeChar = true, include it too.
1643
+ // If forward = true, search forward, else search backwards.
1644
+ // If char is not found on this line, do nothing
1645
+ var idx;
1646
+ if (forward) {
1647
+ idx = line.indexOf(character, start + 1);
1648
+ if (idx != -1 && !includeChar) {
1649
+ idx -= 1;
1650
+ }
1651
+ } else {
1652
+ idx = line.lastIndexOf(character, start - 1);
1653
+ if (idx != -1 && !includeChar) {
1654
+ idx += 1;
1655
+ }
1656
+ }
1657
+ return idx;
1658
+ }
646
1659
 
647
- var end = findMatchedSymbol(cm, cur, revSymb);
648
- var start = findMatchedSymbol(cm, end);
649
- start.ch += inclusive ? 1 : 0;
650
- end.ch += inclusive ? 0 : 1;
1660
+ function findMatchedSymbol(cm, cur, symb) {
1661
+ var line = cur.line;
1662
+ symb = symb ? symb : cm.getLine(line).charAt(cur.ch);
651
1663
 
652
- return {start: start, end: end};
653
- }
1664
+ // Are we at the opening or closing char
1665
+ var forwards = inArray(symb, ['(', '[', '{']);
654
1666
 
655
- // takes in a symbol and a cursor and tries to simulate text objects that have
656
- // identical opening and closing symbols
657
- // TODO support across multiple lines
658
- function findBeginningAndEnd(cm, symb, inclusive) {
659
- var cur = cm.getCursor();
660
- var line = cm.getLine(cur.line);
661
- var chars = line.split('');
662
- var start = undefined;
663
- var end = undefined;
664
- var firstIndex = chars.indexOf(symb);
1667
+ var reverseSymb = ({
1668
+ '(': ')', ')': '(',
1669
+ '[': ']', ']': '[',
1670
+ '{': '}', '}': '{'})[symb];
665
1671
 
666
- // the decision tree is to always look backwards for the beginning first,
667
- // but if the cursor is in front of the first instance of the symb,
668
- // then move the cursor forward
669
- if (cur.ch < firstIndex) {
670
- cur.ch = firstIndex;
671
- cm.setCursor(cur.line, firstIndex+1);
672
- }
673
- // otherwise if the cursor is currently on the closing symbol
674
- else if (firstIndex < cur.ch && chars[cur.ch] == symb) {
675
- end = cur.ch; // assign end to the current cursor
676
- --cur.ch; // make sure to look backwards
677
- }
1672
+ // Couldn't find a matching symbol, abort
1673
+ if (!reverseSymb) {
1674
+ return cur;
1675
+ }
678
1676
 
679
- // if we're currently on the symbol, we've got a start
680
- if (chars[cur.ch] == symb && end == null)
681
- start = cur.ch + 1; // assign start to ahead of the cursor
682
- else {
683
- // go backwards to find the start
684
- for (var i = cur.ch; i > -1 && start == null; i--)
685
- if (chars[i] == symb) start = i + 1;
686
- }
1677
+ // set our increment to move forward (+1) or backwards (-1)
1678
+ // depending on which bracket we're matching
1679
+ var increment = ({'(': 1, '{': 1, '[': 1})[symb] || -1;
1680
+ var depth = 1, nextCh = symb, index = cur.ch, lineText = cm.getLine(line);
1681
+ // Simple search for closing paren--just count openings and closings till
1682
+ // we find our match
1683
+ // TODO: use info from CodeMirror to ignore closing brackets in comments
1684
+ // and quotes, etc.
1685
+ while (nextCh && depth > 0) {
1686
+ index += increment;
1687
+ nextCh = lineText.charAt(index);
1688
+ if (!nextCh) {
1689
+ line += increment;
1690
+ index = 0;
1691
+ lineText = cm.getLine(line) || '';
1692
+ nextCh = lineText.charAt(index);
1693
+ }
1694
+ if (nextCh === symb) {
1695
+ depth++;
1696
+ } else if (nextCh === reverseSymb) {
1697
+ depth--;
1698
+ }
1699
+ }
687
1700
 
688
- // look forwards for the end symbol
689
- if (start != null && end == null) {
690
- for (var i = start, len = chars.length; i < len && end == null; i++) {
691
- if (chars[i] == symb) end = i;
1701
+ if (nextCh) {
1702
+ return { line: line, ch: index };
692
1703
  }
1704
+ return cur;
693
1705
  }
694
1706
 
695
- // nothing found
696
- // FIXME still enters insert mode
697
- if (start == null || end == null) return {
698
- start: cur, end: cur
699
- };
1707
+ function selectCompanionObject(cm, revSymb, inclusive) {
1708
+ var cur = cm.getCursor();
1709
+
1710
+ var end = findMatchedSymbol(cm, cur, revSymb);
1711
+ var start = findMatchedSymbol(cm, end);
1712
+ start.ch += inclusive ? 1 : 0;
1713
+ end.ch += inclusive ? 0 : 1;
700
1714
 
701
- // include the symbols
702
- if (inclusive) {
703
- --start; ++end;
1715
+ return { start: start, end: end };
704
1716
  }
705
1717
 
706
- return {
707
- start: {line: cur.line, ch: start},
708
- end: {line: cur.line, ch: end}
709
- };
710
- }
711
-
712
- function offsetCursor(cm, line, ch) {
713
- var cur = cm.getCursor(); return {line: cur.line + line, ch: cur.ch + ch};
714
- }
715
-
716
- // These are the motion commands we use for navigation and selection with
717
- // certain other commands. All should return a cursor object.
718
- var motions = {
719
- "J": function(cm, times) { return offsetCursor(cm, times, 0); },
720
- "Down": function(cm, times) { return offsetCursor(cm, times, 0); },
721
- "K": function(cm, times) { return offsetCursor(cm, -times, 0); },
722
- "Up": function(cm, times) { return offsetCursor(cm, -times, 0); },
723
- "L": function(cm, times) { return offsetCursor(cm, 0, times); },
724
- "Right": function(cm, times) { return offsetCursor(cm, 0, times); },
725
- "Space": function(cm, times) { return offsetCursor(cm, 0, times); },
726
- "H": function(cm, times) { return offsetCursor(cm, 0, -times); },
727
- "Left": function(cm, times) { return offsetCursor(cm, 0, -times); },
728
- "Backspace": function(cm, times) { return offsetCursor(cm, 0, -times); },
729
- "B": function(cm, times, yank) { return moveToWord(cm, word, -1, times, 'start', yank); },
730
- "Shift-B": function(cm, times, yank) { return moveToWord(cm, bigWord, -1, times, 'start', yank); },
731
- "E": function(cm, times, yank) { return moveToWord(cm, word, 1, times, 'end', yank); },
732
- "Shift-E": function(cm, times, yank) { return moveToWord(cm, bigWord, 1, times, 'end', yank); },
733
- "W": function(cm, times, yank) { return moveToWord(cm, word, 1, times, 'start', yank); },
734
- "Shift-W": function(cm, times, yank) { return moveToWord(cm, bigWord, 1, times, 'start', yank); },
735
- "'^'": function(cm, times) {
736
- var cur = cm.getCursor(), line = cm.getLine(cur.line).split('');
737
- for (var i = 0; i < line.length; i++) {
738
- if (line[i].match(/[^\s]/)) return {line: cur.line, ch: index};
1718
+ function regexLastIndexOf(string, pattern, startIndex) {
1719
+ for (var i = !startIndex ? string.length : startIndex;
1720
+ i >= 0; --i) {
1721
+ if (pattern.test(string.charAt(i))) {
1722
+ return i;
1723
+ }
739
1724
  }
740
- return cur;
741
- },
742
- "'$'": function(cm) {
743
- var cur = cm.getCursor(), ch = cm.getLine(cur.line).length;
744
- return {line: cur.line, ch: ch};
745
- },
746
- "'%'": function(cm) { return findMatchedSymbol(cm, cm.getCursor()); },
747
- "Esc" : function(cm) { cm.setOption("keyMap", "vim"); repeatCount = 0; return cm.getCursor(); }
748
- };
1725
+ return -1;
1726
+ }
749
1727
 
750
- // Map our movement actions each operator and non-operational movement
751
- iterObj(motions, function(key, motion) {
752
- CodeMirror.keyMap['vim-prefix-d'][key] = function(cm) {
753
- // Get our selected range
754
- var start = cm.getCursor();
755
- var end = motion(cm, repeatCount ? repeatCount : 1, true);
1728
+ // Takes in a symbol and a cursor and tries to simulate text objects that
1729
+ // have identical opening and closing symbols
1730
+ // TODO support across multiple lines
1731
+ function findBeginningAndEnd(cm, symb, inclusive) {
1732
+ var cur = cm.getCursor();
1733
+ var line = cm.getLine(cur.line);
1734
+ var chars = line.split('');
1735
+ var start, end, i, len;
1736
+ var firstIndex = chars.indexOf(symb);
1737
+
1738
+ // the decision tree is to always look backwards for the beginning first,
1739
+ // but if the cursor is in front of the first instance of the symb,
1740
+ // then move the cursor forward
1741
+ if (cur.ch < firstIndex) {
1742
+ cur.ch = firstIndex;
1743
+ // Why is this line even here???
1744
+ // cm.setCursor(cur.line, firstIndex+1);
1745
+ }
1746
+ // otherwise if the cursor is currently on the closing symbol
1747
+ else if (firstIndex < cur.ch && chars[cur.ch] == symb) {
1748
+ end = cur.ch; // assign end to the current cursor
1749
+ --cur.ch; // make sure to look backwards
1750
+ }
756
1751
 
757
- // Set swap var if range is of negative length
758
- if ((start.line > end.line) || (start.line == end.line && start.ch > end.ch)) var swap = true;
1752
+ // if we're currently on the symbol, we've got a start
1753
+ if (chars[cur.ch] == symb && !end) {
1754
+ start = cur.ch + 1; // assign start to ahead of the cursor
1755
+ } else {
1756
+ // go backwards to find the start
1757
+ for (i = cur.ch; i > -1 && !start; i--) {
1758
+ if (chars[i] == symb) {
1759
+ start = i + 1;
1760
+ }
1761
+ }
1762
+ }
759
1763
 
760
- // Take action, switching start and end if swap var is set
761
- pushInBuffer(cm.getRange(swap ? end : start, swap ? start : end));
762
- cm.replaceRange("", swap ? end : start, swap ? start : end);
1764
+ // look forwards for the end symbol
1765
+ if (start && !end) {
1766
+ for (i = start, len = chars.length; i < len && !end; i++) {
1767
+ if (chars[i] == symb) {
1768
+ end = i;
1769
+ }
1770
+ }
1771
+ }
763
1772
 
764
- // And clean up
765
- repeatCount = 0;
766
- cm.setOption("keyMap", "vim");
767
- };
1773
+ // nothing found
1774
+ if (!start || !end) {
1775
+ return { start: cur, end: cur };
1776
+ }
768
1777
 
769
- CodeMirror.keyMap['vim-prefix-c'][key] = function(cm) {
770
- var start = cm.getCursor();
771
- var end = motion(cm, repeatCount ? repeatCount : 1, true);
1778
+ // include the symbols
1779
+ if (inclusive) {
1780
+ --start; ++end;
1781
+ }
772
1782
 
773
- if ((start.line > end.line) || (start.line == end.line && start.ch > end.ch)) var swap = true;
774
- pushInBuffer(cm.getRange(swap ? end : start, swap ? start : end));
775
- cm.replaceRange("", swap ? end : start, swap ? start : end);
1783
+ return {
1784
+ start: { line: cur.line, ch: start },
1785
+ end: { line: cur.line, ch: end }
1786
+ };
1787
+ }
776
1788
 
777
- repeatCount = 0;
778
- cm.setOption('keyMap', 'vim-insert');
1789
+ // Search functions
1790
+ function SearchState() {
1791
+ // Highlighted text that match the query.
1792
+ this.marked = null;
1793
+ }
1794
+ SearchState.prototype = {
1795
+ getQuery: function() {
1796
+ return getVimGlobalState().query;
1797
+ },
1798
+ setQuery: function(query) {
1799
+ getVimGlobalState().query = query;
1800
+ },
1801
+ getMarked: function() {
1802
+ return this.marked;
1803
+ },
1804
+ setMarked: function(marked) {
1805
+ this.marked = marked;
1806
+ },
1807
+ isReversed: function() {
1808
+ return getVimGlobalState().isReversed;
1809
+ },
1810
+ setReversed: function(reversed) {
1811
+ getVimGlobalState().isReversed = reversed;
1812
+ }
1813
+ };
1814
+ function getSearchState(cm) {
1815
+ var vim = getVimState(cm);
1816
+ return vim.searchState_ || (vim.searchState_ = new SearchState());
1817
+ }
1818
+ function dialog(cm, text, shortText, callback) {
1819
+ if (cm.openDialog) {
1820
+ cm.openDialog(text, callback, {bottom: true});
1821
+ }
1822
+ else {
1823
+ callback(prompt(shortText, ""));
1824
+ }
1825
+ }
1826
+ function findUnescapedSlashes(str) {
1827
+ var escapeNextChar = false;
1828
+ var slashes = [];
1829
+ for (var i = 0; i < str.length; i++) {
1830
+ var c = str.charAt(i);
1831
+ if (!escapeNextChar && c == '/') {
1832
+ slashes.push(i);
1833
+ }
1834
+ escapeNextChar = (c == '\\');
1835
+ }
1836
+ return slashes;
1837
+ }
1838
+ /**
1839
+ * Extract the regular expression from the query and return a Regexp object.
1840
+ * Returns null if the query is blank.
1841
+ * If ignoreCase is passed in, the Regexp object will have the 'i' flag set.
1842
+ * If smartCase is passed in, and the query contains upper case letters,
1843
+ * then ignoreCase is overridden, and the 'i' flag will not be set.
1844
+ * If the query contains the /i in the flag part of the regular expression,
1845
+ * then both ignoreCase and smartCase are ignored, and 'i' will be passed
1846
+ * through to the Regex object.
1847
+ */
1848
+ function parseQuery(cm, query, ignoreCase, smartCase) {
1849
+ // First try to extract regex + flags from the input. If no flags found,
1850
+ // extract just the regex. IE does not accept flags directly defined in
1851
+ // the regex string in the form /regex/flags
1852
+ var slashes = findUnescapedSlashes(query);
1853
+ var regexPart;
1854
+ var forceIgnoreCase;
1855
+ if (!slashes.length) {
1856
+ // Query looks like 'regexp'
1857
+ regexPart = query;
1858
+ } else {
1859
+ // Query looks like 'regexp/...'
1860
+ regexPart = query.substring(0, slashes[0]);
1861
+ var flagsPart = query.substring(slashes[0]);
1862
+ forceIgnoreCase = (flagsPart.indexOf('i') != -1);
1863
+ }
1864
+ if (!regexPart) {
1865
+ return null;
1866
+ }
1867
+ if (smartCase) {
1868
+ ignoreCase = (/^[^A-Z]*$/).test(regexPart);
1869
+ }
1870
+ try {
1871
+ var regexp = new RegExp(regexPart,
1872
+ (ignoreCase || forceIgnoreCase) ? 'i' : undefined);
1873
+ return regexp;
1874
+ } catch (e) {
1875
+ showConfirm(cm, 'Invalid regex: ' + regexPart);
1876
+ }
1877
+ }
1878
+ function showConfirm(cm, text) {
1879
+ if (cm.openConfirm) {
1880
+ cm.openConfirm('<span style="color: red">' + text +
1881
+ '</span> <button type="button">OK</button>', function() {},
1882
+ {bottom: true});
1883
+ } else {
1884
+ alert(text);
1885
+ }
1886
+ }
1887
+ function makePrompt(prefix, desc) {
1888
+ var raw = '';
1889
+ if (prefix) {
1890
+ raw += '<span style="font-family: monospace">' + prefix + '</span>';
1891
+ }
1892
+ raw += '<input type="text"/> ' +
1893
+ '<span style="color: #888">';
1894
+ if (desc) {
1895
+ raw += '<span style="color: #888">';
1896
+ raw += desc;
1897
+ raw += '</span>';
1898
+ }
1899
+ return raw;
1900
+ }
1901
+ var searchPromptDesc = '(Javascript regexp)';
1902
+ function showPrompt(cm, onPromptClose, prefix, desc) {
1903
+ var shortText = (prefix || '') + ' ' + (desc || '');
1904
+ dialog(cm, makePrompt(prefix, desc), shortText, onPromptClose);
1905
+ }
1906
+ function regexEqual(r1, r2) {
1907
+ if (r1 instanceof RegExp && r2 instanceof RegExp) {
1908
+ var props = ["global", "multiline", "ignoreCase", "source"];
1909
+ for (var i = 0; i < props.length; i++) {
1910
+ var prop = props[i];
1911
+ if (r1[prop] !== r2[prop]) {
1912
+ return(false);
1913
+ }
1914
+ }
1915
+ return(true);
1916
+ }
1917
+ return(false);
1918
+ }
1919
+ function updateSearchQuery(cm, rawQuery, ignoreCase, smartCase) {
1920
+ cm.operation(function() {
1921
+ var state = getSearchState(cm);
1922
+ if (!rawQuery) {
1923
+ return;
1924
+ }
1925
+ var query = parseQuery(cm, rawQuery, !!ignoreCase, !!smartCase);
1926
+ if (!query) {
1927
+ return;
1928
+ }
1929
+ if (regexEqual(query, state.getQuery())) {
1930
+ return;
1931
+ }
1932
+ clearSearchHighlight(cm);
1933
+ highlightSearchMatches(cm, query);
1934
+ state.setQuery(query);
1935
+ });
1936
+ }
1937
+ function highlightSearchMatches(cm, query) {
1938
+ // TODO: Highlight only text inside the viewport. Highlighting everything
1939
+ // is inefficient and expensive.
1940
+ if (cm.lineCount() < 2000) { // This is too expensive on big documents.
1941
+ var marked = [];
1942
+ for (var cursor = cm.getSearchCursor(query);
1943
+ cursor.findNext();) {
1944
+ marked.push(cm.markText(cursor.from(), cursor.to(),
1945
+ { className: 'CodeMirror-searching' }));
1946
+ }
1947
+ getSearchState(cm).setMarked(marked);
1948
+ }
1949
+ }
1950
+ function findNext(cm, prev, repeat) {
1951
+ return cm.operation(function() {
1952
+ var state = getSearchState(cm);
1953
+ var query = state.getQuery();
1954
+ if (!query) {
1955
+ return;
1956
+ }
1957
+ if (!state.getMarked()) {
1958
+ highlightSearchMatches(cm, query);
1959
+ }
1960
+ var pos = cm.getCursor();
1961
+ // If search is initiated with ? instead of /, negate direction.
1962
+ prev = (state.isReversed()) ? !prev : prev;
1963
+ if (!prev) {
1964
+ pos.ch += 1;
1965
+ }
1966
+ var cursor = cm.getSearchCursor(query, pos);
1967
+ for (var i = 0; i < repeat; i++) {
1968
+ if (!cursor.find(prev)) {
1969
+ // SearchCursor may have returned null because it hit EOF, wrap
1970
+ // around and try again.
1971
+ cursor = cm.getSearchCursor(query,
1972
+ (prev) ? { line: cm.lineCount() - 1} : {line: 0, ch: 0} );
1973
+ if (!cursor.find(prev)) {
1974
+ return;
1975
+ }
1976
+ }
1977
+ }
1978
+ return cursor.from();
1979
+ });}
1980
+ function clearSearchHighlight(cm) {
1981
+ cm.operation(function() {
1982
+ var state = getSearchState(cm);
1983
+ if (!state.getQuery()) {
1984
+ return;
1985
+ }
1986
+ var marked = state.getMarked();
1987
+ if (!marked) {
1988
+ return;
1989
+ }
1990
+ for (var i = 0; i < marked.length; ++i) {
1991
+ marked[i].clear();
1992
+ }
1993
+ state.setMarked(null);
1994
+ });}
1995
+
1996
+ // Ex command handling
1997
+ // Care must be taken when adding to the default Ex command map. For any
1998
+ // pair of commands that have a shared prefix, at least one of their
1999
+ // shortNames must not match the prefix of the other command.
2000
+ var defaultExCommandMap = [
2001
+ { name: 'map', type: 'builtIn' },
2002
+ { name: 'write', shortName: 'w', type: 'builtIn' },
2003
+ { name: 'undo', shortName: 'u', type: 'builtIn' },
2004
+ { name: 'redo', shortName: 'red', type: 'builtIn' }
2005
+ ];
2006
+ var ExCommandDispatcher = function() {
2007
+ this.buildCommandMap_();
779
2008
  };
2009
+ ExCommandDispatcher.prototype = {
2010
+ processCommand: function(cm, input) {
2011
+ var params = this.parseInput_(input);
2012
+ var commandName;
2013
+ if (!params.commandName) {
2014
+ // If only a line range is defined, move to the line.
2015
+ if (params.line !== undefined) {
2016
+ commandName = 'move';
2017
+ }
2018
+ } else {
2019
+ var command = this.matchCommand_(params.commandName);
2020
+ if (command) {
2021
+ commandName = command.name;
2022
+ if (command.type == 'exToKey') {
2023
+ // Handle Ex to Key mapping.
2024
+ for (var i = 0; i < command.toKeys.length; i++) {
2025
+ vim.handleKey(cm, command.toKeys[i]);
2026
+ }
2027
+ return;
2028
+ } else if (command.type == 'exToEx') {
2029
+ // Handle Ex to Ex mapping.
2030
+ this.processCommand(cm, command.toInput);
2031
+ return;
2032
+ }
2033
+ }
2034
+ }
2035
+ if (!commandName) {
2036
+ showConfirm(cm, 'Not an editor command ":' + input + '"');
2037
+ return;
2038
+ }
2039
+ exCommands[commandName](cm, params);
2040
+ },
2041
+ parseInput_: function(input) {
2042
+ var result = {};
2043
+ result.input = input;
2044
+ var idx = 0;
2045
+ // Trim preceding ':'.
2046
+ var colons = (/^:+/).exec(input);
2047
+ if (colons) {
2048
+ idx += colons[0].length;
2049
+ }
780
2050
 
781
- CodeMirror.keyMap['vim-prefix-y'][key] = function(cm) {
782
- var start = cm.getCursor();
783
- var end = motion(cm, repeatCount ? repeatCount : 1, true);
2051
+ // Parse range.
2052
+ var numberMatch = (/^(\d+)/).exec(input.substring(idx));
2053
+ if (numberMatch) {
2054
+ result.line = parseInt(numberMatch[1], 10);
2055
+ idx += numberMatch[0].length;
2056
+ }
784
2057
 
785
- if ((start.line > end.line) || (start.line == end.line && start.ch > end.ch)) var swap = true;
786
- pushInBuffer(cm.getRange(swap ? end : start, swap ? start : end));
2058
+ // Parse command name.
2059
+ var commandMatch = (/^(\w+)/).exec(input.substring(idx));
2060
+ if (commandMatch) {
2061
+ result.commandName = commandMatch[1];
2062
+ idx += commandMatch[1].length;
2063
+ }
2064
+
2065
+ // Parse command-line arguments
2066
+ var args = trim(input.substring(idx)).split(/\s+/);
2067
+ if (args.length && args[0]) {
2068
+ result.commandArgs = args;
2069
+ }
787
2070
 
788
- repeatCount = 0;
789
- cm.setOption("keyMap", "vim");
2071
+ return result;
2072
+ },
2073
+ matchCommand_: function(commandName) {
2074
+ // Return the command in the command map that matches the shortest
2075
+ // prefix of the passed in command name. The match is guaranteed to be
2076
+ // unambiguous if the defaultExCommandMap's shortNames are set up
2077
+ // correctly. (see @code{defaultExCommandMap}).
2078
+ for (var i = commandName.length; i > 0; i--) {
2079
+ var prefix = commandName.substring(0, i);
2080
+ if (this.commandMap_[prefix]) {
2081
+ var command = this.commandMap_[prefix];
2082
+ if (command.name.indexOf(commandName) === 0) {
2083
+ return command;
2084
+ }
2085
+ }
2086
+ }
2087
+ return null;
2088
+ },
2089
+ buildCommandMap_: function() {
2090
+ this.commandMap_ = {};
2091
+ for (var i = 0; i < defaultExCommandMap.length; i++) {
2092
+ var command = defaultExCommandMap[i];
2093
+ var key = command.shortName || command.name;
2094
+ this.commandMap_[key] = command;
2095
+ }
2096
+ },
2097
+ map: function(lhs, rhs) {
2098
+ if (lhs.charAt(0) == ':') {
2099
+ var commandName = lhs.substring(1);
2100
+ if (rhs.charAt(0) == ':') {
2101
+ // Ex to Ex mapping
2102
+ this.commandMap_[commandName] = {
2103
+ name: commandName,
2104
+ type: 'exToEx',
2105
+ toInput: rhs.substring(1)
2106
+ };
2107
+ } else {
2108
+ // Ex to key mapping
2109
+ this.commandMap_[commandName] = {
2110
+ name: commandName,
2111
+ type: 'exToKey',
2112
+ toKeys: parseKeyString(rhs)
2113
+ };
2114
+ }
2115
+ } else {
2116
+ if (rhs.charAt(0) == ':') {
2117
+ // Key to Ex mapping.
2118
+ defaultKeymap.unshift({
2119
+ keys: parseKeyString(lhs),
2120
+ type: 'keyToEx',
2121
+ exArgs: { input: rhs.substring(1) }});
2122
+ } else {
2123
+ // Key to key mapping
2124
+ defaultKeymap.unshift({
2125
+ keys: parseKeyString(lhs),
2126
+ type: 'keyToKey',
2127
+ toKeys: parseKeyString(rhs)
2128
+ });
2129
+ }
2130
+ }
2131
+ }
790
2132
  };
791
2133
 
792
- CodeMirror.keyMap['vim'][key] = function(cm) {
793
- var cur = motion(cm, repeatCount ? repeatCount : 1);
794
- cm.setCursor(cur.line, cur.ch);
2134
+ // Converts a key string sequence of the form a<C-w>bd<Left> into Vim's
2135
+ // keymap representation.
2136
+ function parseKeyString(str) {
2137
+ var idx = 0;
2138
+ var keys = [];
2139
+ while (idx < str.length) {
2140
+ if (str.charAt(idx) != '<') {
2141
+ keys.push(str.charAt(idx));
2142
+ idx++;
2143
+ continue;
2144
+ }
2145
+ // Vim key notation here means desktop Vim key-notation.
2146
+ // See :help key-notation in desktop Vim.
2147
+ var vimKeyNotationStart = ++idx;
2148
+ while (str.charAt(idx++) != '>') {}
2149
+ var vimKeyNotation = str.substring(vimKeyNotationStart, idx - 1);
2150
+ var match = (/^C-(.+)$/).exec(vimKeyNotation);
2151
+ if (match) {
2152
+ var key;
2153
+ switch (match[1]) {
2154
+ case 'BS':
2155
+ key = 'Backspace';
2156
+ break;
2157
+ case 'CR':
2158
+ key = 'Enter';
2159
+ break;
2160
+ case 'Del':
2161
+ key = 'Delete';
2162
+ break;
2163
+ default:
2164
+ key = match[1];
2165
+ break;
2166
+ }
2167
+ keys.push('Ctrl-' + key);
2168
+ }
2169
+ }
2170
+ return keys;
2171
+ }
795
2172
 
796
- repeatCount = 0;
797
- };
798
- });
799
-
800
- function addCountBindings(keyMapName) {
801
- // Add bindings for number keys
802
- keyMap = CodeMirror.keyMap[keyMapName];
803
- keyMap["0"] = function(cm) {
804
- if (repeatCount > 0) {
805
- pushRepeatCountDigit(0)(cm);
806
- } else {
807
- CodeMirror.commands.goLineStart(cm);
2173
+ var exCommands = {
2174
+ map: function(cm, params) {
2175
+ var mapArgs = params.commandArgs;
2176
+ if (!mapArgs || mapArgs.length < 2) {
2177
+ if (cm) {
2178
+ showConfirm(cm, 'Invalid mapping: ' + params.input);
2179
+ }
2180
+ return;
2181
+ }
2182
+ exCommandDispatcher.map(mapArgs[0], mapArgs[1], cm);
2183
+ },
2184
+ move: function(cm, params) {
2185
+ commandDispatcher.processMotion(cm, getVimState(cm), {
2186
+ motion: 'moveToLineOrEdgeOfDocument',
2187
+ motionArgs: { forward: false, explicitRepeat: true,
2188
+ linewise: true, repeat: params.line }});
2189
+ },
2190
+ redo: CodeMirror.commands.redo,
2191
+ undo: CodeMirror.commands.undo,
2192
+ write: function(cm) {
2193
+ if (CodeMirror.commands.save) {
2194
+ // If a save command is defined, call it.
2195
+ CodeMirror.commands.save(cm);
2196
+ } else {
2197
+ // Saves to text area if no save command is defined.
2198
+ cm.save();
2199
+ }
808
2200
  }
809
2201
  };
810
- for (var i = 1; i < 10; ++i) {
811
- keyMap[i] = pushRepeatCountDigit(i);
812
- }
813
- }
814
- addCountBindings('vim');
815
- addCountBindings('vim-prefix-d');
816
- addCountBindings('vim-prefix-y');
817
- addCountBindings('vim-prefix-c');
818
-
819
- // Create our keymaps for each operator and make xa and xi where x is an operator
820
- // change to the corrosponding keymap
821
- var operators = ['d', 'y', 'c'];
822
- iterList(operators, function(key, index, array) {
823
- CodeMirror.keyMap['vim-prefix-'+key+'a'] = {
824
- auto: 'vim', nofallthrough: true, style: "fat-cursor"
825
- };
826
- CodeMirror.keyMap['vim-prefix-'+key+'i'] = {
827
- auto: 'vim', nofallthrough: true, style: "fat-cursor"
828
- };
829
2202
 
830
- CodeMirror.keyMap['vim-prefix-'+key]['A'] = function(cm) {
831
- repeatCount = 0;
832
- cm.setOption('keyMap', 'vim-prefix-' + key + 'a');
833
- };
2203
+ var exCommandDispatcher = new ExCommandDispatcher();
2204
+
2205
+ // Register Vim with CodeMirror
2206
+ function buildVimKeyMap() {
2207
+ /**
2208
+ * Handle the raw key event from CodeMirror. Translate the
2209
+ * Shift + key modifier to the resulting letter, while preserving other
2210
+ * modifers.
2211
+ */
2212
+ // TODO: Figure out a way to catch capslock.
2213
+ function handleKeyEvent_(cm, key, modifier) {
2214
+ if (isUpperCase(key)) {
2215
+ // Convert to lower case if shift is not the modifier since the key
2216
+ // we get from CodeMirror is always upper case.
2217
+ if (modifier == 'Shift') {
2218
+ modifier = null;
2219
+ }
2220
+ else {
2221
+ key = key.toLowerCase();
2222
+ }
2223
+ }
2224
+ if (modifier) {
2225
+ // Vim will parse modifier+key combination as a single key.
2226
+ key = modifier + '-' + key;
2227
+ }
2228
+ vim.handleKey(cm, key);
2229
+ }
834
2230
 
835
- CodeMirror.keyMap['vim-prefix-'+key]['I'] = function(cm) {
836
- repeatCount = 0;
837
- cm.setOption('keyMap', 'vim-prefix-' + key + 'i');
2231
+ // Closure to bind CodeMirror, key, modifier.
2232
+ function keyMapper(key, modifier) {
2233
+ return function(cm) {
2234
+ handleKeyEvent_(cm, key, modifier);
2235
+ };
2236
+ }
2237
+
2238
+ var modifiers = ['Shift', 'Ctrl'];
2239
+ var keyMap = {
2240
+ 'nofallthrough': true,
2241
+ 'style': 'fat-cursor'
2242
+ };
2243
+ function bindKeys(keys, modifier) {
2244
+ for (var i = 0; i < keys.length; i++) {
2245
+ var key = keys[i];
2246
+ if (!modifier && inArray(key, specialSymbols)) {
2247
+ // Wrap special symbols with '' because that's how CodeMirror binds
2248
+ // them.
2249
+ key = "'" + key + "'";
2250
+ }
2251
+ if (modifier) {
2252
+ keyMap[modifier + '-' + key] = keyMapper(keys[i], modifier);
2253
+ } else {
2254
+ keyMap[key] = keyMapper(keys[i]);
2255
+ }
2256
+ }
2257
+ }
2258
+ bindKeys(upperCaseAlphabet);
2259
+ bindKeys(upperCaseAlphabet, 'Shift');
2260
+ bindKeys(upperCaseAlphabet, 'Ctrl');
2261
+ bindKeys(specialSymbols);
2262
+ bindKeys(specialSymbols, 'Ctrl');
2263
+ bindKeys(numbers);
2264
+ bindKeys(numbers, 'Ctrl');
2265
+ bindKeys(specialKeys);
2266
+ bindKeys(specialKeys, 'Ctrl');
2267
+ return keyMap;
2268
+ }
2269
+ CodeMirror.keyMap.vim = buildVimKeyMap();
2270
+
2271
+ function exitInsertMode(cm) {
2272
+ cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1, true);
2273
+ cm.setOption('keyMap', 'vim');
2274
+ }
2275
+
2276
+ CodeMirror.keyMap['vim-insert'] = {
2277
+ // TODO: override navigation keys so that Esc will cancel automatic
2278
+ // indentation from o, O, i_<CR>
2279
+ 'Esc': exitInsertMode,
2280
+ 'Ctrl-[': exitInsertMode,
2281
+ 'Ctrl-C': exitInsertMode,
2282
+ 'Ctrl-N': 'autocomplete',
2283
+ 'Ctrl-P': 'autocomplete',
2284
+ 'Enter': function(cm) {
2285
+ var fn = CodeMirror.commands.newlineAndIndentContinueComment ||
2286
+ CodeMirror.commands.newlineAndIndent;
2287
+ fn(cm);
2288
+ },
2289
+ fallthrough: ['default']
838
2290
  };
839
- });
840
-
841
- function regexLastIndexOf(string, pattern, startIndex) {
842
- for (var i = startIndex == null ? string.length : startIndex; i >= 0; --i)
843
- if (pattern.test(string.charAt(i))) return i;
844
- return -1;
845
- }
846
-
847
- // Create our text object functions. They work similar to motions but they
848
- // return a start cursor as well
849
- var textObjectList = ['W', 'Shift-[', 'Shift-9', '[', "'", "Shift-'"];
850
- var textObjects = {
851
- 'W': function(cm, inclusive) {
852
- var cur = cm.getCursor();
853
- var line = cm.getLine(cur.line);
854
2291
 
855
- var line_to_char = new String(line.substring(0, cur.ch));
856
- var start = regexLastIndexOf(line_to_char, /[^a-zA-Z0-9]/) + 1;
857
- var end = motions["E"](cm, 1) ;
858
-
859
- end.ch += inclusive ? 1 : 0 ;
860
- return {start: {line: cur.line, ch: start}, end: end };
861
- },
862
- 'Shift-[': function(cm, inclusive) { return selectCompanionObject(cm, '}', inclusive); },
863
- 'Shift-9': function(cm, inclusive) { return selectCompanionObject(cm, ')', inclusive); },
864
- '[': function(cm, inclusive) { return selectCompanionObject(cm, ']', inclusive); },
865
- "'": function(cm, inclusive) { return findBeginningAndEnd(cm, "'", inclusive); },
866
- "Shift-'": function(cm, inclusive) { return findBeginningAndEnd(cm, '"', inclusive); }
2292
+ return vimApi;
867
2293
  };
868
-
869
- // One function to handle all operation upon text objects. Kinda funky but it works
870
- // better than rewriting this code six times
871
- function textObjectManipulation(cm, object, remove, insert, inclusive) {
872
- // Object is the text object, delete object if remove is true, enter insert
873
- // mode if insert is true, inclusive is the difference between a and i
874
- var tmp = textObjects[object](cm, inclusive);
875
- var start = tmp.start;
876
- var end = tmp.end;
877
-
878
- if ((start.line > end.line) || (start.line == end.line && start.ch > end.ch)) var swap = true ;
879
-
880
- pushInBuffer(cm.getRange(swap ? end : start, swap ? start : end));
881
- if (remove) cm.replaceRange("", swap ? end : start, swap ? start : end);
882
- if (insert) cm.setOption('keyMap', 'vim-insert');
883
- }
884
-
885
- // And finally build the keymaps up from the text objects
886
- for (var i = 0; i < textObjectList.length; ++i) {
887
- var object = textObjectList[i];
888
- (function(object) {
889
- CodeMirror.keyMap['vim-prefix-di'][object] = function(cm) { textObjectManipulation(cm, object, true, false, false); };
890
- CodeMirror.keyMap['vim-prefix-da'][object] = function(cm) { textObjectManipulation(cm, object, true, false, true); };
891
- CodeMirror.keyMap['vim-prefix-yi'][object] = function(cm) { textObjectManipulation(cm, object, false, false, false); };
892
- CodeMirror.keyMap['vim-prefix-ya'][object] = function(cm) { textObjectManipulation(cm, object, false, false, true); };
893
- CodeMirror.keyMap['vim-prefix-ci'][object] = function(cm) { textObjectManipulation(cm, object, true, true, false); };
894
- CodeMirror.keyMap['vim-prefix-ca'][object] = function(cm) { textObjectManipulation(cm, object, true, true, true); };
895
- })(object)
896
- }
897
- })();
2294
+ // Initialize Vim and make it available as an API.
2295
+ var vim = Vim();
2296
+ CodeMirror.Vim = vim;
2297
+ }
2298
+ )();