Skip to content

Commit c6279d8

Browse files
authored
Fix codebase indexing for plain text files (#938)
* fix(code-index): index plain text files * fix(tree-sitter): skip plain text definition parsing * fix(code-index): address plain text review feedback * fix(tree-sitter): preserve Scala structural parsing * test(code-index): remove redundant async usage
1 parent badb82c commit c6279d8

7 files changed

Lines changed: 162 additions & 8 deletions

File tree

src/services/code-index/processors/__tests__/parser.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,31 @@ describe("CodeParser", () => {
267267
})
268268

269269
describe("_chunkTextByLines", () => {
270+
it("should not emit a chunk whose segment hash has already been seen", () => {
271+
const lines = ["Fallback content long enough to produce a chunk without being filtered out."]
272+
const seenSegmentHashes = new Set<string>()
273+
274+
const firstResult = parser["_chunkTextByLines"](
275+
lines,
276+
"manual.txt",
277+
"hash",
278+
"fallback_chunk",
279+
seenSegmentHashes,
280+
)
281+
const duplicateResult = parser["_chunkTextByLines"](
282+
lines,
283+
"manual.txt",
284+
"hash",
285+
"fallback_chunk",
286+
seenSegmentHashes,
287+
)
288+
289+
expect(firstResult).toHaveLength(1)
290+
expect(firstResult[0].segmentHash).toMatch(/^[a-f0-9]{64}$/)
291+
expect(seenSegmentHashes).toEqual(new Set([firstResult[0].segmentHash]))
292+
expect(duplicateResult).toEqual([])
293+
})
294+
270295
it("should handle oversized lines by splitting them", async () => {
271296
const longLine = "a".repeat(2000)
272297
const lines = ["normal", longLine, "normal"]
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { CodeParser } from "../parser"
2+
import { scannerExtensions, shouldUseFallbackChunking } from "../../shared/supported-extensions"
3+
4+
vi.mock("../../../../../packages/telemetry/src/TelemetryService", () => ({
5+
TelemetryService: {
6+
instance: {
7+
captureEvent: vi.fn(),
8+
},
9+
},
10+
}))
11+
12+
describe("CodeParser - plain text support", () => {
13+
it("supports .txt files through fallback chunking", async () => {
14+
expect(scannerExtensions).toContain(".txt")
15+
expect(shouldUseFallbackChunking(".txt")).toBe(true)
16+
17+
const content = [
18+
"Zoo Code plain text indexing regression test.",
19+
"This sentence contains searchable content that only exists in the text file.",
20+
"The fallback parser should preserve every line while creating an indexable chunk.",
21+
].join("\n")
22+
23+
const blocks = await new CodeParser().parseFile("manual.txt", {
24+
content,
25+
fileHash: "txt-file-hash",
26+
})
27+
28+
expect(blocks).toHaveLength(1)
29+
expect(blocks[0]).toMatchObject({
30+
file_path: "manual.txt",
31+
type: "fallback_chunk",
32+
start_line: 1,
33+
end_line: 3,
34+
content,
35+
fileHash: "txt-file-hash",
36+
segmentHash: expect.any(String),
37+
})
38+
})
39+
40+
it("parses uppercase .TXT extensions", async () => {
41+
expect(shouldUseFallbackChunking(".TXT")).toBe(true)
42+
43+
const content = "Uppercase plain-text extension content long enough to produce a fallback chunk."
44+
const blocks = await new CodeParser().parseFile("manual.TXT", {
45+
content,
46+
fileHash: "uppercase-txt-file-hash",
47+
})
48+
49+
expect(blocks).toHaveLength(1)
50+
expect(blocks[0]).toMatchObject({
51+
file_path: "manual.TXT",
52+
type: "fallback_chunk",
53+
content,
54+
fileHash: "uppercase-txt-file-hash",
55+
segmentHash: expect.any(String),
56+
})
57+
})
58+
})

src/services/code-index/processors/parser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export class CodeParser implements ICodeParser {
4545
let content: string
4646
let fileHash: string
4747

48-
if (options?.content) {
48+
if (options?.content !== undefined) {
4949
content = options.content
5050
fileHash = options.fileHash || this.createFileHash(content)
5151
} else {

src/services/code-index/shared/supported-extensions.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { extensions as allExtensions } from "../../tree-sitter"
2+
import { fallbackExtensions, isFallbackExtension } from "../../shared/fallback-extensions"
3+
4+
export { fallbackExtensions }
25

36
// Include all extensions including markdown for the scanner
47
export const scannerExtensions = allExtensions
@@ -18,17 +21,11 @@ export const scannerExtensions = allExtensions
1821
*
1922
* Note: Do NOT remove parser cases from languageParser.ts as they may be used elsewhere
2023
*/
21-
export const fallbackExtensions = [
22-
".vb", // Visual Basic .NET - no dedicated WASM parser
23-
".scala", // Scala - uses fallback chunking instead of Lua query workaround
24-
".swift", // Swift - uses fallback chunking due to parser instability
25-
]
26-
2724
/**
2825
* Check if a file extension should use fallback chunking
2926
* @param extension File extension (including the dot)
3027
* @returns true if the extension should use fallback chunking
3128
*/
3229
export function shouldUseFallbackChunking(extension: string): boolean {
33-
return fallbackExtensions.includes(extension.toLowerCase())
30+
return isFallbackExtension(extension)
3431
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Extensions that should not be parsed for structural definitions and should
3+
* instead use line-based fallback chunking where indexing is supported.
4+
*/
5+
export const fallbackExtensions = [".txt", ".vb", ".scala", ".swift"] as const
6+
7+
/**
8+
* Fallback extensions that do not have a structural parser. Scala and Swift
9+
* still support structural parsing outside code indexing.
10+
*/
11+
export const nonStructuralExtensions = [".txt", ".vb"] as const
12+
13+
/**
14+
* Check whether a file extension should bypass structural parsing.
15+
*
16+
* @param extension File extension, including the leading dot
17+
*/
18+
export function isFallbackExtension(extension: string): boolean {
19+
return (fallbackExtensions as readonly string[]).includes(extension.toLowerCase())
20+
}
21+
22+
/**
23+
* Check whether a file extension should bypass structural parsing entirely.
24+
*
25+
* @param extension File extension, including the leading dot
26+
*/
27+
export function isNonStructuralExtension(extension: string): boolean {
28+
return (nonStructuralExtensions as readonly string[]).includes(extension.toLowerCase())
29+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Mocks must come first, before imports
2+
3+
vi.mock("fs/promises", () => ({
4+
readFile: vi.fn(),
5+
}))
6+
7+
vi.mock("../../../utils/fs", () => ({
8+
fileExistsAtPath: vi.fn().mockResolvedValue(true),
9+
}))
10+
11+
vi.mock("../languageParser", () => ({
12+
loadRequiredLanguageParsers: vi.fn(),
13+
}))
14+
15+
import * as fs from "fs/promises"
16+
import { loadRequiredLanguageParsers } from "../languageParser"
17+
import { parseSourceCodeDefinitionsForFile } from "../index"
18+
19+
describe("Non-structural Extension Integration Tests", () => {
20+
beforeEach(() => {
21+
vi.clearAllMocks()
22+
})
23+
24+
it.each(["manual.txt", "legacy.vb"])(
25+
"returns undefined for %s without loading a tree-sitter parser",
26+
async (filePath) => {
27+
const result = await parseSourceCodeDefinitionsForFile(filePath)
28+
29+
expect(result).toBeUndefined()
30+
},
31+
)
32+
33+
afterEach(() => {
34+
expect(loadRequiredLanguageParsers).not.toHaveBeenCalled()
35+
expect(fs.readFile).not.toHaveBeenCalled()
36+
})
37+
})

src/services/tree-sitter/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { fileExistsAtPath } from "../../utils/fs"
55
import { parseMarkdown } from "./markdownParser"
66
import { RooIgnoreController } from "../../core/ignore/RooIgnoreController"
77
import { QueryCapture } from "web-tree-sitter"
8+
import { isNonStructuralExtension } from "../shared/fallback-extensions"
89

910
// Private constant
1011
const DEFAULT_MIN_COMPONENT_LINES_VALUE = 4
@@ -66,6 +67,8 @@ const extensions = [
6667
// Markdown
6768
"md",
6869
"markdown",
70+
// Plain text
71+
"txt",
6972
// JSON
7073
"json",
7174
// CSS
@@ -111,6 +114,11 @@ export async function parseSourceCodeDefinitionsForFile(
111114
return undefined
112115
}
113116

117+
// Files without a structural parser have no definitions to extract
118+
if (isNonStructuralExtension(ext)) {
119+
return undefined
120+
}
121+
114122
// Special case for markdown files
115123
if (ext === ".md" || ext === ".markdown") {
116124
// Check if we have permission to access this file

0 commit comments

Comments
 (0)