Skip to content

Commit f181da1

Browse files
authored
Move context-mention file/folder search to the server (#1824)
1 parent 499b8e4 commit f181da1

File tree

11 files changed

+362
-90
lines changed

11 files changed

+362
-90
lines changed

package-lock.json

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

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@
345345
"diff-match-patch": "^1.0.5",
346346
"fast-deep-equal": "^3.1.3",
347347
"fastest-levenshtein": "^1.0.16",
348+
"fzf": "^0.5.2",
348349
"get-folder-size": "^5.0.0",
349350
"globby": "^14.0.2",
350351
"i18next": "^24.2.2",

src/core/webview/ClineProvider.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { McpServerManager } from "../../services/mcp/McpServerManager"
3939
import { ShadowCheckpointService } from "../../services/checkpoints/ShadowCheckpointService"
4040
import { BrowserSession } from "../../services/browser/BrowserSession"
4141
import { discoverChromeInstances } from "../../services/browser/browserDiscovery"
42+
import { searchWorkspaceFiles } from "../../services/search/file-search"
4243
import { fileExistsAtPath } from "../../utils/fs"
4344
import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound"
4445
import { playTts, setTtsEnabled, setTtsSpeed, stopTts } from "../../utils/tts"
@@ -1750,6 +1751,46 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
17501751
}
17511752
break
17521753
}
1754+
case "searchFiles": {
1755+
const workspacePath = getWorkspacePath()
1756+
1757+
if (!workspacePath) {
1758+
// Handle case where workspace path is not available
1759+
await this.postMessageToWebview({
1760+
type: "fileSearchResults",
1761+
results: [],
1762+
requestId: message.requestId,
1763+
error: "No workspace path available",
1764+
})
1765+
break
1766+
}
1767+
try {
1768+
// Call file search service with query from message
1769+
const results = await searchWorkspaceFiles(
1770+
message.query || "",
1771+
workspacePath,
1772+
20, // Use default limit, as filtering is now done in the backend
1773+
)
1774+
1775+
// Send results back to webview
1776+
await this.postMessageToWebview({
1777+
type: "fileSearchResults",
1778+
results,
1779+
requestId: message.requestId,
1780+
})
1781+
} catch (error) {
1782+
const errorMessage = error instanceof Error ? error.message : String(error)
1783+
1784+
// Send error response to webview
1785+
await this.postMessageToWebview({
1786+
type: "fileSearchResults",
1787+
results: [],
1788+
error: errorMessage,
1789+
requestId: message.requestId,
1790+
})
1791+
}
1792+
break
1793+
}
17531794
case "saveApiConfiguration":
17541795
if (message.text && message.apiConfiguration) {
17551796
try {

src/services/ripgrep/index.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as path from "path"
44
import * as fs from "fs"
55
import * as readline from "readline"
66
import { RooIgnoreController } from "../../core/ignore/RooIgnoreController"
7+
import { fileExistsAtPath } from "../../utils/fs"
78
/*
89
This file provides functionality to perform regex searches on files using ripgrep.
910
Inspired by: https://github.com/DiscreteTom/vscode-ripgrep-utils
@@ -71,11 +72,13 @@ const MAX_LINE_LENGTH = 500
7172
export function truncateLine(line: string, maxLength: number = MAX_LINE_LENGTH): string {
7273
return line.length > maxLength ? line.substring(0, maxLength) + " [truncated...]" : line
7374
}
74-
75-
async function getBinPath(vscodeAppRoot: string): Promise<string | undefined> {
75+
/**
76+
* Get the path to the ripgrep binary within the VSCode installation
77+
*/
78+
export async function getBinPath(vscodeAppRoot: string): Promise<string | undefined> {
7679
const checkPath = async (pkgFolder: string) => {
7780
const fullPath = path.join(vscodeAppRoot, pkgFolder, binName)
78-
return (await pathExists(fullPath)) ? fullPath : undefined
81+
return (await fileExistsAtPath(fullPath)) ? fullPath : undefined
7982
}
8083

8184
return (
@@ -86,14 +89,6 @@ async function getBinPath(vscodeAppRoot: string): Promise<string | undefined> {
8689
)
8790
}
8891

89-
async function pathExists(path: string): Promise<boolean> {
90-
return new Promise((resolve) => {
91-
fs.access(path, (err) => {
92-
resolve(err === null)
93-
})
94-
})
95-
}
96-
9792
async function execRipgrep(bin: string, args: string[]): Promise<string> {
9893
return new Promise((resolve, reject) => {
9994
const rgProcess = childProcess.spawn(bin, args)

src/services/search/file-search.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import * as vscode from "vscode"
2+
import * as path from "path"
3+
import * as fs from "fs"
4+
import * as childProcess from "child_process"
5+
import * as readline from "readline"
6+
import { Fzf } from "fzf"
7+
import { getBinPath } from "../ripgrep"
8+
9+
async function executeRipgrepForFiles(
10+
rgPath: string,
11+
workspacePath: string,
12+
limit: number = 5000,
13+
): Promise<{ path: string; type: "file" | "folder"; label?: string }[]> {
14+
return new Promise((resolve, reject) => {
15+
const args = [
16+
"--files",
17+
"--follow",
18+
"-g",
19+
"!**/node_modules/**",
20+
"-g",
21+
"!**/.git/**",
22+
"-g",
23+
"!**/out/**",
24+
"-g",
25+
"!**/dist/**",
26+
workspacePath,
27+
]
28+
29+
const rgProcess = childProcess.spawn(rgPath, args)
30+
const rl = readline.createInterface({
31+
input: rgProcess.stdout,
32+
crlfDelay: Infinity,
33+
})
34+
35+
const results: { path: string; type: "file" | "folder"; label?: string }[] = []
36+
let count = 0
37+
38+
rl.on("line", (line) => {
39+
if (count < limit) {
40+
try {
41+
const relativePath = path.relative(workspacePath, line)
42+
results.push({
43+
path: relativePath,
44+
type: "file",
45+
label: path.basename(relativePath),
46+
})
47+
count++
48+
} catch (error) {
49+
// Silently ignore errors processing individual paths
50+
}
51+
} else {
52+
rl.close()
53+
rgProcess.kill()
54+
}
55+
})
56+
57+
let errorOutput = ""
58+
rgProcess.stderr.on("data", (data) => {
59+
errorOutput += data.toString()
60+
})
61+
62+
rl.on("close", () => {
63+
if (errorOutput && results.length === 0) {
64+
reject(new Error(`ripgrep process error: ${errorOutput}`))
65+
} else {
66+
resolve(results)
67+
}
68+
})
69+
70+
rgProcess.on("error", (error) => {
71+
reject(new Error(`ripgrep process error: ${error.message}`))
72+
})
73+
})
74+
}
75+
76+
export async function searchWorkspaceFiles(
77+
query: string,
78+
workspacePath: string,
79+
limit: number = 20,
80+
): Promise<{ path: string; type: "file" | "folder"; label?: string }[]> {
81+
try {
82+
const vscodeAppRoot = vscode.env.appRoot
83+
const rgPath = await getBinPath(vscodeAppRoot)
84+
85+
if (!rgPath) {
86+
throw new Error("Could not find ripgrep binary")
87+
}
88+
89+
const allFiles = await executeRipgrepForFiles(rgPath, workspacePath, 5000)
90+
91+
if (!query.trim()) {
92+
return allFiles.slice(0, limit)
93+
}
94+
95+
const searchItems = allFiles.map((file) => ({
96+
original: file,
97+
searchStr: `${file.path} ${file.label || ""}`,
98+
}))
99+
100+
const fzf = new Fzf(searchItems, {
101+
selector: (item) => item.searchStr,
102+
})
103+
104+
const results = fzf
105+
.find(query)
106+
.slice(0, limit)
107+
.map((result) => result.item.original)
108+
109+
const resultsWithDirectoryCheck = await Promise.all(
110+
results.map(async (result) => {
111+
const fullPath = path.join(workspacePath, result.path)
112+
const isDirectory = fs.existsSync(fullPath) && fs.lstatSync(fullPath).isDirectory()
113+
114+
return {
115+
...result,
116+
type: isDirectory ? ("folder" as const) : ("file" as const),
117+
}
118+
}),
119+
)
120+
121+
return resultsWithDirectoryCheck
122+
} catch (error) {
123+
return []
124+
}
125+
}

src/shared/ExtensionMessage.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export interface ExtensionMessage {
5656
| "remoteBrowserEnabled"
5757
| "ttsStart"
5858
| "ttsStop"
59+
| "fileSearchResults"
5960
text?: string
6061
action?:
6162
| "chatButtonClicked"
@@ -92,6 +93,12 @@ export interface ExtensionMessage {
9293
values?: Record<string, any>
9394
requestId?: string
9495
promptText?: string
96+
results?: Array<{
97+
path: string
98+
type: "file" | "folder"
99+
label?: string
100+
}>
101+
error?: string
95102
}
96103

97104
export interface ApiConfigMeta {

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export interface WebviewMessage {
114114
| "browserConnectionResult"
115115
| "remoteBrowserEnabled"
116116
| "language"
117+
| "searchFiles"
117118
text?: string
118119
disabled?: boolean
119120
askResponse?: ClineAskResponse

0 commit comments

Comments
 (0)