Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/actions/commands/commandLine.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -152,6 +153,11 @@ class ExCommandLineEnter extends CommandLineAction {

protected override async run(vimState: VimState, commandLine: CommandLine): Promise<void> {
await commandLine.run(vimState);

if (commandLine instanceof ExCommandLine && commandLine.getCommand() instanceof ChangeCommand) {
return;
}

await vimState.setCurrentMode(Mode.Normal);
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/cmd_line/commandLine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
103 changes: 103 additions & 0 deletions src/cmd_line/commands/change.ts
Original file line number Diff line number Diff line change
@@ -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<ChangeCommand> = 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<void> {
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<void> {
/**
* 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);
}
}
3 changes: 2 additions & 1 deletion src/vimscript/exCommandParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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],
Expand Down
43 changes: 43 additions & 0 deletions test/cmd_line/change.test.ts
Original file line number Diff line number Diff line change
@@ -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,
});
});