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
63 changes: 63 additions & 0 deletions src/cmd_line/commands/global.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// eslint-disable-next-line id-denylist
import { all, alt, optWhitespace, Parser, regexp, seq, string } from 'parsimmon';
import { VimState } from '../../state/vimState';
import { ExCommand } from '../../vimscript/exCommand';
import { Address, LineRange } from '../../vimscript/lineRange';
import { Pattern, SearchDirection } from '../../vimscript/pattern';

export class GlobalCommand extends ExCommand {
public static argParser(invert: boolean): Parser<GlobalCommand> {
const patternAndCmd = regexp(/[^\w\s\\|"]{1}/).chain((delimiter) =>
seq(Pattern.parser({ direction: SearchDirection.Forward, delimiter }), all),
);

if (invert) {
// :v — always inverted, no ! prefix
return optWhitespace.then(
patternAndCmd.map(
([pattern, commandText]) => new GlobalCommand(pattern, true, commandText),
),
);
} else {
// :g — optionally accept ! prefix for inverted matching
return optWhitespace.then(
alt(
string('!').then(
patternAndCmd.map(
([pattern, commandText]) => new GlobalCommand(pattern, true, commandText),
),
),
patternAndCmd.map(
([pattern, commandText]) => new GlobalCommand(pattern, false, commandText),
),
),
);
}
}

public readonly pattern: Pattern;
public readonly invert: boolean;
public readonly commandText: string;

constructor(pattern: Pattern, invert: boolean, commandText: string) {
super();
this.pattern = pattern;
this.invert = invert;
this.commandText = commandText;
}

async execute(vimState: VimState): Promise<void> {
// Default range for :g is % (entire file), unlike most Ex commands
await this.executeWithRange(vimState, new LineRange(new Address({ type: 'entire_file' })));
}

override async executeWithRange(vimState: VimState, lineRange: LineRange): Promise<void> {
vimState.recordedState.transformer.addTransformation({
type: 'executeGlobal',
pattern: this.pattern,
invert: this.invert,
commandText: this.commandText,
range: lineRange,
});
}
}
62 changes: 62 additions & 0 deletions src/transformations/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from '../vimscript/parserUtils';
import {
Dot,
ExecuteGlobalTransformation,
ExecuteNormalTransformation,
InsertTextVSCodeTransformation,
TextTransformations,
Expand Down Expand Up @@ -243,6 +244,10 @@ export async function executeTransformations(
await doExecuteNormal(modeHandler, transformation);
break;

case 'executeGlobal':
await doExecuteGlobal(modeHandler, transformation);
break;

case 'vscodeCommand':
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
await vscode.commands.executeCommand(transformation.command, ...transformation.args);
Expand Down Expand Up @@ -346,3 +351,60 @@ const doExecuteNormal = async (
}
vimState.normalCommandState = NormalCommandState.Finished;
};

const doExecuteGlobal = async (
modeHandler: IModeHandler,
transformation: ExecuteGlobalTransformation,
) => {
const vimState = modeHandler.vimState;
const { pattern, invert, commandText, range } = transformation;

const { start, end } = range.resolve(vimState);

// First pass: collect all matching line numbers
const matchingLines: number[] = [];
for (let line = start; line <= end; line++) {
const lineText = vimState.document.lineAt(line).text;
pattern.regex.lastIndex = 0;
const matches = pattern.regex.test(lineText);
if (matches !== invert) {
matchingLines.push(line);
}
}

if (matchingLines.length === 0) {
return;
}

// Default sub-command is :p (print) when none is specified
const cmdText = commandText.trim() || 'p';
const cmdKeys = (':' + cmdText + '\n').split('');

// Second pass: execute sub-command on each matching line
vimState.recordedState = new RecordedState();
await vimState.setCurrentMode(Mode.Normal);

let lineOffset = 0;
for (const originalLine of matchingLines) {
const currentLine = originalLine + lineOffset;
if (currentLine < 0 || currentLine >= vimState.document.lineCount) {
continue;
}

const lineCountBefore = vimState.document.lineCount;

vimState.cursorStopPosition = vimState.cursorStartPosition = new vscode.Position(
currentLine,
0,
);

await modeHandler.handleMultipleKeyEvents(cmdKeys);

// Ensure we return to Normal mode after each sub-command
if (vimState.currentMode !== Mode.Normal) {
await modeHandler.handleKeyEvent('<Esc>');
}

lineOffset += vimState.document.lineCount - lineCountBefore;
}
};
10 changes: 10 additions & 0 deletions src/transformations/transformations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Position, Range, TextDocumentContentChangeEvent } from 'vscode';
import { RecordedState } from '../state/recordedState';
import { LineRange } from '../vimscript/lineRange';
import { Pattern } from '../vimscript/pattern';
import { PositionDiff } from './../common/motion/position';

/**
Expand Down Expand Up @@ -193,6 +194,14 @@ export interface ExecuteNormalTransformation {
range?: LineRange;
}

export interface ExecuteGlobalTransformation {
type: 'executeGlobal';
pattern: Pattern;
invert: boolean;
commandText: string;
range: LineRange;
}

export type Transformation =
| InsertTextTransformation
| InsertTextVSCodeTransformation
Expand All @@ -203,6 +212,7 @@ export type Transformation =
| Macro
| ContentChangeTransformation
| ExecuteNormalTransformation
| ExecuteGlobalTransformation
| VSCodeCommandTransformation;

/**
Expand Down
5 changes: 3 additions & 2 deletions src/vimscript/exCommandParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { CallCommand, EvalCommand } from '../cmd_line/commands/eval';
import { ExploreCommand } from '../cmd_line/commands/explore';
import { FileCommand } from '../cmd_line/commands/file';
import { FileInfoCommand } from '../cmd_line/commands/fileInfo';
import { GlobalCommand } from '../cmd_line/commands/global';
import { GotoCommand } from '../cmd_line/commands/goto';
import { GotoLineCommand } from '../cmd_line/commands/gotoLine';
import { GrepCommand } from '../cmd_line/commands/grep';
Expand Down Expand Up @@ -249,7 +250,7 @@ export const builtinExCommands: ReadonlyArray<[[string, string], ArgParser | und
[['foldo', 'pen'], undefined],
[['for', ''], undefined],
[['fu', 'nction'], undefined],
[['g', 'lobal'], undefined],
[['g', 'lobal'], GlobalCommand.argParser(false)],
[['go', 'to'], GotoCommand.argParser],
[['gr', 'ep'], GrepCommand.argParser],
[['grepa', 'dd'], undefined],
Expand Down Expand Up @@ -574,7 +575,7 @@ export const builtinExCommands: ReadonlyArray<[[string, string], ArgParser | und
[['unme', 'nu'], undefined],
[['uns', 'ilent'], undefined],
[['up', 'date'], WriteCommand.argParser],
[['v', 'global'], undefined],
[['v', 'global'], GlobalCommand.argParser(true)],
[['ve', 'rsion'], undefined],
[['verb', 'ose'], undefined],
[['vert', 'ical'], undefined],
Expand Down
Loading