diff --git a/src/actions/commands/actions.ts b/src/actions/commands/actions.ts index 114ee63f38a..9b3a43164f2 100644 --- a/src/actions/commands/actions.ts +++ b/src/actions/commands/actions.ts @@ -1058,6 +1058,30 @@ class CommandEscInSearchMode extends BaseCommand { } } +@RegisterAction +class CommandRemoveWordInSearchMode extends BaseCommand { + modes = [ModeName.SearchInProgressMode]; + keys = ['']; + runsOnceForEveryCursor() { + return false; + } + + public async exec(position: Position, vimState: VimState): Promise { + const searchState = globalState.searchState!; + const pos = vimState.statusBarCursorCharacterPos; + const searchString = searchState.searchString; + const characterAt = Position.getWordLeft(searchString, pos); + // Needs explicit check undefined because zero is falsy and zero is a valid character pos. + if (characterAt !== undefined) { + searchState.searchString = searchString + .substring(0, characterAt) + .concat(searchString.slice(pos)); + vimState.statusBarCursorCharacterPos = pos - (pos - characterAt); + } + + return vimState; + } +} @RegisterAction class CommandPasteInSearchMode extends BaseCommand { modes = [ModeName.SearchInProgressMode]; @@ -2185,6 +2209,32 @@ class CommandEscInCommandline extends BaseCommand { } } +@RegisterAction +class CommandRemoveWordCommandline extends BaseCommand { + modes = [ModeName.CommandlineInProgress]; + keys = ['']; + runsOnceForEveryCursor() { + return false; + } + + public async exec(position: Position, vimState: VimState): Promise { + const key = this.keysPressed[0]; + const pos = vimState.statusBarCursorCharacterPos; + const cmdText = vimState.currentCommandlineText; + const characterAt = Position.getWordLeft(cmdText, pos); + // Needs explicit check undefined because zero is falsy and zero is a valid character pos. + if (characterAt !== undefined) { + vimState.currentCommandlineText = cmdText + .substring(0, characterAt) + .concat(cmdText.slice(pos)); + vimState.statusBarCursorCharacterPos = pos - (pos - characterAt); + } + + commandLine.lastKeyPressed = key; + return vimState; + } +} + @RegisterAction class CommandPasteInCommandline extends BaseCommand { modes = [ModeName.CommandlineInProgress]; diff --git a/src/common/motion/position.ts b/src/common/motion/position.ts index 245d2079e09..967d0fedd79 100644 --- a/src/common/motion/position.ts +++ b/src/common/motion/position.ts @@ -523,6 +523,22 @@ export class Position extends vscode.Position { ); } + /** + * Get the position of the word counting from the position specified. + * @param text The string to search from. + * @param pos The position of text to search from. + * @param inclusive true if we consider the pos a valid result, false otherwise. + * @returns The character position of the word to the left relative to the text and the pos. + * undefined if there is no word to the left of the postion. + */ + public static getWordLeft( + text: string, + pos: number, + inclusive: boolean = false + ): number | undefined { + return Position.getWordLeftWithRegex(text, pos, Position._nonWordCharRegex, inclusive); + } + /** * Inclusive is true if we consider the current position a valid result, false otherwise. */ @@ -1030,7 +1046,7 @@ export class Position extends vscode.Position { return regexp; } - private getAllPositions(line: string, regex: RegExp): number[] { + private static getAllPositions(line: string, regex: RegExp): number[] { let positions: number[] = []; let result = regex.exec(line); @@ -1068,23 +1084,31 @@ export class Position extends vscode.Position { return positions; } + private static getWordLeftWithRegex( + text: string, + pos: number, + regex: RegExp, + forceFirst: boolean = false, + inclusive: boolean = false + ): number | undefined { + const positions = Position.getAllPositions(text, regex); + return positions + .reverse() + .find(index => (index < pos && !inclusive) || (index <= pos && inclusive) || forceFirst); + } + /** * Inclusive is true if we consider the current position a valid result, false otherwise. */ private getWordLeftWithRegex(regex: RegExp, inclusive: boolean = false): Position { for (let currentLine = this.line; currentLine >= 0; currentLine--) { - let positions = this.getAllPositions( + const newCharacter = Position.getWordLeftWithRegex( TextEditor.getLineAt(new vscode.Position(currentLine, 0)).text, - regex + this.character, + regex, + currentLine !== this.line, + inclusive ); - let newCharacter = positions - .reverse() - .find( - index => - (index < this.character && !inclusive) || - (index <= this.character && inclusive) || - currentLine !== this.line - ); if (newCharacter !== undefined) { return new Position(currentLine, newCharacter); @@ -1099,7 +1123,7 @@ export class Position extends vscode.Position { */ private getWordRightWithRegex(regex: RegExp, inclusive: boolean = false): Position { for (let currentLine = this.line; currentLine < TextEditor.getLineCount(); currentLine++) { - let positions = this.getAllPositions( + let positions = Position.getAllPositions( TextEditor.getLineAt(new vscode.Position(currentLine, 0)).text, regex ); @@ -1226,7 +1250,7 @@ export class Position extends vscode.Position { private getCurrentSentenceEndWithRegex(regex: RegExp, inclusive: boolean): Position { let paragraphEnd = this.getCurrentParagraphEnd(); for (let currentLine = this.line; currentLine <= paragraphEnd.line; currentLine++) { - let allPositions = this.getAllPositions( + let allPositions = Position.getAllPositions( TextEditor.getLineAt(new vscode.Position(currentLine, 0)).text, regex ); @@ -1252,7 +1276,7 @@ export class Position extends vscode.Position { return paragraphEnd; } else { for (let currentLine = this.line; currentLine <= paragraphEnd.line; currentLine++) { - const nonWhitePositions = this.getAllPositions( + const nonWhitePositions = Position.getAllPositions( TextEditor.getLineAt(new vscode.Position(currentLine, 0)).text, /\S/g ); diff --git a/test/cmd_line/command.test.ts b/test/cmd_line/command.test.ts new file mode 100644 index 00000000000..a3e3a202dac --- /dev/null +++ b/test/cmd_line/command.test.ts @@ -0,0 +1,96 @@ +import * as assert from 'assert'; +import { getAndUpdateModeHandler } from '../../extension'; +import { ModeHandler } from '../../src/mode/modeHandler'; +import { StatusBar } from '../../src/statusBar'; +import { cleanUpWorkspace, setupWorkspace } from '../testUtils'; + +suite('cmd_line/search command', () => { + let modeHandler: ModeHandler; + + suiteSetup(async () => { + await setupWorkspace(); + modeHandler = await getAndUpdateModeHandler(); + }); + + suiteTeardown(cleanUpWorkspace); + + test('command can remove word in cmd line', async () => { + await modeHandler.handleMultipleKeyEvents([':', 'a', 'b', 'c', '-', '1', '2', '3', '']); + let statusBar = StatusBar.Get().trim(); + assert.equal(statusBar, ':abc-|', 'Failed to remove word'); + + await modeHandler.handleKeyEvent(''); + statusBar = StatusBar.Get().trim(); + assert.equal(statusBar, ':abc|', 'Failed to remove word'); + + await modeHandler.handleKeyEvent(''); + statusBar = StatusBar.Get().trim(); + assert.equal(statusBar, ':|', 'Failed to remove word'); + + await modeHandler.handleKeyEvent(''); + statusBar = StatusBar.Get().trim(); + assert.equal(statusBar, ':|', 'Failed to remove word'); + + await modeHandler.handleKeyEvent(''); + }); + + test('command can remove word in search mode', async () => { + await modeHandler.handleMultipleKeyEvents(['/', 'a', 'b', 'c', '-', '1', '2', '3', '']); + let statusBar = StatusBar.Get().trim(); + assert.equal(statusBar, '/abc-|', 'Failed to remove word'); + + await modeHandler.handleKeyEvent(''); + statusBar = StatusBar.Get().trim(); + assert.equal(statusBar, '/abc|', 'Failed to remove word'); + + await modeHandler.handleKeyEvent(''); + statusBar = StatusBar.Get().trim(); + assert.equal(statusBar, '/|', 'Failed to remove word'); + + await modeHandler.handleKeyEvent(''); + statusBar = StatusBar.Get().trim(); + assert.equal(statusBar, '/|', 'Failed to remove word'); + + await modeHandler.handleKeyEvent(''); + }); + + test('command can remove word in cmd line while retrain cmd on the right of the cursor', async () => { + await modeHandler.handleMultipleKeyEvents([ + ':', + 'a', + 'b', + 'c', + ' ', + '1', + '2', + '3', + '', + '', + '', + '', + ]); + const statusBar = StatusBar.Get().trim(); + assert.equal(statusBar, ':|123', 'Failed to retain the text on the right of the cursor'); + await modeHandler.handleKeyEvent(''); + }); + + test('command can remove word in search mode while retrain cmd on the right of the cursor', async () => { + await modeHandler.handleMultipleKeyEvents([ + '/', + 'a', + 'b', + 'c', + ' ', + '1', + '2', + '3', + '', + '', + '', + '', + ]); + const statusBar = StatusBar.Get().trim(); + assert.equal(statusBar, '/|123', 'Failed to retain the text on the right of the cursor'); + await modeHandler.handleKeyEvent(''); + }); +});