diff --git a/src/actions/commands/commandLine.ts b/src/actions/commands/commandLine.ts index 8ee7c406a4f..90528a362e8 100644 --- a/src/actions/commands/commandLine.ts +++ b/src/actions/commands/commandLine.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import { CommandLine, ExCommandLine, SearchCommandLine } from '../../cmd_line/commandLine'; +import { ChangeCommand } from '../../cmd_line/commands/change'; import { ErrorCode, VimError } from '../../error'; import { Mode } from '../../mode/mode'; import { Register, RegisterMode } from '../../register/register'; @@ -152,6 +153,11 @@ class ExCommandLineEnter extends CommandLineAction { protected override async run(vimState: VimState, commandLine: CommandLine): Promise { await commandLine.run(vimState); + + if (commandLine instanceof ExCommandLine && commandLine.getCommand() instanceof ChangeCommand) { + return; + } + await vimState.setCurrentMode(Mode.Normal); } } diff --git a/src/cmd_line/commandLine.ts b/src/cmd_line/commandLine.ts index c1b8d70536f..fea0c7e2316 100644 --- a/src/cmd_line/commandLine.ts +++ b/src/cmd_line/commandLine.ts @@ -243,6 +243,10 @@ export class ExCommandLine extends CommandLine { return undefined; } + public getCommand(): ExCommand | undefined { + return this.command; + } + public getDecorations(vimState: VimState): SearchDecorations | undefined { return this.command instanceof SubstituteCommand && vimState.currentMode === Mode.CommandlineInProgress diff --git a/src/cmd_line/commands/change.ts b/src/cmd_line/commands/change.ts new file mode 100644 index 00000000000..52ab96c86d5 --- /dev/null +++ b/src/cmd_line/commands/change.ts @@ -0,0 +1,103 @@ +import * as vscode from 'vscode'; +// eslint-disable-next-line id-denylist +import { Parser, alt, any, optWhitespace, seq, whitespace } from 'parsimmon'; +import { Position } from 'vscode'; +import { Mode } from '../../mode/mode'; +import { Register, RegisterMode } from '../../register/register'; +import { VimState } from '../../state/vimState'; +import { ExCommand } from '../../vimscript/exCommand'; +import { LineRange } from '../../vimscript/lineRange'; +import { numberParser } from '../../vimscript/parserUtils'; + +export interface IChangeCommandArguments { + register?: string; + count?: number; +} + +export class ChangeCommand extends ExCommand { + public static readonly argParser: Parser = optWhitespace.then( + alt( + numberParser.map((count) => { + return { register: undefined, count }; + }), + // eslint-disable-next-line id-denylist + seq(any.fallback(undefined), whitespace.then(numberParser).fallback(undefined)).map( + ([register, count]) => { + return { register, count }; + }, + ), + ).map( + ({ register, count }) => + new ChangeCommand({ + register, + count, + }), + ), + ); + + private readonly arguments: IChangeCommandArguments; + constructor(args: IChangeCommandArguments) { + super(); + this.arguments = args; + } + + public override neovimCapable(): boolean { + return true; + } + + /** + * Deletes text between `startLine` and `endLine`, inclusive. + * Puts the cursor at the start of the line where the deleted range was + * Then enters insert mode + */ + private changeRange(startLine: number, endLine: number, vimState: VimState): void { + const start = new Position(startLine, 0); + const end = new Position(endLine + 1, 0); + + const range = new vscode.Range(start, end); + const text = vimState.document.getText(range); + + vimState.recordedState.transformer.addTransformation({ + type: 'replaceText', + text: '\n', + range, + manuallySetCursorPositions: true, + }); + vimState.cursorStopPosition = start; + + if (this.arguments.register) { + vimState.recordedState.registerName = this.arguments.register; + } + vimState.currentRegisterMode = RegisterMode.LineWise; + Register.put(vimState, text, 0, true); + } + + async execute(vimState: VimState): Promise { + const linesToRemove = this.arguments.count ?? 1; + // :c[hange][cnt] changes [cnt] lines + const startLine = vimState.cursorStartPosition.line; + const endLine = startLine + (linesToRemove - 1); + this.changeRange(startLine, endLine, vimState); + + // Enter insert mode + await vimState.setCurrentMode(Mode.Insert); + } + + override async executeWithRange(vimState: VimState, range: LineRange): Promise { + /** + * If a [cnt] and [range] is specified (e.g. :.+2c3), :change + * the end of the [range]. + * Ex. if two lines are VisualLine hightlighted, :<,>c3 will :c3 + * from the end of the selected lines + */ + const { start, end } = range.resolve(vimState); + if (this.arguments.count) { + vimState.cursorStartPosition = new Position(end, 0); + await this.execute(vimState); + return; + } + this.changeRange(start, end, vimState); + // Enter insert mode + await vimState.setCurrentMode(Mode.Insert); + } +} diff --git a/src/vimscript/exCommandParser.ts b/src/vimscript/exCommandParser.ts index 667ea8d8a98..ad851f020fd 100644 --- a/src/vimscript/exCommandParser.ts +++ b/src/vimscript/exCommandParser.ts @@ -4,6 +4,7 @@ import { AsciiCommand } from '../cmd_line/commands/ascii'; import { BangCommand } from '../cmd_line/commands/bang'; import { Breakpoints } from '../cmd_line/commands/breakpoints'; import { BufferDeleteCommand } from '../cmd_line/commands/bufferDelete'; +import { ChangeCommand } from '../cmd_line/commands/change'; import { CloseCommand } from '../cmd_line/commands/close'; import { CopyCommand } from '../cmd_line/commands/copy'; import { DeleteCommand } from '../cmd_line/commands/delete'; @@ -120,7 +121,7 @@ export const builtinExCommands: ReadonlyArray<[[string, string], ArgParser | und [['buffers', ''], undefined], [['bun', 'load'], undefined], [['bw', 'ipeout'], undefined], - [['c', 'hange'], undefined], + [['c', 'hange'], ChangeCommand.argParser], [['cN', 'ext'], undefined], [['cNf', 'ile'], undefined], [['ca', 'bbrev'], undefined], diff --git a/test/cmd_line/change.test.ts b/test/cmd_line/change.test.ts new file mode 100644 index 00000000000..fd22df640ed --- /dev/null +++ b/test/cmd_line/change.test.ts @@ -0,0 +1,43 @@ +import { Mode } from '../../src/mode/mode'; +import { newTest } from '../testSimplifier'; +import { cleanUpWorkspace, setupWorkspace } from '../testUtils'; + +suite('cmd_line change', () => { + setup(async () => { + await setupWorkspace(); + }); + + teardown(cleanUpWorkspace); + + newTest({ + title: 'c deletes current line and enters insert mode', + start: ['first line', 'sec|ond line', 'third line'], + keysPressed: ':c\n', + end: ['first line', '|', 'third line'], + endMode: Mode.Insert, + }); + + newTest({ + title: 'c with count deletes multiple lines and enters insert mode', + start: ['first line', 'sec|ond line', 'third line', 'fourth line'], + keysPressed: ':c2\n', + end: ['first line', '|', 'fourth line'], + endMode: Mode.Insert, + }); + + newTest({ + title: 'c with range deletes specified lines and enters insert mode', + start: ['first line', 'sec|ond line', 'third line', 'fourth line'], + keysPressed: ':2, 3c\n', + end: ['first line', '|', 'fourth line'], + endMode: Mode.Insert, + }); + + newTest({ + title: 'c with range and visual selection', + start: ['first line', 'sec|ond line', 'third line', 'fourth line'], + keysPressed: 'V:c\n', + end: ['first line', '|', 'third line', 'fourth line'], + endMode: Mode.Insert, + }); +});