codemirror-rails 3.22 → 3.23

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4503a996155c96877d36e19f5e461db92383dbf4
4
- data.tar.gz: 5abe2e7b9dc2f39f4ec1185d220e3dc8ab9a8eb9
3
+ metadata.gz: a43600e7cc5c0826859fd737519ab1a93720bfd5
4
+ data.tar.gz: 7bb9599e53f755a14682493259634e7bf3ef485e
5
5
  SHA512:
6
- metadata.gz: 22aef71a6f3827e6f71a38213649a0ad7d081d2b5fc8831ccfadf726246f2d5d59435823dbc6b2119685c98e4d859381a7429debbae6622f3cf655cbc404e4d4
7
- data.tar.gz: 12642596e377573cdcad5b977aa648310069efd0c2278e192aff0758bce6ce9e2d2f81f295a9d942148028eb392c1594f684f685aeb151bb15b52ee1d54c3dc2
6
+ metadata.gz: 62bdf9e6c06fc7e21e468c8f1d346eb0b8bcf6ef4bca06795ec60c61ce0bbec3e503be8443a5e5a9f3a124ec7c96bbbbfe39c986307b335d8c0e0d4e3e3dcde2
7
+ data.tar.gz: 1bc95b49a2b8c34126bed096693b0a7b231d78d0ca361522ab1dc6be1b9f55200ea76a5ab2327ec86b4d8ee5bb1adb162c8315b33e8e19a98f87f55a358a2355
@@ -1,6 +1,6 @@
1
1
  module Codemirror
2
2
  module Rails
3
- VERSION = '3.22'
4
- CODEMIRROR_VERSION = '3.22'
3
+ VERSION = '3.23'
4
+ CODEMIRROR_VERSION = '3.23'
5
5
  end
6
6
  end
@@ -1,5 +1,3 @@
1
- // CodeMirror version 3.22
2
- //
3
1
  // CodeMirror is the only global var we claim
4
2
  window.CodeMirror = (function() {
5
3
  "use strict";
@@ -112,8 +110,8 @@ window.CodeMirror = (function() {
112
110
  // Wraps and hides input textarea
113
111
  d.inputDiv = elt("div", [input], null, "overflow: hidden; position: relative; width: 3px; height: 0px;");
114
112
  // The actual fake scrollbars.
115
- d.scrollbarH = elt("div", [elt("div", null, null, "height: 1px")], "CodeMirror-hscrollbar");
116
- d.scrollbarV = elt("div", [elt("div", null, null, "width: 1px")], "CodeMirror-vscrollbar");
113
+ d.scrollbarH = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar");
114
+ d.scrollbarV = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar");
117
115
  d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler");
118
116
  d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler");
119
117
  // DIVs containing the selection and the actual code
@@ -232,12 +230,17 @@ window.CodeMirror = (function() {
232
230
  var th = textHeight(cm.display), wrapping = cm.options.lineWrapping;
233
231
  var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3);
234
232
  return function(line) {
235
- if (lineIsHidden(cm.doc, line))
236
- return 0;
237
- else if (wrapping)
238
- return (Math.ceil(line.text.length / perLine) || 1) * th;
233
+ if (lineIsHidden(cm.doc, line)) return 0;
234
+
235
+ var widgetsHeight = 0;
236
+ if (line.widgets) for (var i = 0; i < line.widgets.length; i++) {
237
+ if (line.widgets[i].height) widgetsHeight += line.widgets[i].height;
238
+ }
239
+
240
+ if (wrapping)
241
+ return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th;
239
242
  else
240
- return th;
243
+ return widgetsHeight + th;
241
244
  };
242
245
  }
243
246
 
@@ -335,8 +338,8 @@ window.CodeMirror = (function() {
335
338
  d.sizer.style.minHeight = d.heightForcer.style.top = totalHeight + "px";
336
339
  d.gutters.style.height = Math.max(totalHeight, d.scroller.clientHeight - scrollerCutOff) + "px";
337
340
  var scrollHeight = Math.max(totalHeight, d.scroller.scrollHeight);
338
- var needsH = d.scroller.scrollWidth > (d.scroller.clientWidth + 1);
339
- var needsV = scrollHeight > (d.scroller.clientHeight + 1);
341
+ var needsH = d.scroller.scrollWidth > d.scroller.clientWidth;
342
+ var needsV = scrollHeight > d.scroller.clientHeight;
340
343
  if (needsV) {
341
344
  d.scrollbarV.style.display = "block";
342
345
  d.scrollbarV.style.bottom = needsH ? scrollbarWidth(d.measure) + "px" : "0";
@@ -368,7 +371,12 @@ window.CodeMirror = (function() {
368
371
 
369
372
  if (mac_geLion && scrollbarWidth(d.measure) === 0) {
370
373
  d.scrollbarV.style.minWidth = d.scrollbarH.style.minHeight = mac_geMountainLion ? "18px" : "12px";
371
- d.scrollbarV.style.pointerEvents = d.scrollbarH.style.pointerEvents = "none";
374
+ var barMouseDown = function(e) {
375
+ if (e_target(e) != d.scrollbarV && e_target(e) != d.scrollbarH)
376
+ operation(cm, onMouseDown)(e);
377
+ };
378
+ on(d.scrollbarV, "mousedown", barMouseDown);
379
+ on(d.scrollbarH, "mousedown", barMouseDown);
372
380
  }
373
381
  }
374
382
 
@@ -1160,7 +1168,12 @@ window.CodeMirror = (function() {
1160
1168
  var pre = buildLineContent(cm, line, null, true).pre;
1161
1169
  var end = pre.appendChild(zeroWidthElement(cm.display.measure));
1162
1170
  removeChildrenAndAdd(cm.display.measure, pre);
1163
- return getRect(end).right - getRect(cm.display.lineDiv).left;
1171
+ var rect = getRect(end);
1172
+ if (rect.right == 0 && rect.bottom == 0) {
1173
+ end = pre.appendChild(elt("span", "\u00a0"));
1174
+ rect = getRect(end);
1175
+ }
1176
+ return rect.left - getRect(cm.display.lineDiv).left;
1164
1177
  }
1165
1178
 
1166
1179
  function clearCaches(cm) {
@@ -1495,10 +1508,6 @@ window.CodeMirror = (function() {
1495
1508
  function readInput(cm) {
1496
1509
  var input = cm.display.input, prevInput = cm.display.prevInput, doc = cm.doc, sel = doc.sel;
1497
1510
  if (!cm.state.focused || hasSelection(input) || isReadOnly(cm) || cm.options.disableInput) return false;
1498
- if (cm.state.pasteIncoming && cm.state.fakedLastChar) {
1499
- input.value = input.value.substring(0, input.value.length - 1);
1500
- cm.state.fakedLastChar = false;
1501
- }
1502
1511
  var text = input.value;
1503
1512
  if (text == prevInput && posEq(sel.from, sel.to)) return false;
1504
1513
  if (ie && !ie_lt9 && cm.display.inputHasSelection === text) {
@@ -1665,16 +1674,6 @@ window.CodeMirror = (function() {
1665
1674
  fastPoll(cm);
1666
1675
  });
1667
1676
  on(d.input, "paste", function() {
1668
- // Workaround for webkit bug https://bugs.webkit.org/show_bug.cgi?id=90206
1669
- // Add a char to the end of textarea before paste occur so that
1670
- // selection doesn't span to the end of textarea.
1671
- if (webkit && !cm.state.fakedLastChar && !(new Date - cm.state.lastMiddleDown < 200)) {
1672
- var start = d.input.selectionStart, end = d.input.selectionEnd;
1673
- d.input.value += "$";
1674
- d.input.selectionStart = start;
1675
- d.input.selectionEnd = end;
1676
- cm.state.fakedLastChar = true;
1677
- }
1678
1677
  cm.state.pasteIncoming = true;
1679
1678
  fastPoll(cm);
1680
1679
  });
@@ -1708,8 +1707,7 @@ window.CodeMirror = (function() {
1708
1707
  var display = cm.display;
1709
1708
  if (!liberal) {
1710
1709
  var target = e_target(e);
1711
- if (target == display.scrollbarH || target == display.scrollbarH.firstChild ||
1712
- target == display.scrollbarV || target == display.scrollbarV.firstChild ||
1710
+ if (target == display.scrollbarH || target == display.scrollbarV ||
1713
1711
  target == display.scrollbarFiller || target == display.gutterFiller) return null;
1714
1712
  }
1715
1713
  var x, y, space = getRect(display.lineSpace);
@@ -2230,8 +2228,9 @@ window.CodeMirror = (function() {
2230
2228
  var oldCSS = display.input.style.cssText;
2231
2229
  display.inputDiv.style.position = "absolute";
2232
2230
  display.input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) +
2233
- "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: transparent; outline: none;" +
2234
- "border-width: 0; outline: none; overflow: hidden; opacity: .05; -ms-opacity: .05; filter: alpha(opacity=5);";
2231
+ "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: " +
2232
+ (ie ? "rgba(255, 255, 255, .05)" : "transparent") +
2233
+ "; outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);";
2235
2234
  focusInput(cm);
2236
2235
  resetInput(cm, true);
2237
2236
  // Adds "Select all" to context menu in FF
@@ -3064,7 +3063,7 @@ window.CodeMirror = (function() {
3064
3063
  else if (line > last) { line = last; end = true; }
3065
3064
  var lineObj = getLine(this.doc, line);
3066
3065
  return intoCoordSystem(this, getLine(this.doc, line), {top: 0, left: 0}, mode || "page").top +
3067
- (end ? lineObj.height : 0);
3066
+ (end ? this.doc.height - heightAtLine(this, lineObj) : 0);
3068
3067
  },
3069
3068
 
3070
3069
  defaultTextHeight: function() { return textHeight(this.display); },
@@ -3905,6 +3904,7 @@ window.CodeMirror = (function() {
3905
3904
  this.doc.cantEdit = false;
3906
3905
  if (cm) reCheckSelection(cm);
3907
3906
  }
3907
+ if (cm) signalLater(cm, "markerCleared", cm, this);
3908
3908
  if (withOp) endOperation(cm);
3909
3909
  };
3910
3910
 
@@ -4013,6 +4013,7 @@ window.CodeMirror = (function() {
4013
4013
  if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.collapsed)
4014
4014
  regChange(cm, from.line, to.line + 1);
4015
4015
  if (marker.atomic) reCheckSelection(cm);
4016
+ signalLater(cm, "markerAdded", cm, marker);
4016
4017
  }
4017
4018
  return marker;
4018
4019
  }
@@ -5024,6 +5025,7 @@ window.CodeMirror = (function() {
5024
5025
  redo: docOperation(function() {makeChangeFromHistory(this, "redo");}),
5025
5026
 
5026
5027
  setExtending: function(val) {this.sel.extend = val;},
5028
+ getExtending: function() {return this.sel.extend;},
5027
5029
 
5028
5030
  historySize: function() {
5029
5031
  var hist = this.history;
@@ -6087,7 +6089,7 @@ window.CodeMirror = (function() {
6087
6089
 
6088
6090
  // THE END
6089
6091
 
6090
- CodeMirror.version = "3.22.0";
6092
+ CodeMirror.version = "3.23.0";
6091
6093
 
6092
6094
  return CodeMirror;
6093
6095
  })();
@@ -93,6 +93,7 @@
93
93
  data = data_;
94
94
  if (finished) return;
95
95
  if (!data || !data.list.length) return done();
96
+ if (completion.widget) completion.widget.close();
96
97
  completion.widget = new Widget(completion, data);
97
98
  }
98
99
 
@@ -7,6 +7,7 @@
7
7
  QUERY_DIV: ";",
8
8
  ALIAS_KEYWORD: "AS"
9
9
  };
10
+ var Pos = CodeMirror.Pos;
10
11
 
11
12
  function getKeywords(editor) {
12
13
  var mode = editor.doc.modeOption;
@@ -36,7 +37,7 @@
36
37
  var cur = editor.getCursor();
37
38
  var token = editor.getTokenAt(cur);
38
39
  var string = token.string.substr(1);
39
- var prevCur = CodeMirror.Pos(cur.line, token.start);
40
+ var prevCur = Pos(cur.line, token.start);
40
41
  var table = editor.getTokenAt(prevCur).string;
41
42
  if( !tables.hasOwnProperty( table ) ){
42
43
  table = findTableByAlias(table, editor);
@@ -64,7 +65,7 @@
64
65
  }
65
66
 
66
67
  function convertNumberToCur( num ){
67
- return CodeMirror.Pos(Math.floor( num ), +num.toString().split( '.' ).pop());
68
+ return Pos(Math.floor( num ), +num.toString().split( '.' ).pop());
68
69
  }
69
70
 
70
71
  function findTableByAlias(alias, editor) {
@@ -75,8 +76,8 @@
75
76
  var table = "";
76
77
  var separator = [];
77
78
  var validRange = {
78
- start: CodeMirror.Pos( 0, 0 ),
79
- end: CodeMirror.Pos( editor.lastLine(), editor.getLineHandle( editor.lastLine() ).length )
79
+ start: Pos( 0, 0 ),
80
+ end: Pos( editor.lastLine(), editor.getLineHandle( editor.lastLine() ).length )
80
81
  };
81
82
 
82
83
  //add separator
@@ -85,8 +86,8 @@
85
86
  separator.push( doc.posFromIndex(indexOfSeparator));
86
87
  indexOfSeparator = fullQuery.indexOf( CONS.QUERY_DIV, indexOfSeparator+1);
87
88
  }
88
- separator.unshift( CodeMirror.Pos( 0, 0 ) );
89
- separator.push( CodeMirror.Pos( editor.lastLine(), editor.getLineHandle( editor.lastLine() ).text.length ) );
89
+ separator.unshift( Pos( 0, 0 ) );
90
+ separator.push( Pos( editor.lastLine(), editor.getLineHandle( editor.lastLine() ).text.length ) );
90
91
 
91
92
  //find valieRange
92
93
  var prevItem = 0;
@@ -122,24 +123,31 @@
122
123
  tables = (options && options.tables) || {};
123
124
  keywords = keywords || getKeywords(editor);
124
125
  var cur = editor.getCursor();
125
- var token = editor.getTokenAt(cur);
126
+ var token = editor.getTokenAt(cur), end = token.end;
126
127
  var result = [];
127
128
  var search = token.string.trim();
128
129
 
129
- addMatches(result, search, keywords,
130
- function(w) {return w.toUpperCase();});
131
-
132
- addMatches(result, search, tables,
133
- function(w) {return w;});
134
-
135
- if(search.lastIndexOf('.') === 0) {
130
+ if (search.charAt(0) == ".") {
136
131
  columnCompletion(result, editor);
132
+ if (!result.length) {
133
+ while (token.start && search.charAt(0) == ".") {
134
+ token = editor.getTokenAt(Pos(cur.line, token.start - 1));
135
+ search = token.string + search;
136
+ }
137
+ addMatches(result, search, tables,
138
+ function(w) {return w;});
139
+ }
140
+ } else {
141
+ addMatches(result, search, keywords,
142
+ function(w) {return w.toUpperCase();});
143
+ addMatches(result, search, tables,
144
+ function(w) {return w;});
137
145
  }
138
146
 
139
147
  return {
140
148
  list: result,
141
- from: CodeMirror.Pos(cur.line, token.start),
142
- to: CodeMirror.Pos(cur.line, token.end)
149
+ from: Pos(cur.line, token.start),
150
+ to: Pos(cur.line, end)
143
151
  };
144
152
  }
145
153
  CodeMirror.registerHelper("hint", "sql", sqlHint);
@@ -11,22 +11,30 @@
11
11
  var inner = CodeMirror.innerMode(cm.getMode(), token.state);
12
12
  if (inner.mode.name != "xml") return;
13
13
  var result = [], replaceToken = false, prefix;
14
- var isTag = token.string.charAt(0) == "<";
15
- if (!inner.state.tagName || isTag) { // Tag completion
16
- if (isTag) {
17
- prefix = token.string.slice(1);
18
- replaceToken = true;
19
- }
14
+ var tag = /\btag\b/.test(token.type), tagName = tag && /^\w/.test(token.string), tagStart;
15
+ if (tagName) {
16
+ var before = cm.getLine(cur.line).slice(Math.max(0, token.start - 2), token.start);
17
+ var tagType = /<\/$/.test(before) ? "close" : /<$/.test(before) ? "open" : null;
18
+ if (tagType) tagStart = token.start - (tagType == "close" ? 2 : 1);
19
+ } else if (tag && token.string == "<") {
20
+ tagType = "open";
21
+ } else if (tag && token.string == "</") {
22
+ tagType = "close";
23
+ }
24
+ if (!tag && !inner.state.tagName || tagType) {
25
+ if (tagName)
26
+ prefix = token.string;
27
+ replaceToken = tagType;
20
28
  var cx = inner.state.context, curTag = cx && tags[cx.tagName];
21
29
  var childList = cx ? curTag && curTag.children : tags["!top"];
22
- if (childList) {
30
+ if (childList && tagType != "close") {
23
31
  for (var i = 0; i < childList.length; ++i) if (!prefix || childList[i].lastIndexOf(prefix, 0) == 0)
24
32
  result.push("<" + childList[i]);
25
- } else {
33
+ } else if (tagType != "close") {
26
34
  for (var name in tags) if (tags.hasOwnProperty(name) && name != "!top" && (!prefix || name.lastIndexOf(prefix, 0) == 0))
27
35
  result.push("<" + name);
28
36
  }
29
- if (cx && (!prefix || ("/" + cx.tagName).lastIndexOf(prefix, 0) == 0))
37
+ if (cx && (!prefix || tagType == "close" && cx.tagName.lastIndexOf(prefix, 0) == 0))
30
38
  result.push("</" + cx.tagName + ">");
31
39
  } else {
32
40
  // Attribute completion
@@ -59,7 +67,7 @@
59
67
  }
60
68
  return {
61
69
  list: result,
62
- from: replaceToken ? Pos(cur.line, token.start) : cur,
70
+ from: replaceToken ? Pos(cur.line, tagStart == null ? token.start : tagStart) : cur,
63
71
  to: replaceToken ? Pos(cur.line, token.end) : cur
64
72
  };
65
73
  }
@@ -4,6 +4,7 @@
4
4
 
5
5
  CodeMirror.registerHelper("lint", "css", function(text) {
6
6
  var found = [];
7
+ if (!window.CSSLint) return found;
7
8
  var results = CSSLint.verify(text), messages = results.messages, message = null;
8
9
  for ( var i = 0; i < messages.length; i++) {
9
10
  message = messages[i];
@@ -12,6 +12,7 @@
12
12
  "Unclosed string", "Stopping, unable to continue" ];
13
13
 
14
14
  function validator(text, options) {
15
+ if (!window.JSHINT) return [];
15
16
  JSHINT(text, options);
16
17
  var errors = JSHINT.data().errors, result = [];
17
18
  if (errors) parseErrors(errors, result);
@@ -82,6 +82,10 @@
82
82
  }
83
83
  dv.edit.on("change", change);
84
84
  dv.orig.on("change", change);
85
+ dv.edit.on("markerAdded", set);
86
+ dv.edit.on("markerCleared", set);
87
+ dv.orig.on("markerAdded", set);
88
+ dv.orig.on("markerCleared", set);
85
89
  dv.edit.on("viewportChange", set);
86
90
  dv.orig.on("viewportChange", set);
87
91
  update();
@@ -349,6 +353,12 @@
349
353
  setShowDifferences: function(val) {
350
354
  if (this.right) this.right.setShowDifferences(val);
351
355
  if (this.left) this.left.setShowDifferences(val);
356
+ },
357
+ rightChunks: function() {
358
+ return this.right && getChunks(this.right.diff);
359
+ },
360
+ leftChunks: function() {
361
+ return this.left && getChunks(this.left.diff);
352
362
  }
353
363
  };
354
364
 
@@ -399,6 +409,15 @@
399
409
  f(startOrig, orig.line + 1, startEdit, edit.line + 1);
400
410
  }
401
411
 
412
+ function getChunks(diff) {
413
+ var collect = [];
414
+ iterateChunks(diff, function(topOrig, botOrig, topEdit, botEdit) {
415
+ collect.push({origFrom: topOrig, origTo: botOrig,
416
+ editFrom: topEdit, editTo: botEdit});
417
+ });
418
+ return collect;
419
+ }
420
+
402
421
  function endOfLineClean(diff, i) {
403
422
  if (i == diff.length - 1) return true;
404
423
  var next = diff[i + 1][1];
@@ -16,16 +16,11 @@
16
16
  } else {
17
17
  query = new RegExp("^(?:" + query.source + ")", query.ignoreCase ? "i" : "");
18
18
  }
19
- if (typeof query == "string") return {token: function(stream) {
20
- if (stream.match(query)) return "searching";
21
- stream.next();
22
- stream.skipTo(query.charAt(0)) || stream.skipToEnd();
23
- }};
24
19
  return {token: function(stream) {
25
20
  if (stream.match(query)) return "searching";
26
21
  while (!stream.eol()) {
27
22
  stream.next();
28
- if (startChar)
23
+ if (startChar && !caseInsensitive)
29
24
  stream.skipTo(startChar) || stream.skipToEnd();
30
25
  if (stream.match(query, false)) break;
31
26
  }
@@ -74,7 +69,7 @@
74
69
  if (!query || state.query) return;
75
70
  state.query = parseQuery(query);
76
71
  cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
77
- state.overlay = searchOverlay(state.query);
72
+ state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query));
78
73
  cm.addOverlay(state.overlay);
79
74
  state.posFrom = state.posTo = cm.getCursor();
80
75
  findNext(cm, rev);
@@ -197,7 +197,7 @@
197
197
 
198
198
  function setMark(cm) {
199
199
  cm.setCursor(cm.getCursor());
200
- cm.setExtending(true);
200
+ cm.setExtending(!cm.getExtending());
201
201
  cm.on("change", function() { cm.setExtending(false); });
202
202
  }
203
203
 
@@ -32,7 +32,7 @@
32
32
  * ESC - leave insert mode, visual mode, and clear input state.
33
33
  * Ctrl-[, Ctrl-c - same as ESC.
34
34
  *
35
- * Registers: unamed, -, a-z, A-Z, 0-9
35
+ * Registers: unnamed, -, a-z, A-Z, 0-9
36
36
  * (Does not respect the special case for number registers when delete
37
37
  * operator is made with these commands: %, (, ), , /, ?, n, N, {, } )
38
38
  * TODO: Implement the remaining registers.
@@ -194,7 +194,7 @@
194
194
  { keys: [','], type: 'motion', motion: 'repeatLastCharacterSearch',
195
195
  motionArgs: { forward: false }},
196
196
  { keys: ['\'', 'character'], type: 'motion', motion: 'goToMark',
197
- motionArgs: {toJumplist: true}},
197
+ motionArgs: {toJumplist: true, linewise: true}},
198
198
  { keys: ['`', 'character'], type: 'motion', motion: 'goToMark',
199
199
  motionArgs: {toJumplist: true}},
200
200
  { keys: [']', '`'], type: 'motion', motion: 'jumpToMark', motionArgs: { forward: true } },
@@ -210,6 +210,7 @@
210
210
  { keys: ['|'], type: 'motion',
211
211
  motion: 'moveToColumn',
212
212
  motionArgs: { }},
213
+ { keys: ['o'], type: 'motion', motion: 'moveToOtherHighlightedEnd', motionArgs: { },context:'visual'},
213
214
  // Operators
214
215
  { keys: ['d'], type: 'operator', operator: 'delete' },
215
216
  { keys: ['y'], type: 'operator', operator: 'yank' },
@@ -271,6 +272,7 @@
271
272
  { keys: ['v'], type: 'action', action: 'toggleVisualMode' },
272
273
  { keys: ['V'], type: 'action', action: 'toggleVisualMode',
273
274
  actionArgs: { linewise: true }},
275
+ { keys: ['g', 'v'], type: 'action', action: 'reselectLastSelection' },
274
276
  { keys: ['J'], type: 'action', action: 'joinLines', isEdit: true },
275
277
  { keys: ['p'], type: 'action', action: 'paste', isEdit: true,
276
278
  actionArgs: { after: true, isEdit: true }},
@@ -320,9 +322,11 @@
320
322
  { keys: ['?'], type: 'search',
321
323
  searchArgs: { forward: false, querySrc: 'prompt', toJumplist: true }},
322
324
  { keys: ['*'], type: 'search',
323
- searchArgs: { forward: true, querySrc: 'wordUnderCursor', toJumplist: true }},
325
+ searchArgs: { forward: true, querySrc: 'wordUnderCursor', wholeWordOnly: true, toJumplist: true }},
324
326
  { keys: ['#'], type: 'search',
325
- searchArgs: { forward: false, querySrc: 'wordUnderCursor', toJumplist: true }},
327
+ searchArgs: { forward: false, querySrc: 'wordUnderCursor', wholeWordOnly: true, toJumplist: true }},
328
+ { keys: ['g', '*'], type: 'search', searchArgs: { forward: true, querySrc: 'wordUnderCursor', toJumplist: true }},
329
+ { keys: ['g', '#'], type: 'search', searchArgs: { forward: false, querySrc: 'wordUnderCursor', toJumplist: true }},
326
330
  // Ex command
327
331
  { keys: [':'], type: 'ex' }
328
332
  ];
@@ -382,7 +386,7 @@
382
386
  var specialKeys = ['Left', 'Right', 'Up', 'Down', 'Space', 'Backspace',
383
387
  'Esc', 'Home', 'End', 'PageUp', 'PageDown', 'Enter'];
384
388
  var validMarks = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['<', '>']);
385
- var validRegisters = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['-', '"']);
389
+ var validRegisters = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, ['-', '"', '.', ':']);
386
390
 
387
391
  function isLine(cm, line) {
388
392
  return line >= cm.firstLine() && line <= cm.lastLine();
@@ -411,6 +415,41 @@
411
415
  return false;
412
416
  }
413
417
 
418
+ var options = {};
419
+ function defineOption(name, defaultValue, type) {
420
+ if (defaultValue === undefined) { throw Error('defaultValue is required'); }
421
+ if (!type) { type = 'string'; }
422
+ options[name] = {
423
+ type: type,
424
+ defaultValue: defaultValue
425
+ };
426
+ setOption(name, defaultValue);
427
+ }
428
+
429
+ function setOption(name, value) {
430
+ var option = options[name];
431
+ if (!option) {
432
+ throw Error('Unknown option: ' + name);
433
+ }
434
+ if (option.type == 'boolean') {
435
+ if (value && value !== true) {
436
+ throw Error('Invalid argument: ' + name + '=' + value);
437
+ } else if (value !== false) {
438
+ // Boolean options are set to true if value is not defined.
439
+ value = true;
440
+ }
441
+ }
442
+ option.value = option.type == 'boolean' ? !!value : value;
443
+ }
444
+
445
+ function getOption(name) {
446
+ var option = options[name];
447
+ if (!option) {
448
+ throw Error('Unknown option: ' + name);
449
+ }
450
+ return option.value;
451
+ }
452
+
414
453
  var createCircularJumpList = function() {
415
454
  var size = 100;
416
455
  var pointer = -1;
@@ -477,30 +516,52 @@
477
516
  };
478
517
  };
479
518
 
480
- var createMacroState = function() {
519
+ // Returns an object to track the changes associated insert mode. It
520
+ // clones the object that is passed in, or creates an empty object one if
521
+ // none is provided.
522
+ var createInsertModeChanges = function(c) {
523
+ if (c) {
524
+ // Copy construction
525
+ return {
526
+ changes: c.changes,
527
+ expectCursorActivityForChange: c.expectCursorActivityForChange
528
+ };
529
+ }
481
530
  return {
482
- macroKeyBuffer: [],
483
- latestRegister: undefined,
484
- inReplay: false,
485
- lastInsertModeChanges: {
486
- changes: [], // Change list
487
- expectCursorActivityForChange: false // Set to true on change, false on cursorActivity.
488
- },
489
- enteredMacroMode: undefined,
490
- isMacroPlaying: false,
491
- toggle: function(cm, registerName) {
492
- if (this.enteredMacroMode) { //onExit
493
- this.enteredMacroMode(); // close dialog
494
- this.enteredMacroMode = undefined;
495
- } else { //onEnter
496
- this.latestRegister = registerName;
497
- this.enteredMacroMode = cm.openDialog(
498
- '(recording)['+registerName+']', null, {bottom:true});
499
- }
500
- }
531
+ // Change list
532
+ changes: [],
533
+ // Set to true on change, false on cursorActivity.
534
+ expectCursorActivityForChange: false
501
535
  };
502
536
  };
503
537
 
538
+ function MacroModeState() {
539
+ this.latestRegister = undefined;
540
+ this.isPlaying = false;
541
+ this.isRecording = false;
542
+ this.replaySearchQueries = [];
543
+ this.onRecordingDone = undefined;
544
+ this.lastInsertModeChanges = createInsertModeChanges();
545
+ }
546
+ MacroModeState.prototype = {
547
+ exitMacroRecordMode: function() {
548
+ var macroModeState = vimGlobalState.macroModeState;
549
+ macroModeState.onRecordingDone(); // close dialog
550
+ macroModeState.onRecordingDone = undefined;
551
+ macroModeState.isRecording = false;
552
+ },
553
+ enterMacroRecordMode: function(cm, registerName) {
554
+ var register =
555
+ vimGlobalState.registerController.getRegister(registerName);
556
+ if (register) {
557
+ register.clear();
558
+ this.latestRegister = registerName;
559
+ this.onRecordingDone = cm.openDialog(
560
+ '(recording)['+registerName+']', null, {bottom:true});
561
+ this.isRecording = true;
562
+ }
563
+ }
564
+ };
504
565
 
505
566
  function maybeInitVimState(cm) {
506
567
  if (!cm.state.vim) {
@@ -531,7 +592,8 @@
531
592
  insertModeRepeat: undefined,
532
593
  visualMode: false,
533
594
  // If we are in visual line mode. No effect if visualMode is false.
534
- visualLine: false
595
+ visualLine: false,
596
+ lastSelection: null
535
597
  };
536
598
  }
537
599
  return cm.state.vim;
@@ -543,12 +605,18 @@
543
605
  searchQuery: null,
544
606
  // Whether we are searching backwards.
545
607
  searchIsReversed: false,
608
+ // Replace part of the last substituted pattern
609
+ lastSubstituteReplacePart: undefined,
546
610
  jumpList: createCircularJumpList(),
547
- macroModeState: createMacroState(),
611
+ macroModeState: new MacroModeState,
548
612
  // Recording latest f, t, F or T motion command.
549
613
  lastChararacterSearch: {increment:0, forward:true, selectedCharacter:''},
550
614
  registerController: new RegisterController({})
551
615
  };
616
+ for (var optionName in options) {
617
+ var option = options[optionName];
618
+ option.value = option.defaultValue;
619
+ }
552
620
  }
553
621
 
554
622
  var vimApi= {
@@ -576,6 +644,9 @@
576
644
  // Add user defined key bindings.
577
645
  exCommandDispatcher.map(lhs, rhs, ctx);
578
646
  },
647
+ setOption: setOption,
648
+ getOption: getOption,
649
+ defineOption: defineOption,
579
650
  defineEx: function(name, prefix, func){
580
651
  if (name.indexOf(prefix) !== 0) {
581
652
  throw new Error('(Vim.defineEx) "'+prefix+'" is not a prefix of "'+name+'", command not registered');
@@ -589,9 +660,9 @@
589
660
  var command;
590
661
  var vim = maybeInitVimState(cm);
591
662
  var macroModeState = vimGlobalState.macroModeState;
592
- if (macroModeState.enteredMacroMode) {
663
+ if (macroModeState.isRecording) {
593
664
  if (key == 'q') {
594
- actions.exitMacroRecordMode();
665
+ macroModeState.exitMacroRecordMode();
595
666
  vim.inputState = new InputState();
596
667
  return;
597
668
  }
@@ -621,6 +692,9 @@
621
692
  // Increment count unless count is 0 and key is 0.
622
693
  vim.inputState.pushRepeatDigit(key);
623
694
  }
695
+ if (macroModeState.isRecording) {
696
+ logKey(macroModeState, key);
697
+ }
624
698
  return;
625
699
  }
626
700
  if (command.type == 'keyToKey') {
@@ -629,7 +703,7 @@
629
703
  this.handleKey(cm, command.toKeys[i]);
630
704
  }
631
705
  } else {
632
- if (macroModeState.enteredMacroMode) {
706
+ if (macroModeState.isRecording) {
633
707
  logKey(macroModeState, key);
634
708
  }
635
709
  commandDispatcher.processCommand(cm, vim, command);
@@ -650,7 +724,7 @@
650
724
  this.motion = null;
651
725
  this.motionArgs = null;
652
726
  this.keyBuffer = []; // For matching multi-key commands.
653
- this.registerName = null; // Defaults to the unamed register.
727
+ this.registerName = null; // Defaults to the unnamed register.
654
728
  }
655
729
  InputState.prototype.pushRepeatDigit = function(n) {
656
730
  if (!this.operator) {
@@ -681,29 +755,39 @@
681
755
  */
682
756
  function Register(text, linewise) {
683
757
  this.clear();
684
- if (text) {
685
- this.set(text, linewise);
686
- }
758
+ this.keyBuffer = [text || ''];
759
+ this.insertModeChanges = [];
760
+ this.searchQueries = [];
761
+ this.linewise = !!linewise;
687
762
  }
688
763
  Register.prototype = {
689
- set: function(text, linewise) {
690
- this.text = text;
764
+ setText: function(text, linewise) {
765
+ this.keyBuffer = [text || ''];
691
766
  this.linewise = !!linewise;
692
767
  },
693
- append: function(text, linewise) {
768
+ pushText: function(text, linewise) {
694
769
  // if this register has ever been set to linewise, use linewise.
695
770
  if (linewise || this.linewise) {
696
- this.text += '\n' + text;
771
+ this.keyBuffer.push('\n');
697
772
  this.linewise = true;
698
- } else {
699
- this.text += text;
700
773
  }
774
+ this.keyBuffer.push(text);
775
+ },
776
+ pushInsertModeChanges: function(changes) {
777
+ this.insertModeChanges.push(createInsertModeChanges(changes));
778
+ },
779
+ pushSearchQuery: function(query) {
780
+ this.searchQueries.push(query);
701
781
  },
702
782
  clear: function() {
703
- this.text = '';
783
+ this.keyBuffer = [];
784
+ this.insertModeChanges = [];
785
+ this.searchQueries = [];
704
786
  this.linewise = false;
705
787
  },
706
- toString: function() { return this.text; }
788
+ toString: function() {
789
+ return this.keyBuffer.join('');
790
+ }
707
791
  };
708
792
 
709
793
  /*
@@ -716,14 +800,16 @@
716
800
  */
717
801
  function RegisterController(registers) {
718
802
  this.registers = registers;
719
- this.unamedRegister = registers['"'] = new Register();
803
+ this.unnamedRegister = registers['"'] = new Register();
804
+ registers['.'] = new Register();
805
+ registers[':'] = new Register();
720
806
  }
721
807
  RegisterController.prototype = {
722
808
  pushText: function(registerName, operator, text, linewise) {
723
809
  if (linewise && text.charAt(0) == '\n') {
724
810
  text = text.slice(1) + '\n';
725
811
  }
726
- if(linewise && text.charAt(text.length - 1) !== '\n'){
812
+ if (linewise && text.charAt(text.length - 1) !== '\n'){
727
813
  text += '\n';
728
814
  }
729
815
  // Lowercase and uppercase registers refer to the same register.
@@ -752,7 +838,7 @@
752
838
  break;
753
839
  }
754
840
  // Make sure the unnamed register is set to what just happened
755
- this.unamedRegister.set(text, linewise);
841
+ this.unnamedRegister.setText(text, linewise);
756
842
  return;
757
843
  }
758
844
 
@@ -760,22 +846,19 @@
760
846
  var append = isUpperCase(registerName);
761
847
  if (append) {
762
848
  register.append(text, linewise);
763
- // The unamed register always has the same value as the last used
849
+ // The unnamed register always has the same value as the last used
764
850
  // register.
765
- this.unamedRegister.append(text, linewise);
851
+ this.unnamedRegister.append(text, linewise);
766
852
  } else {
767
- register.set(text, linewise);
768
- this.unamedRegister.set(text, linewise);
853
+ register.setText(text, linewise);
854
+ this.unnamedRegister.setText(text, linewise);
769
855
  }
770
856
  },
771
- setRegisterText: function(name, text, linewise) {
772
- this.getRegister(name).set(text, linewise);
773
- },
774
857
  // Gets the register named @name. If one of @name doesn't already exist,
775
- // create it. If @name is invalid, return the unamedRegister.
858
+ // create it. If @name is invalid, return the unnamedRegister.
776
859
  getRegister: function(name) {
777
860
  if (!this.isValidRegister(name)) {
778
- return this.unamedRegister;
861
+ return this.unnamedRegister;
779
862
  }
780
863
  name = name.toLowerCase();
781
864
  if (!this.registers[name]) {
@@ -811,7 +894,7 @@
811
894
  // Match commands that take <character> as an argument.
812
895
  if (command.keys[keys.length - 1] == 'character') {
813
896
  selectedCharacter = keys[keys.length - 1];
814
- if(selectedCharacter.length>1){
897
+ if (selectedCharacter.length>1){
815
898
  switch(selectedCharacter){
816
899
  case '<CR>':
817
900
  selectedCharacter='\n';
@@ -973,6 +1056,7 @@
973
1056
  return;
974
1057
  }
975
1058
  var forward = command.searchArgs.forward;
1059
+ var wholeWordOnly = command.searchArgs.wholeWordOnly;
976
1060
  getSearchState(cm).setReversed(!forward);
977
1061
  var promptPrefix = (forward) ? '/' : '?';
978
1062
  var originalQuery = getSearchState(cm).getQuery();
@@ -993,6 +1077,10 @@
993
1077
  function onPromptClose(query) {
994
1078
  cm.scrollTo(originalScrollPos.left, originalScrollPos.top);
995
1079
  handleQuery(query, true /** ignoreCase */, true /** smartCase */);
1080
+ var macroModeState = vimGlobalState.macroModeState;
1081
+ if (macroModeState.isRecording) {
1082
+ logSearchQuery(macroModeState, query);
1083
+ }
996
1084
  }
997
1085
  function onPromptKeyUp(_e, query) {
998
1086
  var parsedQuery;
@@ -1023,13 +1111,19 @@
1023
1111
  }
1024
1112
  switch (command.searchArgs.querySrc) {
1025
1113
  case 'prompt':
1026
- showPrompt(cm, {
1027
- onClose: onPromptClose,
1028
- prefix: promptPrefix,
1029
- desc: searchPromptDesc,
1030
- onKeyUp: onPromptKeyUp,
1031
- onKeyDown: onPromptKeyDown
1032
- });
1114
+ var macroModeState = vimGlobalState.macroModeState;
1115
+ if (macroModeState.isPlaying) {
1116
+ var query = macroModeState.replaySearchQueries.shift();
1117
+ handleQuery(query, true /** ignoreCase */, false /** smartCase */);
1118
+ } else {
1119
+ showPrompt(cm, {
1120
+ onClose: onPromptClose,
1121
+ prefix: promptPrefix,
1122
+ desc: searchPromptDesc,
1123
+ onKeyUp: onPromptKeyUp,
1124
+ onKeyDown: onPromptKeyDown
1125
+ });
1126
+ }
1033
1127
  break;
1034
1128
  case 'wordUnderCursor':
1035
1129
  var word = expandWordUnderCursor(cm, false /** inclusive */,
@@ -1047,8 +1141,8 @@
1047
1141
  }
1048
1142
  var query = cm.getLine(word.start.line).substring(word.start.ch,
1049
1143
  word.end.ch);
1050
- if (isKeyword) {
1051
- query = '\\b' + query + '\\b';
1144
+ if (isKeyword && wholeWordOnly) {
1145
+ query = '\\b' + query + '\\b';
1052
1146
  } else {
1053
1147
  query = escapeRegex(query);
1054
1148
  }
@@ -1174,6 +1268,7 @@
1174
1268
  selectionStart.ch -= 1;
1175
1269
  }
1176
1270
  selectionEnd = curEnd;
1271
+ selectionStart = (motionResult instanceof Array) ? curStart : selectionStart;
1177
1272
  if (vim.visualLine) {
1178
1273
  if (cursorIsBefore(selectionStart, selectionEnd)) {
1179
1274
  selectionStart.ch = 0;
@@ -1243,7 +1338,7 @@
1243
1338
  },
1244
1339
  recordLastEdit: function(vim, inputState, actionCommand) {
1245
1340
  var macroModeState = vimGlobalState.macroModeState;
1246
- if (macroModeState.inReplay) { return; }
1341
+ if (macroModeState.isPlaying) { return; }
1247
1342
  vim.lastEditInputState = inputState;
1248
1343
  vim.lastEditActionCommand = actionCommand;
1249
1344
  macroModeState.lastInsertModeChanges.changes = [];
@@ -1288,13 +1383,24 @@
1288
1383
  highlightSearchMatches(cm, query);
1289
1384
  return findNext(cm, prev/** prev */, query, motionArgs.repeat);
1290
1385
  },
1291
- goToMark: function(_cm, motionArgs, vim) {
1386
+ goToMark: function(cm, motionArgs, vim) {
1292
1387
  var mark = vim.marks[motionArgs.selectedCharacter];
1293
1388
  if (mark) {
1294
- return mark.find();
1389
+ var pos = mark.find();
1390
+ return motionArgs.linewise ? { line: pos.line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(pos.line)) } : pos;
1295
1391
  }
1296
1392
  return null;
1297
1393
  },
1394
+ moveToOtherHighlightedEnd: function(cm) {
1395
+ var curEnd = copyCursor(cm.getCursor('head'));
1396
+ var curStart = copyCursor(cm.getCursor('anchor'));
1397
+ if (cursorIsBefore(curStart, curEnd)) {
1398
+ curEnd.ch += 1;
1399
+ } else if (cursorIsBefore(curEnd, curStart)) {
1400
+ curStart.ch -= 1;
1401
+ }
1402
+ return ([curEnd,curStart]);
1403
+ },
1298
1404
  jumpToMark: function(cm, motionArgs, vim) {
1299
1405
  var best = cm.getCursor();
1300
1406
  for (var i = 0; i < motionArgs.repeat; i++) {
@@ -1368,7 +1474,7 @@
1368
1474
  (line > last && cur.line == last)) {
1369
1475
  return;
1370
1476
  }
1371
- if(motionArgs.toFirstChar){
1477
+ if (motionArgs.toFirstChar){
1372
1478
  endCh=findFirstNonWhiteSpaceCharacter(cm.getLine(line));
1373
1479
  vim.lastHPos = endCh;
1374
1480
  }
@@ -1740,29 +1846,21 @@
1740
1846
  }
1741
1847
  cm.scrollTo(null, y);
1742
1848
  },
1743
- replayMacro: function(cm, actionArgs) {
1849
+ replayMacro: function(cm, actionArgs, vim) {
1744
1850
  var registerName = actionArgs.selectedCharacter;
1745
1851
  var repeat = actionArgs.repeat;
1746
1852
  var macroModeState = vimGlobalState.macroModeState;
1747
1853
  if (registerName == '@') {
1748
1854
  registerName = macroModeState.latestRegister;
1749
1855
  }
1750
- var keyBuffer = parseRegisterToKeyBuffer(macroModeState, registerName);
1751
1856
  while(repeat--){
1752
- executeMacroKeyBuffer(cm, macroModeState, keyBuffer);
1857
+ executeMacroRegister(cm, vim, macroModeState, registerName);
1753
1858
  }
1754
1859
  },
1755
- exitMacroRecordMode: function() {
1756
- var macroModeState = vimGlobalState.macroModeState;
1757
- macroModeState.toggle();
1758
- parseKeyBufferToRegister(macroModeState.latestRegister,
1759
- macroModeState.macroKeyBuffer);
1760
- },
1761
1860
  enterMacroRecordMode: function(cm, actionArgs) {
1762
1861
  var macroModeState = vimGlobalState.macroModeState;
1763
1862
  var registerName = actionArgs.selectedCharacter;
1764
- macroModeState.toggle(cm, registerName);
1765
- emptyMacroKeyBuffer(macroModeState);
1863
+ macroModeState.enterMacroRecordMode(cm, registerName);
1766
1864
  },
1767
1865
  enterInsertMode: function(cm, actionArgs, vim) {
1768
1866
  if (cm.getOption('readOnly')) { return; }
@@ -1789,7 +1887,7 @@
1789
1887
  cm.setOption('keyMap', 'vim-insert');
1790
1888
  CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"});
1791
1889
  }
1792
- if (!vimGlobalState.macroModeState.inReplay) {
1890
+ if (!vimGlobalState.macroModeState.isPlaying) {
1793
1891
  // Only record if not replaying.
1794
1892
  cm.on('change', onChange);
1795
1893
  cm.on('cursorActivity', onCursorActivity);
@@ -1857,6 +1955,21 @@
1857
1955
  updateMark(cm, vim, '>', cursorIsBefore(curStart, curEnd) ? curEnd
1858
1956
  : curStart);
1859
1957
  },
1958
+ reselectLastSelection: function(cm, _actionArgs, vim) {
1959
+ if (vim.lastSelection) {
1960
+ var lastSelection = vim.lastSelection;
1961
+ cm.setSelection(lastSelection.curStart, lastSelection.curEnd);
1962
+ if (lastSelection.visualLine) {
1963
+ vim.visualMode = true;
1964
+ vim.visualLine = true;
1965
+ }
1966
+ else {
1967
+ vim.visualMode = true;
1968
+ vim.visualLine = false;
1969
+ }
1970
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : ""});
1971
+ }
1972
+ },
1860
1973
  joinLines: function(cm, actionArgs, vim) {
1861
1974
  var curStart, curEnd;
1862
1975
  if (vim.visualMode) {
@@ -1906,11 +2019,12 @@
1906
2019
  var cur = cm.getCursor();
1907
2020
  var register = vimGlobalState.registerController.getRegister(
1908
2021
  actionArgs.registerName);
1909
- if (!register.text) {
2022
+ var text = register.toString();
2023
+ if (!text) {
1910
2024
  return;
1911
2025
  }
1912
- for (var text = '', i = 0; i < actionArgs.repeat; i++) {
1913
- text += register.text;
2026
+ if (actionArgs.repeat > 1) {
2027
+ var text = Array(actionArgs.repeat + 1).join(text);
1914
2028
  }
1915
2029
  var linewise = register.linewise;
1916
2030
  if (linewise) {
@@ -1965,7 +2079,7 @@
1965
2079
  var curStart = cm.getCursor();
1966
2080
  var replaceTo;
1967
2081
  var curEnd;
1968
- if(vim.visualMode){
2082
+ if (vim.visualMode){
1969
2083
  curStart=cm.getCursor('start');
1970
2084
  curEnd=cm.getCursor('end');
1971
2085
  // workaround to catch the character under the cursor
@@ -1979,8 +2093,8 @@
1979
2093
  }
1980
2094
  curEnd = { line: curStart.line, ch: replaceTo };
1981
2095
  }
1982
- if(replaceWith=='\n'){
1983
- if(!vim.visualMode) cm.replaceRange('', curStart, curEnd);
2096
+ if (replaceWith=='\n'){
2097
+ if (!vim.visualMode) cm.replaceRange('', curStart, curEnd);
1984
2098
  // special case, where vim help says to replace by just one line-break
1985
2099
  (CodeMirror.commands.newlineAndIndentContinueComment || CodeMirror.commands.newlineAndIndent)(cm);
1986
2100
  }else {
@@ -1988,7 +2102,7 @@
1988
2102
  //replace all characters in range by selected, but keep linebreaks
1989
2103
  replaceWithStr=replaceWithStr.replace(/[^\n]/g,replaceWith);
1990
2104
  cm.replaceRange(replaceWithStr, curStart, curEnd);
1991
- if(vim.visualMode){
2105
+ if (vim.visualMode){
1992
2106
  cm.setCursor(curStart);
1993
2107
  exitVisualMode(cm);
1994
2108
  }else{
@@ -2009,9 +2123,9 @@
2009
2123
  token = match[0];
2010
2124
  start = match.index;
2011
2125
  end = start + token.length;
2012
- if(cur.ch < end)break;
2126
+ if (cur.ch < end)break;
2013
2127
  }
2014
- if(!actionArgs.backtrack && (end <= cur.ch))return;
2128
+ if (!actionArgs.backtrack && (end <= cur.ch))return;
2015
2129
  if (token) {
2016
2130
  var increment = actionArgs.increase ? 1 : -1;
2017
2131
  var number = parseInt(token) + (increment * actionArgs.repeat);
@@ -2120,6 +2234,10 @@
2120
2234
  function exitVisualMode(cm) {
2121
2235
  cm.off('mousedown', exitVisualMode);
2122
2236
  var vim = cm.state.vim;
2237
+ // can't use selection state here because yank has already reset its cursor
2238
+ vim.lastSelection = {'curStart': vim.marks['<'].find(),
2239
+ 'curEnd': vim.marks['>'].find(), 'visualMode': vim.visualMode,
2240
+ 'visualLine': vim.visualLine};
2123
2241
  vim.visualMode = false;
2124
2242
  vim.visualLine = false;
2125
2243
  var selectionStart = cm.getCursor('anchor');
@@ -2243,7 +2361,7 @@
2243
2361
  }
2244
2362
 
2245
2363
  function recordJumpPosition(cm, oldCur, newCur) {
2246
- if(!cursorEqual(oldCur, newCur)) {
2364
+ if (!cursorEqual(oldCur, newCur)) {
2247
2365
  vimGlobalState.jumpList.add(cm, oldCur, newCur);
2248
2366
  }
2249
2367
  }
@@ -2266,7 +2384,7 @@
2266
2384
  isComplete: function(state) {
2267
2385
  if (state.nextCh === state.symb) {
2268
2386
  state.depth++;
2269
- if(state.depth >= 1)return true;
2387
+ if (state.depth >= 1)return true;
2270
2388
  } else if (state.nextCh === state.reverseSymb) {
2271
2389
  state.depth--;
2272
2390
  }
@@ -2298,7 +2416,7 @@
2298
2416
  state.reverseSymb = state.symb === '{' ? '}' : '{';
2299
2417
  },
2300
2418
  isComplete: function(state) {
2301
- if(state.nextCh === state.symb)return true;
2419
+ if (state.nextCh === state.symb)return true;
2302
2420
  return false;
2303
2421
  }
2304
2422
  },
@@ -2320,7 +2438,7 @@
2320
2438
  }
2321
2439
  state.depth--;
2322
2440
  }
2323
- if(token === 'else' && state.depth === 0)return true;
2441
+ if (token === 'else' && state.depth === 0)return true;
2324
2442
  }
2325
2443
  return false;
2326
2444
  }
@@ -2345,10 +2463,10 @@
2345
2463
  curMoveThrough: false
2346
2464
  };
2347
2465
  var mode = symbolToMode[symb];
2348
- if(!mode)return cur;
2466
+ if (!mode)return cur;
2349
2467
  var init = findSymbolModes[mode].init;
2350
2468
  var isComplete = findSymbolModes[mode].isComplete;
2351
- if(init)init(state);
2469
+ if (init) { init(state); }
2352
2470
  while (line !== endLine && repeat) {
2353
2471
  state.index += increment;
2354
2472
  state.nextCh = state.lineText.charAt(state.index);
@@ -2633,14 +2751,14 @@
2633
2751
  var end = findMatchedSymbol(cm, cur, revSymb);
2634
2752
  var start = findMatchedSymbol(cm, end);
2635
2753
 
2636
- if((start.line == end.line && start.ch > end.ch)
2754
+ if ((start.line == end.line && start.ch > end.ch)
2637
2755
  || (start.line > end.line)) {
2638
2756
  var tmp = start;
2639
2757
  start = end;
2640
2758
  end = tmp;
2641
2759
  }
2642
2760
 
2643
- if(inclusive) {
2761
+ if (inclusive) {
2644
2762
  end.ch += 1;
2645
2763
  } else {
2646
2764
  start.ch += 1;
@@ -2711,6 +2829,7 @@
2711
2829
  }
2712
2830
 
2713
2831
  // Search functions
2832
+ defineOption('pcre', true, 'boolean');
2714
2833
  function SearchState() {}
2715
2834
  SearchState.prototype = {
2716
2835
  getQuery: function() {
@@ -2760,7 +2879,7 @@
2760
2879
  }
2761
2880
 
2762
2881
  // Translates a search string from ex (vim) syntax into javascript form.
2763
- function fixRegex(str) {
2882
+ function translateRegex(str) {
2764
2883
  // When these match, add a '\' if unescaped or remove one if escaped.
2765
2884
  var specials = ['|', '(', ')', '{'];
2766
2885
  // Remove, but never add, a '\' for these.
@@ -2799,15 +2918,17 @@
2799
2918
  }
2800
2919
 
2801
2920
  // Translates the replace part of a search and replace from ex (vim) syntax into
2802
- // javascript form. Similar to fixRegex, but additionally fixes back references
2921
+ // javascript form. Similar to translateRegex, but additionally fixes back references
2803
2922
  // (translates '\[0..9]' to '$[0..9]') and follows different rules for escaping '$'.
2804
- function fixRegexReplace(str) {
2923
+ function translateRegexReplace(str) {
2805
2924
  var escapeNextChar = false;
2806
2925
  var out = [];
2807
2926
  for (var i = -1; i < str.length; i++) {
2808
2927
  var c = str.charAt(i) || '';
2809
2928
  var n = str.charAt(i+1) || '';
2810
2929
  if (escapeNextChar) {
2930
+ // At any point in the loop, escapeNextChar is true if the previous
2931
+ // character was a '\' and was not escaped.
2811
2932
  out.push(c);
2812
2933
  escapeNextChar = false;
2813
2934
  } else {
@@ -2832,6 +2953,29 @@
2832
2953
  return out.join('');
2833
2954
  }
2834
2955
 
2956
+ // Unescape \ and / in the replace part, for PCRE mode.
2957
+ function unescapeRegexReplace(str) {
2958
+ var stream = new CodeMirror.StringStream(str);
2959
+ var output = [];
2960
+ while (!stream.eol()) {
2961
+ // Search for \.
2962
+ while (stream.peek() && stream.peek() != '\\') {
2963
+ output.push(stream.next());
2964
+ }
2965
+ if (stream.match('\\/', true)) {
2966
+ // \/ => /
2967
+ output.push('/');
2968
+ } else if (stream.match('\\\\', true)) {
2969
+ // \\ => \
2970
+ output.push('\\');
2971
+ } else {
2972
+ // Don't change anything
2973
+ output.push(stream.next());
2974
+ }
2975
+ }
2976
+ return output.join('');
2977
+ }
2978
+
2835
2979
  /**
2836
2980
  * Extract the regular expression from the query and return a Regexp object.
2837
2981
  * Returns null if the query is blank.
@@ -2863,7 +3007,9 @@
2863
3007
  if (!regexPart) {
2864
3008
  return null;
2865
3009
  }
2866
- regexPart = fixRegex(regexPart);
3010
+ if (!getOption('pcre')) {
3011
+ regexPart = translateRegex(regexPart);
3012
+ }
2867
3013
  if (smartCase) {
2868
3014
  ignoreCase = (/^[^A-Z]*$/).test(regexPart);
2869
3015
  }
@@ -3045,13 +3191,16 @@
3045
3191
  { name: 'map' },
3046
3192
  { name: 'nmap', shortName: 'nm' },
3047
3193
  { name: 'vmap', shortName: 'vm' },
3194
+ { name: 'unmap' },
3048
3195
  { name: 'write', shortName: 'w' },
3049
3196
  { name: 'undo', shortName: 'u' },
3050
3197
  { name: 'redo', shortName: 'red' },
3198
+ { name: 'set', shortName: 'set' },
3051
3199
  { name: 'sort', shortName: 'sor' },
3052
3200
  { name: 'substitute', shortName: 's' },
3053
3201
  { name: 'nohlsearch', shortName: 'noh' },
3054
- { name: 'delmarks', shortName: 'delm' }
3202
+ { name: 'delmarks', shortName: 'delm' },
3203
+ { name: 'registers', shortName: 'reg', excludeFromCommandHistory: true }
3055
3204
  ];
3056
3205
  Vim.ExCommandDispatcher = function() {
3057
3206
  this.buildCommandMap_();
@@ -3059,17 +3208,21 @@
3059
3208
  Vim.ExCommandDispatcher.prototype = {
3060
3209
  processCommand: function(cm, input) {
3061
3210
  var vim = cm.state.vim;
3211
+ var commandHistoryRegister = vimGlobalState.registerController.getRegister(':');
3212
+ var previousCommand = commandHistoryRegister.toString();
3062
3213
  if (vim.visualMode) {
3063
3214
  exitVisualMode(cm);
3064
3215
  }
3065
3216
  var inputStream = new CodeMirror.StringStream(input);
3217
+ // update ": with the latest command whether valid or invalid
3218
+ commandHistoryRegister.setText(input);
3066
3219
  var params = {};
3067
3220
  params.input = input;
3068
3221
  try {
3069
3222
  this.parseInput_(cm, inputStream, params);
3070
3223
  } catch(e) {
3071
3224
  showConfirm(cm, e);
3072
- return;
3225
+ throw e;
3073
3226
  }
3074
3227
  var commandName;
3075
3228
  if (!params.commandName) {
@@ -3081,6 +3234,9 @@
3081
3234
  var command = this.matchCommand_(params.commandName);
3082
3235
  if (command) {
3083
3236
  commandName = command.name;
3237
+ if (command.excludeFromCommandHistory) {
3238
+ commandHistoryRegister.setText(previousCommand);
3239
+ }
3084
3240
  this.parseCommandArgs_(inputStream, params, command);
3085
3241
  if (command.type == 'exToKey') {
3086
3242
  // Handle Ex to Key mapping.
@@ -3195,14 +3351,16 @@
3195
3351
  this.commandMap_[commandName] = {
3196
3352
  name: commandName,
3197
3353
  type: 'exToEx',
3198
- toInput: rhs.substring(1)
3354
+ toInput: rhs.substring(1),
3355
+ user: true
3199
3356
  };
3200
3357
  } else {
3201
3358
  // Ex to key mapping
3202
3359
  this.commandMap_[commandName] = {
3203
3360
  name: commandName,
3204
3361
  type: 'exToKey',
3205
- toKeys: parseKeyString(rhs)
3362
+ toKeys: parseKeyString(rhs),
3363
+ user: true
3206
3364
  };
3207
3365
  }
3208
3366
  } else {
@@ -3211,7 +3369,8 @@
3211
3369
  var mapping = {
3212
3370
  keys: parseKeyString(lhs),
3213
3371
  type: 'keyToEx',
3214
- exArgs: { input: rhs.substring(1) }};
3372
+ exArgs: { input: rhs.substring(1) },
3373
+ user: true};
3215
3374
  if (ctx) { mapping.context = ctx; }
3216
3375
  defaultKeymap.unshift(mapping);
3217
3376
  } else {
@@ -3219,12 +3378,45 @@
3219
3378
  var mapping = {
3220
3379
  keys: parseKeyString(lhs),
3221
3380
  type: 'keyToKey',
3222
- toKeys: parseKeyString(rhs)
3381
+ toKeys: parseKeyString(rhs),
3382
+ user: true
3223
3383
  };
3224
3384
  if (ctx) { mapping.context = ctx; }
3225
3385
  defaultKeymap.unshift(mapping);
3226
3386
  }
3227
3387
  }
3388
+ },
3389
+ unmap: function(lhs, ctx) {
3390
+ var arrayEquals = function(a, b) {
3391
+ if (a === b) return true;
3392
+ if (a == null || b == null) return true;
3393
+ if (a.length != b.length) return false;
3394
+ for (var i = 0; i < a.length; i++) {
3395
+ if (a[i] !== b[i]) return false;
3396
+ }
3397
+ return true;
3398
+ };
3399
+ if (lhs != ':' && lhs.charAt(0) == ':') {
3400
+ // Ex to Ex or Ex to key mapping
3401
+ if (ctx) { throw Error('Mode not supported for ex mappings'); }
3402
+ var commandName = lhs.substring(1);
3403
+ if (this.commandMap_[commandName] && this.commandMap_[commandName].user) {
3404
+ delete this.commandMap_[commandName];
3405
+ return;
3406
+ }
3407
+ } else {
3408
+ // Key to Ex or key to key mapping
3409
+ var keys = parseKeyString(lhs);
3410
+ for (var i = 0; i < defaultKeymap.length; i++) {
3411
+ if (arrayEquals(keys, defaultKeymap[i].keys)
3412
+ && defaultKeymap[i].context === ctx
3413
+ && defaultKeymap[i].user) {
3414
+ defaultKeymap.splice(i, 1);
3415
+ return;
3416
+ }
3417
+ }
3418
+ }
3419
+ throw Error('No such mapping.');
3228
3420
  }
3229
3421
  };
3230
3422
 
@@ -3235,7 +3427,7 @@
3235
3427
  var keys = [];
3236
3428
  while (str) {
3237
3429
  match = (/<\w+-.+?>|<\w+>|./).exec(str);
3238
- if(match === null)break;
3430
+ if (match === null)break;
3239
3431
  key = match[0];
3240
3432
  str = str.substring(match.index + key.length);
3241
3433
  keys.push(key);
@@ -3256,6 +3448,16 @@
3256
3448
  },
3257
3449
  nmap: function(cm, params) { this.map(cm, params, 'normal'); },
3258
3450
  vmap: function(cm, params) { this.map(cm, params, 'visual'); },
3451
+ unmap: function(cm, params, ctx) {
3452
+ var mapArgs = params.args;
3453
+ if (!mapArgs || mapArgs.length < 1) {
3454
+ if (cm) {
3455
+ showConfirm(cm, 'No such mapping: ' + params.input);
3456
+ }
3457
+ return;
3458
+ }
3459
+ exCommandDispatcher.unmap(mapArgs[0], ctx);
3460
+ },
3259
3461
  move: function(cm, params) {
3260
3462
  commandDispatcher.processCommand(cm, cm.state.vim, {
3261
3463
  type: 'motion',
@@ -3264,6 +3466,73 @@
3264
3466
  linewise: true },
3265
3467
  repeatOverride: params.line+1});
3266
3468
  },
3469
+ set: function(cm, params) {
3470
+ var setArgs = params.args;
3471
+ if (!setArgs || setArgs.length < 1) {
3472
+ if (cm) {
3473
+ showConfirm(cm, 'Invalid mapping: ' + params.input);
3474
+ }
3475
+ return;
3476
+ }
3477
+ var expr = setArgs[0].split('=');
3478
+ var optionName = expr[0];
3479
+ var value = expr[1];
3480
+ var forceGet = false;
3481
+
3482
+ if (optionName.charAt(optionName.length - 1) == '?') {
3483
+ // If post-fixed with ?, then the set is actually a get.
3484
+ if (value) { throw Error('Trailing characters: ' + params.argString); }
3485
+ optionName = optionName.substring(0, optionName.length - 1);
3486
+ forceGet = true;
3487
+ }
3488
+ if (value === undefined && optionName.substring(0, 2) == 'no') {
3489
+ // To set boolean options to false, the option name is prefixed with
3490
+ // 'no'.
3491
+ optionName = optionName.substring(2);
3492
+ value = false;
3493
+ }
3494
+ var optionIsBoolean = options[optionName] && options[optionName].type == 'boolean';
3495
+ if (optionIsBoolean && value == undefined) {
3496
+ // Calling set with a boolean option sets it to true.
3497
+ value = true;
3498
+ }
3499
+ if (!optionIsBoolean && !value || forceGet) {
3500
+ var oldValue = getOption(optionName);
3501
+ // If no value is provided, then we assume this is a get.
3502
+ if (oldValue === true || oldValue === false) {
3503
+ showConfirm(cm, ' ' + (oldValue ? '' : 'no') + optionName);
3504
+ } else {
3505
+ showConfirm(cm, ' ' + optionName + '=' + oldValue);
3506
+ }
3507
+ } else {
3508
+ setOption(optionName, value);
3509
+ }
3510
+ },
3511
+ registers: function(cm,params) {
3512
+ var regArgs = params.args;
3513
+ var registers = vimGlobalState.registerController.registers;
3514
+ var regInfo = '----------Registers----------<br><br>';
3515
+ if (!regArgs) {
3516
+ for (var registerName in registers) {
3517
+ var text = registers[registerName].toString();
3518
+ if (text.length) {
3519
+ regInfo += '"' + registerName + ' ' + text + '<br>';
3520
+ }
3521
+ }
3522
+ } else {
3523
+ var registerName;
3524
+ regArgs = regArgs.join('');
3525
+ for (var i = 0; i < regArgs.length; i++) {
3526
+ registerName = regArgs.charAt(i);
3527
+ if (!vimGlobalState.registerController.isValidRegister(registerName)) {
3528
+ continue;
3529
+ }
3530
+ var register = registers[registerName] || new Register();
3531
+ regInfo += '"' + registerName + ' ' + register.toString() + '<br>';
3532
+ }
3533
+ }
3534
+ showConfirm(cm, regInfo);
3535
+ },
3267
3536
  sort: function(cm, params) {
3268
3537
  var reverse, ignoreCase, unique, number;
3269
3538
  function parseArgs() {
@@ -3345,34 +3614,41 @@
3345
3614
  'any other getSearchCursor implementation.');
3346
3615
  }
3347
3616
  var argString = params.argString;
3348
- var slashes = findUnescapedSlashes(argString);
3349
- if (slashes[0] !== 0) {
3350
- showConfirm(cm, 'Substitutions should be of the form ' +
3351
- ':s/pattern/replace/');
3352
- return;
3353
- }
3354
- var regexPart = argString.substring(slashes[0] + 1, slashes[1]);
3617
+ var slashes = argString ? findUnescapedSlashes(argString) : [];
3355
3618
  var replacePart = '';
3356
- var flagsPart;
3357
- var count;
3358
- var confirm = false; // Whether to confirm each replace.
3359
- if (slashes[1]) {
3360
- replacePart = argString.substring(slashes[1] + 1, slashes[2]);
3361
- replacePart = fixRegexReplace(replacePart);
3362
- }
3363
- if (slashes[2]) {
3364
- // After the 3rd slash, we can have flags followed by a space followed
3365
- // by count.
3366
- var trailing = argString.substring(slashes[2] + 1).split(' ');
3367
- flagsPart = trailing[0];
3368
- count = parseInt(trailing[1]);
3369
- }
3370
- if (flagsPart) {
3371
- if (flagsPart.indexOf('c') != -1) {
3372
- confirm = true;
3373
- flagsPart.replace('c', '');
3374
- }
3375
- regexPart = regexPart + '/' + flagsPart;
3619
+ if (slashes.length) {
3620
+ if (slashes[0] !== 0) {
3621
+ showConfirm(cm, 'Substitutions should be of the form ' +
3622
+ ':s/pattern/replace/');
3623
+ return;
3624
+ }
3625
+ var regexPart = argString.substring(slashes[0] + 1, slashes[1]);
3626
+ var flagsPart;
3627
+ var count;
3628
+ var confirm = false; // Whether to confirm each replace.
3629
+ if (slashes[1]) {
3630
+ replacePart = argString.substring(slashes[1] + 1, slashes[2]);
3631
+ if (getOption('pcre')) {
3632
+ replacePart = unescapeRegexReplace(replacePart);
3633
+ } else {
3634
+ replacePart = translateRegexReplace(replacePart);
3635
+ }
3636
+ vimGlobalState.lastSubstituteReplacePart = replacePart;
3637
+ }
3638
+ if (slashes[2]) {
3639
+ // After the 3rd slash, we can have flags followed by a space followed
3640
+ // by count.
3641
+ var trailing = argString.substring(slashes[2] + 1).split(' ');
3642
+ flagsPart = trailing[0];
3643
+ count = parseInt(trailing[1]);
3644
+ }
3645
+ if (flagsPart) {
3646
+ if (flagsPart.indexOf('c') != -1) {
3647
+ confirm = true;
3648
+ flagsPart.replace('c', '');
3649
+ }
3650
+ regexPart = regexPart + '/' + flagsPart;
3651
+ }
3376
3652
  }
3377
3653
  if (regexPart) {
3378
3654
  // If regex part is empty, then use the previous query. Otherwise use
@@ -3385,6 +3661,11 @@
3385
3661
  return;
3386
3662
  }
3387
3663
  }
3664
+ replacePart = replacePart || vimGlobalState.lastSubstituteReplacePart;
3665
+ if (replacePart === undefined) {
3666
+ showConfirm(cm, 'No previous substitute regular expression');
3667
+ return;
3668
+ }
3388
3669
  var state = getSearchState(cm);
3389
3670
  var query = state.getQuery();
3390
3671
  var lineStart = (params.line !== undefined) ? params.line : cm.getCursor().line;
@@ -3629,13 +3910,15 @@
3629
3910
 
3630
3911
  function exitInsertMode(cm) {
3631
3912
  var vim = cm.state.vim;
3632
- var inReplay = vimGlobalState.macroModeState.inReplay;
3633
- if (!inReplay) {
3913
+ var macroModeState = vimGlobalState.macroModeState;
3914
+ var insertModeChangeRegister = vimGlobalState.registerController.getRegister('.');
3915
+ var isPlaying = macroModeState.isPlaying;
3916
+ if (!isPlaying) {
3634
3917
  cm.off('change', onChange);
3635
3918
  cm.off('cursorActivity', onCursorActivity);
3636
3919
  CodeMirror.off(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown);
3637
3920
  }
3638
- if (!inReplay && vim.insertModeRepeat > 1) {
3921
+ if (!isPlaying && vim.insertModeRepeat > 1) {
3639
3922
  // Perform insert mode repeat for commands like 3,a and 3,o.
3640
3923
  repeatLastEdit(cm, vim, vim.insertModeRepeat - 1,
3641
3924
  true /** repeatForInsert */);
@@ -3647,7 +3930,12 @@
3647
3930
  cm.setOption('keyMap', 'vim');
3648
3931
  cm.setOption('disableInput', true);
3649
3932
  cm.toggleOverwrite(false); // exit replace mode if we were in it.
3933
+ // update the ". register before exiting insert mode
3934
+ insertModeChangeRegister.setText(macroModeState.lastInsertModeChanges.changes.join(''));
3650
3935
  CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
3936
+ if (macroModeState.isRecording) {
3937
+ logInsertModeChange(macroModeState);
3938
+ }
3651
3939
  }
3652
3940
 
3653
3941
  CodeMirror.keyMap['vim-insert'] = {
@@ -3671,45 +3959,57 @@
3671
3959
  fallthrough: ['vim-insert']
3672
3960
  };
3673
3961
 
3674
- function parseRegisterToKeyBuffer(macroModeState, registerName) {
3675
- var match, key;
3962
+ function executeMacroRegister(cm, vim, macroModeState, registerName) {
3676
3963
  var register = vimGlobalState.registerController.getRegister(registerName);
3677
- var text = register.toString();
3678
- var macroKeyBuffer = macroModeState.macroKeyBuffer;
3679
- emptyMacroKeyBuffer(macroModeState);
3680
- do {
3681
- match = (/<\w+-.+?>|<\w+>|./).exec(text);
3682
- if(match === null)break;
3683
- key = match[0];
3684
- text = text.substring(match.index + key.length);
3685
- macroKeyBuffer.push(key);
3686
- } while (text);
3687
- return macroKeyBuffer;
3688
- }
3689
-
3690
- function parseKeyBufferToRegister(registerName, keyBuffer) {
3691
- var text = keyBuffer.join('');
3692
- vimGlobalState.registerController.setRegisterText(registerName, text);
3964
+ var keyBuffer = register.keyBuffer;
3965
+ var imc = 0;
3966
+ macroModeState.isPlaying = true;
3967
+ macroModeState.replaySearchQueries = register.searchQueries.slice(0);
3968
+ for (var i = 0; i < keyBuffer.length; i++) {
3969
+ var text = keyBuffer[i];
3970
+ var match, key;
3971
+ while (text) {
3972
+ // Pull off one command key, which is either a single character
3973
+ // or a special sequence wrapped in '<' and '>', e.g. '<Space>'.
3974
+ match = (/<\w+-.+?>|<\w+>|./).exec(text);
3975
+ key = match[0];
3976
+ text = text.substring(match.index + key.length);
3977
+ CodeMirror.Vim.handleKey(cm, key);
3978
+ if (vim.insertMode) {
3979
+ repeatInsertModeChanges(
3980
+ cm, register.insertModeChanges[imc++].changes, 1);
3981
+ exitInsertMode(cm);
3982
+ }
3983
+ }
3984
+ };
3985
+ macroModeState.isPlaying = false;
3693
3986
  }
3694
3987
 
3695
- function emptyMacroKeyBuffer(macroModeState) {
3696
- if(macroModeState.isMacroPlaying)return;
3697
- var macroKeyBuffer = macroModeState.macroKeyBuffer;
3698
- macroKeyBuffer.length = 0;
3988
+ function logKey(macroModeState, key) {
3989
+ if (macroModeState.isPlaying) { return; }
3990
+ var registerName = macroModeState.latestRegister;
3991
+ var register = vimGlobalState.registerController.getRegister(registerName);
3992
+ if (register) {
3993
+ register.pushText(key);
3994
+ }
3699
3995
  }
3700
3996
 
3701
- function executeMacroKeyBuffer(cm, macroModeState, keyBuffer) {
3702
- macroModeState.isMacroPlaying = true;
3703
- for (var i = 0, len = keyBuffer.length; i < len; i++) {
3704
- CodeMirror.Vim.handleKey(cm, keyBuffer[i]);
3705
- };
3706
- macroModeState.isMacroPlaying = false;
3997
+ function logInsertModeChange(macroModeState) {
3998
+ if (macroModeState.isPlaying) { return; }
3999
+ var registerName = macroModeState.latestRegister;
4000
+ var register = vimGlobalState.registerController.getRegister(registerName);
4001
+ if (register) {
4002
+ register.pushInsertModeChanges(macroModeState.lastInsertModeChanges);
4003
+ }
3707
4004
  }
3708
4005
 
3709
- function logKey(macroModeState, key) {
3710
- if(macroModeState.isMacroPlaying)return;
3711
- var macroKeyBuffer = macroModeState.macroKeyBuffer;
3712
- macroKeyBuffer.push(key);
4006
+ function logSearchQuery(macroModeState, query) {
4007
+ if (macroModeState.isPlaying) { return; }
4008
+ var registerName = macroModeState.latestRegister;
4009
+ var register = vimGlobalState.registerController.getRegister(registerName);
4010
+ if (register) {
4011
+ register.pushSearchQuery(query);
4012
+ }
3713
4013
  }
3714
4014
 
3715
4015
  /**
@@ -3719,15 +4019,17 @@
3719
4019
  function onChange(_cm, changeObj) {
3720
4020
  var macroModeState = vimGlobalState.macroModeState;
3721
4021
  var lastChange = macroModeState.lastInsertModeChanges;
3722
- while (changeObj) {
3723
- lastChange.expectCursorActivityForChange = true;
3724
- if (changeObj.origin == '+input' || changeObj.origin == 'paste'
3725
- || changeObj.origin === undefined /* only in testing */) {
3726
- var text = changeObj.text.join('\n');
3727
- lastChange.changes.push(text);
4022
+ if (!macroModeState.isPlaying) {
4023
+ while(changeObj) {
4024
+ lastChange.expectCursorActivityForChange = true;
4025
+ if (changeObj.origin == '+input' || changeObj.origin == 'paste'
4026
+ || changeObj.origin === undefined /* only in testing */) {
4027
+ var text = changeObj.text.join('\n');
4028
+ lastChange.changes.push(text);
4029
+ }
4030
+ // Change objects may be chained with next.
4031
+ changeObj = changeObj.next;
3728
4032
  }
3729
- // Change objects may be chained with next.
3730
- changeObj = changeObj.next;
3731
4033
  }
3732
4034
  }
3733
4035
 
@@ -3738,6 +4040,7 @@
3738
4040
  */
3739
4041
  function onCursorActivity() {
3740
4042
  var macroModeState = vimGlobalState.macroModeState;
4043
+ if (macroModeState.isPlaying) { return; }
3741
4044
  var lastChange = macroModeState.lastInsertModeChanges;
3742
4045
  if (lastChange.expectCursorActivityForChange) {
3743
4046
  lastChange.expectCursorActivityForChange = false;
@@ -3781,7 +4084,7 @@
3781
4084
  */
3782
4085
  function repeatLastEdit(cm, vim, repeat, repeatForInsert) {
3783
4086
  var macroModeState = vimGlobalState.macroModeState;
3784
- macroModeState.inReplay = true;
4087
+ macroModeState.isPlaying = true;
3785
4088
  var isAction = !!vim.lastEditActionCommand;
3786
4089
  var cachedInputState = vim.inputState;
3787
4090
  function repeatCommand() {
@@ -3793,10 +4096,15 @@
3793
4096
  }
3794
4097
  function repeatInsert(repeat) {
3795
4098
  if (macroModeState.lastInsertModeChanges.changes.length > 0) {
3796
- // For some reason, repeat cw in desktop VIM will does not repeat
4099
+ // For some reason, repeat cw in desktop VIM does not repeat
3797
4100
  // insert mode changes. Will conform to that behavior.
3798
4101
  repeat = !vim.lastEditActionCommand ? 1 : repeat;
3799
- repeatLastInsertModeChanges(cm, repeat, macroModeState);
4102
+ var changeObject = macroModeState.lastInsertModeChanges;
4103
+ // This isn't strictly necessary, but since lastInsertModeChanges is
4104
+ // supposed to be immutable during replay, this helps catch bugs.
4105
+ macroModeState.lastInsertModeChanges = {};
4106
+ repeatInsertModeChanges(cm, changeObject.changes, repeat);
4107
+ macroModeState.lastInsertModeChanges = changeObject;
3800
4108
  }
3801
4109
  }
3802
4110
  vim.inputState = vim.lastEditInputState;
@@ -3822,11 +4130,10 @@
3822
4130
  // were called by an exitInsertMode call lower on the stack.
3823
4131
  exitInsertMode(cm);
3824
4132
  }
3825
- macroModeState.inReplay = false;
4133
+ macroModeState.isPlaying = false;
3826
4134
  };
3827
4135
 
3828
- function repeatLastInsertModeChanges(cm, repeat, macroModeState) {
3829
- var lastChange = macroModeState.lastInsertModeChanges;
4136
+ function repeatInsertModeChanges(cm, changes, repeat) {
3830
4137
  function keyHandler(binding) {
3831
4138
  if (typeof binding == 'string') {
3832
4139
  CodeMirror.commands[binding](cm);
@@ -3836,8 +4143,8 @@
3836
4143
  return true;
3837
4144
  }
3838
4145
  for (var i = 0; i < repeat; i++) {
3839
- for (var j = 0; j < lastChange.changes.length; j++) {
3840
- var change = lastChange.changes[j];
4146
+ for (var j = 0; j < changes.length; j++) {
4147
+ var change = changes[j];
3841
4148
  if (change instanceof InsertModeKey) {
3842
4149
  CodeMirror.lookupKey(change.keyName, ['vim-insert'], keyHandler);
3843
4150
  } else {