luca 0.9.89 → 0.9.91

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