Skip to content

Commit 3786a02

Browse files
authored
Implements :gr[ep] / :vim[grep] (#9630)
Fixes #5991
1 parent b8c9e62 commit 3786a02

File tree

4 files changed

+151
-2
lines changed

4 files changed

+151
-2
lines changed

src/cmd_line/commands/grep.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import * as vscode from 'vscode';
2+
3+
import * as error from '../../error';
4+
import { VimState } from '../../state/vimState';
5+
import { Pattern, SearchDirection } from '../../vimscript/pattern';
6+
import { ExCommand } from '../../vimscript/exCommand';
7+
import { Parser, seq, optWhitespace, whitespace } from 'parsimmon';
8+
import { fileNameParser } from '../../vimscript/parserUtils';
9+
10+
// Still missing:
11+
// When a number is put before the command this is used
12+
// as the maximum number of matches to find. Use
13+
// ":1vimgrep pattern file" to find only the first.
14+
// Useful if you only want to check if there is a match
15+
// and quit quickly when it's found.
16+
17+
// Without the 'j' flag Vim jumps to the first match.
18+
// With 'j' only the quickfix list is updated.
19+
// With the [!] any changes in the current buffer are
20+
// abandoned.
21+
interface IGrepCommandArguments {
22+
pattern: Pattern;
23+
files: string[];
24+
}
25+
26+
// Implements :grep
27+
// https://vimdoc.sourceforge.net/htmldoc/quickfix.html#:vimgrep
28+
export class GrepCommand extends ExCommand {
29+
// TODO: parse the pattern for flags to notify the user that they are not supported yet
30+
public static readonly argParser: Parser<GrepCommand> = optWhitespace.then(
31+
seq(
32+
Pattern.parser({ direction: SearchDirection.Backward, delimiter: ' ' }),
33+
fileNameParser.sepBy(whitespace),
34+
).map(([pattern, files]) => new GrepCommand({ pattern, files })),
35+
);
36+
37+
public readonly arguments: IGrepCommandArguments;
38+
constructor(args: IGrepCommandArguments) {
39+
super();
40+
this.arguments = args;
41+
}
42+
43+
async execute(): Promise<void> {
44+
const { pattern, files } = this.arguments;
45+
if (files.length === 0) {
46+
throw error.VimError.fromCode(error.ErrorCode.NoFileName);
47+
}
48+
// 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
49+
// https://code.visualstudio.com/api/references/commands
50+
// This link on the other hand has the commands and I used this as a reference
51+
// https://stackoverflow.com/questions/62251045/search-find-in-files-keybinding-can-take-arguments-workbench-view-search-can
52+
await vscode.commands.executeCommand('workbench.action.findInFiles', {
53+
query: pattern.patternString,
54+
filesToInclude: files.join(','),
55+
triggerSearch: true,
56+
isRegex: true,
57+
});
58+
await vscode.commands.executeCommand('search.action.focusSearchList');
59+
// TODO: Only if there's no [j] flag
60+
await vscode.commands.executeCommand('search.action.focusNextSearchResult');
61+
}
62+
}

src/vimscript/exCommandParser.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { FileInfoCommand } from '../cmd_line/commands/fileInfo';
1313
import { EchoCommand } from '../cmd_line/commands/echo';
1414
import { GotoCommand } from '../cmd_line/commands/goto';
1515
import { GotoLineCommand } from '../cmd_line/commands/gotoLine';
16+
import { GrepCommand } from '../cmd_line/commands/grep';
1617
import { HistoryCommand } from '../cmd_line/commands/history';
1718
import { ClearJumpsCommand, JumpsCommand } from '../cmd_line/commands/jumps';
1819
import { CenterCommand, LeftCommand, RightCommand } from '../cmd_line/commands/leftRightCenter';
@@ -248,7 +249,7 @@ export const builtinExCommands: ReadonlyArray<[[string, string], ArgParser | und
248249
[['fu', 'nction'], undefined],
249250
[['g', 'lobal'], undefined],
250251
[['go', 'to'], GotoCommand.argParser],
251-
[['gr', 'ep'], undefined],
252+
[['gr', 'ep'], GrepCommand.argParser],
252253
[['grepa', 'dd'], undefined],
253254
[['gu', 'i'], undefined],
254255
[['gv', 'im'], undefined],
@@ -577,7 +578,7 @@ export const builtinExCommands: ReadonlyArray<[[string, string], ArgParser | und
577578
[['vert', 'ical'], undefined],
578579
[['vi', 'sual'], undefined],
579580
[['vie', 'w'], undefined],
580-
[['vim', 'grep'], undefined],
581+
[['vim', 'grep'], GrepCommand.argParser],
581582
[['vimgrepa', 'dd'], undefined],
582583
[['viu', 'sage'], undefined],
583584
[['vm', 'ap'], undefined],

test/cmd_line/grep.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import * as assert from 'assert';
2+
import * as vscode from 'vscode';
3+
4+
import { getAndUpdateModeHandler } from '../../extension';
5+
import { GrepCommand } from '../../src/cmd_line/commands/grep';
6+
import { Pattern, SearchDirection } from '../../src/vimscript/pattern';
7+
import { Mode } from '../../src/mode/mode';
8+
import { createFile, setupWorkspace, cleanUpWorkspace } from '../testUtils';
9+
10+
function grep(pattern: Pattern, files: string[]): GrepCommand {
11+
return new GrepCommand({ pattern, files });
12+
}
13+
14+
suite('Basic grep command', () => {
15+
// 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
16+
// that only happens if the search panel is not open already
17+
// if the search panel is open, it will be in normal mode
18+
// it will also be in normal mode if you run vimgrep from another file
19+
setup(async () => {
20+
await setupWorkspace();
21+
});
22+
test('GrepCommand executes correctly', async () => {
23+
// first file, will have matches
24+
let file1 = await createFile({
25+
fileExtension: '.txt',
26+
contents: 'test, pattern nnnn, t*st, ttst',
27+
});
28+
// second file without a match
29+
let file2 = await createFile({
30+
fileExtension: '.txt',
31+
contents: 'no pattern match here ',
32+
});
33+
// We open the second file where we know there is no match
34+
const document1 = await vscode.workspace.openTextDocument(vscode.Uri.file(file1));
35+
await vscode.window.showTextDocument(document1, { preview: false });
36+
const document2 = await vscode.workspace.openTextDocument(vscode.Uri.file(file2));
37+
await vscode.window.showTextDocument(document2, { preview: false });
38+
const pattern = Pattern.parser({ direction: SearchDirection.Backward });
39+
// The vscode's search doesn't work with the paths of the extension test host, so we strip to the file names only
40+
file1 = file1.substring(file1.lastIndexOf('/') + 1);
41+
file2 = file2.substring(file2.lastIndexOf('/') + 1);
42+
const command = grep(pattern.tryParse('t*st'), [file1, file2]);
43+
await command.execute();
44+
// Despite the fact that we already execute this command in the grep itself, without this focus, there is no active editor
45+
// 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
46+
await vscode.commands.executeCommand('search.action.focusNextSearchResult');
47+
const activeEditor = vscode.window.activeTextEditor;
48+
const modeHandler = await getAndUpdateModeHandler();
49+
assert.ok(activeEditor, 'There should be an active editor');
50+
assert.ok(modeHandler, 'modeHandler should be defined');
51+
const docs = vscode.workspace.textDocuments.map((doc) => doc.fileName);
52+
// After grep, the active editor should be the first file because the search panel focuses the first match and therefore opens the file
53+
assert.ok(
54+
activeEditor.document.fileName.endsWith(file1),
55+
'Active editor should be first file after grep',
56+
);
57+
assert.notStrictEqual(
58+
modeHandler.vimState,
59+
Mode.Visual,
60+
'Should not be in visual mode after grep',
61+
);
62+
});
63+
});

test/vimscript/exCommandParse.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { add, int, str, variable, funcCall, list } from '../../src/vimscript/exp
2828
import { Address } from '../../src/vimscript/lineRange';
2929
import { Pattern, SearchDirection } from '../../src/vimscript/pattern';
3030
import { ShiftCommand } from '../../src/cmd_line/commands/shift';
31+
import { GrepCommand } from '../../src/cmd_line/commands/grep';
3132

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

689+
suite(':vim[grep]', () => {
690+
exParseTest(
691+
':vimgrep t*st foo.txt',
692+
new GrepCommand({
693+
// It expects pattern.closed to be false (check Pattern.parser), so unless there's a delimiter in the pattern, it will fail the test
694+
pattern: Pattern.parser({ direction: SearchDirection.Backward, delimiter: ' ' }).tryParse(
695+
't*st ',
696+
),
697+
files: ['foo.txt'],
698+
}),
699+
);
700+
exParseTest(
701+
':vimgrep t*st foo.txt bar.txt baz.txt',
702+
new GrepCommand({
703+
pattern: Pattern.parser({ direction: SearchDirection.Backward, delimiter: ' ' }).tryParse(
704+
't*st ',
705+
),
706+
files: ['foo.txt', 'bar.txt', 'baz.txt'],
707+
}),
708+
);
709+
});
710+
688711
suite(':y[ank]', () => {
689712
exParseTest(':y', new YankCommand({ register: undefined, count: undefined }));
690713
exParseTest(':y a', new YankCommand({ register: 'a', count: undefined }));

0 commit comments

Comments
 (0)