Add vim emulation support for `gn` and `gN`.
If we are given the following snippet of text: ``` A green green sky. _ ``` We can search for the word "green" with `/green`, and then use `gn` to select the next occurrence of "green" in visual mode. ``` A green green sky. ----- ``` Alternatively, we can use `cgn` and then enter the word "blue" to change the word "green" to "blue". ``` A blue green sky. ``` Then we can use the `.` operator to repeat the change: ``` A blue blue sky. ``` Addresses #3851.
This commit is contained in:
parent
fd2e32250e
commit
b6da8bf16b
|
@ -56,6 +56,7 @@ int getchar(void)
|
|||
}
|
||||
</textarea></form>
|
||||
<div style="font-size: 13px; width: 300px; height: 30px;">Key buffer: <span id="command-display"></span></div>
|
||||
<div style="font-size: 13px; width: 300px; height: 30px;">Vim mode: <span id="vim-mode"></span></div>
|
||||
|
||||
<p>The vim keybindings are enabled by including <code><a
|
||||
href="../keymap/vim.js">keymap/vim.js</a></code> and setting the
|
||||
|
@ -101,12 +102,16 @@ become a complete vim implementation</p>
|
|||
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);
|
||||
});
|
||||
</script>
|
||||
|
||||
</article>
|
||||
|
|
149
keymap/vim.js
149
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());
|
||||
|
|
169
test/vim_test.js
169
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', '<Esc>');
|
||||
helpers.assertCursorAt(0, 15);
|
||||
|
||||
// gn when cursor is at end of match
|
||||
helpers.doKeys('gn', '<Esc>');
|
||||
helpers.doKeys('<Esc>');
|
||||
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', '<Esc>');
|
||||
helpers.assertCursorAt(0, 11);
|
||||
|
||||
// gN when cursor is at end of match
|
||||
helpers.doKeys('e', 'gN', '<Esc>');
|
||||
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', '<Esc>');
|
||||
|
||||
// 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', '<Esc>');
|
||||
|
||||
// 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', '<Esc>');
|
||||
helpers.assertCursorAt(0, 11);
|
||||
|
||||
// gn when cursor is at end of match
|
||||
helpers.doKeys('e', 'gn', '<Esc>');
|
||||
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', '<Esc>');
|
||||
helpers.assertCursorAt(0, 15);
|
||||
|
||||
// gN when cursor is at end of match
|
||||
helpers.doKeys('gN', '<Esc>');
|
||||
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', '<Esc>');
|
||||
|
||||
// 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', '<Esc>');
|
||||
|
||||
// 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('*');
|
||||
|
|
Loading…
Reference in New Issue