fix(core): use ripgrep --json output for robust cross-platform parsing (#12853)

This commit is contained in:
Abhi
2025-11-10 19:01:01 -05:00
committed by GitHub
parent 4af4f8644d
commit 51f952e700
3 changed files with 354 additions and 59 deletions
+239 -24
View File
@@ -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);