diff --git a/demo/vim.html b/demo/vim.html
index 2e874cdfa..172ab6f04 100644
--- a/demo/vim.html
+++ b/demo/vim.html
@@ -56,6 +56,7 @@ int getchar(void)
}
Key buffer:
+Vim mode:
The vim keybindings are enabled by including keymap/vim.js
and setting the
@@ -101,12 +102,16 @@ become a complete vim implementation
var keys = '';
CodeMirror.on(editor, 'vim-keypress', function(key) {
keys = keys + key;
- commandDisplay.innerHTML = keys;
+ commandDisplay.innerText = keys;
});
CodeMirror.on(editor, 'vim-command-done', function(e) {
keys = '';
commandDisplay.innerHTML = keys;
});
+ var vimMode = document.getElementById('vim-mode');
+ CodeMirror.on(editor, 'vim-mode-change', function(e) {
+ vimMode.innerText = JSON.stringify(e);
+ });
diff --git a/keymap/vim.js b/keymap/vim.js
index aca99cfbb..789e1e55b 100644
--- a/keymap/vim.js
+++ b/keymap/vim.js
@@ -141,6 +141,8 @@
{ keys: 'gU', type: 'operator', operator: 'changeCase', operatorArgs: {toLower: false}, isEdit: true },
{ keys: 'n', type: 'motion', motion: 'findNext', motionArgs: { forward: true, toJumplist: true }},
{ keys: 'N', type: 'motion', motion: 'findNext', motionArgs: { forward: false, toJumplist: true }},
+ { keys: 'gn', type: 'motion', motion: 'findAndSelectNextInclusive', motionArgs: { forward: true }},
+ { keys: 'gN', type: 'motion', motion: 'findAndSelectNextInclusive', motionArgs: { forward: false }},
// Operator-Motion dual commands
{ keys: 'x', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: true }, operatorMotionArgs: { visualLine: false }},
{ keys: 'X', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: false }, operatorMotionArgs: { visualLine: true }},
@@ -1576,7 +1578,7 @@
motionArgs.repeat = repeat;
clearInputState(cm);
if (motion) {
- var motionResult = motions[motion](cm, origHead, motionArgs, vim);
+ var motionResult = motions[motion](cm, origHead, motionArgs, vim, inputState);
vim.lastMotion = motions[motion];
if (!motionResult) {
return;
@@ -1774,6 +1776,87 @@
highlightSearchMatches(cm, query);
return findNext(cm, prev/** prev */, query, motionArgs.repeat);
},
+ /**
+ * Find and select the next occurrence of the search query. If the cursor is currently
+ * within a match, then find and select the current match. Otherwise, find the next occurrence in the
+ * appropriate direction.
+ *
+ * This differs from `findNext` in the following ways:
+ *
+ * 1. Instead of only returning the "from", this returns a "from", "to" range.
+ * 2. If the cursor is currently inside a search match, this selects the current match
+ * instead of the next match.
+ * 3. If there is no associated operator, this will turn on visual mode.
+ */
+ findAndSelectNextInclusive: function(cm, _head, motionArgs, vim, prevInputState) {
+ var state = getSearchState(cm);
+ var query = state.getQuery();
+
+ if (!query) {
+ return;
+ }
+
+ var prev = !motionArgs.forward;
+ prev = (state.isReversed()) ? !prev : prev;
+
+ // next: [from, to] | null
+ var next = findNextFromAndToInclusive(cm, prev, query, motionArgs.repeat, vim);
+
+ // No matches.
+ if (!next) {
+ return;
+ }
+
+ // If there's an operator that will be executed, return the selection.
+ if (prevInputState.operator) {
+ return next;
+ }
+
+ // At this point, we know that there is no accompanying operator -- let's
+ // deal with visual mode in order to select an appropriate match.
+
+ var from = next[0];
+ // For whatever reason, when we use the "to" as returned by searchcursor.js directly,
+ // the resulting selection is extended by 1 char. Let's shrink it so that only the
+ // match is selected.
+ var to = Pos(next[1].line, next[1].ch - 1);
+
+ if (vim.visualMode) {
+ // If we were in visualLine or visualBlock mode, get out of it.
+ if (vim.visualLine || vim.visualBlock) {
+ vim.visualLine = false;
+ vim.visualBlock = false;
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: ""});
+ }
+
+ // If we're currently in visual mode, we should extend the selection to include
+ // the search result.
+ var anchor = vim.sel.anchor;
+ if (anchor) {
+ if (state.isReversed()) {
+ if (motionArgs.forward) {
+ return [anchor, from];
+ }
+
+ return [anchor, to];
+ } else {
+ if (motionArgs.forward) {
+ return [anchor, to];
+ }
+
+ return [anchor, from];
+ }
+ }
+ } else {
+ // Let's turn visual mode on.
+ vim.visualMode = true;
+ vim.visualLine = false;
+ vim.visualBlock = false;
+ CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: ""});
+ }
+
+ return prev ? [to, from] : [from, to];
+ },
goToMark: function(cm, _head, motionArgs, vim) {
var pos = getMarkPos(cm, vim, motionArgs.selectedCharacter);
if (pos) {
@@ -1869,8 +1952,8 @@
// move to previous/next line is triggered.
if (line < first && cur.line == first){
return this.moveToStartOfLine(cm, head, motionArgs, vim);
- }else if (line > last && cur.line == last){
- return this.moveToEol(cm, head, motionArgs, vim, true);
+ } else if (line > last && cur.line == last){
+ return moveToEol(cm, head, motionArgs, vim, true);
}
if (motionArgs.toFirstChar){
endCh=findFirstNonWhiteSpaceCharacter(cm.getLine(line));
@@ -1972,16 +2055,8 @@
vim.lastHSPos = cm.charCoords(head,'div').left;
return moveToColumn(cm, repeat);
},
- moveToEol: function(cm, head, motionArgs, vim, keepHPos) {
- var cur = head;
- var retval= Pos(cur.line + motionArgs.repeat - 1, Infinity);
- var end=cm.clipPos(retval);
- end.ch--;
- if (!keepHPos) {
- vim.lastHPos = Infinity;
- vim.lastHSPos = cm.charCoords(end,'div').left;
- }
- return retval;
+ moveToEol: function(cm, head, motionArgs, vim) {
+ return moveToEol(cm, head, motionArgs, vim, false);
},
moveToFirstNonWhiteSpaceCharacter: function(cm, head) {
// Go to the start of the line where the text begins, or the end for
@@ -3609,6 +3684,18 @@
}
}
+ function moveToEol(cm, head, motionArgs, vim, keepHPos) {
+ var cur = head;
+ var retval= Pos(cur.line + motionArgs.repeat - 1, Infinity);
+ var end=cm.clipPos(retval);
+ end.ch--;
+ if (!keepHPos) {
+ vim.lastHPos = Infinity;
+ vim.lastHSPos = cm.charCoords(end,'div').left;
+ }
+ return retval;
+ }
+
function moveToCharacter(cm, repeat, forward, character) {
var cur = cm.getCursor();
var start = cur.ch;
@@ -4350,6 +4437,42 @@
return cursor.from();
});
}
+ /**
+ * Pretty much the same as `findNext`, except for the following differences:
+ *
+ * 1. Before starting the search, move to the previous search. This way if our cursor is
+ * already inside a match, we should return the current match.
+ * 2. Rather than only returning the cursor's from, we return the cursor's from and to as a tuple.
+ */
+ function findNextFromAndToInclusive(cm, prev, query, repeat, vim) {
+ if (repeat === undefined) { repeat = 1; }
+ return cm.operation(function() {
+ var pos = cm.getCursor();
+ var cursor = cm.getSearchCursor(query, pos);
+
+ // Go back one result to ensure that if the cursor is currently a match, we keep it.
+ var found = cursor.find(!prev);
+
+ // If we haven't moved, go back one more (similar to if i==0 logic in findNext).
+ if (!vim.visualMode && found && cursorEqual(cursor.from(), pos)) {
+ cursor.find(!prev);
+ }
+
+ for (var i = 0; i < repeat; i++) {
+ found = cursor.find(prev);
+ if (!found) {
+ // SearchCursor may have returned null because it hit EOF, wrap
+ // around and try again.
+ cursor = cm.getSearchCursor(query,
+ (prev) ? Pos(cm.lastLine()) : Pos(cm.firstLine(), 0) );
+ if (!cursor.find(prev)) {
+ return;
+ }
+ }
+ }
+ return [cursor.from(), cursor.to()];
+ });
+ }
function clearSearchHighlight(cm) {
var state = getSearchState(cm);
cm.removeOverlay(getSearchState(cm).getOverlay());
diff --git a/test/vim_test.js b/test/vim_test.js
index 974304140..c75a1646c 100644
--- a/test/vim_test.js
+++ b/test/vim_test.js
@@ -2579,6 +2579,91 @@ testVim('/ and n/N', function(cm, vim, helpers) {
helpers.doKeys('2', '/');
helpers.assertCursorAt(1, 6);
}, { value: 'match nope match \n nope Match' });
+testVim('/ and gn selects the appropriate word', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('match');
+ helpers.doKeys('/');
+ helpers.assertCursorAt(0, 11);
+
+ // gn should highlight the the current word while it is within a match.
+
+ // gn when cursor is in beginning of match
+ helpers.doKeys('gn', '');
+ helpers.assertCursorAt(0, 15);
+
+ // gn when cursor is at end of match
+ helpers.doKeys('gn', '');
+ helpers.doKeys('');
+ helpers.assertCursorAt(0, 15);
+
+ // consecutive gns should extend the selection
+ helpers.doKeys('gn');
+ helpers.assertCursorAt(0, 16);
+ helpers.doKeys('gn');
+ helpers.assertCursorAt(1, 11);
+
+ // we should have selected the second and third "match"
+ helpers.doKeys('d');
+ eq('match nope ', cm.getValue());
+}, { value: 'match nope match \n nope Match' });
+testVim('/ and gN selects the appropriate word', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('match');
+ helpers.doKeys('/');
+ helpers.assertCursorAt(0, 11);
+
+ // gN when cursor is at beginning of match
+ helpers.doKeys('gN', '');
+ helpers.assertCursorAt(0, 11);
+
+ // gN when cursor is at end of match
+ helpers.doKeys('e', 'gN', '');
+ helpers.assertCursorAt(0, 11);
+
+ // consecutive gNs should extend the selection
+ helpers.doKeys('gN');
+ helpers.assertCursorAt(0, 11);
+ helpers.doKeys('gN');
+ helpers.assertCursorAt(0, 0);
+
+ // we should have selected the first and second "match"
+ helpers.doKeys('d');
+ eq(' \n nope Match', cm.getValue());
+}, { value: 'match nope match \n nope Match' })
+testVim('/ and gn with an associated operator', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('match');
+ helpers.doKeys('/');
+ helpers.assertCursorAt(0, 11);
+
+ helpers.doKeys('c', 'gn', 'changed', '');
+
+ // change the current match.
+ eq('match nope changed \n nope Match', cm.getValue());
+
+ // change the next match.
+ helpers.doKeys('.');
+ eq('match nope changed \n nope changed', cm.getValue());
+
+ // change the final match.
+ helpers.doKeys('.');
+ eq('changed nope changed \n nope changed', cm.getValue());
+}, { value: 'match nope match \n nope Match' });
+testVim('/ and gN with an associated operator', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('match');
+ helpers.doKeys('/');
+ helpers.assertCursorAt(0, 11);
+
+ helpers.doKeys('c', 'gN', 'changed', '');
+
+ // change the current match.
+ eq('match nope changed \n nope Match', cm.getValue());
+
+ // change the next match.
+ helpers.doKeys('.');
+ eq('changed nope changed \n nope Match', cm.getValue());
+
+ // change the final match.
+ helpers.doKeys('.');
+ eq('changed nope changed \n nope changed', cm.getValue());
+}, { value: 'match nope match \n nope Match' });
testVim('/_case', function(cm, vim, helpers) {
cm.openDialog = helpers.fakeOpenDialog('Match');
helpers.doKeys('/');
@@ -2679,6 +2764,90 @@ testVim('? and n/N', function(cm, vim, helpers) {
helpers.doKeys('2', '?');
helpers.assertCursorAt(0, 11);
}, { value: 'match nope match \n nope Match' });
+testVim('? and gn selects the appropriate word', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('match');
+ helpers.doKeys('?', 'n');
+ helpers.assertCursorAt(0, 11);
+
+ // gn should highlight the the current word while it is within a match.
+
+ // gn when cursor is in beginning of match
+ helpers.doKeys('gn', '');
+ helpers.assertCursorAt(0, 11);
+
+ // gn when cursor is at end of match
+ helpers.doKeys('e', 'gn', '');
+ helpers.assertCursorAt(0, 11);
+
+ // consecutive gns should extend the selection
+ helpers.doKeys('gn');
+ helpers.assertCursorAt(0, 11);
+ helpers.doKeys('gn');
+ helpers.assertCursorAt(0, 0);
+
+ // we should have selected the first and second "match"
+ helpers.doKeys('d');
+ eq(' \n nope Match', cm.getValue());
+}, { value: 'match nope match \n nope Match' });
+testVim('? and gN selects the appropriate word', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('match');
+ helpers.doKeys('?', 'n');
+ helpers.assertCursorAt(0, 11);
+
+ // gN when cursor is at beginning of match
+ helpers.doKeys('gN', '');
+ helpers.assertCursorAt(0, 15);
+
+ // gN when cursor is at end of match
+ helpers.doKeys('gN', '');
+ helpers.assertCursorAt(0, 15);
+
+ // consecutive gNs should extend the selection
+ helpers.doKeys('gN');
+ helpers.assertCursorAt(0, 16);
+ helpers.doKeys('gN');
+ helpers.assertCursorAt(1, 11);
+
+ // we should have selected the second and third "match"
+ helpers.doKeys('d');
+ eq('match nope ', cm.getValue());
+}, { value: 'match nope match \n nope Match' })
+testVim('? and gn with an associated operator', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('match');
+ helpers.doKeys('?', 'n');
+ helpers.assertCursorAt(0, 11);
+
+ helpers.doKeys('c', 'gn', 'changed', '');
+
+ // change the current match.
+ eq('match nope changed \n nope Match', cm.getValue());
+
+ // change the next match.
+ helpers.doKeys('.');
+ eq('changed nope changed \n nope Match', cm.getValue());
+
+ // change the final match.
+ helpers.doKeys('.');
+ eq('changed nope changed \n nope changed', cm.getValue());
+}, { value: 'match nope match \n nope Match' });
+testVim('? and gN with an associated operator', function(cm, vim, helpers) {
+ cm.openDialog = helpers.fakeOpenDialog('match');
+ helpers.doKeys('?', 'n');
+ helpers.assertCursorAt(0, 11);
+
+ helpers.doKeys('c', 'gN', 'changed', '');
+
+ // change the current match.
+ eq('match nope changed \n nope Match', cm.getValue());
+
+ // change the next match.
+ helpers.doKeys('.');
+ eq('match nope changed \n nope changed', cm.getValue());
+
+ // change the final match.
+ helpers.doKeys('.');
+ eq('changed nope changed \n nope changed', cm.getValue());
+}, { value: 'match nope match \n nope Match' });
testVim('*', function(cm, vim, helpers) {
cm.setCursor(0, 9);
helpers.doKeys('*');