From db270406fbf6d21f673cabbf8faf11f0a729d2ed Mon Sep 17 00:00:00 2001 From: Howard Jing Date: Mon, 7 Sep 2020 17:21:50 -0400 Subject: [PATCH] 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. --- demo/vim.html | 7 ++- keymap/vim.js | 137 +++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 131 insertions(+), 13 deletions(-) diff --git a/demo/vim.html b/demo/vim.html index c939505d8f0..8a4682b30c9 100644 --- a/demo/vim.html +++ b/demo/vim.html @@ -56,6 +56,7 @@

Vim bindings demo

}
Key buffer:
+
Vim mode:

The vim keybindings are enabled by including keymap/vim.js and setting the @@ -102,12 +103,16 @@

Vim bindings demo

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 5a4860c65af..2a827a3cf49 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, 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 }}, @@ -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]; @@ -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); @@ -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) { @@ -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());