mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-03 08:24:10 -07:00
fix(core): use ripgrep --json output for robust cross-platform parsing (#12853)
This commit is contained in:
@@ -17,7 +17,7 @@ import type { RipGrepToolParams } from './ripGrep.js';
|
||||
import { canUseRipgrep, RipGrepTool, ensureRgPath } from './ripGrep.js';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
import os, { EOL } from 'node:os';
|
||||
import os from 'node:os';
|
||||
import type { Config } from '../config/config.js';
|
||||
import { Storage } from '../config/storage.js';
|
||||
import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';
|
||||
@@ -360,7 +360,34 @@ describe('RipGrepTool', () => {
|
||||
it('should find matches for a simple pattern in all files', async () => {
|
||||
mockSpawn.mockImplementationOnce(
|
||||
createMockSpawn({
|
||||
outputData: `fileA.txt:1:hello world${EOL}fileA.txt:2:second line with world${EOL}sub/fileC.txt:1:another world in sub dir${EOL}`,
|
||||
outputData:
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'fileA.txt' },
|
||||
line_number: 1,
|
||||
lines: { text: 'hello world\n' },
|
||||
},
|
||||
}) +
|
||||
'\n' +
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'fileA.txt' },
|
||||
line_number: 2,
|
||||
lines: { text: 'second line with world\n' },
|
||||
},
|
||||
}) +
|
||||
'\n' +
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'sub/fileC.txt' },
|
||||
line_number: 1,
|
||||
lines: { text: 'another world in sub dir\n' },
|
||||
},
|
||||
}) +
|
||||
'\n',
|
||||
exitCode: 0,
|
||||
}),
|
||||
);
|
||||
@@ -385,7 +412,15 @@ describe('RipGrepTool', () => {
|
||||
// Setup specific mock for this test - searching in 'sub' should only return matches from that directory
|
||||
mockSpawn.mockImplementationOnce(
|
||||
createMockSpawn({
|
||||
outputData: `fileC.txt:1:another world in sub dir${EOL}`,
|
||||
outputData:
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'fileC.txt' },
|
||||
line_number: 1,
|
||||
lines: { text: 'another world in sub dir\n' },
|
||||
},
|
||||
}) + '\n',
|
||||
exitCode: 0,
|
||||
}),
|
||||
);
|
||||
@@ -405,7 +440,15 @@ describe('RipGrepTool', () => {
|
||||
// Setup specific mock for this test
|
||||
mockSpawn.mockImplementationOnce(
|
||||
createMockSpawn({
|
||||
outputData: `fileB.js:2:function baz() { return "hello"; }${EOL}`,
|
||||
outputData:
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'fileB.js' },
|
||||
line_number: 2,
|
||||
lines: { text: 'function baz() { return "hello"; }\n' },
|
||||
},
|
||||
}) + '\n',
|
||||
exitCode: 0,
|
||||
}),
|
||||
);
|
||||
@@ -455,7 +498,18 @@ describe('RipGrepTool', () => {
|
||||
|
||||
if (onData) {
|
||||
// Only return match from the .js file in sub directory
|
||||
onData(Buffer.from(`another.js:1:const greeting = "hello";${EOL}`));
|
||||
onData(
|
||||
Buffer.from(
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'another.js' },
|
||||
line_number: 1,
|
||||
lines: { text: 'const greeting = "hello";\n' },
|
||||
},
|
||||
}) + '\n',
|
||||
),
|
||||
);
|
||||
}
|
||||
if (onClose) {
|
||||
onClose(0);
|
||||
@@ -540,7 +594,18 @@ describe('RipGrepTool', () => {
|
||||
|
||||
if (onData) {
|
||||
// Return match for the regex pattern
|
||||
onData(Buffer.from(`fileB.js:1:const foo = "bar";${EOL}`));
|
||||
onData(
|
||||
Buffer.from(
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'fileB.js' },
|
||||
line_number: 1,
|
||||
lines: { text: 'const foo = "bar";\n' },
|
||||
},
|
||||
}) + '\n',
|
||||
),
|
||||
);
|
||||
}
|
||||
if (onClose) {
|
||||
onClose(0);
|
||||
@@ -589,7 +654,24 @@ describe('RipGrepTool', () => {
|
||||
// Return case-insensitive matches for 'HELLO'
|
||||
onData(
|
||||
Buffer.from(
|
||||
`fileA.txt:1:hello world${EOL}fileB.js:2:function baz() { return "hello"; }${EOL}`,
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'fileA.txt' },
|
||||
line_number: 1,
|
||||
lines: { text: 'hello world\n' },
|
||||
},
|
||||
}) +
|
||||
'\n' +
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'fileB.js' },
|
||||
line_number: 2,
|
||||
lines: { text: 'function baz() { return "hello"; }\n' },
|
||||
},
|
||||
}) +
|
||||
'\n',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -691,18 +773,54 @@ describe('RipGrepTool', () => {
|
||||
if (callCount === 1) {
|
||||
// First directory (tempRootDir)
|
||||
outputData =
|
||||
[
|
||||
'fileA.txt:1:hello world',
|
||||
'fileA.txt:2:second line with world',
|
||||
'sub/fileC.txt:1:another world in sub dir',
|
||||
].join(EOL) + EOL;
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'fileA.txt' },
|
||||
line_number: 1,
|
||||
lines: { text: 'hello world\n' },
|
||||
},
|
||||
}) +
|
||||
'\n' +
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'fileA.txt' },
|
||||
line_number: 2,
|
||||
lines: { text: 'second line with world\n' },
|
||||
},
|
||||
}) +
|
||||
'\n' +
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'sub/fileC.txt' },
|
||||
line_number: 1,
|
||||
lines: { text: 'another world in sub dir\n' },
|
||||
},
|
||||
}) +
|
||||
'\n';
|
||||
} else if (callCount === 2) {
|
||||
// Second directory (secondDir)
|
||||
outputData =
|
||||
[
|
||||
'other.txt:2:world in second',
|
||||
'another.js:1:function world() { return "test"; }',
|
||||
].join(EOL) + EOL;
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'other.txt' },
|
||||
line_number: 2,
|
||||
lines: { text: 'world in second\n' },
|
||||
},
|
||||
}) +
|
||||
'\n' +
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'another.js' },
|
||||
line_number: 1,
|
||||
lines: { text: 'function world() { return "test"; }\n' },
|
||||
},
|
||||
}) +
|
||||
'\n';
|
||||
}
|
||||
|
||||
if (stdoutDataHandler && outputData) {
|
||||
@@ -789,7 +907,18 @@ describe('RipGrepTool', () => {
|
||||
)?.[1];
|
||||
|
||||
if (onData) {
|
||||
onData(Buffer.from(`fileC.txt:1:another world in sub dir${EOL}`));
|
||||
onData(
|
||||
Buffer.from(
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'fileC.txt' },
|
||||
line_number: 1,
|
||||
lines: { text: 'another world in sub dir\n' },
|
||||
},
|
||||
}) + '\n',
|
||||
),
|
||||
);
|
||||
}
|
||||
if (onClose) {
|
||||
onClose(0);
|
||||
@@ -1005,7 +1134,14 @@ describe('RipGrepTool', () => {
|
||||
if (onData) {
|
||||
onData(
|
||||
Buffer.from(
|
||||
`${specialFileName}:1:hello world with special chars${EOL}`,
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: specialFileName },
|
||||
line_number: 1,
|
||||
lines: { text: 'hello world with special chars\n' },
|
||||
},
|
||||
}) + '\n',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1060,7 +1196,14 @@ describe('RipGrepTool', () => {
|
||||
if (onData) {
|
||||
onData(
|
||||
Buffer.from(
|
||||
`a/b/c/d/e/deep.txt:1:content in deep directory${EOL}`,
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'a/b/c/d/e/deep.txt' },
|
||||
line_number: 1,
|
||||
lines: { text: 'content in deep directory\n' },
|
||||
},
|
||||
}) + '\n',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1115,7 +1258,14 @@ describe('RipGrepTool', () => {
|
||||
if (onData) {
|
||||
onData(
|
||||
Buffer.from(
|
||||
`code.js:1:function getName() { return "test"; }${EOL}`,
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'code.js' },
|
||||
line_number: 1,
|
||||
lines: { text: 'function getName() { return "test"; }\n' },
|
||||
},
|
||||
}) + '\n',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1168,7 +1318,33 @@ describe('RipGrepTool', () => {
|
||||
if (onData) {
|
||||
onData(
|
||||
Buffer.from(
|
||||
`case.txt:1:Hello World${EOL}case.txt:2:hello world${EOL}case.txt:3:HELLO WORLD${EOL}`,
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'case.txt' },
|
||||
line_number: 1,
|
||||
lines: { text: 'Hello World\n' },
|
||||
},
|
||||
}) +
|
||||
'\n' +
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'case.txt' },
|
||||
line_number: 2,
|
||||
lines: { text: 'hello world\n' },
|
||||
},
|
||||
}) +
|
||||
'\n' +
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'case.txt' },
|
||||
line_number: 3,
|
||||
lines: { text: 'HELLO WORLD\n' },
|
||||
},
|
||||
}) +
|
||||
'\n',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1220,7 +1396,18 @@ describe('RipGrepTool', () => {
|
||||
)?.[1];
|
||||
|
||||
if (onData) {
|
||||
onData(Buffer.from(`special.txt:1:Price: $19.99${EOL}`));
|
||||
onData(
|
||||
Buffer.from(
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'special.txt' },
|
||||
line_number: 1,
|
||||
lines: { text: 'Price: $19.99\n' },
|
||||
},
|
||||
}) + '\n',
|
||||
),
|
||||
);
|
||||
}
|
||||
if (onClose) {
|
||||
onClose(0);
|
||||
@@ -1279,7 +1466,24 @@ describe('RipGrepTool', () => {
|
||||
if (onData) {
|
||||
onData(
|
||||
Buffer.from(
|
||||
`test.ts:1:typescript content${EOL}test.tsx:1:tsx content${EOL}`,
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'test.ts' },
|
||||
line_number: 1,
|
||||
lines: { text: 'typescript content\n' },
|
||||
},
|
||||
}) +
|
||||
'\n' +
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'test.tsx' },
|
||||
line_number: 1,
|
||||
lines: { text: 'tsx content\n' },
|
||||
},
|
||||
}) +
|
||||
'\n',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1337,7 +1541,18 @@ describe('RipGrepTool', () => {
|
||||
)?.[1];
|
||||
|
||||
if (onData) {
|
||||
onData(Buffer.from(`src/main.ts:1:source code${EOL}`));
|
||||
onData(
|
||||
Buffer.from(
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'src/main.ts' },
|
||||
line_number: 1,
|
||||
lines: { text: 'source code\n' },
|
||||
},
|
||||
}) + '\n',
|
||||
),
|
||||
);
|
||||
}
|
||||
if (onClose) {
|
||||
onClose(0);
|
||||
|
||||
Reference in New Issue
Block a user