Skip to content

Implements vimgrep, #5991 #9630

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Jun 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c793d57
pattern and file argument reading
AzimovParviz May 17, 2025
e771364
basic vimgrep functionality
AzimovParviz May 17, 2025
29cfb70
removed assignment to grepResults, since this is unnecessary
AzimovParviz May 17, 2025
fc6aee0
update link to the reference documentation
AzimovParviz May 17, 2025
8b0a5d6
adds focus on the search window
AzimovParviz May 17, 2025
0791d2a
adds focus on the first search result when running vimgrep without th…
AzimovParviz May 17, 2025
f0b441b
fixed pattern separator being accidentally removed
AzimovParviz May 18, 2025
09cbf5f
removed the console log and unneeded argument
AzimovParviz May 20, 2025
48aa9f8
Basic grep test that checks if arguments are correctly parsed and whe…
AzimovParviz May 20, 2025
910411f
Merge branch 'master' into master
J-Fields May 22, 2025
7991c7d
remove eslint-disable
AzimovParviz May 23, 2025
5c448b0
remove console.log from grep.ts
AzimovParviz May 23, 2025
dc00e9f
commit code review changes
AzimovParviz May 23, 2025
dc44f05
Merge branch 'master' into master
J-Fields May 28, 2025
d8702e6
Merge branch 'master' into master
AzimovParviz May 29, 2025
7fe9546
Merge branch 'master' into master
AzimovParviz May 31, 2025
ff83245
removed unneeded grep command from the parse list
AzimovParviz May 29, 2025
9d886be
added comments
AzimovParviz May 31, 2025
7447634
vimgrep command parse test
AzimovParviz May 31, 2025
5c4cdb6
wip on refactoring the grep test to use testutils
AzimovParviz May 31, 2025
15bc1a3
reworked the grep test to use testUtils's createFile
AzimovParviz May 31, 2025
eea4d23
Merge branch 'master' into master
J-Fields May 31, 2025
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
62 changes: 62 additions & 0 deletions src/cmd_line/commands/grep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import * as vscode from 'vscode';

import * as error from '../../error';
import { VimState } from '../../state/vimState';
import { Pattern, SearchDirection } from '../../vimscript/pattern';
import { ExCommand } from '../../vimscript/exCommand';
import { Parser, seq, optWhitespace, whitespace } from 'parsimmon';
import { fileNameParser } from '../../vimscript/parserUtils';

// Still missing:
// When a number is put before the command this is used
// as the maximum number of matches to find. Use
// ":1vimgrep pattern file" to find only the first.
// Useful if you only want to check if there is a match
// and quit quickly when it's found.

// Without the 'j' flag Vim jumps to the first match.
// With 'j' only the quickfix list is updated.
// With the [!] any changes in the current buffer are
// abandoned.
interface IGrepCommandArguments {
pattern: Pattern;
files: string[];
}

// Implements :grep
// https://vimdoc.sourceforge.net/htmldoc/quickfix.html#:vimgrep
export class GrepCommand extends ExCommand {
// TODO: parse the pattern for flags to notify the user that they are not supported yet
public static readonly argParser: Parser<GrepCommand> = optWhitespace.then(
seq(
Pattern.parser({ direction: SearchDirection.Backward, delimiter: ' ' }),
fileNameParser.sepBy(whitespace),
).map(([pattern, files]) => new GrepCommand({ pattern, files })),
);

public readonly arguments: IGrepCommandArguments;
constructor(args: IGrepCommandArguments) {
super();
this.arguments = args;
}

async execute(): Promise<void> {
const { pattern, files } = this.arguments;
if (files.length === 0) {
throw error.VimError.fromCode(error.ErrorCode.NoFileName);
}
// There are other arguments that can be passed, but probably need to dig into the VSCode source code, since they are not listed in the API reference
// https://code.visualstudio.com/api/references/commands
// This link on the other hand has the commands and I used this as a reference
// https://stackoverflow.com/questions/62251045/search-find-in-files-keybinding-can-take-arguments-workbench-view-search-can
await vscode.commands.executeCommand('workbench.action.findInFiles', {
query: pattern.patternString,
filesToInclude: files.join(','),
triggerSearch: true,
isRegex: true,
});
await vscode.commands.executeCommand('search.action.focusSearchList');
// TODO: Only if there's no [j] flag
await vscode.commands.executeCommand('search.action.focusNextSearchResult');
}
}
5 changes: 3 additions & 2 deletions src/vimscript/exCommandParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { FileInfoCommand } from '../cmd_line/commands/fileInfo';
import { EchoCommand } from '../cmd_line/commands/echo';
import { GotoCommand } from '../cmd_line/commands/goto';
import { GotoLineCommand } from '../cmd_line/commands/gotoLine';
import { GrepCommand } from '../cmd_line/commands/grep';
import { HistoryCommand } from '../cmd_line/commands/history';
import { ClearJumpsCommand, JumpsCommand } from '../cmd_line/commands/jumps';
import { CenterCommand, LeftCommand, RightCommand } from '../cmd_line/commands/leftRightCenter';
Expand Down Expand Up @@ -248,7 +249,7 @@ export const builtinExCommands: ReadonlyArray<[[string, string], ArgParser | und
[['fu', 'nction'], undefined],
[['g', 'lobal'], undefined],
[['go', 'to'], GotoCommand.argParser],
[['gr', 'ep'], undefined],
[['gr', 'ep'], GrepCommand.argParser],
[['grepa', 'dd'], undefined],
[['gu', 'i'], undefined],
[['gv', 'im'], undefined],
Expand Down Expand Up @@ -577,7 +578,7 @@ export const builtinExCommands: ReadonlyArray<[[string, string], ArgParser | und
[['vert', 'ical'], undefined],
[['vi', 'sual'], undefined],
[['vie', 'w'], undefined],
[['vim', 'grep'], undefined],
[['vim', 'grep'], GrepCommand.argParser],
[['vimgrepa', 'dd'], undefined],
[['viu', 'sage'], undefined],
[['vm', 'ap'], undefined],
Expand Down
63 changes: 63 additions & 0 deletions test/cmd_line/grep.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import * as assert from 'assert';
import * as vscode from 'vscode';

import { getAndUpdateModeHandler } from '../../extension';
import { GrepCommand } from '../../src/cmd_line/commands/grep';
import { Pattern, SearchDirection } from '../../src/vimscript/pattern';
import { Mode } from '../../src/mode/mode';
import { createFile, setupWorkspace, cleanUpWorkspace } from '../testUtils';

function grep(pattern: Pattern, files: string[]): GrepCommand {
return new GrepCommand({ pattern, files });
}

suite('Basic grep command', () => {
// when you search.action.focusNextSearchResult , it will enter the file in visual mode for some reason, we can test whether it is in visual mode or not after running that command
// that only happens if the search panel is not open already
// if the search panel is open, it will be in normal mode
// it will also be in normal mode if you run vimgrep from another file
setup(async () => {
await setupWorkspace();
});
test('GrepCommand executes correctly', async () => {
// first file, will have matches
let file1 = await createFile({
fileExtension: '.txt',
contents: 'test, pattern nnnn, t*st, ttst',
});
// second file without a match
let file2 = await createFile({
fileExtension: '.txt',
contents: 'no pattern match here ',
});
// We open the second file where we know there is no match
const document1 = await vscode.workspace.openTextDocument(vscode.Uri.file(file1));
await vscode.window.showTextDocument(document1, { preview: false });
const document2 = await vscode.workspace.openTextDocument(vscode.Uri.file(file2));
await vscode.window.showTextDocument(document2, { preview: false });
const pattern = Pattern.parser({ direction: SearchDirection.Backward });
// The vscode's search doesn't work with the paths of the extension test host, so we strip to the file names only
file1 = file1.substring(file1.lastIndexOf('/') + 1);
file2 = file2.substring(file2.lastIndexOf('/') + 1);
const command = grep(pattern.tryParse('t*st'), [file1, file2]);
await command.execute();
// Despite the fact that we already execute this command in the grep itself, without this focus, there is no active editor
// I've tested visually and without this command you are still in the editor in the file with the match, I have no idea why it won't work without this
await vscode.commands.executeCommand('search.action.focusNextSearchResult');
const activeEditor = vscode.window.activeTextEditor;
const modeHandler = await getAndUpdateModeHandler();
assert.ok(activeEditor, 'There should be an active editor');
assert.ok(modeHandler, 'modeHandler should be defined');
const docs = vscode.workspace.textDocuments.map((doc) => doc.fileName);
// After grep, the active editor should be the first file because the search panel focuses the first match and therefore opens the file
assert.ok(
activeEditor.document.fileName.endsWith(file1),
'Active editor should be first file after grep',
);
assert.notStrictEqual(
modeHandler.vimState,
Mode.Visual,
'Should not be in visual mode after grep',
);
});
});
23 changes: 23 additions & 0 deletions test/vimscript/exCommandParse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { add, int, str, variable, funcCall, list } from '../../src/vimscript/exp
import { Address } from '../../src/vimscript/lineRange';
import { Pattern, SearchDirection } from '../../src/vimscript/pattern';
import { ShiftCommand } from '../../src/cmd_line/commands/shift';
import { GrepCommand } from '../../src/cmd_line/commands/grep';

function exParseTest(input: string, parsed: ExCommand) {
test(input, () => {
Expand Down Expand Up @@ -685,6 +686,28 @@ suite('Ex command parsing', () => {
exParseTest(':tabonly! 5', new TabCommand({ type: TabCommandType.Only, bang: true, count: 5 }));
});

suite(':vim[grep]', () => {
exParseTest(
':vimgrep t*st foo.txt',
new GrepCommand({
// It expects pattern.closed to be false (check Pattern.parser), so unless there's a delimiter in the pattern, it will fail the test
pattern: Pattern.parser({ direction: SearchDirection.Backward, delimiter: ' ' }).tryParse(
't*st ',
),
files: ['foo.txt'],
}),
);
exParseTest(
':vimgrep t*st foo.txt bar.txt baz.txt',
new GrepCommand({
pattern: Pattern.parser({ direction: SearchDirection.Backward, delimiter: ' ' }).tryParse(
't*st ',
),
files: ['foo.txt', 'bar.txt', 'baz.txt'],
}),
);
});

suite(':y[ank]', () => {
exParseTest(':y', new YankCommand({ register: undefined, count: undefined }));
exParseTest(':y a', new YankCommand({ register: 'a', count: undefined }));
Expand Down