codemirror-rails 2.36 → 3.00

Sign up to get free protection for your applications and to get access to all the features.
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
+ )();