From c793d57f85fd8cb448e4cf5fb68faf5045715d5a Mon Sep 17 00:00:00 2001 From: Parviz Azimov Date: Sat, 17 May 2025 21:54:09 +0300 Subject: [PATCH 01/17] pattern and file argument reading --- src/cmd_line/commands/grep.ts | 56 +++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/cmd_line/commands/grep.ts diff --git a/src/cmd_line/commands/grep.ts b/src/cmd_line/commands/grep.ts new file mode 100644 index 00000000000..6d40a9df109 --- /dev/null +++ b/src/cmd_line/commands/grep.ts @@ -0,0 +1,56 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +import * as vscode from 'vscode'; + +import * as error from '../../error'; +import { VimState } from '../../state/vimState'; +import { Pattern, SearchDirection } from '../../vimscript/pattern'; +import { StatusBar } from '../../statusBar'; +import { ExCommand } from '../../vimscript/exCommand'; +import { Parser, seq, regexp, optWhitespace, whitespace } from 'parsimmon'; +import { fileNameParser } from '../../vimscript/parserUtils'; + +interface IGrepCommandArguments { + pattern: Pattern; + files: string[]; +} + +// Implements :grep +// http://vimdoc.sourceforge.net/htmldoc/quickref.html#:grep +export class GrepCommand extends ExCommand { + public static readonly argParser: Parser = 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(vimState: VimState): Promise { + const { pattern, files } = this.arguments; + console.log('GrepCommand', pattern.patternString, files); + if (files.length === 0) { + throw error.VimError.fromCode(error.ErrorCode.NoFileName); + } + const listOfCommands = await vscode.commands.getCommands(); + const filteredCommands = listOfCommands.filter( + (cmd) => cmd.toLowerCase().includes('search') || cmd.toLowerCase().includes('find'), + ); + console.log('Filtered commands:', filteredCommands); + // workbench.view.search is one of the commands that can be used to search + const grepResults = await vscode.commands.executeCommand( + 'search.action.getSearchResults', + pattern.patternString, + files, + ); + + if (!grepResults) { + throw error.VimError.fromCode(error.ErrorCode.PatternNotFound); + } + } +} From e7713646c3e2e6dc665244e1a8484e9cf543e6fe Mon Sep 17 00:00:00 2001 From: Parviz Azimov Date: Sat, 17 May 2025 22:26:49 +0300 Subject: [PATCH 02/17] basic vimgrep functionality --- src/cmd_line/commands/grep.ts | 24 ++++++++++++++---------- src/vimscript/exCommandParser.ts | 8 ++++++-- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/cmd_line/commands/grep.ts b/src/cmd_line/commands/grep.ts index 6d40a9df109..0b1998fbf8f 100644 --- a/src/cmd_line/commands/grep.ts +++ b/src/cmd_line/commands/grep.ts @@ -42,15 +42,19 @@ export class GrepCommand extends ExCommand { (cmd) => cmd.toLowerCase().includes('search') || cmd.toLowerCase().includes('find'), ); console.log('Filtered commands:', filteredCommands); - // workbench.view.search is one of the commands that can be used to search - const grepResults = await vscode.commands.executeCommand( - 'search.action.getSearchResults', - pattern.patternString, - files, - ); - - if (!grepResults) { - throw error.VimError.fromCode(error.ErrorCode.PatternNotFound); - } + // 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 + const grepResults = await vscode.commands.executeCommand('workbench.action.findInFiles', { + query: pattern.patternString, + filesToInclude: files.join(','), + triggerSearch: true, + isRegex: true, + }); + // TODO: this will always throw an error, since the command returns nothing + // if (!grepResults) { + // throw error.VimError.fromCode(error.ErrorCode.PatternNotFound); + // } } } diff --git a/src/vimscript/exCommandParser.ts b/src/vimscript/exCommandParser.ts index 66334625307..feeaad7d933 100644 --- a/src/vimscript/exCommandParser.ts +++ b/src/vimscript/exCommandParser.ts @@ -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'; @@ -247,7 +248,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], @@ -576,7 +577,10 @@ export const builtinExCommands: ReadonlyArray<[[string, string], ArgParser | und [['vert', 'ical'], undefined], [['vi', 'sual'], undefined], [['vie', 'w'], undefined], - [['vim', 'grep'], undefined], + [['vim', 'grep'], GrepCommand.argParser], + [['vimg', 'rep'], GrepCommand.argParser], + [['vimgrep', ''], GrepCommand.argParser], + [['grep', ''], GrepCommand.argParser], [['vimgrepa', 'dd'], undefined], [['viu', 'sage'], undefined], [['vm', 'ap'], undefined], From 29cfb707e24e828a202071f0f254012c9da8267d Mon Sep 17 00:00:00 2001 From: Parviz Azimov Date: Sat, 17 May 2025 22:33:37 +0300 Subject: [PATCH 03/17] removed assignment to grepResults, since this is unnecessary --- src/cmd_line/commands/grep.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/cmd_line/commands/grep.ts b/src/cmd_line/commands/grep.ts index 0b1998fbf8f..ae7d7e126fb 100644 --- a/src/cmd_line/commands/grep.ts +++ b/src/cmd_line/commands/grep.ts @@ -5,9 +5,8 @@ import * as vscode from 'vscode'; import * as error from '../../error'; import { VimState } from '../../state/vimState'; import { Pattern, SearchDirection } from '../../vimscript/pattern'; -import { StatusBar } from '../../statusBar'; import { ExCommand } from '../../vimscript/exCommand'; -import { Parser, seq, regexp, optWhitespace, whitespace } from 'parsimmon'; +import { Parser, seq, optWhitespace, whitespace } from 'parsimmon'; import { fileNameParser } from '../../vimscript/parserUtils'; interface IGrepCommandArguments { @@ -46,7 +45,7 @@ export class GrepCommand extends ExCommand { // 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 - const grepResults = await vscode.commands.executeCommand('workbench.action.findInFiles', { + await vscode.commands.executeCommand('workbench.action.findInFiles', { query: pattern.patternString, filesToInclude: files.join(','), triggerSearch: true, From fc6aee057a63e8343c9b75868ed6f6ce606a607f Mon Sep 17 00:00:00 2001 From: Parviz Azimov Date: Sat, 17 May 2025 22:35:49 +0300 Subject: [PATCH 04/17] update link to the reference documentation --- src/cmd_line/commands/grep.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmd_line/commands/grep.ts b/src/cmd_line/commands/grep.ts index ae7d7e126fb..c03554d3d4e 100644 --- a/src/cmd_line/commands/grep.ts +++ b/src/cmd_line/commands/grep.ts @@ -15,7 +15,7 @@ interface IGrepCommandArguments { } // Implements :grep -// http://vimdoc.sourceforge.net/htmldoc/quickref.html#:grep +// https://vimdoc.sourceforge.net/htmldoc/quickfix.html#:vimgrep export class GrepCommand extends ExCommand { public static readonly argParser: Parser = optWhitespace.then( seq( From 8b0a5d6eaccdb56825aec5a3d3c1739dcd51c41e Mon Sep 17 00:00:00 2001 From: Parviz Azimov Date: Sat, 17 May 2025 22:42:44 +0300 Subject: [PATCH 05/17] adds focus on the search window --- src/cmd_line/commands/grep.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/cmd_line/commands/grep.ts b/src/cmd_line/commands/grep.ts index c03554d3d4e..07f3b3717c1 100644 --- a/src/cmd_line/commands/grep.ts +++ b/src/cmd_line/commands/grep.ts @@ -9,6 +9,17 @@ 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[]; @@ -20,6 +31,7 @@ export class GrepCommand extends ExCommand { public static readonly argParser: Parser = optWhitespace.then( seq( Pattern.parser({ direction: SearchDirection.Backward, delimiter: ' ' }), + fileNameParser.sepBy(whitespace), ).map(([pattern, files]) => new GrepCommand({ pattern, files })), ); @@ -51,6 +63,7 @@ export class GrepCommand extends ExCommand { triggerSearch: true, isRegex: true, }); + await vscode.commands.executeCommand('search.action.focusSearchList'); // TODO: this will always throw an error, since the command returns nothing // if (!grepResults) { // throw error.VimError.fromCode(error.ErrorCode.PatternNotFound); From 0791d2ac43e7ec518df70eb128a9fe0e4b99065a Mon Sep 17 00:00:00 2001 From: Parviz Azimov Date: Sat, 17 May 2025 22:53:34 +0300 Subject: [PATCH 06/17] adds focus on the first search result when running vimgrep without the `j` flag --- src/cmd_line/commands/grep.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cmd_line/commands/grep.ts b/src/cmd_line/commands/grep.ts index 07f3b3717c1..3330f46e78c 100644 --- a/src/cmd_line/commands/grep.ts +++ b/src/cmd_line/commands/grep.ts @@ -28,10 +28,10 @@ interface IGrepCommandArguments { // Implements :grep // https://vimdoc.sourceforge.net/htmldoc/quickfix.html#:vimgrep export class GrepCommand extends ExCommand { + // TODO: pattern match the entire command to see if there are slashes and a flag to determine what to parse public static readonly argParser: Parser = optWhitespace.then( seq( - Pattern.parser({ direction: SearchDirection.Backward, delimiter: ' ' }), - + Pattern.parser({ direction: SearchDirection.Backward, delimiter: '' }), fileNameParser.sepBy(whitespace), ).map(([pattern, files]) => new GrepCommand({ pattern, files })), ); @@ -64,6 +64,8 @@ export class GrepCommand extends ExCommand { 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); From f0b441b2a93fe5a6032aaa97de3b9908cd5fb464 Mon Sep 17 00:00:00 2001 From: Parviz Azimov Date: Sun, 18 May 2025 19:45:52 +0300 Subject: [PATCH 07/17] fixed pattern separator being accidentally removed --- src/cmd_line/commands/grep.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/cmd_line/commands/grep.ts b/src/cmd_line/commands/grep.ts index 3330f46e78c..3bec7c32ea2 100644 --- a/src/cmd_line/commands/grep.ts +++ b/src/cmd_line/commands/grep.ts @@ -28,10 +28,9 @@ interface IGrepCommandArguments { // Implements :grep // https://vimdoc.sourceforge.net/htmldoc/quickfix.html#:vimgrep export class GrepCommand extends ExCommand { - // TODO: pattern match the entire command to see if there are slashes and a flag to determine what to parse public static readonly argParser: Parser = optWhitespace.then( seq( - Pattern.parser({ direction: SearchDirection.Backward, delimiter: '' }), + Pattern.parser({ direction: SearchDirection.Backward, delimiter: ' ' }), fileNameParser.sepBy(whitespace), ).map(([pattern, files]) => new GrepCommand({ pattern, files })), ); From 09cbf5f27e8a34c893378fdd26521aeb980fe06c Mon Sep 17 00:00:00 2001 From: Parviz Azimov Date: Tue, 20 May 2025 22:15:20 +0300 Subject: [PATCH 08/17] removed the console log and unneeded argument --- src/cmd_line/commands/grep.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/cmd_line/commands/grep.ts b/src/cmd_line/commands/grep.ts index 3bec7c32ea2..c3534717dc7 100644 --- a/src/cmd_line/commands/grep.ts +++ b/src/cmd_line/commands/grep.ts @@ -41,17 +41,12 @@ export class GrepCommand extends ExCommand { this.arguments = args; } - async execute(vimState: VimState): Promise { + async execute(): Promise { const { pattern, files } = this.arguments; console.log('GrepCommand', pattern.patternString, files); if (files.length === 0) { throw error.VimError.fromCode(error.ErrorCode.NoFileName); } - const listOfCommands = await vscode.commands.getCommands(); - const filteredCommands = listOfCommands.filter( - (cmd) => cmd.toLowerCase().includes('search') || cmd.toLowerCase().includes('find'), - ); - console.log('Filtered commands:', filteredCommands); // 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 From 48aa9f8a060778821757f470b898408f8c135983 Mon Sep 17 00:00:00 2001 From: Parviz Azimov Date: Tue, 20 May 2025 22:16:58 +0300 Subject: [PATCH 09/17] Basic grep test that checks if arguments are correctly parsed and whether basic grepping performs accurately --- test/cmd_line/grep.test.ts | 84 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 test/cmd_line/grep.test.ts diff --git a/test/cmd_line/grep.test.ts b/test/cmd_line/grep.test.ts new file mode 100644 index 00000000000..263865aa721 --- /dev/null +++ b/test/cmd_line/grep.test.ts @@ -0,0 +1,84 @@ +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.strictEqual( + activeEditor?.document.fileName.endsWith('Untitled-1'), + true, + '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', + ); + }); +}); From 7991c7d5fee10e8c80e0428677b925c65440512d Mon Sep 17 00:00:00 2001 From: Parviz Azimov Date: Fri, 23 May 2025 06:32:23 +0300 Subject: [PATCH 10/17] remove eslint-disable --- src/cmd_line/commands/grep.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/cmd_line/commands/grep.ts b/src/cmd_line/commands/grep.ts index c3534717dc7..a8b7f6caa68 100644 --- a/src/cmd_line/commands/grep.ts +++ b/src/cmd_line/commands/grep.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ import * as vscode from 'vscode'; import * as error from '../../error'; From 5c448b03323adab0980eb93f17f2b8ce98716434 Mon Sep 17 00:00:00 2001 From: Parviz Azimov Date: Fri, 23 May 2025 06:32:50 +0300 Subject: [PATCH 11/17] remove console.log from grep.ts --- src/cmd_line/commands/grep.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cmd_line/commands/grep.ts b/src/cmd_line/commands/grep.ts index a8b7f6caa68..f474a4a7192 100644 --- a/src/cmd_line/commands/grep.ts +++ b/src/cmd_line/commands/grep.ts @@ -41,7 +41,6 @@ export class GrepCommand extends ExCommand { async execute(): Promise { const { pattern, files } = this.arguments; - console.log('GrepCommand', pattern.patternString, files); if (files.length === 0) { throw error.VimError.fromCode(error.ErrorCode.NoFileName); } From dc00e9f7a0801ff4e1cd69d4feeb1d6f407c1935 Mon Sep 17 00:00:00 2001 From: Parviz Azimov Date: Fri, 23 May 2025 06:44:14 +0300 Subject: [PATCH 12/17] commit code review changes --- src/vimscript/exCommandParser.ts | 2 -- test/cmd_line/grep.test.ts | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/vimscript/exCommandParser.ts b/src/vimscript/exCommandParser.ts index 4e6175330e9..5e96cff1b6d 100644 --- a/src/vimscript/exCommandParser.ts +++ b/src/vimscript/exCommandParser.ts @@ -579,8 +579,6 @@ export const builtinExCommands: ReadonlyArray<[[string, string], ArgParser | und [['vi', 'sual'], undefined], [['vie', 'w'], undefined], [['vim', 'grep'], GrepCommand.argParser], - [['vimg', 'rep'], GrepCommand.argParser], - [['vimgrep', ''], GrepCommand.argParser], [['grep', ''], GrepCommand.argParser], [['vimgrepa', 'dd'], undefined], [['viu', 'sage'], undefined], diff --git a/test/cmd_line/grep.test.ts b/test/cmd_line/grep.test.ts index 263865aa721..00f461ce038 100644 --- a/test/cmd_line/grep.test.ts +++ b/test/cmd_line/grep.test.ts @@ -69,9 +69,8 @@ suite('Basic grep command', () => { // Assert that the active editor is Untitled-1 const activeEditor = vscode.window.activeTextEditor; assert.ok(activeEditor, 'There should be an active editor'); - assert.strictEqual( + assert.ok( activeEditor?.document.fileName.endsWith('Untitled-1'), - true, 'Active editor should be Untitled-1 after grep', ); assert.ok(modeHandler, 'modeHandler should be defined'); From ff83245ade23d5257760042d33f75043734c46e7 Mon Sep 17 00:00:00 2001 From: Parviz Azimov Date: Thu, 29 May 2025 09:18:05 +0300 Subject: [PATCH 13/17] removed unneeded grep command from the parse list --- src/vimscript/exCommandParser.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vimscript/exCommandParser.ts b/src/vimscript/exCommandParser.ts index 5e96cff1b6d..880438d1117 100644 --- a/src/vimscript/exCommandParser.ts +++ b/src/vimscript/exCommandParser.ts @@ -579,7 +579,6 @@ export const builtinExCommands: ReadonlyArray<[[string, string], ArgParser | und [['vi', 'sual'], undefined], [['vie', 'w'], undefined], [['vim', 'grep'], GrepCommand.argParser], - [['grep', ''], GrepCommand.argParser], [['vimgrepa', 'dd'], undefined], [['viu', 'sage'], undefined], [['vm', 'ap'], undefined], From 9d886be298476e14507330d6237b1632f48ba2b5 Mon Sep 17 00:00:00 2001 From: Parviz Azimov Date: Sat, 31 May 2025 10:23:23 +0300 Subject: [PATCH 14/17] added comments --- src/cmd_line/commands/grep.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/cmd_line/commands/grep.ts b/src/cmd_line/commands/grep.ts index f474a4a7192..effe80c9f21 100644 --- a/src/cmd_line/commands/grep.ts +++ b/src/cmd_line/commands/grep.ts @@ -26,6 +26,7 @@ interface IGrepCommandArguments { // 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 = optWhitespace.then( seq( Pattern.parser({ direction: SearchDirection.Backward, delimiter: ' ' }), @@ -55,11 +56,7 @@ export class GrepCommand extends ExCommand { isRegex: true, }); await vscode.commands.executeCommand('search.action.focusSearchList'); - // Only if there's no [j] flag + // TODO: 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); - // } } } From 7447634947cbaedb926f8db8562fe3fc27c5b97f Mon Sep 17 00:00:00 2001 From: Parviz Azimov Date: Sat, 31 May 2025 10:23:43 +0300 Subject: [PATCH 15/17] vimgrep command parse test --- test/vimscript/exCommandParse.test.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/vimscript/exCommandParse.test.ts b/test/vimscript/exCommandParse.test.ts index 0b9fcfbd34b..8648ed2582d 100644 --- a/test/vimscript/exCommandParse.test.ts +++ b/test/vimscript/exCommandParse.test.ts @@ -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, () => { @@ -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 })); From 5c4cdb6710a1d1be5179cee8d3e8e42c01c8c8a9 Mon Sep 17 00:00:00 2001 From: Parviz Azimov Date: Sat, 31 May 2025 10:24:18 +0300 Subject: [PATCH 16/17] wip on refactoring the grep test to use testutils --- test/cmd_line/grep.test.ts | 87 +++++++++++++++----------------------- 1 file changed, 34 insertions(+), 53 deletions(-) diff --git a/test/cmd_line/grep.test.ts b/test/cmd_line/grep.test.ts index 00f461ce038..979f93529b5 100644 --- a/test/cmd_line/grep.test.ts +++ b/test/cmd_line/grep.test.ts @@ -2,78 +2,59 @@ 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'], -// }), -// ); -// }); +import { createFile, setupWorkspace, cleanUpWorkspace } from '../testUtils'; 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 + setup(async () => { + // await cleanUpWorkspace(); + await setupWorkspace(); + }); 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']); + // 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); + const document2 = await vscode.workspace.openTextDocument(vscode.Uri.file(file2)); + await vscode.window.showTextDocument(document2); + const pattern = Pattern.parser({ direction: SearchDirection.Backward }); + file1 = file1.replace('/tmp/', ''); + file2 = file2.replace('/tmp/', ''); + const command = grep(pattern.tryParse('t*st'), [file1, file2]); await command.execute(); - await vscode.commands.executeCommand('search.action.focusNextSearchResult'); - // Assert that the active editor is Untitled-1 + // 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'); + console.log(`Active editor: ${activeEditor.document.fileName}`); + console.log(`Current mode: ${modeHandler.vimState.currentMode}`); + const docs = vscode.workspace.textDocuments.map((doc) => doc.fileName); + console.log(`open documents: ${docs}`); + // 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('Untitled-1'), - 'Active editor should be Untitled-1 after grep', + activeEditor.document.fileName.endsWith(file1), + 'Active editor should be first file after grep', ); - assert.ok(modeHandler, 'modeHandler should be defined'); assert.notStrictEqual( modeHandler.vimState, Mode.Visual, From 15bc1a34f0a3116552c7fe278719902f9e8b28ee Mon Sep 17 00:00:00 2001 From: Parviz Azimov Date: Sat, 31 May 2025 20:26:08 +0300 Subject: [PATCH 17/17] reworked the grep test to use testUtils's createFile --- test/cmd_line/grep.test.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/test/cmd_line/grep.test.ts b/test/cmd_line/grep.test.ts index 979f93529b5..bb6c67488c6 100644 --- a/test/cmd_line/grep.test.ts +++ b/test/cmd_line/grep.test.ts @@ -17,7 +17,6 @@ suite('Basic grep command', () => { // 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 cleanUpWorkspace(); await setupWorkspace(); }); test('GrepCommand executes correctly', async () => { @@ -33,23 +32,23 @@ suite('Basic grep command', () => { }); // 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); + await vscode.window.showTextDocument(document1, { preview: false }); const document2 = await vscode.workspace.openTextDocument(vscode.Uri.file(file2)); - await vscode.window.showTextDocument(document2); + await vscode.window.showTextDocument(document2, { preview: false }); const pattern = Pattern.parser({ direction: SearchDirection.Backward }); - file1 = file1.replace('/tmp/', ''); - file2 = file2.replace('/tmp/', ''); + // 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(); - // await vscode.commands.executeCommand('search.action.focusNextSearchResult'); + // 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'); - console.log(`Active editor: ${activeEditor.document.fileName}`); - console.log(`Current mode: ${modeHandler.vimState.currentMode}`); const docs = vscode.workspace.textDocuments.map((doc) => doc.fileName); - console.log(`open documents: ${docs}`); // 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),