Skip to content

Commit

Permalink
Add vim emulation support for gn and gN.
Browse files Browse the repository at this point in the history
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 codemirror#3851.
  • Loading branch information
howardjing committed Sep 7, 2020
1 parent fdbc04a commit db27040
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 13 deletions.
7 changes: 6 additions & 1 deletion demo/vim.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ <h2>Vim bindings demo</h2>
}
</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
Expand Down Expand Up @@ -102,12 +103,16 @@ <h2>Vim bindings demo</h2>
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>
137 changes: 125 additions & 12 deletions keymap/vim.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, toJumplist: true }},
{ keys: 'gN', type: 'motion', motion: 'findAndSelectNextInclusive', motionArgs: { forward: false, toJumplist: true }},
// 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 }},
Expand Down Expand Up @@ -1574,22 +1576,11 @@
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;
}
if (motionArgs.toJumplist) {
var jumpList = vimGlobalState.jumpList;
// if the current motion is # or *, use cachedCursor
var cachedCursor = jumpList.cachedCursor;
if (cachedCursor) {
recordJumpPosition(cm, cachedCursor, motionResult);
delete jumpList.cachedCursor;
} else {
recordJumpPosition(cm, origHead, motionResult);
}
}
if (motionResult instanceof Array) {
newAnchor = motionResult[0];
newHead = motionResult[1];
Expand All @@ -1600,6 +1591,17 @@
if (!newHead) {
newHead = copyCursor(origHead);
}
if (motionArgs.toJumplist) {
var jumpList = vimGlobalState.jumpList;
// if the current motion is # or *, use cachedCursor
var cachedCursor = jumpList.cachedCursor;
if (cachedCursor) {
recordJumpPosition(cm, cachedCursor, newHead);
delete jumpList.cachedCursor;
} else {
recordJumpPosition(cm, origHead, newHead);
}
}
if (vim.visualMode) {
if (!(vim.visualBlock && newHead.ch === Infinity)) {
newHead = clipCursorToContent(cm, newHead);
Expand Down Expand Up @@ -1772,6 +1774,84 @@
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. This attempts to turn on visual mode.
*/
findAndSelectNextInclusive: function(cm, _head, motionArgs, vim, prevInputState) {
console.log("cm", cm)
console.log("head", _head)
console.log("motionArgs", motionArgs)
console.log("vim", vim)
console.log("prevInputState", 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);

// 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 (prev) {
return [from, anchor];
}

return [anchor, to];
}
} 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 [from, to];
},
goToMark: function(cm, _head, motionArgs, vim) {
var pos = getMarkPos(cm, vim, motionArgs.selectedCharacter);
if (pos) {
Expand Down Expand Up @@ -4348,6 +4428,39 @@
return cursor.from();
});
}
/**
* Pretty much the same as `findNext`, except for the following differences:
*
* 1. Removed the `if (i==0)` branch -- we do not want to skip to the next match if the
* cursor is already on a match.
* 2. 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.
* 3. 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) {
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.
cursor.find(!prev);

for (var i = 0; i < repeat; i++) {
var 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());
Expand Down

0 comments on commit db27040

Please sign in to comment.