From aba5d3363074f1e1c98742889b956b51b3d37cae Mon Sep 17 00:00:00 2001 From: "koalazf.99" Date: Tue, 21 Oct 2025 03:47:00 +0800 Subject: [PATCH] try fix --- packages/core/src/tools/ripGrep.test.ts | 84 +++++++++++++------ packages/core/src/tools/ripGrep.ts | 94 ++++++++++++++++++++-- packages/core/src/utils/gitIgnoreParser.ts | 34 +++++++- 3 files changed, 178 insertions(+), 34 deletions(-) diff --git a/packages/core/src/tools/ripGrep.test.ts b/packages/core/src/tools/ripGrep.test.ts index 06cc4ccc..86c2c5de 100644 --- a/packages/core/src/tools/ripGrep.test.ts +++ b/packages/core/src/tools/ripGrep.test.ts @@ -14,6 +14,7 @@ import type { Config } from '../config/config.js'; import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js'; import type { ChildProcess } from 'node:child_process'; import { spawn } from 'node:child_process'; +import { FileDiscoveryService } from '../services/fileDiscoveryService.js'; // Mock @lvce-editor/ripgrep for testing vi.mock('@lvce-editor/ripgrep', () => ({ @@ -75,21 +76,35 @@ function createMockSpawn( }; } +function createTestConfig( + rootDir: string, + extraDirectories: string[] = [], +): Config { + const fileService = new FileDiscoveryService(rootDir); + return { + getTargetDir: () => rootDir, + getWorkspaceContext: () => + createMockWorkspaceContext(rootDir, extraDirectories), + getDebugMode: () => false, + getFileService: () => fileService, + getFileFilteringOptions: () => ({ + respectGitIgnore: true, + respectGeminiIgnore: true, + }), + } as unknown as Config; +} + describe('RipGrepTool', () => { let tempRootDir: string; let grepTool: RipGrepTool; + let mockConfig: Config; const abortSignal = new AbortController().signal; - const mockConfig = { - getTargetDir: () => tempRootDir, - getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir), - getDebugMode: () => false, - } as unknown as Config; - beforeEach(async () => { vi.clearAllMocks(); mockSpawn.mockClear(); tempRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'grep-tool-root-')); + mockConfig = createTestConfig(tempRootDir); grepTool = new RipGrepTool(mockConfig); // Create some test files and directories @@ -293,6 +308,42 @@ describe('RipGrepTool', () => { expect(result.returnDisplay).toBe('Found 1 match'); }); + it('should filter out matches ignored by .qwenignore', async () => { + await fs.writeFile( + path.join(tempRootDir, '.qwenignore'), + 'logs/\n', + 'utf8', + ); + await fs.mkdir(path.join(tempRootDir, 'logs'), { recursive: true }); + await fs.writeFile( + path.join(tempRootDir, 'logs', 'ignored.txt'), + 'Got it. Thanks for the context!', + 'utf8', + ); + + mockSpawn.mockImplementationOnce( + createMockSpawn({ + outputData: `logs/ignored.txt:1:Got it. Thanks for the context!${EOL}`, + exitCode: 0, + }), + ); + + const params: RipGrepToolParams = { + pattern: 'Got it\\. Thanks for the context!', + }; + const invocation = grepTool.build(params); + const result = await invocation.execute(abortSignal); + + expect(result.returnDisplay).toBe('No matches found'); + expect(result.llmContent).toContain( + 'No matches found for pattern "Got it\\. Thanks for the context!" in the workspace directory', + ); + + const spawnArgs = mockSpawn.mock.calls[0]?.[1] ?? []; + expect(spawnArgs).toContain('--ignore-file'); + expect(spawnArgs).toContain(path.join(tempRootDir, '.qwenignore')); + }); + it('should return "No matches found" when pattern does not exist', async () => { // Setup specific mock for no matches mockSpawn.mockImplementationOnce( @@ -452,12 +503,7 @@ describe('RipGrepTool', () => { ); // Create a mock config with multiple directories - const multiDirConfig = { - getTargetDir: () => tempRootDir, - getWorkspaceContext: () => - createMockWorkspaceContext(tempRootDir, [secondDir]), - getDebugMode: () => false, - } as unknown as Config; + const multiDirConfig = createTestConfig(tempRootDir, [secondDir]); // Setup specific mock for this test - multi-directory search for 'world' // Mock will be called twice - once for each directory @@ -557,12 +603,7 @@ describe('RipGrepTool', () => { ); // Create a mock config with multiple directories - const multiDirConfig = { - getTargetDir: () => tempRootDir, - getWorkspaceContext: () => - createMockWorkspaceContext(tempRootDir, [secondDir]), - getDebugMode: () => false, - } as unknown as Config; + const multiDirConfig = createTestConfig(tempRootDir, [secondDir]); // Setup specific mock for this test - searching in 'sub' should only return matches from that directory mockSpawn.mockImplementationOnce(() => { @@ -1187,12 +1228,7 @@ describe('RipGrepTool', () => { it('should indicate searching across all workspace directories when no path specified', () => { // Create a mock config with multiple directories - const multiDirConfig = { - getTargetDir: () => tempRootDir, - getWorkspaceContext: () => - createMockWorkspaceContext(tempRootDir, ['/another/dir']), - getDebugMode: () => false, - } as unknown as Config; + const multiDirConfig = createTestConfig(tempRootDir, ['/another/dir']); const multiDirGrepTool = new RipGrepTool(multiDirConfig); const params: RipGrepToolParams = { pattern: 'testPattern' }; diff --git a/packages/core/src/tools/ripGrep.ts b/packages/core/src/tools/ripGrep.ts index 7c8f6314..b40b6e37 100644 --- a/packages/core/src/tools/ripGrep.ts +++ b/packages/core/src/tools/ripGrep.ts @@ -6,7 +6,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { EOL } from 'node:os'; +import { EOL, tmpdir } from 'node:os'; import { spawn } from 'node:child_process'; import type { ToolInvocation, ToolResult } from './tools.js'; import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js'; @@ -58,6 +58,8 @@ class GrepToolInvocation extends BaseToolInvocation< RipGrepToolParams, ToolResult > { + private readonly tempIgnoreFiles: string[] = []; + constructor( private readonly config: Config, params: RipGrepToolParams, @@ -109,9 +111,28 @@ class GrepToolInvocation extends BaseToolInvocation< async execute(signal: AbortSignal): Promise { try { const workspaceContext = this.config.getWorkspaceContext(); + const fileService = this.config.getFileService(); + const fileFilteringOptions = this.config.getFileFilteringOptions(); + const qwenIgnoreFiles = + fileFilteringOptions.respectGeminiIgnore === false + ? [] + : this.getQwenIgnoreFilePaths(); const searchDirAbs = this.resolveAndValidatePath(this.params.path); const searchDirDisplay = this.params.path || '.'; + // if (this.config.getDebugMode()) { + console.log( + `[GrepTool] Using qwenignore files: ${ + qwenIgnoreFiles.length > 0 + ? qwenIgnoreFiles.join(', ') + : 'none (qwenignore disabled or file missing)' + }`, + ); + console.log( + `[GrepTool] File filtering: respectGitIgnore=${fileFilteringOptions.respectGitIgnore ?? true}, respectQwenIgnore=${fileFilteringOptions.respectGeminiIgnore ?? true}`, + ); + // } + // Determine which directories to search let searchDirectories: readonly string[]; if (searchDirAbs === null) { @@ -125,26 +146,37 @@ class GrepToolInvocation extends BaseToolInvocation< let allMatches: GrepMatch[] = []; const totalMaxMatches = DEFAULT_TOTAL_MAX_MATCHES; - if (this.config.getDebugMode()) { - console.log(`[GrepTool] Total result limit: ${totalMaxMatches}`); - } - for (const searchDir of searchDirectories) { const searchResult = await this.performRipgrepSearch({ pattern: this.params.pattern, path: searchDir, include: this.params.include, signal, + ignoreFiles: qwenIgnoreFiles, }); + let filteredMatches = searchResult; + if ( + fileFilteringOptions.respectGitIgnore || + fileFilteringOptions.respectGeminiIgnore + ) { + filteredMatches = searchResult.filter((match) => { + const absoluteMatchPath = path.resolve(searchDir, match.filePath); + return !fileService.shouldIgnoreFile( + absoluteMatchPath, + fileFilteringOptions, + ); + }); + } + if (searchDirectories.length > 1) { const dirName = path.basename(searchDir); - searchResult.forEach((match) => { + filteredMatches.forEach((match) => { match.filePath = path.join(dirName, match.filePath); }); } - allMatches = allMatches.concat(searchResult); + allMatches = allMatches.concat(filteredMatches); if (allMatches.length >= totalMaxMatches) { allMatches = allMatches.slice(0, totalMaxMatches); @@ -219,6 +251,8 @@ class GrepToolInvocation extends BaseToolInvocation< llmContent: `Error during grep search operation: ${errorMessage}`, returnDisplay: `Error: ${errorMessage}`, }; + } finally { + this.cleanupTemporaryIgnoreFiles(); } } @@ -265,8 +299,9 @@ class GrepToolInvocation extends BaseToolInvocation< path: string; include?: string; signal: AbortSignal; + ignoreFiles?: string[]; }): Promise { - const { pattern, path: absolutePath, include } = options; + const { pattern, path: absolutePath, include, ignoreFiles } = options; const rgArgs = [ '--line-number', @@ -281,6 +316,12 @@ class GrepToolInvocation extends BaseToolInvocation< rgArgs.push('--glob', include); } + if (ignoreFiles && ignoreFiles.length > 0) { + for (const ignoreFile of ignoreFiles) { + rgArgs.push('--ignore-file', ignoreFile); + } + } + const excludes = [ '.git', 'node_modules', @@ -389,6 +430,43 @@ class GrepToolInvocation extends BaseToolInvocation< } return description; } + + private getQwenIgnoreFilePaths(): string[] { + const patterns = this.config.getFileService().getGeminiIgnorePatterns(); + if (patterns.length === 0) { + return []; + } + + const tempFilePath = path.join( + tmpdir(), + `qwen-ignore-${process.pid}-${Date.now()}-${Math.random() + .toString(16) + .slice(2)}.rgignore`, + ); + + try { + const fileContents = `${patterns.join(EOL)}${EOL}`; + fs.writeFileSync(tempFilePath, fileContents, 'utf8'); + this.tempIgnoreFiles.push(tempFilePath); + return [tempFilePath]; + } catch (error: unknown) { + console.warn( + `Failed to create temporary .qwenignore for ripgrep: ${getErrorMessage(error)}`, + ); + return []; + } + } + + private cleanupTemporaryIgnoreFiles(): void { + for (const filePath of this.tempIgnoreFiles) { + try { + fs.unlinkSync(filePath); + } catch { + // ignore cleanup errors + } + } + this.tempIgnoreFiles.length = 0; + } } /** diff --git a/packages/core/src/utils/gitIgnoreParser.ts b/packages/core/src/utils/gitIgnoreParser.ts index 177a0d2c..91137339 100644 --- a/packages/core/src/utils/gitIgnoreParser.ts +++ b/packages/core/src/utils/gitIgnoreParser.ts @@ -52,8 +52,38 @@ export class GitIgnoreParser implements GitIgnoreFilter { } private addPatterns(patterns: string[]) { - this.ig.add(patterns); - this.patterns.push(...patterns); + const normalizedPatterns = patterns.map((pattern) => { + if (!pattern) { + return pattern; + } + + if (path.isAbsolute(pattern)) { + const relativePattern = path.relative(this.projectRoot, pattern); + + if (relativePattern === '' || relativePattern === '.') { + return '/'; + } + + if (!relativePattern.startsWith('..')) { + let normalized = relativePattern.replace(/\\/g, '/'); + + if (pattern.endsWith('/') && !normalized.endsWith('/')) { + normalized += '/'; + } + + if (!normalized.startsWith('/')) { + normalized = `/${normalized}`; + } + + return normalized; + } + } + + return pattern; + }); + + this.ig.add(normalizedPatterns); + this.patterns.push(...normalizedPatterns); } isIgnored(filePath: string): boolean {