code_sync 0.6.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (213) hide show
  1. data/.rvmrc +1 -0
  2. data/CNAME +1 -0
  3. data/Gemfile +24 -0
  4. data/Gemfile.lock +117 -0
  5. data/LICENSE.md +22 -0
  6. data/ROADMAP.md +20 -0
  7. data/TODO.md +14 -0
  8. data/bin/codesync +11 -0
  9. data/code_sync.gemspec +30 -0
  10. data/config/routes.rb +26 -0
  11. data/lib/assets/javascripts/canvas.coffee +12 -0
  12. data/lib/assets/javascripts/canvas/editors.coffee +48 -0
  13. data/lib/assets/javascripts/canvas/index.coffee +15 -0
  14. data/lib/assets/javascripts/canvas/layer_controller.coffee +47 -0
  15. data/lib/assets/javascripts/code_sync.coffee +3 -0
  16. data/lib/assets/javascripts/code_sync/backends/gist.coffee +78 -0
  17. data/lib/assets/javascripts/code_sync/client/index.coffee +104 -0
  18. data/lib/assets/javascripts/code_sync/client/util.coffee +61 -0
  19. data/lib/assets/javascripts/code_sync/codemirror.coffee +12 -0
  20. data/lib/assets/javascripts/code_sync/config.js.coffee.erb +38 -0
  21. data/lib/assets/javascripts/code_sync/console/index.coffee +54 -0
  22. data/lib/assets/javascripts/code_sync/console/templates/console.jst.skim +10 -0
  23. data/lib/assets/javascripts/code_sync/dependencies.coffee +6 -0
  24. data/lib/assets/javascripts/code_sync/editor/advanced.coffee +0 -0
  25. data/lib/assets/javascripts/code_sync/editor/datasources/document.coffee +212 -0
  26. data/lib/assets/javascripts/code_sync/editor/datasources/gist_loader.coffee +7 -0
  27. data/lib/assets/javascripts/code_sync/editor/datasources/modes.coffee +117 -0
  28. data/lib/assets/javascripts/code_sync/editor/datasources/project_assets.coffee +14 -0
  29. data/lib/assets/javascripts/code_sync/editor/index.coffee +415 -0
  30. data/lib/assets/javascripts/code_sync/editor/plugins/asset_selector.coffee +106 -0
  31. data/lib/assets/javascripts/code_sync/editor/plugins/color_picker.coffee +83 -0
  32. data/lib/assets/javascripts/code_sync/editor/plugins/document_manager.coffee +55 -0
  33. data/lib/assets/javascripts/code_sync/editor/plugins/document_tabs.coffee +178 -0
  34. data/lib/assets/javascripts/code_sync/editor/plugins/element_sync.coffee +111 -0
  35. data/lib/assets/javascripts/code_sync/editor/plugins/keymap_selector.coffee +44 -0
  36. data/lib/assets/javascripts/code_sync/editor/plugins/mode_selector.coffee +53 -0
  37. data/lib/assets/javascripts/code_sync/editor/plugins/name_input.coffee +44 -0
  38. data/lib/assets/javascripts/code_sync/editor/plugins/preferences.coffee +71 -0
  39. data/lib/assets/javascripts/code_sync/editor/templates/asset_editor.jst.skim +2 -0
  40. data/lib/assets/javascripts/code_sync/editor/templates/asset_selector.jst.skim +5 -0
  41. data/lib/assets/javascripts/code_sync/editor/templates/document_manager_tab.jst.skim +15 -0
  42. data/lib/assets/javascripts/code_sync/editor/templates/element_sync.jst.skim +19 -0
  43. data/lib/assets/javascripts/code_sync/editor/templates/preferences_panel.jst.skim +38 -0
  44. data/lib/assets/javascripts/code_sync/editor/views/asset_selector.coffee +106 -0
  45. data/lib/assets/javascripts/code_sync/editor/views/color_picker.coffee +76 -0
  46. data/lib/assets/javascripts/code_sync/editor/views/document_manager.coffee +176 -0
  47. data/lib/assets/javascripts/code_sync/editor/views/keymap_selector.coffee +37 -0
  48. data/lib/assets/javascripts/code_sync/editor/views/mode_selector.coffee +47 -0
  49. data/lib/assets/javascripts/code_sync/editor/views/name_input.coffee +44 -0
  50. data/lib/assets/javascripts/code_sync/editor/views/preferences.coffee +71 -0
  51. data/lib/assets/javascripts/code_sync/index.coffee +4 -0
  52. data/lib/assets/javascripts/code_sync/reloader.coffee +2 -0
  53. data/lib/assets/javascripts/code_sync_basic.coffee +1 -0
  54. data/lib/assets/javascripts/demos.coffee +48 -0
  55. data/lib/assets/javascripts/demos/default-content.coffee +72 -0
  56. data/lib/assets/javascripts/demos/layout_selector.coffee +19 -0
  57. data/lib/assets/javascripts/demos/tour.coffee +70 -0
  58. data/lib/assets/javascripts/demos/tour.jst.skim +29 -0
  59. data/lib/assets/javascripts/marketing.coffee +0 -0
  60. data/lib/assets/javascripts/vendor/.DS_Store +0 -0
  61. data/lib/assets/javascripts/vendor/backbone-events.js +160 -0
  62. data/lib/assets/javascripts/vendor/backbone-min.js +4 -0
  63. data/lib/assets/javascripts/vendor/codemirror-coffeescript.js +346 -0
  64. data/lib/assets/javascripts/vendor/codemirror-css.js +570 -0
  65. data/lib/assets/javascripts/vendor/codemirror-haml.js +153 -0
  66. data/lib/assets/javascripts/vendor/codemirror-htmlmixed.js +104 -0
  67. data/lib/assets/javascripts/vendor/codemirror-javascript.js +468 -0
  68. data/lib/assets/javascripts/vendor/codemirror-markdown.js +526 -0
  69. data/lib/assets/javascripts/vendor/codemirror-ruby.js +194 -0
  70. data/lib/assets/javascripts/vendor/codemirror-sass.js +330 -0
  71. data/lib/assets/javascripts/vendor/codemirror-skim.js +330 -0
  72. data/lib/assets/javascripts/vendor/codemirror-vim.js +3159 -0
  73. data/lib/assets/javascripts/vendor/codemirror-xml.js +328 -0
  74. data/lib/assets/javascripts/vendor/console.js +339 -0
  75. data/lib/assets/javascripts/vendor/gisted.js +27 -0
  76. data/lib/assets/javascripts/vendor/jquery-ui-resize-drag.min.js +6 -0
  77. data/lib/assets/javascripts/vendor/jquery.js +5 -0
  78. data/lib/assets/javascripts/vendor/keylauncher.js +4 -0
  79. data/lib/assets/javascripts/vendor/keymaster.min.js +4 -0
  80. data/lib/assets/javascripts/vendor/spectrum.js +1868 -0
  81. data/lib/assets/javascripts/vendor/underscore-min.js +1 -0
  82. data/lib/assets/javascripts/vendor/underscore.string.min.js +1 -0
  83. data/lib/assets/javascripts/vendor/vendored_codemirror.js +5558 -0
  84. data/lib/assets/javascripts/vendor/zepto.js +2 -0
  85. data/lib/assets/stylesheets/canvas.css.scss +101 -0
  86. data/lib/assets/stylesheets/code_sync.css.scss +4 -0
  87. data/lib/assets/stylesheets/code_sync/codemirror.css +7 -0
  88. data/lib/assets/stylesheets/code_sync/console.css +86 -0
  89. data/lib/assets/stylesheets/code_sync/editor/asset-name-input.css.scss +12 -0
  90. data/lib/assets/stylesheets/code_sync/editor/asset-selector.css.scss +58 -0
  91. data/lib/assets/stylesheets/code_sync/editor/codesync-color-picker.css.sass +5 -0
  92. data/lib/assets/stylesheets/code_sync/editor/document-tabs.css.scss +61 -0
  93. data/lib/assets/stylesheets/code_sync/editor/element-sync.css.scss +72 -0
  94. data/lib/assets/stylesheets/code_sync/editor/mode-selector.css.scss +0 -0
  95. data/lib/assets/stylesheets/code_sync/editor/preferences-panel.css.scss +26 -0
  96. data/lib/assets/stylesheets/code_sync/index.css.scss +141 -0
  97. data/lib/assets/stylesheets/demos.css.scss +96 -0
  98. data/lib/assets/stylesheets/marketing.css.sass +46 -0
  99. data/lib/assets/stylesheets/marketing/syntax.css.scss +1 -0
  100. data/lib/assets/stylesheets/vendor/animate.css +1 -0
  101. data/lib/assets/stylesheets/vendor/codemirror-ambiance.css +75 -0
  102. data/lib/assets/stylesheets/vendor/codemirror-lesserdark.css +44 -0
  103. data/lib/assets/stylesheets/vendor/codemirror-monokai.css +28 -0
  104. data/lib/assets/stylesheets/vendor/codemirror-xq-light.css +43 -0
  105. data/lib/assets/stylesheets/vendor/grid-layout.css +1406 -0
  106. data/lib/assets/stylesheets/vendor/spectrum.css +481 -0
  107. data/lib/assets/stylesheets/vendor/vendored_codemirror.css +246 -0
  108. data/lib/code_sync.rb +41 -0
  109. data/lib/code_sync/cli.rb +73 -0
  110. data/lib/code_sync/manager.rb +238 -0
  111. data/lib/code_sync/processors.rb +18 -0
  112. data/lib/code_sync/processors/basic.rb +9 -0
  113. data/lib/code_sync/processors/jst_processor.rb +17 -0
  114. data/lib/code_sync/pry_console.rb +132 -0
  115. data/lib/code_sync/rails.rb +7 -0
  116. data/lib/code_sync/rails/engine.rb +12 -0
  117. data/lib/code_sync/server.rb +225 -0
  118. data/lib/code_sync/sprockets_adapter.rb +145 -0
  119. data/lib/code_sync/temp_asset.rb +20 -0
  120. data/lib/code_sync/version.rb +3 -0
  121. data/lib/middleman_extension.rb +43 -0
  122. data/readme.md +26 -0
  123. data/site/.gitignore +14 -0
  124. data/site/Gemfile +13 -0
  125. data/site/Gemfile.lock +183 -0
  126. data/site/config.rb +26 -0
  127. data/site/source/canvas.html.slim +21 -0
  128. data/site/source/codepen-style-demo.html.slim +21 -0
  129. data/site/source/demo.html.slim +30 -0
  130. data/site/source/index.html.slim +128 -0
  131. data/site/source/layouts/layout.slim +18 -0
  132. data/site/source/samples/_client.html.md +13 -0
  133. data/site/source/samples/_editor.html.md +19 -0
  134. data/site/source/samples/_hooks.html.md +8 -0
  135. data/site/source/samples/_middleman.html.md +7 -0
  136. data/site/source/samples/_rails.html.md +8 -0
  137. data/site/source/samples/_standalone.html.md +36 -0
  138. data/spec/lib/code_sync/sprockets_adapter_spec.rb +44 -0
  139. data/spec/spec_helper.rb +21 -0
  140. data/spec/support/.DS_Store +0 -0
  141. data/spec/support/dummy_middleman/.gitignore +14 -0
  142. data/spec/support/dummy_middleman/Gemfile +5 -0
  143. data/spec/support/dummy_middleman/Gemfile.lock +100 -0
  144. data/spec/support/dummy_middleman/config.rb +77 -0
  145. data/spec/support/dummy_middleman/source/images/background.png +0 -0
  146. data/spec/support/dummy_middleman/source/images/middleman.png +0 -0
  147. data/spec/support/dummy_middleman/source/index.html.erb +10 -0
  148. data/spec/support/dummy_middleman/source/javascripts/all.js +1 -0
  149. data/spec/support/dummy_middleman/source/layouts/layout.erb +19 -0
  150. data/spec/support/dummy_middleman/source/stylesheets/all.css +55 -0
  151. data/spec/support/dummy_middleman/source/stylesheets/normalize.css +375 -0
  152. data/spec/support/dummy_rails/.gitignore +15 -0
  153. data/spec/support/dummy_rails/Gemfile +38 -0
  154. data/spec/support/dummy_rails/Gemfile.lock +112 -0
  155. data/spec/support/dummy_rails/README.rdoc +261 -0
  156. data/spec/support/dummy_rails/Rakefile +7 -0
  157. data/spec/support/dummy_rails/app/assets/images/rails.png +0 -0
  158. data/spec/support/dummy_rails/app/assets/javascripts/application.js +15 -0
  159. data/spec/support/dummy_rails/app/assets/stylesheets/application.css +13 -0
  160. data/spec/support/dummy_rails/app/controllers/application_controller.rb +3 -0
  161. data/spec/support/dummy_rails/app/helpers/application_helper.rb +2 -0
  162. data/spec/support/dummy_rails/app/mailers/.gitkeep +0 -0
  163. data/spec/support/dummy_rails/app/models/.gitkeep +0 -0
  164. data/spec/support/dummy_rails/app/views/layouts/application.html.erb +14 -0
  165. data/spec/support/dummy_rails/config.ru +4 -0
  166. data/spec/support/dummy_rails/config/application.rb +62 -0
  167. data/spec/support/dummy_rails/config/boot.rb +6 -0
  168. data/spec/support/dummy_rails/config/database.yml +25 -0
  169. data/spec/support/dummy_rails/config/environment.rb +5 -0
  170. data/spec/support/dummy_rails/config/environments/development.rb +37 -0
  171. data/spec/support/dummy_rails/config/environments/production.rb +67 -0
  172. data/spec/support/dummy_rails/config/environments/test.rb +37 -0
  173. data/spec/support/dummy_rails/config/initializers/backtrace_silencers.rb +7 -0
  174. data/spec/support/dummy_rails/config/initializers/inflections.rb +15 -0
  175. data/spec/support/dummy_rails/config/initializers/mime_types.rb +5 -0
  176. data/spec/support/dummy_rails/config/initializers/secret_token.rb +7 -0
  177. data/spec/support/dummy_rails/config/initializers/session_store.rb +8 -0
  178. data/spec/support/dummy_rails/config/initializers/wrap_parameters.rb +14 -0
  179. data/spec/support/dummy_rails/config/locales/en.yml +5 -0
  180. data/spec/support/dummy_rails/config/routes.rb +58 -0
  181. data/spec/support/dummy_rails/db/seeds.rb +7 -0
  182. data/spec/support/dummy_rails/doc/README_FOR_APP +2 -0
  183. data/spec/support/dummy_rails/lib/assets/.gitkeep +0 -0
  184. data/spec/support/dummy_rails/lib/tasks/.gitkeep +0 -0
  185. data/spec/support/dummy_rails/log/.gitkeep +0 -0
  186. data/spec/support/dummy_rails/public/404.html +26 -0
  187. data/spec/support/dummy_rails/public/422.html +26 -0
  188. data/spec/support/dummy_rails/public/500.html +25 -0
  189. data/spec/support/dummy_rails/public/favicon.ico +0 -0
  190. data/spec/support/dummy_rails/public/index.html +241 -0
  191. data/spec/support/dummy_rails/public/robots.txt +5 -0
  192. data/spec/support/dummy_rails/script/rails +6 -0
  193. data/spec/support/dummy_rails/test/fixtures/.gitkeep +0 -0
  194. data/spec/support/dummy_rails/test/functional/.gitkeep +0 -0
  195. data/spec/support/dummy_rails/test/integration/.gitkeep +0 -0
  196. data/spec/support/dummy_rails/test/performance/browsing_test.rb +12 -0
  197. data/spec/support/dummy_rails/test/test_helper.rb +13 -0
  198. data/spec/support/dummy_rails/test/unit/.gitkeep +0 -0
  199. data/spec/support/dummy_rails/vendor/assets/javascripts/.gitkeep +0 -0
  200. data/spec/support/dummy_rails/vendor/assets/stylesheets/.gitkeep +0 -0
  201. data/spec/support/dummy_rails/vendor/plugins/.gitkeep +0 -0
  202. data/spec/support/dummy_static/.DS_Store +0 -0
  203. data/spec/support/dummy_static/app/.DS_Store +0 -0
  204. data/spec/support/dummy_static/app/assets/.DS_Store +0 -0
  205. data/spec/support/dummy_static/app/assets/javascripts/manifest.coffee +4 -0
  206. data/spec/support/dummy_static/app/assets/javascripts/spec_application_javascript.coffee +4 -0
  207. data/spec/support/dummy_static/app/assets/stylesheets/spec_application_stylesheet.css.scss +5 -0
  208. data/spec/support/dummy_static/lib/assets/javascripts/spec_library_javascript.coffee +3 -0
  209. data/spec/support/dummy_static/lib/assets/stylesheets/spec_library_stylesheet.css.scss +5 -0
  210. data/spec/support/dummy_static/vendor/assets/javascripts/spec_vendor_javascript.js +5 -0
  211. data/spec/support/dummy_static/vendor/assets/stylesheets/spec_vendor_stylesheets.css +3 -0
  212. data/vendor/assets/stylesheets/code_sync.css +1 -0
  213. metadata +492 -0
@@ -0,0 +1,330 @@
1
+ CodeMirror.defineMode("skim", function(config) {
2
+ var tokenRegexp = function(words){
3
+ return new RegExp("^" + words.join("|"));
4
+ };
5
+
6
+ var keywords = ["true", "false", "null", "auto"];
7
+ var keywordsRegexp = new RegExp("^" + keywords.join("|"));
8
+
9
+ var operators = ["\\(", "\\)", "=", ">", "<", "==", ">=", "<=", "\\+", "-", "\\!=", "/", "\\*", "%", "and", "or", "not"];
10
+ var opRegexp = tokenRegexp(operators);
11
+
12
+ var pseudoElementsRegexp = /^::?[\w\-]+/;
13
+
14
+ var urlTokens = function(stream, state){
15
+ var ch = stream.peek();
16
+
17
+ if (ch === ")"){
18
+ stream.next();
19
+ state.tokenizer = tokenBase;
20
+ return "operator";
21
+ }else if (ch === "("){
22
+ stream.next();
23
+ stream.eatSpace();
24
+
25
+ return "operator";
26
+ }else if (ch === "'" || ch === '"'){
27
+ state.tokenizer = buildStringTokenizer(stream.next());
28
+ return "string";
29
+ }else{
30
+ state.tokenizer = buildStringTokenizer(")", false);
31
+ return "string";
32
+ }
33
+ };
34
+ var multilineComment = function(stream, state) {
35
+ if (stream.skipTo("*/")){
36
+ stream.next();
37
+ stream.next();
38
+ state.tokenizer = tokenBase;
39
+ }else {
40
+ stream.next();
41
+ }
42
+
43
+ return "comment";
44
+ };
45
+
46
+ var buildStringTokenizer = function(quote, greedy){
47
+ if(greedy == null){ greedy = true; }
48
+
49
+ function stringTokenizer(stream, state){
50
+ var nextChar = stream.next();
51
+ var peekChar = stream.peek();
52
+ var previousChar = stream.string.charAt(stream.pos-2);
53
+
54
+ var endingString = ((nextChar !== "\\" && peekChar === quote) || (nextChar === quote && previousChar !== "\\"));
55
+
56
+ /*
57
+ console.log("previousChar: " + previousChar);
58
+ console.log("nextChar: " + nextChar);
59
+ console.log("peekChar: " + peekChar);
60
+ console.log("ending: " + endingString);
61
+ */
62
+
63
+ if (endingString){
64
+ if (nextChar !== quote && greedy) { stream.next(); }
65
+ state.tokenizer = tokenBase;
66
+ return "string";
67
+ }else if (nextChar === "#" && peekChar === "{"){
68
+ state.tokenizer = buildInterpolationTokenizer(stringTokenizer);
69
+ stream.next();
70
+ return "operator";
71
+ }else {
72
+ return "string";
73
+ }
74
+ }
75
+
76
+ return stringTokenizer;
77
+ };
78
+
79
+ var buildInterpolationTokenizer = function(currentTokenizer){
80
+ return function(stream, state){
81
+ if (stream.peek() === "}"){
82
+ stream.next();
83
+ state.tokenizer = currentTokenizer;
84
+ return "operator";
85
+ }else{
86
+ return tokenBase(stream, state);
87
+ }
88
+ };
89
+ };
90
+
91
+ var indent = function(state){
92
+ if (state.indentCount == 0){
93
+ state.indentCount++;
94
+ var lastScopeOffset = state.scopes[0].offset;
95
+ var currentOffset = lastScopeOffset + config.indentUnit;
96
+ state.scopes.unshift({ offset:currentOffset });
97
+ }
98
+ };
99
+
100
+ var dedent = function(state){
101
+ if (state.scopes.length == 1) { return; }
102
+
103
+ state.scopes.shift();
104
+ };
105
+
106
+ var tokenBase = function(stream, state) {
107
+ var ch = stream.peek();
108
+
109
+ // Single line Comment
110
+ if (stream.match('//')) {
111
+ stream.skipToEnd();
112
+ return "comment";
113
+ }
114
+
115
+ // Multiline Comment
116
+ if (stream.match('/*')){
117
+ state.tokenizer = multilineComment;
118
+ return state.tokenizer(stream, state);
119
+ }
120
+
121
+ // Interpolation
122
+ if (stream.match('#{')){
123
+ state.tokenizer = buildInterpolationTokenizer(tokenBase);
124
+ return "operator";
125
+ }
126
+
127
+ if (ch === "."){
128
+ stream.next();
129
+
130
+ // Match class selectors
131
+ if (stream.match(/^[\w-]+/)){
132
+ indent(state);
133
+ return "atom";
134
+ }else if (stream.peek() === "#"){
135
+ indent(state);
136
+ return "atom";
137
+ }else{
138
+ return "operator";
139
+ }
140
+ }
141
+
142
+ if (ch === "#"){
143
+ stream.next();
144
+
145
+ // Hex numbers
146
+ if (stream.match(/[0-9a-fA-F]{6}|[0-9a-fA-F]{3}/)){
147
+ return "number";
148
+ }
149
+
150
+ // ID selectors
151
+ if (stream.match(/^[\w-]+/)){
152
+ indent(state);
153
+ return "atom";
154
+ }
155
+
156
+ if (stream.peek() === "#"){
157
+ indent(state);
158
+ return "atom";
159
+ }
160
+ }
161
+
162
+ // Numbers
163
+ if (stream.match(/^-?[0-9\.]+/)){
164
+ return "number";
165
+ }
166
+
167
+ // Units
168
+ if (stream.match(/^(px|em|in)\b/)){
169
+ return "unit";
170
+ }
171
+
172
+ if (stream.match(keywordsRegexp)){
173
+ return "keyword";
174
+ }
175
+
176
+ if (stream.match(/^url/) && stream.peek() === "("){
177
+ state.tokenizer = urlTokens;
178
+ return "atom";
179
+ }
180
+
181
+ // Variables
182
+ if (ch === "$"){
183
+ stream.next();
184
+ stream.eatWhile(/[\w-]/);
185
+
186
+ if (stream.peek() === ":"){
187
+ stream.next();
188
+ return "variable-2";
189
+ }else{
190
+ return "variable-3";
191
+ }
192
+ }
193
+
194
+ if (ch === "!"){
195
+ stream.next();
196
+
197
+ if (stream.match(/^[\w]+/)){
198
+ return "keyword";
199
+ }
200
+
201
+ return "operator";
202
+ }
203
+
204
+ if (ch === "="){
205
+ stream.next();
206
+
207
+ // Match shortcut mixin definition
208
+ if (stream.match(/^[\w-]+/)){
209
+ indent(state);
210
+ return "meta";
211
+ }else {
212
+ return "operator";
213
+ }
214
+ }
215
+
216
+ if (ch === "+"){
217
+ stream.next();
218
+
219
+ // Match shortcut mixin definition
220
+ if (stream.match(/^[\w-]+/)){
221
+ return "variable-3";
222
+ }else {
223
+ return "operator";
224
+ }
225
+ }
226
+
227
+ // Indent Directives
228
+ if (stream.match(/^@(else if|if|media|else|for|each|while|mixin|function)/)){
229
+ indent(state);
230
+ return "meta";
231
+ }
232
+
233
+ // Other Directives
234
+ if (ch === "@"){
235
+ stream.next();
236
+ stream.eatWhile(/[\w-]/);
237
+ return "meta";
238
+ }
239
+
240
+ // Strings
241
+ if (ch === '"' || ch === "'"){
242
+ stream.next();
243
+ state.tokenizer = buildStringTokenizer(ch);
244
+ return "string";
245
+ }
246
+
247
+ // Pseudo element selectors
248
+ if (ch == ':' && stream.match(pseudoElementsRegexp)){
249
+ return "keyword";
250
+ }
251
+
252
+ // atoms
253
+ if (stream.eatWhile(/[\w-&]/)){
254
+ // matches a property definition
255
+ if (stream.peek() === ":" && !stream.match(pseudoElementsRegexp, false))
256
+ return "property";
257
+ else
258
+ return "atom";
259
+ }
260
+
261
+ if (stream.match(opRegexp)){
262
+ return "operator";
263
+ }
264
+
265
+ // If we haven't returned by now, we move 1 character
266
+ // and return an error
267
+ stream.next();
268
+ return null;
269
+ };
270
+
271
+ var tokenLexer = function(stream, state) {
272
+ if (stream.sol()){
273
+ state.indentCount = 0;
274
+ }
275
+ var style = state.tokenizer(stream, state);
276
+ var current = stream.current();
277
+
278
+ if (current === "@return"){
279
+ dedent(state);
280
+ }
281
+
282
+ if (style === "atom"){
283
+ indent(state);
284
+ }
285
+
286
+ if (style !== null){
287
+ var startOfToken = stream.pos - current.length;
288
+ var withCurrentIndent = startOfToken + (config.indentUnit * state.indentCount);
289
+
290
+ var newScopes = [];
291
+
292
+ for (var i = 0; i < state.scopes.length; i++){
293
+ var scope = state.scopes[i];
294
+
295
+ if (scope.offset <= withCurrentIndent){
296
+ newScopes.push(scope);
297
+ }
298
+ }
299
+
300
+ state.scopes = newScopes;
301
+ }
302
+
303
+
304
+ return style;
305
+ };
306
+
307
+ return {
308
+ startState: function() {
309
+ return {
310
+ tokenizer: tokenBase,
311
+ scopes: [{offset: 0, type: 'skim'}],
312
+ definedVars: [],
313
+ definedMixins: []
314
+ };
315
+ },
316
+ token: function(stream, state) {
317
+ var style = tokenLexer(stream, state);
318
+
319
+ state.lastToken = { style: style, content: stream.current() };
320
+
321
+ return style;
322
+ },
323
+
324
+ indent: function(state) {
325
+ return state.scopes[0].offset;
326
+ }
327
+ };
328
+ });
329
+
330
+ CodeMirror.defineMIME("text/x-skim", "skim");
@@ -0,0 +1,3159 @@
1
+ /**
2
+ * Supported keybindings:
3
+ *
4
+ * Motion:
5
+ * h, j, k, l
6
+ * gj, gk
7
+ * e, E, w, W, b, B, ge, gE
8
+ * f<character>, F<character>, t<character>, T<character>
9
+ * $, ^, 0, -, +, _
10
+ * gg, G
11
+ * %
12
+ * '<character>, `<character>
13
+ *
14
+ * Operator:
15
+ * d, y, c
16
+ * dd, yy, cc
17
+ * g~, g~g~
18
+ * >, <, >>, <<
19
+ *
20
+ * Operator-Motion:
21
+ * x, X, D, Y, C, ~
22
+ *
23
+ * Action:
24
+ * a, i, s, A, I, S, o, O
25
+ * zz, z., z<CR>, zt, zb, z-
26
+ * J
27
+ * u, Ctrl-r
28
+ * m<character>
29
+ * r<character>
30
+ *
31
+ * Modes:
32
+ * ESC - leave insert mode, visual mode, and clear input state.
33
+ * Ctrl-[, Ctrl-c - same as ESC.
34
+ *
35
+ * Registers: unamed, -, a-z, A-Z, 0-9
36
+ * (Does not respect the special case for number registers when delete
37
+ * operator is made with these commands: %, (, ), , /, ?, n, N, {, } )
38
+ * TODO: Implement the remaining registers.
39
+ * Marks: a-z, A-Z, and 0-9
40
+ * TODO: Implement the remaining special marks. They have more complex
41
+ * behavior.
42
+ *
43
+ * Code structure:
44
+ * 1. Default keymap
45
+ * 2. Variable declarations and short basic helpers
46
+ * 3. Instance (External API) implementation
47
+ * 4. Internal state tracking objects (input state, counter) implementation
48
+ * and instanstiation
49
+ * 5. Key handler (the main command dispatcher) implementation
50
+ * 6. Motion, operator, and action implementations
51
+ * 7. Helper functions for the key handler, motions, operators, and actions
52
+ * 8. Set up Vim to work as a keymap for CodeMirror.
53
+ */
54
+
55
+ (function() {
56
+ 'use strict';
57
+
58
+ var defaultKeymap = [
59
+ // Key to key mapping. This goes first to make it possible to override
60
+ // existing mappings.
61
+ { keys: ['Left'], type: 'keyToKey', toKeys: ['h'] },
62
+ { keys: ['Right'], type: 'keyToKey', toKeys: ['l'] },
63
+ { keys: ['Up'], type: 'keyToKey', toKeys: ['k'] },
64
+ { keys: ['Down'], type: 'keyToKey', toKeys: ['j'] },
65
+ { keys: ['Space'], type: 'keyToKey', toKeys: ['l'] },
66
+ { keys: ['Backspace'], type: 'keyToKey', toKeys: ['h'] },
67
+ { keys: ['Ctrl-Space'], type: 'keyToKey', toKeys: ['W'] },
68
+ { keys: ['Ctrl-Backspace'], type: 'keyToKey', toKeys: ['B'] },
69
+ { keys: ['Shift-Space'], type: 'keyToKey', toKeys: ['w'] },
70
+ { keys: ['Shift-Backspace'], type: 'keyToKey', toKeys: ['b'] },
71
+ { keys: ['Ctrl-n'], type: 'keyToKey', toKeys: ['j'] },
72
+ { keys: ['Ctrl-p'], type: 'keyToKey', toKeys: ['k'] },
73
+ { keys: ['Ctrl-['], type: 'keyToKey', toKeys: ['Esc'] },
74
+ { keys: ['Ctrl-c'], type: 'keyToKey', toKeys: ['Esc'] },
75
+ { keys: ['s'], type: 'keyToKey', toKeys: ['c', 'l'] },
76
+ { keys: ['S'], type: 'keyToKey', toKeys: ['c', 'c'] },
77
+ { keys: ['Home'], type: 'keyToKey', toKeys: ['0'] },
78
+ { keys: ['End'], type: 'keyToKey', toKeys: ['$'] },
79
+ { keys: ['PageUp'], type: 'keyToKey', toKeys: ['Ctrl-b'] },
80
+ { keys: ['PageDown'], type: 'keyToKey', toKeys: ['Ctrl-f'] },
81
+ // Motions
82
+ { keys: ['H'], type: 'motion',
83
+ motion: 'moveToTopLine',
84
+ motionArgs: { linewise: true, toJumplist: true }},
85
+ { keys: ['M'], type: 'motion',
86
+ motion: 'moveToMiddleLine',
87
+ motionArgs: { linewise: true, toJumplist: true }},
88
+ { keys: ['L'], type: 'motion',
89
+ motion: 'moveToBottomLine',
90
+ motionArgs: { linewise: true, toJumplist: true }},
91
+ { keys: ['h'], type: 'motion',
92
+ motion: 'moveByCharacters',
93
+ motionArgs: { forward: false }},
94
+ { keys: ['l'], type: 'motion',
95
+ motion: 'moveByCharacters',
96
+ motionArgs: { forward: true }},
97
+ { keys: ['j'], type: 'motion',
98
+ motion: 'moveByLines',
99
+ motionArgs: { forward: true, linewise: true }},
100
+ { keys: ['k'], type: 'motion',
101
+ motion: 'moveByLines',
102
+ motionArgs: { forward: false, linewise: true }},
103
+ { keys: ['g','j'], type: 'motion',
104
+ motion: 'moveByDisplayLines',
105
+ motionArgs: { forward: true }},
106
+ { keys: ['g','k'], type: 'motion',
107
+ motion: 'moveByDisplayLines',
108
+ motionArgs: { forward: false }},
109
+ { keys: ['w'], type: 'motion',
110
+ motion: 'moveByWords',
111
+ motionArgs: { forward: true, wordEnd: false }},
112
+ { keys: ['W'], type: 'motion',
113
+ motion: 'moveByWords',
114
+ motionArgs: { forward: true, wordEnd: false, bigWord: true }},
115
+ { keys: ['e'], type: 'motion',
116
+ motion: 'moveByWords',
117
+ motionArgs: { forward: true, wordEnd: true, inclusive: true }},
118
+ { keys: ['E'], type: 'motion',
119
+ motion: 'moveByWords',
120
+ motionArgs: { forward: true, wordEnd: true, bigWord: true,
121
+ inclusive: true }},
122
+ { keys: ['b'], type: 'motion',
123
+ motion: 'moveByWords',
124
+ motionArgs: { forward: false, wordEnd: false }},
125
+ { keys: ['B'], type: 'motion',
126
+ motion: 'moveByWords',
127
+ motionArgs: { forward: false, wordEnd: false, bigWord: true }},
128
+ { keys: ['g', 'e'], type: 'motion',
129
+ motion: 'moveByWords',
130
+ motionArgs: { forward: false, wordEnd: true, inclusive: true }},
131
+ { keys: ['g', 'E'], type: 'motion',
132
+ motion: 'moveByWords',
133
+ motionArgs: { forward: false, wordEnd: true, bigWord: true,
134
+ inclusive: true }},
135
+ { keys: ['{'], type: 'motion', motion: 'moveByParagraph',
136
+ motionArgs: { forward: false, toJumplist: true }},
137
+ { keys: ['}'], type: 'motion', motion: 'moveByParagraph',
138
+ motionArgs: { forward: true, toJumplist: true }},
139
+ { keys: ['Ctrl-f'], type: 'motion',
140
+ motion: 'moveByPage', motionArgs: { forward: true }},
141
+ { keys: ['Ctrl-b'], type: 'motion',
142
+ motion: 'moveByPage', motionArgs: { forward: false }},
143
+ { keys: ['Ctrl-d'], type: 'motion',
144
+ motion: 'moveByScroll',
145
+ motionArgs: { forward: true, explicitRepeat: true }},
146
+ { keys: ['Ctrl-u'], type: 'motion',
147
+ motion: 'moveByScroll',
148
+ motionArgs: { forward: false, explicitRepeat: true }},
149
+ { keys: ['g', 'g'], type: 'motion',
150
+ motion: 'moveToLineOrEdgeOfDocument',
151
+ motionArgs: { forward: false, explicitRepeat: true, linewise: true, toJumplist: true }},
152
+ { keys: ['G'], type: 'motion',
153
+ motion: 'moveToLineOrEdgeOfDocument',
154
+ motionArgs: { forward: true, explicitRepeat: true, linewise: true, toJumplist: true }},
155
+ { keys: ['0'], type: 'motion', motion: 'moveToStartOfLine' },
156
+ { keys: ['^'], type: 'motion',
157
+ motion: 'moveToFirstNonWhiteSpaceCharacter' },
158
+ { keys: ['+'], type: 'motion',
159
+ motion: 'moveByLines',
160
+ motionArgs: { forward: true, toFirstChar:true }},
161
+ { keys: ['-'], type: 'motion',
162
+ motion: 'moveByLines',
163
+ motionArgs: { forward: false, toFirstChar:true }},
164
+ { keys: ['_'], type: 'motion',
165
+ motion: 'moveByLines',
166
+ motionArgs: { forward: true, toFirstChar:true, repeatOffset:-1 }},
167
+ { keys: ['$'], type: 'motion',
168
+ motion: 'moveToEol',
169
+ motionArgs: { inclusive: true }},
170
+ { keys: ['%'], type: 'motion',
171
+ motion: 'moveToMatchedSymbol',
172
+ motionArgs: { inclusive: true, toJumplist: true }},
173
+ { keys: ['f', 'character'], type: 'motion',
174
+ motion: 'moveToCharacter',
175
+ motionArgs: { forward: true , inclusive: true }},
176
+ { keys: ['F', 'character'], type: 'motion',
177
+ motion: 'moveToCharacter',
178
+ motionArgs: { forward: false }},
179
+ { keys: ['t', 'character'], type: 'motion',
180
+ motion: 'moveTillCharacter',
181
+ motionArgs: { forward: true, inclusive: true }},
182
+ { keys: ['T', 'character'], type: 'motion',
183
+ motion: 'moveTillCharacter',
184
+ motionArgs: { forward: false }},
185
+ { keys: [';'], type: 'motion', motion: 'repeatLastCharacterSearch',
186
+ motionArgs: { forward: true }},
187
+ { keys: [','], type: 'motion', motion: 'repeatLastCharacterSearch',
188
+ motionArgs: { forward: false }},
189
+ { keys: ['\'', 'character'], type: 'motion', motion: 'goToMark',
190
+ motionArgs: {toJumplist: true}},
191
+ { keys: ['`', 'character'], type: 'motion', motion: 'goToMark',
192
+ motionArgs: {toJumplist: true}},
193
+ { keys: [']', '`',], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true } },
194
+ { keys: ['[', '`',], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false } },
195
+ { keys: [']', '\''], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true, linewise: true } },
196
+ { keys: ['[', '\''], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: false, linewise: true } },
197
+ { keys: [']', 'character'], type: 'motion',
198
+ motion: 'moveToSymbol',
199
+ motionArgs: { forward: true, toJumplist: true}},
200
+ { keys: ['[', 'character'], type: 'motion',
201
+ motion: 'moveToSymbol',
202
+ motionArgs: { forward: false, toJumplist: true}},
203
+ { keys: ['|'], type: 'motion',
204
+ motion: 'moveToColumn',
205
+ motionArgs: { }},
206
+ // Operators
207
+ { keys: ['d'], type: 'operator', operator: 'delete' },
208
+ { keys: ['y'], type: 'operator', operator: 'yank' },
209
+ { keys: ['c'], type: 'operator', operator: 'change',
210
+ operatorArgs: { enterInsertMode: true } },
211
+ { keys: ['>'], type: 'operator', operator: 'indent',
212
+ operatorArgs: { indentRight: true }},
213
+ { keys: ['<'], type: 'operator', operator: 'indent',
214
+ operatorArgs: { indentRight: false }},
215
+ { keys: ['g', '~'], type: 'operator', operator: 'swapcase' },
216
+ { keys: ['n'], type: 'motion', motion: 'findNext',
217
+ motionArgs: { forward: true, toJumplist: true }},
218
+ { keys: ['N'], type: 'motion', motion: 'findNext',
219
+ motionArgs: { forward: false, toJumplist: true }},
220
+ // Operator-Motion dual commands
221
+ { keys: ['x'], type: 'operatorMotion', operator: 'delete',
222
+ motion: 'moveByCharacters', motionArgs: { forward: true },
223
+ operatorMotionArgs: { visualLine: false }},
224
+ { keys: ['X'], type: 'operatorMotion', operator: 'delete',
225
+ motion: 'moveByCharacters', motionArgs: { forward: false },
226
+ operatorMotionArgs: { visualLine: true }},
227
+ { keys: ['D'], type: 'operatorMotion', operator: 'delete',
228
+ motion: 'moveToEol', motionArgs: { inclusive: true },
229
+ operatorMotionArgs: { visualLine: true }},
230
+ { keys: ['Y'], type: 'operatorMotion', operator: 'yank',
231
+ motion: 'moveToEol', motionArgs: { inclusive: true },
232
+ operatorMotionArgs: { visualLine: true }},
233
+ { keys: ['C'], type: 'operatorMotion',
234
+ operator: 'change', operatorArgs: { enterInsertMode: true },
235
+ motion: 'moveToEol', motionArgs: { inclusive: true },
236
+ operatorMotionArgs: { visualLine: true }},
237
+ { keys: ['~'], type: 'operatorMotion', operator: 'swapcase',
238
+ motion: 'moveByCharacters', motionArgs: { forward: true }},
239
+ // Actions
240
+ { keys: ['Ctrl-i'], type: 'action', action: 'jumpListWalk',
241
+ actionArgs: { forward: true }},
242
+ { keys: ['Ctrl-o'], type: 'action', action: 'jumpListWalk',
243
+ actionArgs: { forward: false }},
244
+ { keys: ['a'], type: 'action', action: 'enterInsertMode',
245
+ actionArgs: { insertAt: 'charAfter' }},
246
+ { keys: ['A'], type: 'action', action: 'enterInsertMode',
247
+ actionArgs: { insertAt: 'eol' }},
248
+ { keys: ['i'], type: 'action', action: 'enterInsertMode' },
249
+ { keys: ['I'], type: 'action', action: 'enterInsertMode',
250
+ motion: 'moveToFirstNonWhiteSpaceCharacter' },
251
+ { keys: ['o'], type: 'action', action: 'newLineAndEnterInsertMode',
252
+ actionArgs: { after: true }},
253
+ { keys: ['O'], type: 'action', action: 'newLineAndEnterInsertMode',
254
+ actionArgs: { after: false }},
255
+ { keys: ['v'], type: 'action', action: 'toggleVisualMode' },
256
+ { keys: ['V'], type: 'action', action: 'toggleVisualMode',
257
+ actionArgs: { linewise: true }},
258
+ { keys: ['J'], type: 'action', action: 'joinLines' },
259
+ { keys: ['p'], type: 'action', action: 'paste',
260
+ actionArgs: { after: true }},
261
+ { keys: ['P'], type: 'action', action: 'paste',
262
+ actionArgs: { after: false }},
263
+ { keys: ['r', 'character'], type: 'action', action: 'replace' },
264
+ { keys: ['R'], type: 'action', action: 'enterReplaceMode' },
265
+ { keys: ['u'], type: 'action', action: 'undo' },
266
+ { keys: ['Ctrl-r'], type: 'action', action: 'redo' },
267
+ { keys: ['m', 'character'], type: 'action', action: 'setMark' },
268
+ { keys: ['\"', 'character'], type: 'action', action: 'setRegister' },
269
+ { keys: ['z', 'z'], type: 'action', action: 'scrollToCursor',
270
+ actionArgs: { position: 'center' }},
271
+ { keys: ['z', '.'], type: 'action', action: 'scrollToCursor',
272
+ actionArgs: { position: 'center' },
273
+ motion: 'moveToFirstNonWhiteSpaceCharacter' },
274
+ { keys: ['z', 't'], type: 'action', action: 'scrollToCursor',
275
+ actionArgs: { position: 'top' }},
276
+ { keys: ['z', 'Enter'], type: 'action', action: 'scrollToCursor',
277
+ actionArgs: { position: 'top' },
278
+ motion: 'moveToFirstNonWhiteSpaceCharacter' },
279
+ { keys: ['z', '-'], type: 'action', action: 'scrollToCursor',
280
+ actionArgs: { position: 'bottom' }},
281
+ { keys: ['z', 'b'], type: 'action', action: 'scrollToCursor',
282
+ actionArgs: { position: 'bottom' },
283
+ motion: 'moveToFirstNonWhiteSpaceCharacter' },
284
+ { keys: ['.'], type: 'action', action: 'repeatLastEdit' },
285
+ { keys: ['Ctrl-a'], type: 'action', action: 'incrementNumberToken',
286
+ actionArgs: {increase: true, backtrack: false}},
287
+ { keys: ['Ctrl-x'], type: 'action', action: 'incrementNumberToken',
288
+ actionArgs: {increase: false, backtrack: false}},
289
+ // Text object motions
290
+ { keys: ['a', 'character'], type: 'motion',
291
+ motion: 'textObjectManipulation' },
292
+ { keys: ['i', 'character'], type: 'motion',
293
+ motion: 'textObjectManipulation',
294
+ motionArgs: { textObjectInner: true }},
295
+ // Search
296
+ { keys: ['/'], type: 'search',
297
+ searchArgs: { forward: true, querySrc: 'prompt', toJumplist: true }},
298
+ { keys: ['?'], type: 'search',
299
+ searchArgs: { forward: false, querySrc: 'prompt', toJumplist: true }},
300
+ { keys: ['*'], type: 'search',
301
+ searchArgs: { forward: true, querySrc: 'wordUnderCursor', toJumplist: true }},
302
+ { keys: ['#'], type: 'search',
303
+ searchArgs: { forward: false, querySrc: 'wordUnderCursor', toJumplist: true }},
304
+ // Ex command
305
+ { keys: [':'], type: 'ex' }
306
+ ];
307
+
308
+ var Vim = function() {
309
+ var alphabetRegex = /[A-Za-z]/;
310
+ var numberRegex = /[\d]/;
311
+ var whiteSpaceRegex = /\s/;
312
+ var wordRegexp = [(/\w/), (/[^\w\s]/)], bigWordRegexp = [(/\S/)];
313
+ function makeKeyRange(start, size) {
314
+ var keys = [];
315
+ for (var i = start; i < start + size; i++) {
316
+ keys.push(String.fromCharCode(i));
317
+ }
318
+ return keys;
319
+ }
320
+ var upperCaseAlphabet = makeKeyRange(65, 26);
321
+ var lowerCaseAlphabet = makeKeyRange(97, 26);
322
+ var numbers = makeKeyRange(48, 10);
323
+ var SPECIAL_SYMBOLS = '~`!@#$%^&*()_-+=[{}]\\|/?.,<>:;\"\'';
324
+ var specialSymbols = SPECIAL_SYMBOLS.split('');
325
+ var specialKeys = ['Left', 'Right', 'Up', 'Down', 'Space', 'Backspace',
326
+ 'Esc', 'Home', 'End', 'PageUp', 'PageDown', 'Enter'];
327
+ var validMarks = upperCaseAlphabet.concat(lowerCaseAlphabet).concat(
328
+ numbers).concat(['<', '>']);
329
+ var validRegisters = upperCaseAlphabet.concat(lowerCaseAlphabet).concat(
330
+ numbers).concat('-\"'.split(''));
331
+
332
+ function isAlphabet(k) {
333
+ return alphabetRegex.test(k);
334
+ }
335
+ function isLine(cm, line) {
336
+ return line >= cm.firstLine() && line <= cm.lastLine();
337
+ }
338
+ function isLowerCase(k) {
339
+ return (/^[a-z]$/).test(k);
340
+ }
341
+ function isMatchableSymbol(k) {
342
+ return '()[]{}'.indexOf(k) != -1;
343
+ }
344
+ function isNumber(k) {
345
+ return numberRegex.test(k);
346
+ }
347
+ function isUpperCase(k) {
348
+ return (/^[A-Z]$/).test(k);
349
+ }
350
+ function isAlphanumeric(k) {
351
+ return (/^[\w]$/).test(k);
352
+ }
353
+ function isWhiteSpace(k) {
354
+ return whiteSpaceRegex.test(k);
355
+ }
356
+ function isWhiteSpaceString(k) {
357
+ return (/^\s*$/).test(k);
358
+ }
359
+ function inRangeInclusive(x, start, end) {
360
+ return x >= start && x <= end;
361
+ }
362
+ function inArray(val, arr) {
363
+ for (var i = 0; i < arr.length; i++) {
364
+ if (arr[i] == val) {
365
+ return true;
366
+ }
367
+ }
368
+ return false;
369
+ }
370
+
371
+ var circularJumpList = (function(){
372
+ var size = 100;
373
+ var pointer = -1;
374
+ var head = 0;
375
+ var tail = 0;
376
+ var buffer = new Array(size);
377
+ function add(cm, oldCur, newCur) {
378
+ var current = pointer % size;
379
+ var curMark = buffer[current];
380
+ function useNextSlot(cursor) {
381
+ var next = ++pointer % size;
382
+ var trashMark = buffer[next];
383
+ if (trashMark) {
384
+ trashMark.clear();
385
+ }
386
+ buffer[next] = cm.setBookmark(cursor);
387
+ }
388
+ if (!curMark || !cursorEqual(curMark.find(), oldCur)) {
389
+ useNextSlot(oldCur);
390
+ }
391
+ useNextSlot(newCur);
392
+ head = pointer;
393
+ tail = pointer - size + 1;
394
+ if (tail < 0) {
395
+ tail = 0;
396
+ }
397
+ }
398
+ function move(offset) {
399
+ pointer += offset;
400
+ if (pointer > head) {
401
+ pointer = head;
402
+ } else if (pointer < tail) {
403
+ pointer = tail;
404
+ }
405
+ return buffer[(size + pointer) % size];
406
+ }
407
+ return {
408
+ cachedCursor: undefined, //used for # and * jumps
409
+ add: add,
410
+ move: move
411
+ };
412
+ })();
413
+ // Global Vim state. Call getVimGlobalState to get and initialize.
414
+ var vimGlobalState;
415
+ function getVimGlobalState() {
416
+ if (!vimGlobalState) {
417
+ vimGlobalState = {
418
+ // The current search query.
419
+ searchQuery: null,
420
+ // Whether we are searching backwards.
421
+ searchIsReversed: false,
422
+ jumpList: circularJumpList,
423
+ // Recording latest f, t, F or T motion command.
424
+ lastChararacterSearch: {increment:0, forward:true, selectedCharacter:''},
425
+ registerController: new RegisterController({})
426
+ };
427
+ }
428
+ return vimGlobalState;
429
+ }
430
+ function getVimState(cm) {
431
+ if (!cm.vimState) {
432
+ // Store instance state in the CodeMirror object.
433
+ cm.vimState = {
434
+ inputState: new InputState(),
435
+ // When using jk for navigation, if you move from a longer line to a
436
+ // shorter line, the cursor may clip to the end of the shorter line.
437
+ // If j is pressed again and cursor goes to the next line, the
438
+ // cursor should go back to its horizontal position on the longer
439
+ // line if it can. This is to keep track of the horizontal position.
440
+ lastHPos: -1,
441
+ // Doing the same with screen-position for gj/gk
442
+ lastHSPos: -1,
443
+ // The last motion command run. Cleared if a non-motion command gets
444
+ // executed in between.
445
+ lastMotion: null,
446
+ marks: {},
447
+ visualMode: false,
448
+ // If we are in visual line mode. No effect if visualMode is false.
449
+ visualLine: false
450
+ };
451
+ }
452
+ return cm.vimState;
453
+ }
454
+
455
+ var vimApi= {
456
+ buildKeyMap: function() {
457
+ // TODO: Convert keymap into dictionary format for fast lookup.
458
+ },
459
+ // Testing hook, though it might be useful to expose the register
460
+ // controller anyways.
461
+ getRegisterController: function() {
462
+ return getVimGlobalState().registerController;
463
+ },
464
+ // Testing hook.
465
+ clearVimGlobalState_: function() {
466
+ vimGlobalState = null;
467
+ },
468
+ map: function(lhs, rhs) {
469
+ // Add user defined key bindings.
470
+ exCommandDispatcher.map(lhs, rhs);
471
+ },
472
+ defineEx: function(name, prefix, func){
473
+ if (name.indexOf(prefix) === 0) {
474
+ exCommands[name]=func;
475
+ exCommandDispatcher.commandMap_[prefix]={name:name, shortName:prefix, type:'api'};
476
+ }else throw new Error("(Vim.defineEx) \""+prefix+"\" is not a prefix of \""+name+"\", command not registered");
477
+ },
478
+ // Initializes vim state variable on the CodeMirror object. Should only be
479
+ // called lazily by handleKey or for testing.
480
+ maybeInitState: function(cm) {
481
+ getVimState(cm);
482
+ },
483
+ // This is the outermost function called by CodeMirror, after keys have
484
+ // been mapped to their Vim equivalents.
485
+ handleKey: function(cm, key) {
486
+ var command;
487
+ var vim = getVimState(cm);
488
+ if (key == 'Esc') {
489
+ // Clear input state and get back to normal mode.
490
+ vim.inputState = new InputState();
491
+ if (vim.visualMode) {
492
+ exitVisualMode(cm, vim);
493
+ }
494
+ return;
495
+ }
496
+ if (vim.visualMode &&
497
+ cursorEqual(cm.getCursor('head'), cm.getCursor('anchor'))) {
498
+ // The selection was cleared. Exit visual mode.
499
+ exitVisualMode(cm, vim);
500
+ }
501
+ if (!vim.visualMode &&
502
+ !cursorEqual(cm.getCursor('head'), cm.getCursor('anchor'))) {
503
+ vim.visualMode = true;
504
+ vim.visualLine = false;
505
+ }
506
+ if (key != '0' || (key == '0' && vim.inputState.getRepeat() === 0)) {
507
+ // Have to special case 0 since it's both a motion and a number.
508
+ command = commandDispatcher.matchCommand(key, defaultKeymap, vim);
509
+ }
510
+ if (!command) {
511
+ if (isNumber(key)) {
512
+ // Increment count unless count is 0 and key is 0.
513
+ vim.inputState.pushRepeatDigit(key);
514
+ }
515
+ return;
516
+ }
517
+ if (command.type == 'keyToKey') {
518
+ // TODO: prevent infinite recursion.
519
+ for (var i = 0; i < command.toKeys.length; i++) {
520
+ this.handleKey(cm, command.toKeys[i]);
521
+ }
522
+ } else {
523
+ commandDispatcher.processCommand(cm, vim, command);
524
+ }
525
+ }
526
+ };
527
+
528
+ // Represents the current input state.
529
+ function InputState() {
530
+ this.prefixRepeat = [];
531
+ this.motionRepeat = [];
532
+
533
+ this.operator = null;
534
+ this.operatorArgs = null;
535
+ this.motion = null;
536
+ this.motionArgs = null;
537
+ this.keyBuffer = []; // For matching multi-key commands.
538
+ this.registerName = null; // Defaults to the unamed register.
539
+ }
540
+ InputState.prototype.pushRepeatDigit = function(n) {
541
+ if (!this.operator) {
542
+ this.prefixRepeat = this.prefixRepeat.concat(n);
543
+ } else {
544
+ this.motionRepeat = this.motionRepeat.concat(n);
545
+ }
546
+ };
547
+ InputState.prototype.getRepeat = function() {
548
+ var repeat = 0;
549
+ if (this.prefixRepeat.length > 0 || this.motionRepeat.length > 0) {
550
+ repeat = 1;
551
+ if (this.prefixRepeat.length > 0) {
552
+ repeat *= parseInt(this.prefixRepeat.join(''), 10);
553
+ }
554
+ if (this.motionRepeat.length > 0) {
555
+ repeat *= parseInt(this.motionRepeat.join(''), 10);
556
+ }
557
+ }
558
+ return repeat;
559
+ };
560
+
561
+ /*
562
+ * Register stores information about copy and paste registers. Besides
563
+ * text, a register must store whether it is linewise (i.e., when it is
564
+ * pasted, should it insert itself into a new line, or should the text be
565
+ * inserted at the cursor position.)
566
+ */
567
+ function Register(text, linewise) {
568
+ this.clear();
569
+ if (text) {
570
+ this.set(text, linewise);
571
+ }
572
+ }
573
+ Register.prototype = {
574
+ set: function(text, linewise) {
575
+ this.text = text;
576
+ this.linewise = !!linewise;
577
+ },
578
+ append: function(text, linewise) {
579
+ // if this register has ever been set to linewise, use linewise.
580
+ if (linewise || this.linewise) {
581
+ this.text += '\n' + text;
582
+ this.linewise = true;
583
+ } else {
584
+ this.text += text;
585
+ }
586
+ },
587
+ clear: function() {
588
+ this.text = '';
589
+ this.linewise = false;
590
+ },
591
+ toString: function() { return this.text; }
592
+ };
593
+
594
+ /*
595
+ * vim registers allow you to keep many independent copy and paste buffers.
596
+ * See http://usevim.com/2012/04/13/registers/ for an introduction.
597
+ *
598
+ * RegisterController keeps the state of all the registers. An initial
599
+ * state may be passed in. The unnamed register '"' will always be
600
+ * overridden.
601
+ */
602
+ function RegisterController(registers) {
603
+ this.registers = registers;
604
+ this.unamedRegister = registers['\"'] = new Register();
605
+ }
606
+ RegisterController.prototype = {
607
+ pushText: function(registerName, operator, text, linewise) {
608
+ // Lowercase and uppercase registers refer to the same register.
609
+ // Uppercase just means append.
610
+ var register = this.isValidRegister(registerName) ?
611
+ this.getRegister(registerName) : null;
612
+ // if no register/an invalid register was specified, things go to the
613
+ // default registers
614
+ if (!register) {
615
+ switch (operator) {
616
+ case 'yank':
617
+ // The 0 register contains the text from the most recent yank.
618
+ this.registers['0'] = new Register(text, linewise);
619
+ break;
620
+ case 'delete':
621
+ case 'change':
622
+ if (text.indexOf('\n') == -1) {
623
+ // Delete less than 1 line. Update the small delete register.
624
+ this.registers['-'] = new Register(text, linewise);
625
+ } else {
626
+ // Shift down the contents of the numbered registers and put the
627
+ // deleted text into register 1.
628
+ this.shiftNumericRegisters_();
629
+ this.registers['1'] = new Register(text, linewise);
630
+ }
631
+ break;
632
+ }
633
+ // Make sure the unnamed register is set to what just happened
634
+ this.unamedRegister.set(text, linewise);
635
+ return;
636
+ }
637
+
638
+ // If we've gotten to this point, we've actually specified a register
639
+ var append = isUpperCase(registerName);
640
+ if (append) {
641
+ register.append(text, linewise);
642
+ // The unamed register always has the same value as the last used
643
+ // register.
644
+ this.unamedRegister.append(text, linewise);
645
+ } else {
646
+ register.set(text, linewise);
647
+ this.unamedRegister.set(text, linewise);
648
+ }
649
+ },
650
+ // Gets the register named @name. If one of @name doesn't already exist,
651
+ // create it. If @name is invalid, return the unamedRegister.
652
+ getRegister: function(name) {
653
+ if (!this.isValidRegister(name)) {
654
+ return this.unamedRegister;
655
+ }
656
+ name = name.toLowerCase();
657
+ if (!this.registers[name]) {
658
+ this.registers[name] = new Register();
659
+ }
660
+ return this.registers[name];
661
+ },
662
+ isValidRegister: function(name) {
663
+ return name && inArray(name, validRegisters);
664
+ },
665
+ shiftNumericRegisters_: function() {
666
+ for (var i = 9; i >= 2; i--) {
667
+ this.registers[i] = this.getRegister('' + (i - 1));
668
+ }
669
+ }
670
+ };
671
+
672
+ var commandDispatcher = {
673
+ matchCommand: function(key, keyMap, vim) {
674
+ var inputState = vim.inputState;
675
+ var keys = inputState.keyBuffer.concat(key);
676
+ for (var i = 0; i < keyMap.length; i++) {
677
+ var command = keyMap[i];
678
+ if (matchKeysPartial(keys, command.keys)) {
679
+ if (keys.length < command.keys.length) {
680
+ // Matches part of a multi-key command. Buffer and wait for next
681
+ // stroke.
682
+ inputState.keyBuffer.push(key);
683
+ return null;
684
+ } else {
685
+ if (inputState.operator && command.type == 'action') {
686
+ // Ignore matched action commands after an operator. Operators
687
+ // only operate on motions. This check is really for text
688
+ // objects since aW, a[ etcs conflicts with a.
689
+ continue;
690
+ }
691
+ // Matches whole comand. Return the command.
692
+ if (command.keys[keys.length - 1] == 'character') {
693
+ inputState.selectedCharacter = keys[keys.length - 1];
694
+ if(inputState.selectedCharacter.length>1){
695
+ switch(inputState.selectedCharacter){
696
+ case "Enter":
697
+ inputState.selectedCharacter='\n';
698
+ break;
699
+ case "Space":
700
+ inputState.selectedCharacter=' ';
701
+ break;
702
+ default:
703
+ continue;
704
+ }
705
+ }
706
+ }
707
+ inputState.keyBuffer = [];
708
+ return command;
709
+ }
710
+ }
711
+ }
712
+ // Clear the buffer since there are no partial matches.
713
+ inputState.keyBuffer = [];
714
+ return null;
715
+ },
716
+ processCommand: function(cm, vim, command) {
717
+ vim.inputState.repeatOverride = command.repeatOverride;
718
+ switch (command.type) {
719
+ case 'motion':
720
+ this.processMotion(cm, vim, command);
721
+ break;
722
+ case 'operator':
723
+ this.processOperator(cm, vim, command);
724
+ break;
725
+ case 'operatorMotion':
726
+ this.processOperatorMotion(cm, vim, command);
727
+ break;
728
+ case 'action':
729
+ this.processAction(cm, vim, command);
730
+ break;
731
+ case 'search':
732
+ this.processSearch(cm, vim, command);
733
+ break;
734
+ case 'ex':
735
+ case 'keyToEx':
736
+ this.processEx(cm, vim, command);
737
+ break;
738
+ default:
739
+ break;
740
+ }
741
+ },
742
+ processMotion: function(cm, vim, command) {
743
+ vim.inputState.motion = command.motion;
744
+ vim.inputState.motionArgs = copyArgs(command.motionArgs);
745
+ this.evalInput(cm, vim);
746
+ },
747
+ processOperator: function(cm, vim, command) {
748
+ var inputState = vim.inputState;
749
+ if (inputState.operator) {
750
+ if (inputState.operator == command.operator) {
751
+ // Typing an operator twice like 'dd' makes the operator operate
752
+ // linewise
753
+ inputState.motion = 'expandToLine';
754
+ inputState.motionArgs = { linewise: true };
755
+ this.evalInput(cm, vim);
756
+ return;
757
+ } else {
758
+ // 2 different operators in a row doesn't make sense.
759
+ vim.inputState = new InputState();
760
+ }
761
+ }
762
+ inputState.operator = command.operator;
763
+ inputState.operatorArgs = copyArgs(command.operatorArgs);
764
+ if (vim.visualMode) {
765
+ // Operating on a selection in visual mode. We don't need a motion.
766
+ this.evalInput(cm, vim);
767
+ }
768
+ },
769
+ processOperatorMotion: function(cm, vim, command) {
770
+ var visualMode = vim.visualMode;
771
+ var operatorMotionArgs = copyArgs(command.operatorMotionArgs);
772
+ if (operatorMotionArgs) {
773
+ // Operator motions may have special behavior in visual mode.
774
+ if (visualMode && operatorMotionArgs.visualLine) {
775
+ vim.visualLine = true;
776
+ }
777
+ }
778
+ this.processOperator(cm, vim, command);
779
+ if (!visualMode) {
780
+ this.processMotion(cm, vim, command);
781
+ }
782
+ },
783
+ processAction: function(cm, vim, command) {
784
+ var inputState = vim.inputState;
785
+ var repeat = inputState.getRepeat();
786
+ var repeatIsExplicit = !!repeat;
787
+ var actionArgs = copyArgs(command.actionArgs) || {};
788
+ if (inputState.selectedCharacter) {
789
+ actionArgs.selectedCharacter = inputState.selectedCharacter;
790
+ }
791
+ // Actions may or may not have motions and operators. Do these first.
792
+ if (command.operator) {
793
+ this.processOperator(cm, vim, command);
794
+ }
795
+ if (command.motion) {
796
+ this.processMotion(cm, vim, command);
797
+ }
798
+ if (command.motion || command.operator) {
799
+ this.evalInput(cm, vim);
800
+ }
801
+ actionArgs.repeat = repeat || 1;
802
+ actionArgs.repeatIsExplicit = repeatIsExplicit;
803
+ actionArgs.registerName = inputState.registerName;
804
+ vim.inputState = new InputState();
805
+ vim.lastMotion = null,
806
+ actions[command.action](cm, actionArgs, vim);
807
+ },
808
+ processSearch: function(cm, vim, command) {
809
+ if (!cm.getSearchCursor) {
810
+ // Search depends on SearchCursor.
811
+ return;
812
+ }
813
+ var forward = command.searchArgs.forward;
814
+ getSearchState(cm).setReversed(!forward);
815
+ var promptPrefix = (forward) ? '/' : '?';
816
+ var originalQuery = getSearchState(cm).getQuery();
817
+ var originalScrollPos = cm.getScrollInfo();
818
+ function handleQuery(query, ignoreCase, smartCase) {
819
+ try {
820
+ updateSearchQuery(cm, query, ignoreCase, smartCase);
821
+ } catch (e) {
822
+ showConfirm(cm, 'Invalid regex: ' + regexPart);
823
+ return;
824
+ }
825
+ commandDispatcher.processMotion(cm, vim, {
826
+ type: 'motion',
827
+ motion: 'findNext',
828
+ motionArgs: { forward: true, toJumplist: command.searchArgs.toJumplist }
829
+ });
830
+ }
831
+ function onPromptClose(query) {
832
+ cm.scrollTo(originalScrollPos.left, originalScrollPos.top);
833
+ handleQuery(query, true /** ignoreCase */, true /** smartCase */);
834
+ }
835
+ function onPromptKeyUp(e, query) {
836
+ var parsedQuery;
837
+ try {
838
+ parsedQuery = updateSearchQuery(cm, query,
839
+ true /** ignoreCase */, true /** smartCase */)
840
+ } catch (e) {
841
+ // Swallow bad regexes for incremental search.
842
+ }
843
+ if (parsedQuery) {
844
+ cm.scrollIntoView(findNext(cm, !forward, parsedQuery), 30);
845
+ } else {
846
+ clearSearchHighlight(cm);
847
+ cm.scrollTo(originalScrollPos.left, originalScrollPos.top);
848
+ }
849
+ }
850
+ function onPromptKeyDown(e, query, close) {
851
+ var keyName = CodeMirror.keyName(e);
852
+ if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[') {
853
+ updateSearchQuery(cm, originalQuery);
854
+ clearSearchHighlight(cm);
855
+ cm.scrollTo(originalScrollPos.left, originalScrollPos.top);
856
+
857
+ CodeMirror.e_stop(e);
858
+ close();
859
+ cm.focus();
860
+ }
861
+ }
862
+ switch (command.searchArgs.querySrc) {
863
+ case 'prompt':
864
+ showPrompt(cm, {
865
+ onClose: onPromptClose,
866
+ prefix: promptPrefix,
867
+ desc: searchPromptDesc,
868
+ onKeyUp: onPromptKeyUp,
869
+ onKeyDown: onPromptKeyDown
870
+ });
871
+ break;
872
+ case 'wordUnderCursor':
873
+ var word = expandWordUnderCursor(cm, false /** inclusive */,
874
+ true /** forward */, false /** bigWord */,
875
+ true /** noSymbol */);
876
+ var isKeyword = true;
877
+ if (!word) {
878
+ word = expandWordUnderCursor(cm, false /** inclusive */,
879
+ true /** forward */, false /** bigWord */,
880
+ false /** noSymbol */);
881
+ isKeyword = false;
882
+ }
883
+ if (!word) {
884
+ return;
885
+ }
886
+ var query = cm.getLine(word.start.line).substring(word.start.ch,
887
+ word.end.ch);
888
+ if (isKeyword) {
889
+ query = '\\b' + query + '\\b';
890
+ } else {
891
+ query = escapeRegex(query);
892
+ }
893
+
894
+ // cachedCursor is used to save the old position of the cursor
895
+ // when * or # causes vim to seek for the nearest word and shift
896
+ // the cursor before entering the motion.
897
+ getVimGlobalState().jumpList.cachedCursor = cm.getCursor();
898
+ cm.setCursor(word.start);
899
+
900
+ handleQuery(query, true /** ignoreCase */, false /** smartCase */);
901
+ break;
902
+ }
903
+ },
904
+ processEx: function(cm, vim, command) {
905
+ function onPromptClose(input) {
906
+ exCommandDispatcher.processCommand(cm, input);
907
+ }
908
+ function onPromptKeyDown(e, input, close) {
909
+ var keyName = CodeMirror.keyName(e);
910
+ if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[') {
911
+ CodeMirror.e_stop(e);
912
+ close();
913
+ cm.focus();
914
+ }
915
+ }
916
+ if (command.type == 'keyToEx') {
917
+ // Handle user defined Ex to Ex mappings
918
+ exCommandDispatcher.processCommand(cm, command.exArgs.input);
919
+ } else {
920
+ if (vim.visualMode) {
921
+ showPrompt(cm, { onClose: onPromptClose, prefix: ':', value: '\'<,\'>',
922
+ onKeyDown: onPromptKeyDown});
923
+ } else {
924
+ showPrompt(cm, { onClose: onPromptClose, prefix: ':',
925
+ onKeyDown: onPromptKeyDown});
926
+ }
927
+ }
928
+ },
929
+ evalInput: function(cm, vim) {
930
+ // If the motion comand is set, execute both the operator and motion.
931
+ // Otherwise return.
932
+ var inputState = vim.inputState;
933
+ var motion = inputState.motion;
934
+ var motionArgs = inputState.motionArgs || {};
935
+ var operator = inputState.operator;
936
+ var operatorArgs = inputState.operatorArgs || {};
937
+ var registerName = inputState.registerName;
938
+ var selectionEnd = cm.getCursor('head');
939
+ var selectionStart = cm.getCursor('anchor');
940
+ // The difference between cur and selection cursors are that cur is
941
+ // being operated on and ignores that there is a selection.
942
+ var curStart = copyCursor(selectionEnd);
943
+ var curOriginal = copyCursor(curStart);
944
+ var curEnd;
945
+ var repeat;
946
+ if (operator) {
947
+ this.recordLastEdit(cm, vim, inputState);
948
+ }
949
+ if (inputState.repeatOverride !== undefined) {
950
+ // If repeatOverride is specified, that takes precedence over the
951
+ // input state's repeat. Used by Ex mode and can be user defined.
952
+ repeat = inputState.repeatOverride;
953
+ } else {
954
+ repeat = inputState.getRepeat();
955
+ }
956
+ if (repeat > 0 && motionArgs.explicitRepeat) {
957
+ motionArgs.repeatIsExplicit = true;
958
+ } else if (motionArgs.noRepeat ||
959
+ (!motionArgs.explicitRepeat && repeat === 0)) {
960
+ repeat = 1;
961
+ motionArgs.repeatIsExplicit = false;
962
+ }
963
+ if (inputState.selectedCharacter) {
964
+ // If there is a character input, stick it in all of the arg arrays.
965
+ motionArgs.selectedCharacter = operatorArgs.selectedCharacter =
966
+ inputState.selectedCharacter;
967
+ }
968
+ motionArgs.repeat = repeat;
969
+ vim.inputState = new InputState();
970
+ if (motion) {
971
+ var motionResult = motions[motion](cm, motionArgs, vim);
972
+ vim.lastMotion = motions[motion];
973
+ if (!motionResult) {
974
+ return;
975
+ }
976
+ if (motionArgs.toJumplist) {
977
+ var jumpList = getVimGlobalState().jumpList;
978
+ // if the current motion is # or *, use cachedCursor
979
+ var cachedCursor = jumpList.cachedCursor;
980
+ if (cachedCursor) {
981
+ recordJumpPosition(cm, cachedCursor, motionResult);
982
+ delete jumpList.cachedCursor;
983
+ } else {
984
+ recordJumpPosition(cm, curOriginal, motionResult);
985
+ }
986
+ }
987
+ if (motionResult instanceof Array) {
988
+ curStart = motionResult[0];
989
+ curEnd = motionResult[1];
990
+ } else {
991
+ curEnd = motionResult;
992
+ }
993
+ // TODO: Handle null returns from motion commands better.
994
+ if (!curEnd) {
995
+ curEnd = { ch: curStart.ch, line: curStart.line };
996
+ }
997
+ if (vim.visualMode) {
998
+ // Check if the selection crossed over itself. Will need to shift
999
+ // the start point if that happened.
1000
+ if (cursorIsBefore(selectionStart, selectionEnd) &&
1001
+ (cursorEqual(selectionStart, curEnd) ||
1002
+ cursorIsBefore(curEnd, selectionStart))) {
1003
+ // The end of the selection has moved from after the start to
1004
+ // before the start. We will shift the start right by 1.
1005
+ selectionStart.ch += 1;
1006
+ } else if (cursorIsBefore(selectionEnd, selectionStart) &&
1007
+ (cursorEqual(selectionStart, curEnd) ||
1008
+ cursorIsBefore(selectionStart, curEnd))) {
1009
+ // The opposite happened. We will shift the start left by 1.
1010
+ selectionStart.ch -= 1;
1011
+ }
1012
+ selectionEnd = curEnd;
1013
+ if (vim.visualLine) {
1014
+ if (cursorIsBefore(selectionStart, selectionEnd)) {
1015
+ selectionStart.ch = 0;
1016
+ selectionEnd.ch = lineLength(cm, selectionEnd.line);
1017
+ } else {
1018
+ selectionEnd.ch = 0;
1019
+ selectionStart.ch = lineLength(cm, selectionStart.line);
1020
+ }
1021
+ }
1022
+ cm.setSelection(selectionStart, selectionEnd);
1023
+ updateMark(cm, vim, '<',
1024
+ cursorIsBefore(selectionStart, selectionEnd) ? selectionStart
1025
+ : selectionEnd);
1026
+ updateMark(cm, vim, '>',
1027
+ cursorIsBefore(selectionStart, selectionEnd) ? selectionEnd
1028
+ : selectionStart);
1029
+ } else if (!operator) {
1030
+ curEnd = clipCursorToContent(cm, curEnd);
1031
+ cm.setCursor(curEnd.line, curEnd.ch);
1032
+ }
1033
+ }
1034
+
1035
+ if (operator) {
1036
+ var inverted = false;
1037
+ vim.lastMotion = null;
1038
+ operatorArgs.repeat = repeat; // Indent in visual mode needs this.
1039
+ if (vim.visualMode) {
1040
+ curStart = selectionStart;
1041
+ curEnd = selectionEnd;
1042
+ motionArgs.inclusive = true;
1043
+ }
1044
+ // Swap start and end if motion was backward.
1045
+ if (cursorIsBefore(curEnd, curStart)) {
1046
+ var tmp = curStart;
1047
+ curStart = curEnd;
1048
+ curEnd = tmp;
1049
+ inverted = true;
1050
+ }
1051
+ if (motionArgs.inclusive && !(vim.visualMode && inverted)) {
1052
+ // Move the selection end one to the right to include the last
1053
+ // character.
1054
+ curEnd.ch++;
1055
+ }
1056
+ var linewise = motionArgs.linewise ||
1057
+ (vim.visualMode && vim.visualLine);
1058
+ if (linewise) {
1059
+ // Expand selection to entire line.
1060
+ expandSelectionToLine(cm, curStart, curEnd);
1061
+ } else if (motionArgs.forward) {
1062
+ // Clip to trailing newlines only if we the motion goes forward.
1063
+ clipToLine(cm, curStart, curEnd);
1064
+ }
1065
+ operatorArgs.registerName = registerName;
1066
+ // Keep track of linewise as it affects how paste and change behave.
1067
+ operatorArgs.linewise = linewise;
1068
+ operators[operator](cm, operatorArgs, vim, curStart,
1069
+ curEnd, curOriginal);
1070
+ if (vim.visualMode) {
1071
+ exitVisualMode(cm, vim);
1072
+ }
1073
+ if (operatorArgs.enterInsertMode) {
1074
+ actions.enterInsertMode(cm);
1075
+ }
1076
+ }
1077
+ },
1078
+ recordLastEdit: function(cm, vim, inputState) {
1079
+ vim.lastEdit = inputState;
1080
+ }
1081
+ };
1082
+
1083
+ /**
1084
+ * typedef {Object{line:number,ch:number}} Cursor An object containing the
1085
+ * position of the cursor.
1086
+ */
1087
+ // All of the functions below return Cursor objects.
1088
+ var motions = {
1089
+ moveToTopLine: function(cm, motionArgs) {
1090
+ var line = getUserVisibleLines(cm).top + motionArgs.repeat -1;
1091
+ return { line: line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(line)) };
1092
+ },
1093
+ moveToMiddleLine: function(cm) {
1094
+ var range = getUserVisibleLines(cm);
1095
+ var line = Math.floor((range.top + range.bottom) * 0.5);
1096
+ return { line: line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(line)) };
1097
+ },
1098
+ moveToBottomLine: function(cm, motionArgs) {
1099
+ var line = getUserVisibleLines(cm).bottom - motionArgs.repeat +1;
1100
+ return { line: line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(line)) };
1101
+ },
1102
+ expandToLine: function(cm, motionArgs) {
1103
+ // Expands forward to end of line, and then to next line if repeat is
1104
+ // >1. Does not handle backward motion!
1105
+ var cur = cm.getCursor();
1106
+ return { line: cur.line + motionArgs.repeat - 1, ch: Infinity };
1107
+ },
1108
+ findNext: function(cm, motionArgs, vim) {
1109
+ var state = getSearchState(cm);
1110
+ var query = state.getQuery();
1111
+ if (!query) {
1112
+ return;
1113
+ }
1114
+ var prev = !motionArgs.forward;
1115
+ // If search is initiated with ? instead of /, negate direction.
1116
+ prev = (state.isReversed()) ? !prev : prev;
1117
+ highlightSearchMatches(cm, query);
1118
+ return findNext(cm, prev/** prev */, query, motionArgs.repeat);
1119
+ },
1120
+ goToMark: function(cm, motionArgs, vim) {
1121
+ var mark = vim.marks[motionArgs.selectedCharacter];
1122
+ if (mark) {
1123
+ return mark.find();
1124
+ }
1125
+ return null;
1126
+ },
1127
+ jumpToMark: function(cm, motionArgs, vim) {
1128
+ var best = cm.getCursor();
1129
+ for (var i = 0; i < motionArgs.repeat; i++) {
1130
+ var cursor = best;
1131
+ for (var key in vim.marks) {
1132
+ if (!isLowerCase(key)) {
1133
+ continue;
1134
+ }
1135
+ var mark = vim.marks[key].find();
1136
+ var isWrongDirection = (motionArgs.forward) ?
1137
+ cursorIsBefore(mark, cursor) : cursorIsBefore(cursor, mark)
1138
+
1139
+ if (isWrongDirection) {
1140
+ continue;
1141
+ }
1142
+ if (motionArgs.linewise && (mark.line == cursor.line)) {
1143
+ continue;
1144
+ }
1145
+
1146
+ var equal = cursorEqual(cursor, best);
1147
+ var between = (motionArgs.forward) ?
1148
+ cusrorIsBetween(cursor, mark, best) :
1149
+ cusrorIsBetween(best, mark, cursor);
1150
+
1151
+ if (equal || between) {
1152
+ best = mark;
1153
+ }
1154
+ }
1155
+ }
1156
+
1157
+ if (motionArgs.linewise) {
1158
+ // Vim places the cursor on the first non-whitespace character of
1159
+ // the line if there is one, else it places the cursor at the end
1160
+ // of the line, regardless of whether a mark was found.
1161
+ best.ch = findFirstNonWhiteSpaceCharacter(cm.getLine(best.line));
1162
+ }
1163
+ return best;
1164
+ },
1165
+ moveByCharacters: function(cm, motionArgs) {
1166
+ var cur = cm.getCursor();
1167
+ var repeat = motionArgs.repeat;
1168
+ var ch = motionArgs.forward ? cur.ch + repeat : cur.ch - repeat;
1169
+ return { line: cur.line, ch: ch };
1170
+ },
1171
+ moveByLines: function(cm, motionArgs, vim) {
1172
+ var cur = cm.getCursor();
1173
+ var endCh = cur.ch;
1174
+ // Depending what our last motion was, we may want to do different
1175
+ // things. If our last motion was moving vertically, we want to
1176
+ // preserve the HPos from our last horizontal move. If our last motion
1177
+ // was going to the end of a line, moving vertically we should go to
1178
+ // the end of the line, etc.
1179
+ switch (vim.lastMotion) {
1180
+ case this.moveByLines:
1181
+ case this.moveByDisplayLines:
1182
+ case this.moveByScroll:
1183
+ case this.moveToColumn:
1184
+ case this.moveToEol:
1185
+ endCh = vim.lastHPos;
1186
+ break;
1187
+ default:
1188
+ vim.lastHPos = endCh;
1189
+ }
1190
+ var repeat = motionArgs.repeat+(motionArgs.repeatOffset||0);
1191
+ var line = motionArgs.forward ? cur.line + repeat : cur.line - repeat;
1192
+ if (line < cm.firstLine() || line > cm.lastLine() ) {
1193
+ return null;
1194
+ }
1195
+ if(motionArgs.toFirstChar){
1196
+ endCh=findFirstNonWhiteSpaceCharacter(cm.getLine(line));
1197
+ vim.lastHPos = endCh;
1198
+ }
1199
+ vim.lastHSPos = cm.charCoords({line:line, ch:endCh},"div").left;
1200
+ return { line: line, ch: endCh };
1201
+ },
1202
+ moveByDisplayLines: function(cm, motionArgs, vim) {
1203
+ var cur = cm.getCursor();
1204
+ switch (vim.lastMotion) {
1205
+ case this.moveByDisplayLines:
1206
+ case this.moveByScroll:
1207
+ case this.moveByLines:
1208
+ case this.moveToColumn:
1209
+ case this.moveToEol:
1210
+ break;
1211
+ default:
1212
+ vim.lastHSPos = cm.charCoords(cur,"div").left;
1213
+ }
1214
+ var repeat = motionArgs.repeat;
1215
+ var res=cm.findPosV(cur,(motionArgs.forward ? repeat : -repeat),"line",vim.lastHSPos);
1216
+ if (res.hitSide) {
1217
+ if (motionArgs.forward) {
1218
+ var lastCharCoords = cm.charCoords(res, 'div');
1219
+ var goalCoords = { top: lastCharCoords.top + 8, left: vim.lastHSPos };
1220
+ var res = cm.coordsChar(goalCoords, 'div');
1221
+ } else {
1222
+ var resCoords = cm.charCoords({ line: cm.firstLine(), ch: 0}, 'div');
1223
+ resCoords.left = vim.lastHSPos;
1224
+ res = cm.coordsChar(resCoords, 'div');
1225
+ }
1226
+ }
1227
+ vim.lastHPos = res.ch;
1228
+ return res;
1229
+ },
1230
+ moveByPage: function(cm, motionArgs) {
1231
+ // CodeMirror only exposes functions that move the cursor page down, so
1232
+ // doing this bad hack to move the cursor and move it back. evalInput
1233
+ // will move the cursor to where it should be in the end.
1234
+ var curStart = cm.getCursor();
1235
+ var repeat = motionArgs.repeat;
1236
+ cm.moveV((motionArgs.forward ? repeat : -repeat), 'page');
1237
+ var curEnd = cm.getCursor();
1238
+ cm.setCursor(curStart);
1239
+ return curEnd;
1240
+ },
1241
+ moveByParagraph: function(cm, motionArgs) {
1242
+ var line = cm.getCursor().line;
1243
+ var repeat = motionArgs.repeat;
1244
+ var inc = motionArgs.forward ? 1 : -1;
1245
+ for (var i = 0; i < repeat; i++) {
1246
+ if ((!motionArgs.forward && line === cm.firstLine() ) ||
1247
+ (motionArgs.forward && line == cm.lastLine())) {
1248
+ break;
1249
+ }
1250
+ line += inc;
1251
+ while (line !== cm.firstLine() && line != cm.lastLine() && cm.getLine(line)) {
1252
+ line += inc;
1253
+ }
1254
+ }
1255
+ return { line: line, ch: 0 };
1256
+ },
1257
+ moveByScroll: function(cm, motionArgs, vim) {
1258
+ var globalState = getVimGlobalState();
1259
+ var scrollbox = cm.getScrollInfo();
1260
+ var curEnd = null;
1261
+ var repeat = motionArgs.repeat;
1262
+ if (!repeat) {
1263
+ repeat = scrollbox.clientHeight / (2 * cm.defaultTextHeight());
1264
+ }
1265
+ var orig = cm.charCoords(cm.getCursor(), 'local');
1266
+ motionArgs.repeat = repeat;
1267
+ var curEnd = motions.moveByDisplayLines(cm, motionArgs, vim);
1268
+ if (!curEnd) {
1269
+ return null;
1270
+ }
1271
+ var dest = cm.charCoords(curEnd, 'local');
1272
+ cm.scrollTo(null, scrollbox.top + dest.top - orig.top);
1273
+ return curEnd;
1274
+ },
1275
+ moveByWords: function(cm, motionArgs) {
1276
+ return moveToWord(cm, motionArgs.repeat, !!motionArgs.forward,
1277
+ !!motionArgs.wordEnd, !!motionArgs.bigWord);
1278
+ },
1279
+ moveTillCharacter: function(cm, motionArgs) {
1280
+ var repeat = motionArgs.repeat;
1281
+ var curEnd = moveToCharacter(cm, repeat, motionArgs.forward,
1282
+ motionArgs.selectedCharacter);
1283
+ var increment = motionArgs.forward ? -1 : 1;
1284
+ recordLastCharacterSearch(increment, motionArgs);
1285
+ if(!curEnd)return cm.getCursor();
1286
+ curEnd.ch += increment;
1287
+ return curEnd;
1288
+ },
1289
+ moveToCharacter: function(cm, motionArgs) {
1290
+ var repeat = motionArgs.repeat;
1291
+ recordLastCharacterSearch(0, motionArgs);
1292
+ return moveToCharacter(cm, repeat, motionArgs.forward,
1293
+ motionArgs.selectedCharacter) || cm.getCursor();
1294
+ },
1295
+ moveToSymbol: function(cm, motionArgs) {
1296
+ var repeat = motionArgs.repeat;
1297
+ return findSymbol(cm, repeat, motionArgs.forward,
1298
+ motionArgs.selectedCharacter) || cm.getCursor();
1299
+ },
1300
+ moveToColumn: function(cm, motionArgs, vim) {
1301
+ var repeat = motionArgs.repeat;
1302
+ // repeat is equivalent to which column we want to move to!
1303
+ vim.lastHPos = repeat - 1;
1304
+ vim.lastHSPos = cm.charCoords(cm.getCursor(),"div").left;
1305
+ return moveToColumn(cm, repeat);
1306
+ },
1307
+ moveToEol: function(cm, motionArgs, vim) {
1308
+ var cur = cm.getCursor();
1309
+ vim.lastHPos = Infinity;
1310
+ var retval={ line: cur.line + motionArgs.repeat - 1, ch: Infinity }
1311
+ var end=cm.clipPos(retval);
1312
+ end.ch--;
1313
+ vim.lastHSPos = cm.charCoords(end,"div").left;
1314
+ return retval;
1315
+ },
1316
+ moveToFirstNonWhiteSpaceCharacter: function(cm) {
1317
+ // Go to the start of the line where the text begins, or the end for
1318
+ // whitespace-only lines
1319
+ var cursor = cm.getCursor();
1320
+ return { line: cursor.line,
1321
+ ch: findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line)) };
1322
+ },
1323
+ moveToMatchedSymbol: function(cm, motionArgs) {
1324
+ var cursor = cm.getCursor();
1325
+ var symbol = cm.getLine(cursor.line).charAt(cursor.ch);
1326
+ if (isMatchableSymbol(symbol)) {
1327
+ return findMatchedSymbol(cm, cm.getCursor(), motionArgs.symbol);
1328
+ } else {
1329
+ return cursor;
1330
+ }
1331
+ },
1332
+ moveToStartOfLine: function(cm) {
1333
+ var cursor = cm.getCursor();
1334
+ return { line: cursor.line, ch: 0 };
1335
+ },
1336
+ moveToLineOrEdgeOfDocument: function(cm, motionArgs) {
1337
+ var lineNum = motionArgs.forward ? cm.lastLine() : cm.firstLine();
1338
+ if (motionArgs.repeatIsExplicit) {
1339
+ lineNum = motionArgs.repeat - cm.getOption('firstLineNumber');
1340
+ }
1341
+ return { line: lineNum,
1342
+ ch: findFirstNonWhiteSpaceCharacter(cm.getLine(lineNum)) };
1343
+ },
1344
+ textObjectManipulation: function(cm, motionArgs) {
1345
+ var character = motionArgs.selectedCharacter;
1346
+ // Inclusive is the difference between a and i
1347
+ // TODO: Instead of using the additional text object map to perform text
1348
+ // object operations, merge the map into the defaultKeyMap and use
1349
+ // motionArgs to define behavior. Define separate entries for 'aw',
1350
+ // 'iw', 'a[', 'i[', etc.
1351
+ var inclusive = !motionArgs.textObjectInner;
1352
+ if (!textObjects[character]) {
1353
+ // No text object defined for this, don't move.
1354
+ return null;
1355
+ }
1356
+ var tmp = textObjects[character](cm, inclusive);
1357
+ var start = tmp.start;
1358
+ var end = tmp.end;
1359
+ return [start, end];
1360
+ },
1361
+ repeatLastCharacterSearch: function(cm, motionArgs) {
1362
+ var lastSearch = getVimGlobalState().lastChararacterSearch;
1363
+ var repeat = motionArgs.repeat;
1364
+ var forward = motionArgs.forward === lastSearch.forward;
1365
+ var increment = (lastSearch.increment ? 1 : 0) * (forward ? -1 : 1);
1366
+ cm.moveH(-increment, 'char');
1367
+ motionArgs.inclusive = forward ? true : false;
1368
+ var curEnd = moveToCharacter(cm, repeat, forward, lastSearch.selectedCharacter);
1369
+ if (!curEnd) {
1370
+ cm.moveH(increment, 'char')
1371
+ return cm.getCursor();
1372
+ }
1373
+ curEnd.ch += increment;
1374
+ return curEnd;
1375
+ }
1376
+ };
1377
+
1378
+ var operators = {
1379
+ change: function(cm, operatorArgs, vim, curStart, curEnd) {
1380
+ getVimGlobalState().registerController.pushText(
1381
+ operatorArgs.registerName, 'change', cm.getRange(curStart, curEnd),
1382
+ operatorArgs.linewise);
1383
+ if (operatorArgs.linewise) {
1384
+ // Delete starting at the first nonwhitespace character of the first
1385
+ // line, instead of from the start of the first line. This way we get
1386
+ // an indent when we get into insert mode. This behavior isn't quite
1387
+ // correct because we should treat this as a completely new line, and
1388
+ // indent should be whatever codemirror thinks is the right indent.
1389
+ // But cm.indentLine doesn't seem work on empty lines.
1390
+ // TODO: Fix the above.
1391
+ curStart.ch =
1392
+ findFirstNonWhiteSpaceCharacter(cm.getLine(curStart.line));
1393
+ // Insert an additional newline so that insert mode can start there.
1394
+ // curEnd should be on the first character of the new line.
1395
+ cm.replaceRange('\n', curStart, curEnd);
1396
+ } else {
1397
+ cm.replaceRange('', curStart, curEnd);
1398
+ }
1399
+ cm.setCursor(curStart);
1400
+ },
1401
+ // delete is a javascript keyword.
1402
+ 'delete': function(cm, operatorArgs, vim, curStart, curEnd) {
1403
+ getVimGlobalState().registerController.pushText(
1404
+ operatorArgs.registerName, 'delete', cm.getRange(curStart, curEnd),
1405
+ operatorArgs.linewise);
1406
+ cm.replaceRange('', curStart, curEnd);
1407
+ if (operatorArgs.linewise) {
1408
+ cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm));
1409
+ } else {
1410
+ cm.setCursor(curStart);
1411
+ }
1412
+ },
1413
+ indent: function(cm, operatorArgs, vim, curStart, curEnd) {
1414
+ var startLine = curStart.line;
1415
+ var endLine = curEnd.line;
1416
+ // In visual mode, n> shifts the selection right n times, instead of
1417
+ // shifting n lines right once.
1418
+ var repeat = (vim.visualMode) ? operatorArgs.repeat : 1;
1419
+ if (operatorArgs.linewise) {
1420
+ // The only way to delete a newline is to delete until the start of
1421
+ // the next line, so in linewise mode evalInput will include the next
1422
+ // line. We don't want this in indent, so we go back a line.
1423
+ endLine--;
1424
+ }
1425
+ for (var i = startLine; i <= endLine; i++) {
1426
+ for (var j = 0; j < repeat; j++) {
1427
+ cm.indentLine(i, operatorArgs.indentRight);
1428
+ }
1429
+ }
1430
+ cm.setCursor(curStart);
1431
+ cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm));
1432
+ },
1433
+ swapcase: function(cm, operatorArgs, vim, curStart, curEnd, curOriginal) {
1434
+ var toSwap = cm.getRange(curStart, curEnd);
1435
+ var swapped = '';
1436
+ for (var i = 0; i < toSwap.length; i++) {
1437
+ var character = toSwap.charAt(i);
1438
+ swapped += isUpperCase(character) ? character.toLowerCase() :
1439
+ character.toUpperCase();
1440
+ }
1441
+ cm.replaceRange(swapped, curStart, curEnd);
1442
+ cm.setCursor(curOriginal);
1443
+ },
1444
+ yank: function(cm, operatorArgs, vim, curStart, curEnd, curOriginal) {
1445
+ getVimGlobalState().registerController.pushText(
1446
+ operatorArgs.registerName, 'yank',
1447
+ cm.getRange(curStart, curEnd), operatorArgs.linewise);
1448
+ cm.setCursor(curOriginal);
1449
+ }
1450
+ };
1451
+
1452
+ var actions = {
1453
+ jumpListWalk: function(cm, actionArgs, vim) {
1454
+ if (vim.visualMode) {
1455
+ return;
1456
+ }
1457
+ var repeat = actionArgs.repeat;
1458
+ var forward = actionArgs.forward;
1459
+ var jumpList = getVimGlobalState().jumpList;
1460
+
1461
+ var mark = jumpList.move(forward ? repeat : -repeat);
1462
+ var markPos = mark ? mark.find() : cm.getCursor();
1463
+ cm.setCursor(markPos);
1464
+ },
1465
+ scrollToCursor: function(cm, actionArgs) {
1466
+ var lineNum = cm.getCursor().line;
1467
+ var heightProp = window.getComputedStyle(cm.getScrollerElement()).
1468
+ getPropertyValue('height');
1469
+ var height = parseInt(heightProp);
1470
+ var y = cm.charCoords({line: lineNum, ch: 0}, "local").top;
1471
+ var halfHeight = parseInt(height) / 2;
1472
+ switch (actionArgs.position) {
1473
+ case 'center': y = y - (height / 2) + 10;
1474
+ break;
1475
+ case 'bottom': y = y - height;
1476
+ break;
1477
+ case 'top': break;
1478
+ }
1479
+ cm.scrollTo(null, y);
1480
+ // The calculations are slightly off, use scrollIntoView to nudge the
1481
+ // view into the right place.
1482
+ cm.scrollIntoView();
1483
+ },
1484
+ enterInsertMode: function(cm, actionArgs) {
1485
+ var insertAt = (actionArgs) ? actionArgs.insertAt : null;
1486
+ if (insertAt == 'eol') {
1487
+ var cursor = cm.getCursor();
1488
+ cursor = { line: cursor.line, ch: lineLength(cm, cursor.line) };
1489
+ cm.setCursor(cursor);
1490
+ } else if (insertAt == 'charAfter') {
1491
+ cm.setCursor(offsetCursor(cm.getCursor(), 0, 1));
1492
+ }
1493
+ cm.setOption('keyMap', 'vim-insert');
1494
+ },
1495
+ toggleVisualMode: function(cm, actionArgs, vim) {
1496
+ var repeat = actionArgs.repeat;
1497
+ var curStart = cm.getCursor();
1498
+ var curEnd;
1499
+ // TODO: The repeat should actually select number of characters/lines
1500
+ // equal to the repeat times the size of the previous visual
1501
+ // operation.
1502
+ if (!vim.visualMode) {
1503
+ vim.visualMode = true;
1504
+ vim.visualLine = !!actionArgs.linewise;
1505
+ if (vim.visualLine) {
1506
+ curStart.ch = 0;
1507
+ curEnd = clipCursorToContent(cm, {
1508
+ line: curStart.line + repeat - 1,
1509
+ ch: lineLength(cm, curStart.line)
1510
+ }, true /** includeLineBreak */);
1511
+ } else {
1512
+ curEnd = clipCursorToContent(cm, {
1513
+ line: curStart.line,
1514
+ ch: curStart.ch + repeat
1515
+ }, true /** includeLineBreak */);
1516
+ }
1517
+ // Make the initial selection.
1518
+ if (!actionArgs.repeatIsExplicit && !vim.visualLine) {
1519
+ // This is a strange case. Here the implicit repeat is 1. The
1520
+ // following commands lets the cursor hover over the 1 character
1521
+ // selection.
1522
+ cm.setCursor(curEnd);
1523
+ cm.setSelection(curEnd, curStart);
1524
+ } else {
1525
+ cm.setSelection(curStart, curEnd);
1526
+ }
1527
+ } else {
1528
+ curStart = cm.getCursor('anchor');
1529
+ curEnd = cm.getCursor('head');
1530
+ if (!vim.visualLine && actionArgs.linewise) {
1531
+ // Shift-V pressed in characterwise visual mode. Switch to linewise
1532
+ // visual mode instead of exiting visual mode.
1533
+ vim.visualLine = true;
1534
+ curStart.ch = cursorIsBefore(curStart, curEnd) ? 0 :
1535
+ lineLength(cm, curStart.line);
1536
+ curEnd.ch = cursorIsBefore(curStart, curEnd) ?
1537
+ lineLength(cm, curEnd.line) : 0;
1538
+ cm.setSelection(curStart, curEnd);
1539
+ } else if (vim.visualLine && !actionArgs.linewise) {
1540
+ // v pressed in linewise visual mode. Switch to characterwise visual
1541
+ // mode instead of exiting visual mode.
1542
+ vim.visualLine = false;
1543
+ } else {
1544
+ exitVisualMode(cm, vim);
1545
+ }
1546
+ }
1547
+ updateMark(cm, vim, '<', cursorIsBefore(curStart, curEnd) ? curStart
1548
+ : curEnd);
1549
+ updateMark(cm, vim, '>', cursorIsBefore(curStart, curEnd) ? curEnd
1550
+ : curStart);
1551
+ },
1552
+ joinLines: function(cm, actionArgs, vim) {
1553
+ var curStart, curEnd;
1554
+ if (vim.visualMode) {
1555
+ curStart = cm.getCursor('anchor');
1556
+ curEnd = cm.getCursor('head');
1557
+ curEnd.ch = lineLength(cm, curEnd.line) - 1;
1558
+ } else {
1559
+ // Repeat is the number of lines to join. Minimum 2 lines.
1560
+ var repeat = Math.max(actionArgs.repeat, 2);
1561
+ curStart = cm.getCursor();
1562
+ curEnd = clipCursorToContent(cm, { line: curStart.line + repeat - 1,
1563
+ ch: Infinity });
1564
+ }
1565
+ var finalCh = 0;
1566
+ cm.operation(function() {
1567
+ for (var i = curStart.line; i < curEnd.line; i++) {
1568
+ finalCh = lineLength(cm, curStart.line);
1569
+ var tmp = { line: curStart.line + 1,
1570
+ ch: lineLength(cm, curStart.line + 1) };
1571
+ var text = cm.getRange(curStart, tmp);
1572
+ text = text.replace(/\n\s*/g, ' ');
1573
+ cm.replaceRange(text, curStart, tmp);
1574
+ }
1575
+ var curFinalPos = { line: curStart.line, ch: finalCh };
1576
+ cm.setCursor(curFinalPos);
1577
+ });
1578
+ },
1579
+ newLineAndEnterInsertMode: function(cm, actionArgs) {
1580
+ var insertAt = cm.getCursor();
1581
+ if (insertAt.line === cm.firstLine() && !actionArgs.after) {
1582
+ // Special case for inserting newline before start of document.
1583
+ cm.replaceRange('\n', { line: cm.firstLine(), ch: 0 });
1584
+ cm.setCursor(cm.firstLine(), 0);
1585
+ } else {
1586
+ insertAt.line = (actionArgs.after) ? insertAt.line :
1587
+ insertAt.line - 1;
1588
+ insertAt.ch = lineLength(cm, insertAt.line);
1589
+ cm.setCursor(insertAt);
1590
+ var newlineFn = CodeMirror.commands.newlineAndIndentContinueComment ||
1591
+ CodeMirror.commands.newlineAndIndent;
1592
+ newlineFn(cm);
1593
+ }
1594
+ this.enterInsertMode(cm);
1595
+ },
1596
+ paste: function(cm, actionArgs, vim) {
1597
+ var cur = cm.getCursor();
1598
+ var register = getVimGlobalState().registerController.getRegister(
1599
+ actionArgs.registerName);
1600
+ if (!register.text) {
1601
+ return;
1602
+ }
1603
+ for (var text = '', i = 0; i < actionArgs.repeat; i++) {
1604
+ text += register.text;
1605
+ }
1606
+ var linewise = register.linewise;
1607
+ if (linewise) {
1608
+ if (actionArgs.after) {
1609
+ // Move the newline at the end to the start instead, and paste just
1610
+ // before the newline character of the line we are on right now.
1611
+ text = '\n' + text.slice(0, text.length - 1);
1612
+ cur.ch = lineLength(cm, cur.line);
1613
+ } else {
1614
+ cur.ch = 0;
1615
+ }
1616
+ } else {
1617
+ cur.ch += actionArgs.after ? 1 : 0;
1618
+ }
1619
+ cm.replaceRange(text, cur);
1620
+ // Now fine tune the cursor to where we want it.
1621
+ var curPosFinal;
1622
+ var idx;
1623
+ if (linewise && actionArgs.after) {
1624
+ curPosFinal = { line: cur.line + 1,
1625
+ ch: findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line + 1)) };
1626
+ } else if (linewise && !actionArgs.after) {
1627
+ curPosFinal = { line: cur.line,
1628
+ ch: findFirstNonWhiteSpaceCharacter(cm.getLine(cur.line)) };
1629
+ } else if (!linewise && actionArgs.after) {
1630
+ idx = cm.indexFromPos(cur);
1631
+ curPosFinal = cm.posFromIndex(idx + text.length - 1);
1632
+ } else {
1633
+ idx = cm.indexFromPos(cur);
1634
+ curPosFinal = cm.posFromIndex(idx + text.length);
1635
+ }
1636
+ cm.setCursor(curPosFinal);
1637
+ },
1638
+ undo: function(cm, actionArgs) {
1639
+ repeatFn(cm, CodeMirror.commands.undo, actionArgs.repeat)();
1640
+ },
1641
+ redo: function(cm, actionArgs) {
1642
+ repeatFn(cm, CodeMirror.commands.redo, actionArgs.repeat)();
1643
+ },
1644
+ setRegister: function(cm, actionArgs, vim) {
1645
+ vim.inputState.registerName = actionArgs.selectedCharacter;
1646
+ },
1647
+ setMark: function(cm, actionArgs, vim) {
1648
+ var markName = actionArgs.selectedCharacter;
1649
+ updateMark(cm, vim, markName, cm.getCursor());
1650
+ },
1651
+ replace: function(cm, actionArgs, vim) {
1652
+ var replaceWith = actionArgs.selectedCharacter;
1653
+ var curStart = cm.getCursor();
1654
+ var replaceTo;
1655
+ var curEnd;
1656
+ if(vim.visualMode){
1657
+ curStart=cm.getCursor('start');
1658
+ curEnd=cm.getCursor('end');
1659
+ // workaround to catch the character under the cursor
1660
+ // existing workaround doesn't cover actions
1661
+ curEnd=cm.clipPos({line: curEnd.line, ch: curEnd.ch+1});
1662
+ }else{
1663
+ var line = cm.getLine(curStart.line);
1664
+ replaceTo = curStart.ch + actionArgs.repeat;
1665
+ if (replaceTo > line.length) {
1666
+ replaceTo=line.length;
1667
+ }
1668
+ curEnd = { line: curStart.line, ch: replaceTo };
1669
+ }
1670
+ if(replaceWith=='\n'){
1671
+ if(!vim.visualMode) cm.replaceRange('', curStart, curEnd);
1672
+ // special case, where vim help says to replace by just one line-break
1673
+ (CodeMirror.commands.newlineAndIndentContinueComment || CodeMirror.commands.newlineAndIndent)(cm);
1674
+ }else {
1675
+ var replaceWithStr=cm.getRange(curStart, curEnd);
1676
+ //replace all characters in range by selected, but keep linebreaks
1677
+ replaceWithStr=replaceWithStr.replace(/[^\n]/g,replaceWith);
1678
+ cm.replaceRange(replaceWithStr, curStart, curEnd);
1679
+ if(vim.visualMode){
1680
+ cm.setCursor(curStart);
1681
+ exitVisualMode(cm,vim);
1682
+ }else{
1683
+ cm.setCursor(offsetCursor(curEnd, 0, -1));
1684
+ }
1685
+ }
1686
+ },
1687
+ enterReplaceMode: function(cm, actionArgs) {
1688
+ cm.setOption('keyMap', 'vim-replace');
1689
+ cm.toggleOverwrite();
1690
+ },
1691
+ incrementNumberToken: function(cm, actionArgs, vim) {
1692
+ var cur = cm.getCursor();
1693
+ var lineStr = cm.getLine(cur.line);
1694
+ var re = /-?\d+/g;
1695
+ var match;
1696
+ var start;
1697
+ var end;
1698
+ var numberStr;
1699
+ var token;
1700
+ while ((match = re.exec(lineStr)) !== null) {
1701
+ token = match[0];
1702
+ start = match.index;
1703
+ end = start + token.length;
1704
+ if(cur.ch < end)break;
1705
+ }
1706
+ if(!actionArgs.backtrack && (end <= cur.ch))return;
1707
+ if (token) {
1708
+ var increment = actionArgs.increase ? 1 : -1;
1709
+ var number = parseInt(token) + (increment * actionArgs.repeat);
1710
+ var from = {ch:start, line:cur.line};
1711
+ var to = {ch:end, line:cur.line};
1712
+ numberStr = number.toString();
1713
+ cm.replaceRange(numberStr, from, to);
1714
+ } else {
1715
+ return;
1716
+ }
1717
+ cm.setCursor({line: cur.line, ch: start + numberStr.length - 1});
1718
+ },
1719
+ repeatLastEdit: function(cm, actionArgs, vim) {
1720
+ // TODO: Make this repeat insert mode changes.
1721
+ var lastEdit = vim.lastEdit;
1722
+ if (lastEdit) {
1723
+ if (actionArgs.repeat && actionArgs.repeatIsExplicit) {
1724
+ vim.lastEdit.repeatOverride = actionArgs.repeat;
1725
+ }
1726
+ var currentInputState = vim.inputState;
1727
+ vim.inputState = vim.lastEdit;
1728
+ commandDispatcher.evalInput(cm, vim);
1729
+ vim.inputState = currentInputState;
1730
+ }
1731
+ }
1732
+ };
1733
+
1734
+ var textObjects = {
1735
+ // TODO: lots of possible exceptions that can be thrown here. Try da(
1736
+ // outside of a () block.
1737
+ // TODO: implement text objects for the reverse like }. Should just be
1738
+ // an additional mapping after moving to the defaultKeyMap.
1739
+ 'w': function(cm, inclusive) {
1740
+ return expandWordUnderCursor(cm, inclusive, true /** forward */,
1741
+ false /** bigWord */);
1742
+ },
1743
+ 'W': function(cm, inclusive) {
1744
+ return expandWordUnderCursor(cm, inclusive,
1745
+ true /** forward */, true /** bigWord */);
1746
+ },
1747
+ '{': function(cm, inclusive) {
1748
+ return selectCompanionObject(cm, '}', inclusive);
1749
+ },
1750
+ '(': function(cm, inclusive) {
1751
+ return selectCompanionObject(cm, ')', inclusive);
1752
+ },
1753
+ '[': function(cm, inclusive) {
1754
+ return selectCompanionObject(cm, ']', inclusive);
1755
+ },
1756
+ '\'': function(cm, inclusive) {
1757
+ return findBeginningAndEnd(cm, "'", inclusive);
1758
+ },
1759
+ '\"': function(cm, inclusive) {
1760
+ return findBeginningAndEnd(cm, '"', inclusive);
1761
+ }
1762
+ };
1763
+
1764
+ /*
1765
+ * Below are miscellaneous utility functions used by vim.js
1766
+ */
1767
+
1768
+ /**
1769
+ * Clips cursor to ensure that line is within the buffer's range
1770
+ * If includeLineBreak is true, then allow cur.ch == lineLength.
1771
+ */
1772
+ function clipCursorToContent(cm, cur, includeLineBreak) {
1773
+ var line = Math.min(Math.max(cm.firstLine(), cur.line), cm.lastLine() );
1774
+ var maxCh = lineLength(cm, line) - 1;
1775
+ maxCh = (includeLineBreak) ? maxCh + 1 : maxCh;
1776
+ var ch = Math.min(Math.max(0, cur.ch), maxCh);
1777
+ return { line: line, ch: ch };
1778
+ }
1779
+ // Merge arguments in place, for overriding arguments.
1780
+ function mergeArgs(to, from) {
1781
+ for (var prop in from) {
1782
+ if (from.hasOwnProperty(prop)) {
1783
+ to[prop] = from[prop];
1784
+ }
1785
+ }
1786
+ }
1787
+ function copyArgs(args) {
1788
+ var ret = {};
1789
+ for (var prop in args) {
1790
+ if (args.hasOwnProperty(prop)) {
1791
+ ret[prop] = args[prop];
1792
+ }
1793
+ }
1794
+ return ret;
1795
+ }
1796
+ function offsetCursor(cur, offsetLine, offsetCh) {
1797
+ return { line: cur.line + offsetLine, ch: cur.ch + offsetCh };
1798
+ }
1799
+ function arrayEq(a1, a2) {
1800
+ if (a1.length != a2.length) {
1801
+ return false;
1802
+ }
1803
+ for (var i = 0; i < a1.length; i++) {
1804
+ if (a1[i] != a2[i]) {
1805
+ return false;
1806
+ }
1807
+ }
1808
+ return true;
1809
+ }
1810
+ function matchKeysPartial(pressed, mapped) {
1811
+ for (var i = 0; i < pressed.length; i++) {
1812
+ // 'character' means any character. For mark, register commads, etc.
1813
+ if (pressed[i] != mapped[i] && mapped[i] != 'character') {
1814
+ return false;
1815
+ }
1816
+ }
1817
+ return true;
1818
+ }
1819
+ function arrayIsSubsetFromBeginning(small, big) {
1820
+ for (var i = 0; i < small.length; i++) {
1821
+ if (small[i] != big[i]) {
1822
+ return false;
1823
+ }
1824
+ }
1825
+ return true;
1826
+ }
1827
+ function repeatFn(cm, fn, repeat) {
1828
+ return function() {
1829
+ for (var i = 0; i < repeat; i++) {
1830
+ fn(cm);
1831
+ }
1832
+ };
1833
+ }
1834
+ function copyCursor(cur) {
1835
+ return { line: cur.line, ch: cur.ch };
1836
+ }
1837
+ function cursorEqual(cur1, cur2) {
1838
+ return cur1.ch == cur2.ch && cur1.line == cur2.line;
1839
+ }
1840
+ function cursorIsBefore(cur1, cur2) {
1841
+ if (cur1.line < cur2.line) {
1842
+ return true;
1843
+ } else if (cur1.line == cur2.line && cur1.ch < cur2.ch) {
1844
+ return true;
1845
+ }
1846
+ return false;
1847
+ }
1848
+ function cusrorIsBetween(cur1, cur2, cur3) {
1849
+ // returns true if cur2 is between cur1 and cur3.
1850
+ var cur1before2 = cursorIsBefore(cur1, cur2);
1851
+ var cur2before3 = cursorIsBefore(cur2, cur3);
1852
+ return cur1before2 && cur2before3;
1853
+ }
1854
+ function lineLength(cm, lineNum) {
1855
+ return cm.getLine(lineNum).length;
1856
+ }
1857
+ function reverse(s){
1858
+ return s.split("").reverse().join("");
1859
+ }
1860
+ function trim(s) {
1861
+ if (s.trim) {
1862
+ return s.trim();
1863
+ } else {
1864
+ return s.replace(/^\s+|\s+$/g, '');
1865
+ }
1866
+ }
1867
+ function escapeRegex(s) {
1868
+ return s.replace(/([.?*+$\[\]\/\\(){}|\-])/g, "\\$1");
1869
+ }
1870
+
1871
+ function exitVisualMode(cm, vim) {
1872
+ vim.visualMode = false;
1873
+ vim.visualLine = false;
1874
+ var selectionStart = cm.getCursor('anchor');
1875
+ var selectionEnd = cm.getCursor('head');
1876
+ if (!cursorEqual(selectionStart, selectionEnd)) {
1877
+ // Clear the selection and set the cursor only if the selection has not
1878
+ // already been cleared. Otherwise we risk moving the cursor somewhere
1879
+ // it's not supposed to be.
1880
+ cm.setCursor(clipCursorToContent(cm, selectionEnd));
1881
+ }
1882
+ }
1883
+
1884
+ // Remove any trailing newlines from the selection. For
1885
+ // example, with the caret at the start of the last word on the line,
1886
+ // 'dw' should word, but not the newline, while 'w' should advance the
1887
+ // caret to the first character of the next line.
1888
+ function clipToLine(cm, curStart, curEnd) {
1889
+ var selection = cm.getRange(curStart, curEnd);
1890
+ var lines = selection.split('\n');
1891
+ if (lines.length > 1 && isWhiteSpaceString(lines.pop())) {
1892
+ curEnd.line--;
1893
+ curEnd.ch = lineLength(cm, curEnd.line);
1894
+ }
1895
+ }
1896
+
1897
+ // Expand the selection to line ends.
1898
+ function expandSelectionToLine(cm, curStart, curEnd) {
1899
+ curStart.ch = 0;
1900
+ curEnd.ch = 0;
1901
+ curEnd.line++;
1902
+ }
1903
+
1904
+ function findFirstNonWhiteSpaceCharacter(text) {
1905
+ if (!text) {
1906
+ return 0;
1907
+ }
1908
+ var firstNonWS = text.search(/\S/);
1909
+ return firstNonWS == -1 ? text.length : firstNonWS;
1910
+ }
1911
+
1912
+ function expandWordUnderCursor(cm, inclusive, forward, bigWord, noSymbol) {
1913
+ var cur = cm.getCursor();
1914
+ var line = cm.getLine(cur.line);
1915
+ var idx = cur.ch;
1916
+
1917
+ // Seek to first word or non-whitespace character, depending on if
1918
+ // noSymbol is true.
1919
+ var textAfterIdx = line.substring(idx);
1920
+ var firstMatchedChar;
1921
+ if (noSymbol) {
1922
+ firstMatchedChar = textAfterIdx.search(/\w/);
1923
+ } else {
1924
+ firstMatchedChar = textAfterIdx.search(/\S/);
1925
+ }
1926
+ if (firstMatchedChar == -1) {
1927
+ return null;
1928
+ }
1929
+ idx += firstMatchedChar;
1930
+ textAfterIdx = line.substring(idx);
1931
+ var textBeforeIdx = line.substring(0, idx);
1932
+
1933
+ var matchRegex;
1934
+ // Greedy matchers for the "word" we are trying to expand.
1935
+ if (bigWord) {
1936
+ matchRegex = /^\S+/;
1937
+ } else {
1938
+ if ((/\w/).test(line.charAt(idx))) {
1939
+ matchRegex = /^\w+/;
1940
+ } else {
1941
+ matchRegex = /^[^\w\s]+/;
1942
+ }
1943
+ }
1944
+
1945
+ var wordAfterRegex = matchRegex.exec(textAfterIdx);
1946
+ var wordStart = idx;
1947
+ var wordEnd = idx + wordAfterRegex[0].length;
1948
+ // TODO: Find a better way to do this. It will be slow on very long lines.
1949
+ var revTextBeforeIdx = reverse(textBeforeIdx);
1950
+ var wordBeforeRegex = matchRegex.exec(revTextBeforeIdx);
1951
+ if (wordBeforeRegex) {
1952
+ wordStart -= wordBeforeRegex[0].length;
1953
+ }
1954
+
1955
+ if (inclusive) {
1956
+ // If present, trim all whitespace after word.
1957
+ // Otherwise, trim all whitespace before word.
1958
+ var textAfterWordEnd = line.substring(wordEnd);
1959
+ var whitespacesAfterWord = textAfterWordEnd.match(/^\s*/)[0].length;
1960
+ if (whitespacesAfterWord > 0) {
1961
+ wordEnd += whitespacesAfterWord;
1962
+ } else {
1963
+ var revTrim = revTextBeforeIdx.length - wordStart;
1964
+ var textBeforeWordStart = revTextBeforeIdx.substring(revTrim);
1965
+ var whitespacesBeforeWord = textBeforeWordStart.match(/^\s*/)[0].length;
1966
+ wordStart -= whitespacesBeforeWord;
1967
+ }
1968
+ }
1969
+
1970
+ return { start: { line: cur.line, ch: wordStart },
1971
+ end: { line: cur.line, ch: wordEnd }};
1972
+ }
1973
+
1974
+ function recordJumpPosition(cm, oldCur, newCur) {
1975
+ if(!cursorEqual(oldCur, newCur)) {
1976
+ getVimGlobalState().jumpList.add(cm, oldCur, newCur);
1977
+ }
1978
+ }
1979
+
1980
+ function recordLastCharacterSearch(increment, args) {
1981
+ var vimGlobalState = getVimGlobalState();
1982
+ vimGlobalState.lastChararacterSearch.increment = increment;
1983
+ vimGlobalState.lastChararacterSearch.forward = args.forward;
1984
+ vimGlobalState.lastChararacterSearch.selectedCharacter = args.selectedCharacter;
1985
+ }
1986
+
1987
+ var symbolToMode = {
1988
+ '(': 'bracket', ')': 'bracket', '{': 'bracket', '}': 'bracket',
1989
+ '[': 'section', ']': 'section',
1990
+ '*': 'comment', '/': 'comment',
1991
+ 'm': 'method', 'M': 'method',
1992
+ '#': 'preprocess'
1993
+ };
1994
+ var findSymbolModes = {
1995
+ bracket: {
1996
+ isComplete: function(state) {
1997
+ if (state.nextCh === state.symb) {
1998
+ state.depth++;
1999
+ if(state.depth >= 1)return true;
2000
+ } else if (state.nextCh === state.reverseSymb) {
2001
+ state.depth--;
2002
+ }
2003
+ return false;
2004
+ }
2005
+ },
2006
+ section: {
2007
+ init: function(state) {
2008
+ state.curMoveThrough = true;
2009
+ state.symb = (state.forward ? ']' : '[') === state.symb ? '{' : '}';
2010
+ },
2011
+ isComplete: function(state) {
2012
+ return state.index === 0 && state.nextCh === state.symb;
2013
+ }
2014
+ },
2015
+ comment: {
2016
+ isComplete: function(state) {
2017
+ var found = state.lastCh === '*' && state.nextCh === '/';
2018
+ state.lastCh = state.nextCh;
2019
+ return found;
2020
+ }
2021
+ },
2022
+ // TODO: The original Vim implementation only operates on level 1 and 2.
2023
+ // The current implementation doesn't check for code block level and
2024
+ // therefore it operates on any levels.
2025
+ method: {
2026
+ init: function(state) {
2027
+ state.symb = (state.symb === 'm' ? '{' : '}');
2028
+ state.reverseSymb = state.symb === '{' ? '}' : '{';
2029
+ },
2030
+ isComplete: function(state) {
2031
+ if(state.nextCh === state.symb)return true;
2032
+ return false;
2033
+ }
2034
+ },
2035
+ preprocess: {
2036
+ init: function(state) {
2037
+ state.index = 0;
2038
+ },
2039
+ isComplete: function(state) {
2040
+ if (state.nextCh === '#') {
2041
+ var token = state.lineText.match(/#(\w+)/)[1];
2042
+ if (token === 'endif') {
2043
+ if (state.forward && state.depth === 0) {
2044
+ return true;
2045
+ }
2046
+ state.depth++;
2047
+ } else if (token === 'if') {
2048
+ if (!state.forward && state.depth === 0) {
2049
+ return true;
2050
+ }
2051
+ state.depth--;
2052
+ }
2053
+ if(token === 'else' && state.depth === 0)return true;
2054
+ }
2055
+ return false;
2056
+ }
2057
+ }
2058
+ };
2059
+ function findSymbol(cm, repeat, forward, symb) {
2060
+ var cur = cm.getCursor();
2061
+ var increment = forward ? 1 : -1;
2062
+ var endLine = forward ? cm.lineCount() : -1;
2063
+ var curCh = cur.ch;
2064
+ var line = cur.line;
2065
+ var lineText = cm.getLine(line);
2066
+ var state = {
2067
+ lineText: lineText,
2068
+ nextCh: lineText.charAt(curCh),
2069
+ lastCh: null,
2070
+ index: curCh,
2071
+ symb: symb,
2072
+ reverseSymb: (forward ? { ')': '(', '}': '{' } : { '(': ')', '{': '}' })[symb],
2073
+ forward: forward,
2074
+ depth: 0,
2075
+ curMoveThrough: false
2076
+ };
2077
+ var mode = symbolToMode[symb];
2078
+ if(!mode)return cur;
2079
+ var init = findSymbolModes[mode].init;
2080
+ var isComplete = findSymbolModes[mode].isComplete;
2081
+ if(init)init(state);
2082
+ while (line !== endLine && repeat) {
2083
+ state.index += increment;
2084
+ state.nextCh = state.lineText.charAt(state.index);
2085
+ if (!state.nextCh) {
2086
+ line += increment;
2087
+ state.lineText = cm.getLine(line) || '';
2088
+ if (increment > 0) {
2089
+ state.index = 0;
2090
+ } else {
2091
+ var lineLen = state.lineText.length;
2092
+ state.index = (lineLen > 0) ? (lineLen-1) : 0;
2093
+ }
2094
+ state.nextCh = state.lineText.charAt(state.index);
2095
+ }
2096
+ if (isComplete(state)) {
2097
+ cur.line = line;
2098
+ cur.ch = state.index;
2099
+ repeat--;
2100
+ }
2101
+ }
2102
+ if (state.nextCh || state.curMoveThrough) {
2103
+ return { line: line, ch: state.index };
2104
+ }
2105
+ return cur;
2106
+ }
2107
+
2108
+ /*
2109
+ * Returns the boundaries of the next word. If the cursor in the middle of
2110
+ * the word, then returns the boundaries of the current word, starting at
2111
+ * the cursor. If the cursor is at the start/end of a word, and we are going
2112
+ * forward/backward, respectively, find the boundaries of the next word.
2113
+ *
2114
+ * @param {CodeMirror} cm CodeMirror object.
2115
+ * @param {Cursor} cur The cursor position.
2116
+ * @param {boolean} forward True to search forward. False to search
2117
+ * backward.
2118
+ * @param {boolean} bigWord True if punctuation count as part of the word.
2119
+ * False if only [a-zA-Z0-9] characters count as part of the word.
2120
+ * @return {Object{from:number, to:number, line: number}} The boundaries of
2121
+ * the word, or null if there are no more words.
2122
+ */
2123
+ // TODO: Treat empty lines (with no whitespace) as words.
2124
+ function findWord(cm, cur, forward, bigWord) {
2125
+ var lineNum = cur.line;
2126
+ var pos = cur.ch;
2127
+ var line = cm.getLine(lineNum);
2128
+ var dir = forward ? 1 : -1;
2129
+ var regexps = bigWord ? bigWordRegexp : wordRegexp;
2130
+
2131
+ while (true) {
2132
+ var stop = (dir > 0) ? line.length : -1;
2133
+ var wordStart = stop, wordEnd = stop;
2134
+ // Find bounds of next word.
2135
+ while (pos != stop) {
2136
+ var foundWord = false;
2137
+ for (var i = 0; i < regexps.length && !foundWord; ++i) {
2138
+ if (regexps[i].test(line.charAt(pos))) {
2139
+ wordStart = pos;
2140
+ // Advance to end of word.
2141
+ while (pos != stop && regexps[i].test(line.charAt(pos))) {
2142
+ pos += dir;
2143
+ }
2144
+ wordEnd = pos;
2145
+ foundWord = wordStart != wordEnd;
2146
+ if (wordStart == cur.ch && lineNum == cur.line &&
2147
+ wordEnd == wordStart + dir) {
2148
+ // We started at the end of a word. Find the next one.
2149
+ continue;
2150
+ } else {
2151
+ return {
2152
+ from: Math.min(wordStart, wordEnd + 1),
2153
+ to: Math.max(wordStart, wordEnd),
2154
+ line: lineNum };
2155
+ }
2156
+ }
2157
+ }
2158
+ if (!foundWord) {
2159
+ pos += dir;
2160
+ }
2161
+ }
2162
+ // Advance to next/prev line.
2163
+ lineNum += dir;
2164
+ if (!isLine(cm, lineNum)) {
2165
+ return null;
2166
+ }
2167
+ line = cm.getLine(lineNum);
2168
+ pos = (dir > 0) ? 0 : line.length;
2169
+ }
2170
+ // Should never get here.
2171
+ throw 'The impossible happened.';
2172
+ }
2173
+
2174
+ /**
2175
+ * @param {CodeMirror} cm CodeMirror object.
2176
+ * @param {int} repeat Number of words to move past.
2177
+ * @param {boolean} forward True to search forward. False to search
2178
+ * backward.
2179
+ * @param {boolean} wordEnd True to move to end of word. False to move to
2180
+ * beginning of word.
2181
+ * @param {boolean} bigWord True if punctuation count as part of the word.
2182
+ * False if only alphabet characters count as part of the word.
2183
+ * @return {Cursor} The position the cursor should move to.
2184
+ */
2185
+ function moveToWord(cm, repeat, forward, wordEnd, bigWord) {
2186
+ var cur = cm.getCursor();
2187
+ for (var i = 0; i < repeat; i++) {
2188
+ var startCh = cur.ch, startLine = cur.line, word;
2189
+ var movedToNextWord = false;
2190
+ while (!movedToNextWord) {
2191
+ // Search and advance.
2192
+ word = findWord(cm, cur, forward, bigWord);
2193
+ movedToNextWord = true;
2194
+ if (word) {
2195
+ // Move to the word we just found. If by moving to the word we end
2196
+ // up in the same spot, then move an extra character and search
2197
+ // again.
2198
+ cur.line = word.line;
2199
+ if (forward && wordEnd) {
2200
+ // 'e'
2201
+ cur.ch = word.to - 1;
2202
+ } else if (forward && !wordEnd) {
2203
+ // 'w'
2204
+ if (inRangeInclusive(cur.ch, word.from, word.to) &&
2205
+ word.line == startLine) {
2206
+ // Still on the same word. Go to the next one.
2207
+ movedToNextWord = false;
2208
+ cur.ch = word.to - 1;
2209
+ } else {
2210
+ cur.ch = word.from;
2211
+ }
2212
+ } else if (!forward && wordEnd) {
2213
+ // 'ge'
2214
+ if (inRangeInclusive(cur.ch, word.from, word.to) &&
2215
+ word.line == startLine) {
2216
+ // still on the same word. Go to the next one.
2217
+ movedToNextWord = false;
2218
+ cur.ch = word.from;
2219
+ } else {
2220
+ cur.ch = word.to;
2221
+ }
2222
+ } else if (!forward && !wordEnd) {
2223
+ // 'b'
2224
+ cur.ch = word.from;
2225
+ }
2226
+ } else {
2227
+ // No more words to be found. Move to the end.
2228
+ if (forward) {
2229
+ return { line: cur.line, ch: lineLength(cm, cur.line) };
2230
+ } else {
2231
+ return { line: cur.line, ch: 0 };
2232
+ }
2233
+ }
2234
+ }
2235
+ }
2236
+ return cur;
2237
+ }
2238
+
2239
+ function moveToCharacter(cm, repeat, forward, character) {
2240
+ var cur = cm.getCursor();
2241
+ var start = cur.ch;
2242
+ var idx;
2243
+ for (var i = 0; i < repeat; i ++) {
2244
+ var line = cm.getLine(cur.line);
2245
+ idx = charIdxInLine(start, line, character, forward, true);
2246
+ if (idx == -1) {
2247
+ return null;
2248
+ }
2249
+ start = idx;
2250
+ }
2251
+ return { line: cm.getCursor().line, ch: idx };
2252
+ }
2253
+
2254
+ function moveToColumn(cm, repeat) {
2255
+ // repeat is always >= 1, so repeat - 1 always corresponds
2256
+ // to the column we want to go to.
2257
+ var line = cm.getCursor().line;
2258
+ return clipCursorToContent(cm, { line: line, ch: repeat - 1 });
2259
+ }
2260
+
2261
+ function updateMark(cm, vim, markName, pos) {
2262
+ if (!inArray(markName, validMarks)) {
2263
+ return;
2264
+ }
2265
+ if (vim.marks[markName]) {
2266
+ vim.marks[markName].clear();
2267
+ }
2268
+ vim.marks[markName] = cm.setBookmark(pos);
2269
+ }
2270
+
2271
+ function charIdxInLine(start, line, character, forward, includeChar) {
2272
+ // Search for char in line.
2273
+ // motion_options: {forward, includeChar}
2274
+ // If includeChar = true, include it too.
2275
+ // If forward = true, search forward, else search backwards.
2276
+ // If char is not found on this line, do nothing
2277
+ var idx;
2278
+ if (forward) {
2279
+ idx = line.indexOf(character, start + 1);
2280
+ if (idx != -1 && !includeChar) {
2281
+ idx -= 1;
2282
+ }
2283
+ } else {
2284
+ idx = line.lastIndexOf(character, start - 1);
2285
+ if (idx != -1 && !includeChar) {
2286
+ idx += 1;
2287
+ }
2288
+ }
2289
+ return idx;
2290
+ }
2291
+
2292
+ function findMatchedSymbol(cm, cur, symb) {
2293
+ var line = cur.line;
2294
+ symb = symb ? symb : cm.getLine(line).charAt(cur.ch);
2295
+
2296
+ var reverseSymb = ({
2297
+ '(': ')', ')': '(',
2298
+ '[': ']', ']': '[',
2299
+ '{': '}', '}': '{'})[symb];
2300
+
2301
+ // Couldn't find a matching symbol, abort
2302
+ if (!reverseSymb) {
2303
+ return cur;
2304
+ }
2305
+
2306
+ // set our increment to move forward (+1) or backwards (-1)
2307
+ // depending on which bracket we're matching
2308
+ var increment = ({'(': 1, '{': 1, '[': 1})[symb] || -1;
2309
+ var endLine = increment === 1 ? cm.lineCount() : -1;
2310
+ var depth = 1, nextCh = symb, index = cur.ch, lineText = cm.getLine(line);
2311
+ // Simple search for closing paren--just count openings and closings till
2312
+ // we find our match
2313
+ // TODO: use info from CodeMirror to ignore closing brackets in comments
2314
+ // and quotes, etc.
2315
+ while (line !== endLine && depth > 0) {
2316
+ index += increment;
2317
+ nextCh = lineText.charAt(index);
2318
+ if (!nextCh) {
2319
+ line += increment;
2320
+ lineText = cm.getLine(line) || '';
2321
+ if (increment > 0) {
2322
+ index = 0;
2323
+ } else {
2324
+ var lineLen = lineText.length;
2325
+ index = (lineLen > 0) ? (lineLen-1) : 0;
2326
+ }
2327
+ nextCh = lineText.charAt(index);
2328
+ }
2329
+ if (nextCh === symb) {
2330
+ depth++;
2331
+ } else if (nextCh === reverseSymb) {
2332
+ depth--;
2333
+ }
2334
+ }
2335
+
2336
+ if (nextCh) {
2337
+ return { line: line, ch: index };
2338
+ }
2339
+ return cur;
2340
+ }
2341
+
2342
+ function selectCompanionObject(cm, revSymb, inclusive) {
2343
+ var cur = cm.getCursor();
2344
+
2345
+ var end = findMatchedSymbol(cm, cur, revSymb);
2346
+ var start = findMatchedSymbol(cm, end);
2347
+ start.ch += inclusive ? 1 : 0;
2348
+ end.ch += inclusive ? 0 : 1;
2349
+
2350
+ return { start: start, end: end };
2351
+ }
2352
+
2353
+ function regexLastIndexOf(string, pattern, startIndex) {
2354
+ for (var i = !startIndex ? string.length : startIndex;
2355
+ i >= 0; --i) {
2356
+ if (pattern.test(string.charAt(i))) {
2357
+ return i;
2358
+ }
2359
+ }
2360
+ return -1;
2361
+ }
2362
+
2363
+ // Takes in a symbol and a cursor and tries to simulate text objects that
2364
+ // have identical opening and closing symbols
2365
+ // TODO support across multiple lines
2366
+ function findBeginningAndEnd(cm, symb, inclusive) {
2367
+ var cur = cm.getCursor();
2368
+ var line = cm.getLine(cur.line);
2369
+ var chars = line.split('');
2370
+ var start, end, i, len;
2371
+ var firstIndex = chars.indexOf(symb);
2372
+
2373
+ // the decision tree is to always look backwards for the beginning first,
2374
+ // but if the cursor is in front of the first instance of the symb,
2375
+ // then move the cursor forward
2376
+ if (cur.ch < firstIndex) {
2377
+ cur.ch = firstIndex;
2378
+ // Why is this line even here???
2379
+ // cm.setCursor(cur.line, firstIndex+1);
2380
+ }
2381
+ // otherwise if the cursor is currently on the closing symbol
2382
+ else if (firstIndex < cur.ch && chars[cur.ch] == symb) {
2383
+ end = cur.ch; // assign end to the current cursor
2384
+ --cur.ch; // make sure to look backwards
2385
+ }
2386
+
2387
+ // if we're currently on the symbol, we've got a start
2388
+ if (chars[cur.ch] == symb && !end) {
2389
+ start = cur.ch + 1; // assign start to ahead of the cursor
2390
+ } else {
2391
+ // go backwards to find the start
2392
+ for (i = cur.ch; i > -1 && !start; i--) {
2393
+ if (chars[i] == symb) {
2394
+ start = i + 1;
2395
+ }
2396
+ }
2397
+ }
2398
+
2399
+ // look forwards for the end symbol
2400
+ if (start && !end) {
2401
+ for (i = start, len = chars.length; i < len && !end; i++) {
2402
+ if (chars[i] == symb) {
2403
+ end = i;
2404
+ }
2405
+ }
2406
+ }
2407
+
2408
+ // nothing found
2409
+ if (!start || !end) {
2410
+ return { start: cur, end: cur };
2411
+ }
2412
+
2413
+ // include the symbols
2414
+ if (inclusive) {
2415
+ --start; ++end;
2416
+ }
2417
+
2418
+ return {
2419
+ start: { line: cur.line, ch: start },
2420
+ end: { line: cur.line, ch: end }
2421
+ };
2422
+ }
2423
+
2424
+ // Search functions
2425
+ function SearchState() {}
2426
+ SearchState.prototype = {
2427
+ getQuery: function() {
2428
+ return getVimGlobalState().query;
2429
+ },
2430
+ setQuery: function(query) {
2431
+ getVimGlobalState().query = query;
2432
+ },
2433
+ getOverlay: function() {
2434
+ return this.searchOverlay;
2435
+ },
2436
+ setOverlay: function(overlay) {
2437
+ this.searchOverlay = overlay;
2438
+ },
2439
+ isReversed: function() {
2440
+ return getVimGlobalState().isReversed;
2441
+ },
2442
+ setReversed: function(reversed) {
2443
+ getVimGlobalState().isReversed = reversed;
2444
+ }
2445
+ };
2446
+ function getSearchState(cm) {
2447
+ var vim = getVimState(cm);
2448
+ return vim.searchState_ || (vim.searchState_ = new SearchState());
2449
+ }
2450
+ function dialog(cm, template, shortText, onClose, options) {
2451
+ if (cm.openDialog) {
2452
+ cm.openDialog(template, onClose, { bottom: true, value: options.value,
2453
+ onKeyDown: options.onKeyDown, onKeyUp: options.onKeyUp });
2454
+ }
2455
+ else {
2456
+ onClose(prompt(shortText, ""));
2457
+ }
2458
+ }
2459
+ function findUnescapedSlashes(str) {
2460
+ var escapeNextChar = false;
2461
+ var slashes = [];
2462
+ for (var i = 0; i < str.length; i++) {
2463
+ var c = str.charAt(i);
2464
+ if (!escapeNextChar && c == '/') {
2465
+ slashes.push(i);
2466
+ }
2467
+ escapeNextChar = (c == '\\');
2468
+ }
2469
+ return slashes;
2470
+ }
2471
+ /**
2472
+ * Extract the regular expression from the query and return a Regexp object.
2473
+ * Returns null if the query is blank.
2474
+ * If ignoreCase is passed in, the Regexp object will have the 'i' flag set.
2475
+ * If smartCase is passed in, and the query contains upper case letters,
2476
+ * then ignoreCase is overridden, and the 'i' flag will not be set.
2477
+ * If the query contains the /i in the flag part of the regular expression,
2478
+ * then both ignoreCase and smartCase are ignored, and 'i' will be passed
2479
+ * through to the Regex object.
2480
+ */
2481
+ function parseQuery(cm, query, ignoreCase, smartCase) {
2482
+ // Check if the query is already a regex.
2483
+ if (query instanceof RegExp) { return query; }
2484
+ // First try to extract regex + flags from the input. If no flags found,
2485
+ // extract just the regex. IE does not accept flags directly defined in
2486
+ // the regex string in the form /regex/flags
2487
+ var slashes = findUnescapedSlashes(query);
2488
+ var regexPart;
2489
+ var forceIgnoreCase;
2490
+ if (!slashes.length) {
2491
+ // Query looks like 'regexp'
2492
+ regexPart = query;
2493
+ } else {
2494
+ // Query looks like 'regexp/...'
2495
+ regexPart = query.substring(0, slashes[0]);
2496
+ var flagsPart = query.substring(slashes[0]);
2497
+ forceIgnoreCase = (flagsPart.indexOf('i') != -1);
2498
+ }
2499
+ if (!regexPart) {
2500
+ return null;
2501
+ }
2502
+ if (smartCase) {
2503
+ ignoreCase = (/^[^A-Z]*$/).test(regexPart);
2504
+ }
2505
+ var regexp = new RegExp(regexPart,
2506
+ (ignoreCase || forceIgnoreCase) ? 'i' : undefined);
2507
+ return regexp;
2508
+ }
2509
+ function showConfirm(cm, text) {
2510
+ if (cm.openConfirm) {
2511
+ cm.openConfirm('<span style="color: red">' + text +
2512
+ '</span> <button type="button">OK</button>', function() {},
2513
+ {bottom: true});
2514
+ } else {
2515
+ alert(text);
2516
+ }
2517
+ }
2518
+ function makePrompt(prefix, desc) {
2519
+ var raw = '';
2520
+ if (prefix) {
2521
+ raw += '<span style="font-family: monospace">' + prefix + '</span>';
2522
+ }
2523
+ raw += '<input type="text"/> ' +
2524
+ '<span style="color: #888">';
2525
+ if (desc) {
2526
+ raw += '<span style="color: #888">';
2527
+ raw += desc;
2528
+ raw += '</span>';
2529
+ }
2530
+ return raw;
2531
+ }
2532
+ var searchPromptDesc = '(Javascript regexp)';
2533
+ function showPrompt(cm, options) {
2534
+ var shortText = (options.prefix || '') + ' ' + (options.desc || '');
2535
+ var prompt = makePrompt(options.prefix, options.desc);
2536
+ dialog(cm, prompt, shortText, options.onClose, options);
2537
+ }
2538
+ function regexEqual(r1, r2) {
2539
+ if (r1 instanceof RegExp && r2 instanceof RegExp) {
2540
+ var props = ["global", "multiline", "ignoreCase", "source"];
2541
+ for (var i = 0; i < props.length; i++) {
2542
+ var prop = props[i];
2543
+ if (r1[prop] !== r2[prop]) {
2544
+ return(false);
2545
+ }
2546
+ }
2547
+ return(true);
2548
+ }
2549
+ return(false);
2550
+ }
2551
+ // Returns true if the query is valid.
2552
+ function updateSearchQuery(cm, rawQuery, ignoreCase, smartCase) {
2553
+ if (!rawQuery) {
2554
+ return;
2555
+ }
2556
+ var state = getSearchState(cm);
2557
+ var query = parseQuery(cm, rawQuery, !!ignoreCase, !!smartCase);
2558
+ if (!query) {
2559
+ return;
2560
+ }
2561
+ highlightSearchMatches(cm, query);
2562
+ if (regexEqual(query, state.getQuery())) {
2563
+ return query;
2564
+ }
2565
+ state.setQuery(query);
2566
+ return query;
2567
+ }
2568
+ function searchOverlay(query) {
2569
+ if (query.source.charAt(0) == '^') {
2570
+ var matchSol = true;
2571
+ }
2572
+ return {
2573
+ token: function(stream) {
2574
+ if (matchSol && !stream.sol()) {
2575
+ stream.skipToEnd();
2576
+ return;
2577
+ }
2578
+ var match = stream.match(query, false);
2579
+ if (match) {
2580
+ if (match[0].length == 0) {
2581
+ // Matched empty string, skip to next.
2582
+ stream.next();
2583
+ return;
2584
+ }
2585
+ if (!stream.sol()) {
2586
+ // Backtrack 1 to match \b
2587
+ stream.backUp(1);
2588
+ if (!query.exec(stream.next() + match[0])) {
2589
+ stream.next();
2590
+ return null;
2591
+ }
2592
+ }
2593
+ stream.match(query);
2594
+ return "searching";
2595
+ }
2596
+ while (!stream.eol()) {
2597
+ stream.next();
2598
+ if (stream.match(query, false)) break;
2599
+ }
2600
+ },
2601
+ query: query
2602
+ };
2603
+ }
2604
+ function highlightSearchMatches(cm, query) {
2605
+ var overlay = getSearchState(cm).getOverlay();
2606
+ if (!overlay || query != overlay.query) {
2607
+ if (overlay) {
2608
+ cm.removeOverlay(overlay);
2609
+ }
2610
+ overlay = searchOverlay(query);
2611
+ cm.addOverlay(overlay);
2612
+ getSearchState(cm).setOverlay(overlay);
2613
+ }
2614
+ }
2615
+ function findNext(cm, prev, query, repeat) {
2616
+ if (repeat === undefined) { repeat = 1; }
2617
+ return cm.operation(function() {
2618
+ var pos = cm.getCursor();
2619
+ if (!prev) {
2620
+ pos.ch += 1;
2621
+ }
2622
+ var cursor = cm.getSearchCursor(query, pos);
2623
+ for (var i = 0; i < repeat; i++) {
2624
+ if (!cursor.find(prev)) {
2625
+ // SearchCursor may have returned null because it hit EOF, wrap
2626
+ // around and try again.
2627
+ cursor = cm.getSearchCursor(query,
2628
+ (prev) ? { line: cm.lastLine() } : {line: cm.firstLine(), ch: 0} );
2629
+ if (!cursor.find(prev)) {
2630
+ return;
2631
+ }
2632
+ }
2633
+ }
2634
+ return cursor.from();
2635
+ });}
2636
+ function clearSearchHighlight(cm) {
2637
+ cm.removeOverlay(getSearchState(cm).getOverlay());
2638
+ getSearchState(cm).setOverlay(null);
2639
+ }
2640
+ /**
2641
+ * Check if pos is in the specified range, INCLUSIVE.
2642
+ * Range can be specified with 1 or 2 arguments.
2643
+ * If the first range argument is an array, treat it as an array of line
2644
+ * numbers. Match pos against any of the lines.
2645
+ * If the first range argument is a number,
2646
+ * if there is only 1 range argument, check if pos has the same line
2647
+ * number
2648
+ * if there are 2 range arguments, then check if pos is in between the two
2649
+ * range arguments.
2650
+ */
2651
+ function isInRange(pos, start, end) {
2652
+ if (typeof pos != 'number') {
2653
+ // Assume it is a cursor position. Get the line number.
2654
+ pos = pos.line;
2655
+ }
2656
+ if (start instanceof Array) {
2657
+ return inArray(pos, start);
2658
+ } else {
2659
+ if (end) {
2660
+ return (pos >= start && pos <= end);
2661
+ } else {
2662
+ return pos == start;
2663
+ }
2664
+ }
2665
+ }
2666
+ function getUserVisibleLines(cm) {
2667
+ var scrollInfo = cm.getScrollInfo();
2668
+ var occludeTorleranceTop = 6;
2669
+ var occludeTorleranceBottom = 10;
2670
+ var from = cm.coordsChar({left:0, top: occludeTorleranceTop}, 'local');
2671
+ var bottomY = scrollInfo.clientHeight - occludeTorleranceBottom;
2672
+ var to = cm.coordsChar({left:0, top: bottomY}, 'local');
2673
+ return {top: from.line, bottom: to.line};
2674
+ }
2675
+
2676
+ // Ex command handling
2677
+ // Care must be taken when adding to the default Ex command map. For any
2678
+ // pair of commands that have a shared prefix, at least one of their
2679
+ // shortNames must not match the prefix of the other command.
2680
+ var defaultExCommandMap = [
2681
+ { name: 'map', type: 'builtIn' },
2682
+ { name: 'write', shortName: 'w', type: 'builtIn' },
2683
+ { name: 'undo', shortName: 'u', type: 'builtIn' },
2684
+ { name: 'redo', shortName: 'red', type: 'builtIn' },
2685
+ { name: 'substitute', shortName: 's', type: 'builtIn'},
2686
+ { name: 'nohlsearch', shortName: 'noh', type: 'builtIn'},
2687
+ { name: 'delmarks', shortName: 'delm', type: 'builtin'}
2688
+ ];
2689
+ Vim.ExCommandDispatcher = function() {
2690
+ this.buildCommandMap_();
2691
+ };
2692
+ Vim.ExCommandDispatcher.prototype = {
2693
+ processCommand: function(cm, input) {
2694
+ var inputStream = new CodeMirror.StringStream(input);
2695
+ var params = {};
2696
+ params.input = input;
2697
+ try {
2698
+ this.parseInput_(cm, inputStream, params);
2699
+ } catch(e) {
2700
+ showConfirm(cm, e);
2701
+ return;
2702
+ }
2703
+ var commandName;
2704
+ if (!params.commandName) {
2705
+ // If only a line range is defined, move to the line.
2706
+ if (params.line !== undefined) {
2707
+ commandName = 'move';
2708
+ }
2709
+ } else {
2710
+ var command = this.matchCommand_(params.commandName);
2711
+ if (command) {
2712
+ commandName = command.name;
2713
+ this.parseCommandArgs_(inputStream, params, command);
2714
+ if (command.type == 'exToKey') {
2715
+ // Handle Ex to Key mapping.
2716
+ for (var i = 0; i < command.toKeys.length; i++) {
2717
+ vim.handleKey(cm, command.toKeys[i]);
2718
+ }
2719
+ return;
2720
+ } else if (command.type == 'exToEx') {
2721
+ // Handle Ex to Ex mapping.
2722
+ this.processCommand(cm, command.toInput);
2723
+ return;
2724
+ }
2725
+ }
2726
+ }
2727
+ if (!commandName) {
2728
+ showConfirm(cm, 'Not an editor command ":' + input + '"');
2729
+ return;
2730
+ }
2731
+ exCommands[commandName](cm, params);
2732
+ },
2733
+ parseInput_: function(cm, inputStream, result) {
2734
+ inputStream.eatWhile(':');
2735
+ // Parse range.
2736
+ if (inputStream.eat('%')) {
2737
+ result.line = cm.firstLine();
2738
+ result.lineEnd = cm.lastLine();
2739
+ } else {
2740
+ result.line = this.parseLineSpec_(cm, inputStream);
2741
+ if (result.line !== undefined && inputStream.eat(',')) {
2742
+ result.lineEnd = this.parseLineSpec_(cm, inputStream);
2743
+ }
2744
+ }
2745
+
2746
+ // Parse command name.
2747
+ var commandMatch = inputStream.match(/^(\w+)/);
2748
+ if (commandMatch) {
2749
+ result.commandName = commandMatch[1];
2750
+ } else {
2751
+ result.commandName = inputStream.match(/.*/)[0];
2752
+ }
2753
+
2754
+ return result;
2755
+ },
2756
+ parseLineSpec_: function(cm, inputStream) {
2757
+ var numberMatch = inputStream.match(/^(\d+)/);
2758
+ if (numberMatch) {
2759
+ return parseInt(numberMatch[1], 10) - 1;
2760
+ }
2761
+ switch (inputStream.next()) {
2762
+ case '.':
2763
+ return cm.getCursor().line;
2764
+ case '$':
2765
+ return cm.lastLine();
2766
+ case '\'':
2767
+ var mark = getVimState(cm).marks[inputStream.next()];
2768
+ if (mark && mark.find()) {
2769
+ return mark.find().line;
2770
+ } else {
2771
+ throw "Mark not set";
2772
+ }
2773
+ break;
2774
+ default:
2775
+ inputStream.backUp(1);
2776
+ return cm.getCursor().line;
2777
+ }
2778
+ },
2779
+ parseCommandArgs_: function(inputStream, params, command) {
2780
+ if (inputStream.eol()) {
2781
+ return;
2782
+ }
2783
+ params.argString = inputStream.match(/.*/)[0];
2784
+ // Parse command-line arguments
2785
+ var delim = command.argDelimiter || /\s+/;
2786
+ var args = trim(params.argString).split(delim);
2787
+ if (args.length && args[0]) {
2788
+ params.args = args;
2789
+ }
2790
+ },
2791
+ matchCommand_: function(commandName) {
2792
+ // Return the command in the command map that matches the shortest
2793
+ // prefix of the passed in command name. The match is guaranteed to be
2794
+ // unambiguous if the defaultExCommandMap's shortNames are set up
2795
+ // correctly. (see @code{defaultExCommandMap}).
2796
+ for (var i = commandName.length; i > 0; i--) {
2797
+ var prefix = commandName.substring(0, i);
2798
+ if (this.commandMap_[prefix]) {
2799
+ var command = this.commandMap_[prefix];
2800
+ if (command.name.indexOf(commandName) === 0) {
2801
+ return command;
2802
+ }
2803
+ }
2804
+ }
2805
+ return null;
2806
+ },
2807
+ buildCommandMap_: function() {
2808
+ this.commandMap_ = {};
2809
+ for (var i = 0; i < defaultExCommandMap.length; i++) {
2810
+ var command = defaultExCommandMap[i];
2811
+ var key = command.shortName || command.name;
2812
+ this.commandMap_[key] = command;
2813
+ }
2814
+ },
2815
+ map: function(lhs, rhs) {
2816
+ if (lhs != ':' && lhs.charAt(0) == ':') {
2817
+ var commandName = lhs.substring(1);
2818
+ if (rhs != ':' && rhs.charAt(0) == ':') {
2819
+ // Ex to Ex mapping
2820
+ this.commandMap_[commandName] = {
2821
+ name: commandName,
2822
+ type: 'exToEx',
2823
+ toInput: rhs.substring(1)
2824
+ };
2825
+ } else {
2826
+ // Ex to key mapping
2827
+ this.commandMap_[commandName] = {
2828
+ name: commandName,
2829
+ type: 'exToKey',
2830
+ toKeys: parseKeyString(rhs)
2831
+ };
2832
+ }
2833
+ } else {
2834
+ if (rhs != ':' && rhs.charAt(0) == ':') {
2835
+ // Key to Ex mapping.
2836
+ defaultKeymap.unshift({
2837
+ keys: parseKeyString(lhs),
2838
+ type: 'keyToEx',
2839
+ exArgs: { input: rhs.substring(1) }});
2840
+ } else {
2841
+ // Key to key mapping
2842
+ defaultKeymap.unshift({
2843
+ keys: parseKeyString(lhs),
2844
+ type: 'keyToKey',
2845
+ toKeys: parseKeyString(rhs)
2846
+ });
2847
+ }
2848
+ }
2849
+ }
2850
+ };
2851
+
2852
+ // Converts a key string sequence of the form a<C-w>bd<Left> into Vim's
2853
+ // keymap representation.
2854
+ function parseKeyString(str) {
2855
+ var idx = 0;
2856
+ var keys = [];
2857
+ while (idx < str.length) {
2858
+ if (str.charAt(idx) != '<') {
2859
+ keys.push(str.charAt(idx));
2860
+ idx++;
2861
+ continue;
2862
+ }
2863
+ // Vim key notation here means desktop Vim key-notation.
2864
+ // See :help key-notation in desktop Vim.
2865
+ var vimKeyNotationStart = ++idx;
2866
+ while (str.charAt(idx++) != '>') {}
2867
+ var vimKeyNotation = str.substring(vimKeyNotationStart, idx - 1);
2868
+ var mod='';
2869
+ var match = (/^C-(.+)$/).exec(vimKeyNotation);
2870
+ if (match) {
2871
+ mod='Ctrl-';
2872
+ vimKeyNotation=match[1];
2873
+ }
2874
+ var key;
2875
+ switch (vimKeyNotation) {
2876
+ case 'BS':
2877
+ key = 'Backspace';
2878
+ break;
2879
+ case 'CR':
2880
+ key = 'Enter';
2881
+ break;
2882
+ case 'Del':
2883
+ key = 'Delete';
2884
+ break;
2885
+ default:
2886
+ key = vimKeyNotation;
2887
+ break;
2888
+ }
2889
+ keys.push(mod + key);
2890
+ }
2891
+ return keys;
2892
+ }
2893
+
2894
+ var exCommands = {
2895
+ map: function(cm, params) {
2896
+ var mapArgs = params.args;
2897
+ if (!mapArgs || mapArgs.length < 2) {
2898
+ if (cm) {
2899
+ showConfirm(cm, 'Invalid mapping: ' + params.input);
2900
+ }
2901
+ return;
2902
+ }
2903
+ exCommandDispatcher.map(mapArgs[0], mapArgs[1], cm);
2904
+ },
2905
+ move: function(cm, params) {
2906
+ commandDispatcher.processCommand(cm, getVimState(cm), {
2907
+ type: 'motion',
2908
+ motion: 'moveToLineOrEdgeOfDocument',
2909
+ motionArgs: { forward: false, explicitRepeat: true,
2910
+ linewise: true },
2911
+ repeatOverride: params.line+1});
2912
+ },
2913
+ substitute: function(cm, params) {
2914
+ var argString = params.argString;
2915
+ var slashes = findUnescapedSlashes(argString);
2916
+ if (slashes[0] !== 0) {
2917
+ showConfirm(cm, 'Substitutions should be of the form ' +
2918
+ ':s/pattern/replace/');
2919
+ return;
2920
+ }
2921
+ var regexPart = argString.substring(slashes[0] + 1, slashes[1]);
2922
+ var replacePart = '';
2923
+ var flagsPart;
2924
+ var count;
2925
+ if (slashes[1]) {
2926
+ replacePart = argString.substring(slashes[1] + 1, slashes[2]);
2927
+ }
2928
+ if (slashes[2]) {
2929
+ // After the 3rd slash, we can have flags followed by a space followed
2930
+ // by count.
2931
+ var trailing = argString.substring(slashes[2] + 1).split(' ');
2932
+ flagsPart = trailing[0];
2933
+ count = parseInt(trailing[1]);
2934
+ }
2935
+ if (flagsPart) {
2936
+ regexPart = regexPart + '/' + flagsPart;
2937
+ }
2938
+ if (regexPart) {
2939
+ // If regex part is empty, then use the previous query. Otherwise use
2940
+ // the regex part as the new query.
2941
+ try {
2942
+ updateSearchQuery(cm, regexPart, true /** ignoreCase */,
2943
+ true /** smartCase */);
2944
+ } catch (e) {
2945
+ showConfirm(cm, 'Invalid regex: ' + regexPart);
2946
+ return;
2947
+ }
2948
+ }
2949
+ var state = getSearchState(cm);
2950
+ var query = state.getQuery();
2951
+ var lineStart = params.line || cm.firstLine();
2952
+ var lineEnd = params.lineEnd || lineStart;
2953
+ if (count) {
2954
+ lineStart = lineEnd;
2955
+ lineEnd = lineStart + count - 1;
2956
+ }
2957
+ var startPos = clipCursorToContent(cm, { line: lineStart, ch: 0 });
2958
+ function doReplace() {
2959
+ for (var cursor = cm.getSearchCursor(query, startPos);
2960
+ cursor.findNext() &&
2961
+ isInRange(cursor.from(), lineStart, lineEnd);) {
2962
+ var text = cm.getRange(cursor.from(), cursor.to());
2963
+ var newText = text.replace(query, replacePart);
2964
+ cursor.replace(newText);
2965
+ }
2966
+ var vim = getVimState(cm);
2967
+ if (vim.visualMode) {
2968
+ exitVisualMode(cm, vim);
2969
+ }
2970
+ }
2971
+ cm.operation(doReplace);
2972
+ },
2973
+ redo: CodeMirror.commands.redo,
2974
+ undo: CodeMirror.commands.undo,
2975
+ write: function(cm) {
2976
+ if (CodeMirror.commands.save) {
2977
+ // If a save command is defined, call it.
2978
+ CodeMirror.commands.save(cm);
2979
+ } else {
2980
+ // Saves to text area if no save command is defined.
2981
+ cm.save();
2982
+ }
2983
+ },
2984
+ nohlsearch: function(cm) {
2985
+ clearSearchHighlight(cm);
2986
+ },
2987
+ delmarks: function(cm, params) {
2988
+ if (!params.argString || !params.argString.trim()) {
2989
+ showConfirm(cm, 'Argument required');
2990
+ return;
2991
+ }
2992
+
2993
+ var state = getVimState(cm);
2994
+ var stream = new CodeMirror.StringStream(params.argString.trim());
2995
+ while (!stream.eol()) {
2996
+ stream.eatSpace();
2997
+
2998
+ // Record the streams position at the beginning of the loop for use
2999
+ // in error messages.
3000
+ var count = stream.pos;
3001
+
3002
+ if (!stream.match(/[a-zA-Z]/, false)) {
3003
+ showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));
3004
+ return;
3005
+ }
3006
+
3007
+ var sym = stream.next();
3008
+ // Check if this symbol is part of a range
3009
+ if (stream.match('-', true)) {
3010
+ // This symbol is part of a range.
3011
+
3012
+ // The range must terminate at an alphabetic character.
3013
+ if (!stream.match(/[a-zA-Z]/, false)) {
3014
+ showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));
3015
+ return;
3016
+ }
3017
+
3018
+ var startMark = sym;
3019
+ var finishMark = stream.next();
3020
+ // The range must terminate at an alphabetic character which
3021
+ // shares the same case as the start of the range.
3022
+ if (isLowerCase(startMark) && isLowerCase(finishMark) ||
3023
+ isUpperCase(startMark) && isUpperCase(finishMark)) {
3024
+ var start = startMark.charCodeAt(0);
3025
+ var finish = finishMark.charCodeAt(0);
3026
+ if (start >= finish) {
3027
+ showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count));
3028
+ return;
3029
+ }
3030
+
3031
+ // Because marks are always ASCII values, and we have
3032
+ // determined that they are the same case, we can use
3033
+ // their char codes to iterate through the defined range.
3034
+ for (var j = 0; j <= finish - start; j++) {
3035
+ var mark = String.fromCharCode(start + j);
3036
+ delete state.marks[mark];
3037
+ }
3038
+ } else {
3039
+ showConfirm(cm, 'Invalid argument: ' + startMark + "-");
3040
+ return;
3041
+ }
3042
+ } else {
3043
+ // This symbol is a valid mark, and is not part of a range.
3044
+ delete state.marks[sym];
3045
+ }
3046
+ }
3047
+ }
3048
+ };
3049
+
3050
+ var exCommandDispatcher = new Vim.ExCommandDispatcher();
3051
+
3052
+ // Register Vim with CodeMirror
3053
+ function buildVimKeyMap() {
3054
+ /**
3055
+ * Handle the raw key event from CodeMirror. Translate the
3056
+ * Shift + key modifier to the resulting letter, while preserving other
3057
+ * modifers.
3058
+ */
3059
+ // TODO: Figure out a way to catch capslock.
3060
+ function handleKeyEvent_(cm, key, modifier) {
3061
+ if (isUpperCase(key)) {
3062
+ // Convert to lower case if shift is not the modifier since the key
3063
+ // we get from CodeMirror is always upper case.
3064
+ if (modifier == 'Shift') {
3065
+ modifier = null;
3066
+ }
3067
+ else {
3068
+ key = key.toLowerCase();
3069
+ }
3070
+ }
3071
+ if (modifier) {
3072
+ // Vim will parse modifier+key combination as a single key.
3073
+ key = modifier + '-' + key;
3074
+ }
3075
+ vim.handleKey(cm, key);
3076
+ }
3077
+
3078
+ // Closure to bind CodeMirror, key, modifier.
3079
+ function keyMapper(key, modifier) {
3080
+ return function(cm) {
3081
+ handleKeyEvent_(cm, key, modifier);
3082
+ };
3083
+ }
3084
+
3085
+ var modifiers = ['Shift', 'Ctrl'];
3086
+ var keyMap = {
3087
+ 'nofallthrough': true,
3088
+ 'style': 'fat-cursor'
3089
+ };
3090
+ function bindKeys(keys, modifier) {
3091
+ for (var i = 0; i < keys.length; i++) {
3092
+ var key = keys[i];
3093
+ if (!modifier && inArray(key, specialSymbols)) {
3094
+ // Wrap special symbols with '' because that's how CodeMirror binds
3095
+ // them.
3096
+ key = "'" + key + "'";
3097
+ }
3098
+ if (modifier) {
3099
+ keyMap[modifier + '-' + key] = keyMapper(keys[i], modifier);
3100
+ } else {
3101
+ keyMap[key] = keyMapper(keys[i]);
3102
+ }
3103
+ }
3104
+ }
3105
+ bindKeys(upperCaseAlphabet);
3106
+ bindKeys(upperCaseAlphabet, 'Shift');
3107
+ bindKeys(upperCaseAlphabet, 'Ctrl');
3108
+ bindKeys(specialSymbols);
3109
+ bindKeys(specialSymbols, 'Ctrl');
3110
+ bindKeys(numbers);
3111
+ bindKeys(numbers, 'Ctrl');
3112
+ bindKeys(specialKeys);
3113
+ bindKeys(specialKeys, 'Ctrl');
3114
+ return keyMap;
3115
+ }
3116
+ CodeMirror.keyMap.vim = buildVimKeyMap();
3117
+
3118
+ function exitInsertMode(cm) {
3119
+ cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1, true);
3120
+ cm.setOption('keyMap', 'vim');
3121
+ }
3122
+
3123
+ CodeMirror.keyMap['vim-insert'] = {
3124
+ // TODO: override navigation keys so that Esc will cancel automatic
3125
+ // indentation from o, O, i_<CR>
3126
+ 'Esc': exitInsertMode,
3127
+ 'Ctrl-[': exitInsertMode,
3128
+ 'Ctrl-C': exitInsertMode,
3129
+ 'Ctrl-N': 'autocomplete',
3130
+ 'Ctrl-P': 'autocomplete',
3131
+ 'Enter': function(cm) {
3132
+ var fn = CodeMirror.commands.newlineAndIndentContinueComment ||
3133
+ CodeMirror.commands.newlineAndIndent;
3134
+ fn(cm);
3135
+ },
3136
+ fallthrough: ['default']
3137
+ };
3138
+
3139
+ function exitReplaceMode(cm) {
3140
+ cm.toggleOverwrite();
3141
+ cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1, true);
3142
+ cm.setOption('keyMap', 'vim');
3143
+ }
3144
+
3145
+ CodeMirror.keyMap['vim-replace'] = {
3146
+ 'Esc': exitReplaceMode,
3147
+ 'Ctrl-[': exitReplaceMode,
3148
+ 'Ctrl-C': exitReplaceMode,
3149
+ 'Backspace': 'goCharLeft',
3150
+ fallthrough: ['default']
3151
+ };
3152
+
3153
+ return vimApi;
3154
+ };
3155
+ // Initialize Vim and make it available as an API.
3156
+ var vim = Vim();
3157
+ CodeMirror.Vim = vim;
3158
+ }
3159
+ )();