From 3643de0fbe032f296c238d0c585aa6dfb2517d2a Mon Sep 17 00:00:00 2001 From: Rhys van der Waerden Date: Mon, 6 Feb 2017 23:54:55 +1100 Subject: [PATCH 1/3] Support exact and inexact current word search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename `CommandStar` and `CommandHash` to `CommandSearchCurrentWordExactForward` and `CommandSearchCurrentWorldExactBackward`, which describe their effect. Also include regex word bounds in the search so that they act the same as in Vim. Add `CommandSearchCurrentWordForward` and `…Backward` (commands `g*` and `g#` respectively), that match without word bounds. Closes #1266 --- src/actions/actions.ts | 64 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/src/actions/actions.ts b/src/actions/actions.ts index 69a5e65a223..98a57c1a9f9 100644 --- a/src/actions/actions.ts +++ b/src/actions/actions.ts @@ -1571,12 +1571,40 @@ class CommandCmdA extends BaseCommand { } @RegisterAction -class CommandStar extends BaseCommand { +class CommandSearchCurrentWordExactForward extends BaseCommand { modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; keys = ["*"]; isMotion = true; runsOnceForEachCountPrefix = true; + public async exec(position: Position, vimState: VimState): Promise { + const currentWord = TextEditor.getWord(position); + if (currentWord === undefined) { + return vimState; + } + + vimState.globalState.searchState = new SearchState( + SearchDirection.Forward, vimState.cursorPosition, `\\b${currentWord}\\b`, { isRegex: true } + ); + + do { + vimState.cursorPosition = vimState.globalState.searchState.getNextSearchMatchPosition(vimState.cursorPosition).pos; + } while (TextEditor.getWord(vimState.cursorPosition) !== currentWord); + + // Turn one of the highlighting flags back on (turned off with :nohl) + Configuration.hl = true; + + return vimState; + } +} + +@RegisterAction +class CommandSearchCurrentWordForward extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + keys = ["g", "*"]; + isMotion = true; + runsOnceForEachCountPrefix = true; + public async exec(position: Position, vimState: VimState): Promise { const currentWord = TextEditor.getWord(position); if (currentWord === undefined) { @@ -1597,12 +1625,42 @@ class CommandStar extends BaseCommand { } @RegisterAction -class CommandHash extends BaseCommand { +class CommandSearchCurrentWordExactBackward extends BaseCommand { modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; keys = ["#"]; isMotion = true; runsOnceForEachCountPrefix = true; + public async exec(position: Position, vimState: VimState): Promise { + const currentWord = TextEditor.getWord(position); + if (currentWord === undefined) { + return vimState; + } + + vimState.globalState.searchState = new SearchState( + SearchDirection.Backward, vimState.cursorPosition, `\\b${currentWord}\\b`, { isRegex: true } + ); + do { + // use getWordLeft() on position to start at the beginning of the word. + // this ensures that any matches happen outside of the word currently selected, + // which are the desired semantics for this motion. + vimState.cursorPosition = vimState.globalState.searchState.getNextSearchMatchPosition(vimState.cursorPosition.getWordLeft(true)).pos; + } while (TextEditor.getWord(vimState.cursorPosition) !== currentWord); + + // Turn one of the highlighting flags back on (turned off with :nohl) + Configuration.hl = true; + + return vimState; + } +} + +@RegisterAction +class CommandSearchCurrentWordBackward extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + keys = ["g", "#"]; + isMotion = true; + runsOnceForEachCountPrefix = true; + public async exec(position: Position, vimState: VimState): Promise { const currentWord = TextEditor.getWord(position); if (currentWord === undefined) { @@ -1613,7 +1671,7 @@ class CommandHash extends BaseCommand { do { // use getWordLeft() on position to start at the beginning of the word. - // this ensures that any matches happen ounside of the word currently selected, + // this ensures that any matches happen outside of the word currently selected, // which are the desired semantics for this motion. vimState.cursorPosition = vimState.globalState.searchState.getNextSearchMatchPosition(vimState.cursorPosition.getWordLeft(true)).pos; } while (TextEditor.getWord(vimState.cursorPosition) !== currentWord); From 20d0bdf721a4985e7aa1029ed5816a667bfda799 Mon Sep 17 00:00:00 2001 From: Rhys van der Waerden Date: Wed, 8 Feb 2017 00:23:33 +1100 Subject: [PATCH 2/3] Test and fix current word search commands Add tests for `g*` and `g#` by themselves and in combination with `n` (to step to next command). Previously the `#` and `*` commands (and, by virtue of having been copied, the `g#` and `g*` commands introduced in a previous commit) would immediately search and filter any matches that were not full word matches. ie. they would skip matches "hello" when searching for "he". This would work fine for `*` or `#` directly, but subsequent uses of `n` or `N` would match incorrectly since the word bounds were not included in the search state. --- src/actions/actions.ts | 29 ++++++---------- test/mode/normalModeTests/motions.test.ts | 42 +++++++++++++++++------ 2 files changed, 43 insertions(+), 28 deletions(-) diff --git a/src/actions/actions.ts b/src/actions/actions.ts index 98a57c1a9f9..fa8e6151168 100644 --- a/src/actions/actions.ts +++ b/src/actions/actions.ts @@ -1587,9 +1587,7 @@ class CommandSearchCurrentWordExactForward extends BaseCommand { SearchDirection.Forward, vimState.cursorPosition, `\\b${currentWord}\\b`, { isRegex: true } ); - do { - vimState.cursorPosition = vimState.globalState.searchState.getNextSearchMatchPosition(vimState.cursorPosition).pos; - } while (TextEditor.getWord(vimState.cursorPosition) !== currentWord); + vimState.cursorPosition = vimState.globalState.searchState.getNextSearchMatchPosition(vimState.cursorPosition).pos; // Turn one of the highlighting flags back on (turned off with :nohl) Configuration.hl = true; @@ -1613,9 +1611,7 @@ class CommandSearchCurrentWordForward extends BaseCommand { vimState.globalState.searchState = new SearchState(SearchDirection.Forward, vimState.cursorPosition, currentWord); - do { - vimState.cursorPosition = vimState.globalState.searchState.getNextSearchMatchPosition(vimState.cursorPosition).pos; - } while (TextEditor.getWord(vimState.cursorPosition) !== currentWord); + vimState.cursorPosition = vimState.globalState.searchState.getNextSearchMatchPosition(vimState.cursorPosition).pos; // Turn one of the highlighting flags back on (turned off with :nohl) Configuration.hl = true; @@ -1640,12 +1636,11 @@ class CommandSearchCurrentWordExactBackward extends BaseCommand { vimState.globalState.searchState = new SearchState( SearchDirection.Backward, vimState.cursorPosition, `\\b${currentWord}\\b`, { isRegex: true } ); - do { - // use getWordLeft() on position to start at the beginning of the word. - // this ensures that any matches happen outside of the word currently selected, - // which are the desired semantics for this motion. - vimState.cursorPosition = vimState.globalState.searchState.getNextSearchMatchPosition(vimState.cursorPosition.getWordLeft(true)).pos; - } while (TextEditor.getWord(vimState.cursorPosition) !== currentWord); + + // use getWordLeft() on position to start at the beginning of the word. + // this ensures that any matches happen outside of the word currently selected, + // which are the desired semantics for this motion. + vimState.cursorPosition = vimState.globalState.searchState.getNextSearchMatchPosition(vimState.cursorPosition.getWordLeft(true)).pos; // Turn one of the highlighting flags back on (turned off with :nohl) Configuration.hl = true; @@ -1669,12 +1664,10 @@ class CommandSearchCurrentWordBackward extends BaseCommand { vimState.globalState.searchState = new SearchState(SearchDirection.Backward, vimState.cursorPosition, currentWord); - do { - // use getWordLeft() on position to start at the beginning of the word. - // this ensures that any matches happen outside of the word currently selected, - // which are the desired semantics for this motion. - vimState.cursorPosition = vimState.globalState.searchState.getNextSearchMatchPosition(vimState.cursorPosition.getWordLeft(true)).pos; - } while (TextEditor.getWord(vimState.cursorPosition) !== currentWord); + // use getWordLeft() on position to start at the beginning of the word. + // this ensures that any matches happen outside of the word currently selected, + // which are the desired semantics for this motion. + vimState.cursorPosition = vimState.globalState.searchState.getNextSearchMatchPosition(vimState.cursorPosition.getWordLeft(true)).pos; // Turn one of the highlighting flags back on (turned off with :nohl) Configuration.hl = true; diff --git a/test/mode/normalModeTests/motions.test.ts b/test/mode/normalModeTests/motions.test.ts index 8c9a4f16c17..c5ae73d02a9 100644 --- a/test/mode/normalModeTests/motions.test.ts +++ b/test/mode/normalModeTests/motions.test.ts @@ -463,17 +463,25 @@ suite("Motions in Normal Mode", () => { }); newTest({ - title: "Can handle *", - start: ['|blah duh blah duh blah'], - keysPressed: '*', - end: ['blah duh |blah duh blah'] + title: "Can handle g*", + start: ['|blah duh blahblah duh blah'], + keysPressed: 'g*', + end: ['blah duh |blahblah duh blah'] + }); + + newTest({ + title: "Can handle g*n", + start: ['|blah duh blahblah duh blah'], + keysPressed: 'g*n', + end: ['blah duh blah|blah duh blah'] }); + newTest({ - title: "Can handle tricky *", - start: ['|blah blahblah duh blah'], + title: "Can handle *", + start: ['|blah blahblah duh blah blah'], keysPressed: '*', - end: ['blah blahblah duh |blah'] + end: ['blah blahblah duh |blah blah'] }); newTest({ @@ -497,11 +505,25 @@ suite("Motions in Normal Mode", () => { end: ['abc abcdef abc| '], }); + newTest({ + title: "Can handle g#", + start: ['blah duh blahblah duh |blah'], + keysPressed: 'g#', + end: ['blah duh blah|blah duh blah'] + }); + + newTest({ + title: "Can handle g#n", + start: ['blah duh blahblah duh |blah'], + keysPressed: 'g#n', + end: ['blah duh |blahblah duh blah'] + }); + newTest({ title: "Can handle #", - start: ['blah duh |blah duh blah'], + start: ['blah blah blahblah duh |blah'], keysPressed: '#', - end: ['|blah duh blah duh blah'] + end: ['blah |blah blahblah duh blah'] }); newTest({ @@ -630,4 +652,4 @@ suite("Motions in Normal Mode", () => { keysPressed: '', end: ['blah', 'duh', 'd|ur', 'hur'] }); -}); \ No newline at end of file +}); From cc2df02113dbec1140c39e5989bdb9b30abf5c84 Mon Sep 17 00:00:00 2001 From: Rhys van der Waerden Date: Wed, 8 Feb 2017 00:51:45 +1100 Subject: [PATCH 3/3] Remove duplicated search current word code Combine implementation of all four search commands (`*`, `#`, `g*`, `g#`) into a single function taking a `direction` and `isExact` argument. --- src/actions/actions.ts | 87 +++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 57 deletions(-) diff --git a/src/actions/actions.ts b/src/actions/actions.ts index fa8e6151168..d954af87f1c 100644 --- a/src/actions/actions.ts +++ b/src/actions/actions.ts @@ -1570,29 +1570,46 @@ class CommandCmdA extends BaseCommand { } } -@RegisterAction -class CommandSearchCurrentWordExactForward extends BaseCommand { - modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; - keys = ["*"]; - isMotion = true; - runsOnceForEachCountPrefix = true; - - public async exec(position: Position, vimState: VimState): Promise { +function searchCurrentWord(position: Position, vimState: VimState, direction: SearchDirection, isExact: boolean) { const currentWord = TextEditor.getWord(position); if (currentWord === undefined) { return vimState; } + // For an exact search we need to use a regex with word bounds. + const searchString = isExact + ? `\\b${currentWord}\\b` + : currentWord; + + // Start a search for the given term. vimState.globalState.searchState = new SearchState( - SearchDirection.Forward, vimState.cursorPosition, `\\b${currentWord}\\b`, { isRegex: true } + direction, vimState.cursorPosition, searchString, { isRegex: isExact } ); - vimState.cursorPosition = vimState.globalState.searchState.getNextSearchMatchPosition(vimState.cursorPosition).pos; + // If the search is going left then use `getWordLeft()` on position to start + // at the beginning of the word. This ensures that any matches happen + // outside of the currently selected word. + const searchStartCursorPosition = direction === SearchDirection.Backward + ? vimState.cursorPosition.getWordLeft(true) + : vimState.cursorPosition; + + vimState.cursorPosition = vimState.globalState.searchState.getNextSearchMatchPosition(searchStartCursorPosition).pos; // Turn one of the highlighting flags back on (turned off with :nohl) Configuration.hl = true; return vimState; +} + +@RegisterAction +class CommandSearchCurrentWordExactForward extends BaseCommand { + modes = [ModeName.Normal, ModeName.Visual, ModeName.VisualLine]; + keys = ["*"]; + isMotion = true; + runsOnceForEachCountPrefix = true; + + public async exec(position: Position, vimState: VimState): Promise { + return searchCurrentWord(position, vimState, SearchDirection.Forward, true); } } @@ -1604,19 +1621,7 @@ class CommandSearchCurrentWordForward extends BaseCommand { runsOnceForEachCountPrefix = true; public async exec(position: Position, vimState: VimState): Promise { - const currentWord = TextEditor.getWord(position); - if (currentWord === undefined) { - return vimState; - } - - vimState.globalState.searchState = new SearchState(SearchDirection.Forward, vimState.cursorPosition, currentWord); - - vimState.cursorPosition = vimState.globalState.searchState.getNextSearchMatchPosition(vimState.cursorPosition).pos; - - // Turn one of the highlighting flags back on (turned off with :nohl) - Configuration.hl = true; - - return vimState; + return searchCurrentWord(position, vimState, SearchDirection.Forward, false); } } @@ -1628,24 +1633,7 @@ class CommandSearchCurrentWordExactBackward extends BaseCommand { runsOnceForEachCountPrefix = true; public async exec(position: Position, vimState: VimState): Promise { - const currentWord = TextEditor.getWord(position); - if (currentWord === undefined) { - return vimState; - } - - vimState.globalState.searchState = new SearchState( - SearchDirection.Backward, vimState.cursorPosition, `\\b${currentWord}\\b`, { isRegex: true } - ); - - // use getWordLeft() on position to start at the beginning of the word. - // this ensures that any matches happen outside of the word currently selected, - // which are the desired semantics for this motion. - vimState.cursorPosition = vimState.globalState.searchState.getNextSearchMatchPosition(vimState.cursorPosition.getWordLeft(true)).pos; - - // Turn one of the highlighting flags back on (turned off with :nohl) - Configuration.hl = true; - - return vimState; + return searchCurrentWord(position, vimState, SearchDirection.Backward, true); } } @@ -1657,22 +1645,7 @@ class CommandSearchCurrentWordBackward extends BaseCommand { runsOnceForEachCountPrefix = true; public async exec(position: Position, vimState: VimState): Promise { - const currentWord = TextEditor.getWord(position); - if (currentWord === undefined) { - return vimState; - } - - vimState.globalState.searchState = new SearchState(SearchDirection.Backward, vimState.cursorPosition, currentWord); - - // use getWordLeft() on position to start at the beginning of the word. - // this ensures that any matches happen outside of the word currently selected, - // which are the desired semantics for this motion. - vimState.cursorPosition = vimState.globalState.searchState.getNextSearchMatchPosition(vimState.cursorPosition.getWordLeft(true)).pos; - - // Turn one of the highlighting flags back on (turned off with :nohl) - Configuration.hl = true; - - return vimState; + return searchCurrentWord(position, vimState, SearchDirection.Backward, false); } }