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 14 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
65 changes: 65 additions & 0 deletions src/cmd_line/commands/grep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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 {
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');
// Only if there's no [j] flag
await vscode.commands.executeCommand('search.action.focusNextSearchResult');
// TODO: this will always throw an error, since the command returns nothing
// if (!grepResults) {
// throw error.VimError.fromCode(error.ErrorCode.PatternNotFound);
// }
}
}
6 changes: 4 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,8 @@ export const builtinExCommands: ReadonlyArray<[[string, string], ArgParser | und
[['vert', 'ical'], undefined],
[['vi', 'sual'], undefined],
[['vie', 'w'], undefined],
[['vim', 'grep'], undefined],
[['vim', 'grep'], GrepCommand.argParser],
[['grep', ''], GrepCommand.argParser],
[['vimgrepa', 'dd'], undefined],
[['viu', 'sage'], undefined],
[['vm', 'ap'], undefined],
Expand Down
83 changes: 83 additions & 0 deletions test/cmd_line/grep.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import * as assert from 'assert';
import * as vscode from 'vscode';

import { getAndUpdateModeHandler } from '../../extension';
import * as t from '../testUtils';
import { GrepCommand } from '../../src/cmd_line/commands/grep';
import { Pattern, SearchDirection } from '../../src/vimscript/pattern';
import { Mode } from '../../src/mode/mode';

// This will go into the exCommandParse test
// function exParseTest(input: string, parsed: ExCommand) {
// test(input, () => {
// const { command } = exCommandParser.tryParse(input);
// assert.deepStrictEqual(command, parsed);
// });
// }

// suite('grep', () => {
// const pattern = Pattern.parser({ direction: SearchDirection.Forward, delimiter: '/' });
// exParseTest(':vimgrep "t*st" foo.txt',
// new GrepCommand({
// pattern: pattern.tryParse('t*st'),
// files: ['foo.txt'],
// }),
// );
// });

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

suite('Basic grep command', () => {
setup(t.setupWorkspace);
suiteTeardown(t.cleanUpWorkspace);
test('GrepCommand parses correctly', async () => {
await vscode.commands.executeCommand('workbench.action.files.newUntitledFile');
const pattern = Pattern.parser({ direction: SearchDirection.Backward, delimiter: '/' });
const command = grep(pattern.tryParse('t*st'), ['Untitled-1']);
assert.deepStrictEqual(command.arguments, {
pattern: pattern.tryParse('t*st'),
files: ['Untitled-1'],
});
});
// 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
test('GrepCommand executes correctly', async () => {
// Untitled-1
await vscode.commands.executeCommand('workbench.action.files.newUntitledFile');
const editor = vscode.window.activeTextEditor;
if (editor) {
await editor.edit((editBuilder) => {
editBuilder.insert(
new vscode.Position(0, 0),
'this is a test\nanother t*st line\nno match here\n',
);
});
// Because of the save confirmation dialog, it will timeout
// await editor.document.save()
}
// Untitled-2
await vscode.commands.executeCommand('workbench.action.files.newUntitledFile');
const modeHandler = await getAndUpdateModeHandler();
const pattern = Pattern.parser({ direction: SearchDirection.Backward, delimiter: '/' });
const command = grep(pattern.tryParse('t*st'), ['Untitled-1']);
await command.execute();
await vscode.commands.executeCommand('search.action.focusNextSearchResult');
// Assert that the active editor is Untitled-1
const activeEditor = vscode.window.activeTextEditor;
assert.ok(activeEditor, 'There should be an active editor');
assert.ok(
activeEditor?.document.fileName.endsWith('Untitled-1'),
'Active editor should be Untitled-1 after grep',
);
assert.ok(modeHandler, 'modeHandler should be defined');
assert.notStrictEqual(
modeHandler.vimState,
Mode.Visual,
'Should not be in visual mode after grep',
);
});
});