Skip to content

Commit 8e2a40b

Browse files
author
Roo Code
committed
File search service
1 parent 9d588e3 commit 8e2a40b

File tree

8 files changed

+1470
-75
lines changed

8 files changed

+1470
-75
lines changed

package-lock.json

Lines changed: 1331 additions & 50 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@
276276
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
277277
"pretest": "npm run compile && npm run compile:integration",
278278
"dev": "cd webview-ui && npm run dev",
279-
"test": "jest && npm run test:webview",
279+
"test": "jest && vitest && npm run test:webview",
280280
"test:webview": "cd webview-ui && npm run test",
281281
"test:integration": "npm run build && npm run compile:integration && npx dotenvx run -f .env.integration -- vscode-test",
282282
"prepare": "husky",
@@ -314,7 +314,8 @@
314314
"diff-match-patch": "^1.0.5",
315315
"fast-deep-equal": "^3.1.3",
316316
"fastest-levenshtein": "^1.0.16",
317-
"globby": "^14.0.2",
317+
"fuzzysort": "^3.1.0",
318+
"globby": "^14.1.0",
318319
"isbinaryfile": "^5.0.2",
319320
"mammoth": "^1.8.0",
320321
"monaco-vscode-textmate-theme-converter": "^0.1.7",
@@ -351,17 +352,18 @@
351352
"@vscode/test-cli": "^0.0.9",
352353
"@vscode/test-electron": "^2.4.0",
353354
"esbuild": "^0.24.0",
354-
"mkdirp": "^3.0.1",
355-
"rimraf": "^6.0.1",
356355
"eslint": "^8.57.0",
357356
"husky": "^9.1.7",
358357
"jest": "^29.7.0",
359358
"jest-simple-dot-reporter": "^1.0.5",
360359
"lint-staged": "^15.2.11",
360+
"mkdirp": "^3.0.1",
361361
"npm-run-all": "^4.1.5",
362362
"prettier": "^3.4.2",
363+
"rimraf": "^6.0.1",
363364
"ts-jest": "^29.2.5",
364-
"typescript": "^5.4.5"
365+
"typescript": "^5.4.5",
366+
"vitest": "^3.0.5"
365367
},
366368
"lint-staged": {
367369
"*.{js,jsx,ts,tsx,json,css,md}": [
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { globbyStream, Options } from "globby"
2+
import fuzzysort from "fuzzysort"
3+
4+
import { IGNORE } from "../glob/ignore"
5+
6+
export class FileSearchService {
7+
private files: string[] = []
8+
9+
constructor(private readonly dirPath: string) {}
10+
11+
async indexFiles(globbyOptions: Options = {}) {
12+
const options: Options = {
13+
cwd: this.dirPath,
14+
// Do not ignore hidden files/directories.
15+
dot: true,
16+
absolute: true,
17+
// Append a / on any directories matched (/ is used on windows as well,
18+
// so dont use path.sep).
19+
markDirectories: true,
20+
// Globby ignores any files that are in .gitignore.
21+
gitignore: true,
22+
// Just in case there is no gitignore, we ignore sensible defaults.
23+
ignore: IGNORE,
24+
// List directories on their own too.
25+
onlyFiles: false,
26+
...globbyOptions,
27+
}
28+
29+
const stream = globbyStream("**", options)
30+
31+
for await (const file of stream) {
32+
this.files.push(file.toString())
33+
}
34+
}
35+
36+
public addFiles(files: string[]) {
37+
this.files.push(...files)
38+
}
39+
40+
public removeFiles(files: string[]) {
41+
this.files = this.files.filter((file) => !files.includes(file))
42+
}
43+
44+
public get count() {
45+
return this.files.length
46+
}
47+
48+
public search(query: string, { limit = 25, threshold = 0.5 }: { limit?: number; threshold?: number } = {}) {
49+
return fuzzysort.go(query, this.files, { limit, threshold })
50+
}
51+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// npx vitest src/services/file-search/__tests__/FileSearchService.spec.ts
2+
3+
import path from "path"
4+
import { describe, expect, test, beforeAll } from "vitest"
5+
6+
import { FileSearchService } from "../FileSearchService"
7+
8+
describe("listFiles", () => {
9+
const service = new FileSearchService(path.join(process.cwd(), "node_modules"))
10+
11+
beforeAll(async () => {
12+
await service.indexFiles({ gitignore: false, ignore: undefined })
13+
})
14+
15+
test("it indexes files", async () => {
16+
expect(service.count).toBeGreaterThan(25_000)
17+
})
18+
19+
test("it searches for files", async () => {
20+
const results = await service.search("zod")
21+
expect(results.length).toBe(25)
22+
expect(results.every((result) => result.target.includes("zod"))).toBe(true)
23+
expect(results.every((result) => result.score > 0.5)).toBe(true)
24+
25+
const results2 = await service.search("zod/index.d.ts")
26+
expect(results2.length).toBe(2)
27+
expect(results2.some((result) => result.target.includes("zod/index.d.ts"))).toBe(true)
28+
expect(results2.some((result) => result.target.includes("zod/lib/index.d.ts"))).toBe(true)
29+
})
30+
})
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// npx vitest src/services/glob/__tests__/list-files.spec.ts
2+
3+
import path from "path"
4+
import { describe, expect, test } from "vitest"
5+
6+
import { listFiles } from "../list-files"
7+
8+
describe("listFiles", () => {
9+
test("it lists files in a directory", async () => {
10+
const dirPath = path.join(process.cwd(), "node_modules")
11+
const [files, _] = await listFiles(dirPath, true, 100_000, { gitignore: false, ignore: undefined })
12+
expect(files.length).toBeGreaterThan(25_000)
13+
})
14+
})

src/services/glob/ignore.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export const IGNORE = [
2+
"node_modules",
3+
"__pycache__",
4+
"env",
5+
"venv",
6+
"target/dependency",
7+
"build/dependencies",
8+
"dist",
9+
"out",
10+
"bundle",
11+
"vendor",
12+
"tmp",
13+
"temp",
14+
"deps",
15+
"pkg",
16+
"Pods",
17+
// '!**/.*' excludes hidden directories, while '!**/.*/**' excludes only
18+
// their contents. This way we are at least aware of the existence of
19+
// hidden directories.
20+
".*",
21+
].map((dir) => `**/${dir}/**`)

src/services/glob/list-files.ts

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@ import { globby, Options } from "globby"
22
import os from "os"
33
import * as path from "path"
44
import { arePathsEqual } from "../../utils/path"
5+
import { IGNORE as dirsToIgnore } from "./ignore"
56

6-
export async function listFiles(dirPath: string, recursive: boolean, limit: number): Promise<[string[], boolean]> {
7+
export async function listFiles(
8+
dirPath: string,
9+
recursive: boolean,
10+
limit: number,
11+
globbyOptions: Options = {},
12+
): Promise<[string[], boolean]> {
713
const absolutePath = path.resolve(dirPath)
814
// Do not allow listing files in root or home directory, which cline tends to want to do when the user's prompt is vague.
915
const root = process.platform === "win32" ? path.parse(absolutePath).root : "/"
@@ -17,25 +23,6 @@ export async function listFiles(dirPath: string, recursive: boolean, limit: numb
1723
return [[homeDir], false]
1824
}
1925

20-
const dirsToIgnore = [
21-
"node_modules",
22-
"__pycache__",
23-
"env",
24-
"venv",
25-
"target/dependency",
26-
"build/dependencies",
27-
"dist",
28-
"out",
29-
"bundle",
30-
"vendor",
31-
"tmp",
32-
"temp",
33-
"deps",
34-
"pkg",
35-
"Pods",
36-
".*", // '!**/.*' excludes hidden directories, while '!**/.*/**' excludes only their contents. This way we are at least aware of the existence of hidden directories.
37-
].map((dir) => `**/${dir}/**`)
38-
3926
const options = {
4027
cwd: dirPath,
4128
dot: true, // do not ignore hidden files/directories
@@ -44,6 +31,7 @@ export async function listFiles(dirPath: string, recursive: boolean, limit: numb
4431
gitignore: recursive, // globby ignores any files that are gitignored
4532
ignore: recursive ? dirsToIgnore : undefined, // just in case there is no gitignore, we ignore sensible defaults
4633
onlyFiles: false, // true by default, false means it will list directories on their own too
34+
...globbyOptions,
4735
}
4836
// * globs all files in one dir, ** globs files in nested directories
4937
const files = recursive ? await globbyLevelByLevel(limit, options) : (await globby("*", options)).slice(0, limit)

vitest.config.mts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { defineConfig } from "vitest/config"
2+
3+
export default defineConfig({
4+
test: {
5+
include: ["src/**/*.spec.ts"],
6+
watch: false,
7+
},
8+
})

0 commit comments

Comments
 (0)