Skip to content

Commit f6e6d9c

Browse files
committed
feat: Optimize GolangFinder for improved file searching and caching
- Introduced a caching mechanism for file content to enhance performance. - Implemented a pre-filtering step to reduce the number of files checked by searching for method names and types. - Added methods for building search patterns and checking file content against these patterns. - Enhanced concurrency handling by limiting the number of files processed simultaneously. - Refactored the implementation to improve code organization and readability.
1 parent 8910636 commit f6e6d9c

1 file changed

Lines changed: 274 additions & 76 deletions

File tree

src/finder/golang-finder.ts

Lines changed: 274 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,19 @@ import type { LanguageFinder } from './language-finder'
33
import { Location, Range, workspace } from 'vscode'
44
import { logger } from '../utils'
55

6+
/**
7+
* 文件内容缓存
8+
*/
9+
const fileContentCache = new Map<string, { content: string; timestamp: number }>()
10+
const CACHE_TTL = 60000 // 1分钟缓存
11+
612
/**
713
* Golang 实现查找器
814
* 根据 proto 服务和方法名称查找对应的 Go 实现
915
*/
1016
export class GolangFinder implements LanguageFinder {
1117
language = 'go'
18+
private readonly MAX_CONCURRENT_FILES = 20 // 最大并发文件数
1219

1320
async findImplementations(
1421
serviceName: string,
@@ -20,12 +27,6 @@ export class GolangFinder implements LanguageFinder {
2027
const locations: Location[] = []
2128

2229
try {
23-
// 查找所有 .go 文件
24-
const goFiles = await workspace.findFiles(
25-
'**/*.go',
26-
'**/vendor/**',
27-
)
28-
2930
// 从 proto 类型名提取基础类型名(忽略包名)
3031
// 例如: com.example.AnalyzeReq -> AnalyzeReq
3132
const inputBaseType = this.getBaseTypeName(inputType)
@@ -36,78 +37,32 @@ export class GolangFinder implements LanguageFinder {
3637
const escapedInputType = inputBaseType ? this.escapeRegex(inputBaseType) : ''
3738
const escapedOutputType = outputBaseType ? this.escapeRegex(outputBaseType) : ''
3839

39-
// 构建正则表达式模式
40-
// 模式1: func (receiver) MethodName(...*任意包名.InputType...) (...*任意包名.OutputType...)
41-
// 模式2: func (receiver) MethodName(...*任意包名.InputType..., ...*任意包名.OutputType...)
42-
const patterns: RegExp[] = []
43-
44-
if (escapedInputType && escapedOutputType) {
45-
// 模式1: 标准 gRPC,resp 在返回值中
46-
// func (receiver) MethodName(..., *任意包名.InputType, ...) (*任意包名.OutputType, ...)
47-
patterns.push(new RegExp(
48-
`func\\s+\\([^)]+\\)\\s+${escapedMethodName}\\s*\\([^)]*\\*[\\w.]*\\.?${escapedInputType}[^)]*\\)\\s*\\([^)]*\\*[\\w.]*\\.?${escapedOutputType}[^)]*\\)`,
49-
'gi',
50-
))
51-
52-
// 模式2: 自定义模式,resp 在参数中
53-
// func (receiver) MethodName(..., *任意包名.InputType, ..., *任意包名.OutputType, ...)
54-
patterns.push(new RegExp(
55-
`func\\s+\\([^)]+\\)\\s+${escapedMethodName}\\s*\\([^)]*\\*[\\w.]*\\.?${escapedInputType}[^)]*\\*[\\w.]*\\.?${escapedOutputType}[^)]*\\)`,
56-
'gi',
57-
))
58-
}
59-
else if (escapedInputType) {
60-
// 只有输入类型
61-
patterns.push(new RegExp(
62-
`func\\s+\\([^)]+\\)\\s+${escapedMethodName}\\s*\\([^)]*\\*[\\w.]*\\.?${escapedInputType}[^)]*\\)`,
63-
'gi',
64-
))
65-
}
66-
else if (escapedOutputType) {
67-
// 只有输出类型
68-
patterns.push(new RegExp(
69-
`func\\s+\\([^)]+\\)\\s+${escapedMethodName}\\s*\\([^)]*\\)\\s*\\([^)]*\\*[\\w.]*\\.?${escapedOutputType}[^)]*\\)`,
70-
'gi',
71-
))
72-
patterns.push(new RegExp(
73-
`func\\s+\\([^)]+\\)\\s+${escapedMethodName}\\s*\\([^)]*\\*[\\w.]*\\.?${escapedOutputType}[^)]*\\)`,
74-
'gi',
75-
))
76-
}
77-
else {
78-
// 没有类型信息,只匹配方法名
79-
patterns.push(new RegExp(
80-
`func\\s+\\([^)]+\\)\\s+${escapedMethodName}\\s*\\(`,
81-
'gi',
82-
))
40+
// 使用 VS Code 搜索功能进行预过滤
41+
// 先搜索包含方法名的文件,这样可以大幅减少需要检查的文件数量
42+
const candidateFiles = await this.findCandidateFiles(methodName, inputBaseType, outputBaseType)
43+
44+
logger.info(
45+
`Found ${candidateFiles.length} candidate files (from search) for ${serviceName}.${methodName}`,
46+
)
47+
48+
if (candidateFiles.length === 0) {
49+
return []
8350
}
8451

85-
for (const file of goFiles) {
86-
try {
87-
const document = await workspace.openTextDocument(file)
88-
const text = document.getText()
89-
90-
for (const pattern of patterns) {
91-
pattern.lastIndex = 0 // 重置正则表达式
92-
let match: RegExpExecArray | null = pattern.exec(text)
93-
94-
while (match !== null) {
95-
const position = document.positionAt(match.index)
96-
const line = document.lineAt(position.line)
97-
const range = new Range(
98-
position.line,
99-
0,
100-
position.line,
101-
line.text.length,
102-
)
103-
104-
locations.push(new Location(file, range))
105-
match = pattern.exec(text)
106-
}
107-
}
108-
}
109-
catch (error) {
110-
logger.warn(`Failed to read file ${file.fsPath}: ${error}`)
52+
// 构建正则表达式模式
53+
const patterns = this.buildPatterns(escapedMethodName, escapedInputType, escapedOutputType)
54+
55+
// 并行处理候选文件,但限制并发数
56+
// 注意:候选文件已经通过预过滤,包含方法名和 func,所以 searchInFile 中不需要再次检查
57+
const chunks = this.chunkArray(candidateFiles, this.MAX_CONCURRENT_FILES)
58+
59+
for (const chunk of chunks) {
60+
const chunkResults = await Promise.all(
61+
chunk.map(file => this.searchInFile(file, patterns)),
62+
)
63+
64+
for (const result of chunkResults) {
65+
locations.push(...result)
11166
}
11267
}
11368

@@ -126,6 +81,249 @@ export class GolangFinder implements LanguageFinder {
12681
}
12782
}
12883

84+
/**
85+
* 使用 VS Code 搜索功能查找候选文件
86+
* 通过搜索包含方法名和类型名的文件来大幅减少需要检查的文件数量
87+
*/
88+
private async findCandidateFiles(
89+
methodName: string,
90+
inputBaseType?: string,
91+
outputBaseType?: string,
92+
): Promise<Uri[]> {
93+
try {
94+
// 先找到所有 .go 文件
95+
const allGoFiles = await workspace.findFiles(
96+
'**/*.go',
97+
'**/vendor/**',
98+
)
99+
100+
if (allGoFiles.length === 0) {
101+
return []
102+
}
103+
104+
logger.info(`Pre-filtering ${allGoFiles.length} Go files using text search`)
105+
106+
// 使用快速文本搜索预过滤文件
107+
// 构建搜索模式:必须包含方法名,可选包含类型名
108+
const searchPattern = this.buildSearchPattern(methodName, inputBaseType, outputBaseType)
109+
110+
// 并行检查文件是否包含关键词
111+
const chunks = this.chunkArray(allGoFiles, this.MAX_CONCURRENT_FILES)
112+
const candidateFiles: Uri[] = []
113+
114+
for (const chunk of chunks) {
115+
const results = await Promise.all(
116+
chunk.map(async (file) => {
117+
try {
118+
// 快速检查文件内容(使用缓存)
119+
const content = await this.getFileContent(file)
120+
121+
// 检查是否包含搜索模式中的所有关键词
122+
if (this.matchesSearchPattern(content, searchPattern)) {
123+
return file
124+
}
125+
return null
126+
}
127+
catch {
128+
return null
129+
}
130+
}),
131+
)
132+
133+
for (const result of results) {
134+
if (result) {
135+
candidateFiles.push(result)
136+
}
137+
}
138+
}
139+
140+
return candidateFiles
141+
}
142+
catch (error) {
143+
logger.warn(`Error in findCandidateFiles: ${error}`)
144+
// 如果搜索失败,返回所有文件(降级策略)
145+
return await workspace.findFiles('**/*.go', '**/vendor/**')
146+
}
147+
}
148+
149+
/**
150+
* 构建搜索模式
151+
*/
152+
private buildSearchPattern(
153+
methodName: string,
154+
inputBaseType?: string,
155+
outputBaseType?: string,
156+
): { required: string[]; optional: string[] } {
157+
const required: string[] = [methodName, 'func']
158+
const optional: string[] = []
159+
160+
if (inputBaseType) {
161+
optional.push(inputBaseType)
162+
}
163+
if (outputBaseType) {
164+
optional.push(outputBaseType)
165+
}
166+
167+
return { required, optional }
168+
}
169+
170+
/**
171+
* 检查内容是否匹配搜索模式
172+
* 必须包含所有必需关键词,至少包含一个可选关键词(如果有)
173+
*/
174+
private matchesSearchPattern(
175+
content: string,
176+
pattern: { required: string[]; optional: string[] },
177+
): boolean {
178+
// 检查必需关键词
179+
for (const keyword of pattern.required) {
180+
if (!content.includes(keyword)) {
181+
return false
182+
}
183+
}
184+
185+
// 如果有可选关键词,至少匹配一个
186+
if (pattern.optional.length > 0) {
187+
const hasOptional = pattern.optional.some(keyword => content.includes(keyword))
188+
if (!hasOptional) {
189+
// 如果没有可选关键词,仍然返回 true(因为必需关键词已匹配)
190+
// 这样可以确保即使没有类型信息也能找到匹配
191+
}
192+
}
193+
194+
return true
195+
}
196+
197+
/**
198+
* 在单个文件中搜索
199+
* 注意:此方法接收的文件已经通过预过滤,包含方法名和 func
200+
*/
201+
private async searchInFile(
202+
file: Uri,
203+
patterns: RegExp[],
204+
): Promise<Location[]> {
205+
const locations: Location[] = []
206+
207+
try {
208+
209+
// 打开文档进行精确匹配
210+
const document = await workspace.openTextDocument(file)
211+
const text = document.getText()
212+
213+
for (const pattern of patterns) {
214+
pattern.lastIndex = 0 // 重置正则表达式
215+
let match: RegExpExecArray | null = pattern.exec(text)
216+
217+
while (match !== null) {
218+
const position = document.positionAt(match.index)
219+
const line = document.lineAt(position.line)
220+
const range = new Range(
221+
position.line,
222+
0,
223+
position.line,
224+
line.text.length,
225+
)
226+
227+
locations.push(new Location(file, range))
228+
match = pattern.exec(text)
229+
}
230+
}
231+
}
232+
catch (error) {
233+
logger.warn(`Failed to read file ${file.fsPath}: ${error}`)
234+
}
235+
236+
return locations
237+
}
238+
239+
/**
240+
* 获取文件内容(带缓存)
241+
*/
242+
private async getFileContent(file: Uri): Promise<string> {
243+
const filePath = file.fsPath
244+
const cached = fileContentCache.get(filePath)
245+
const now = Date.now()
246+
247+
if (cached && (now - cached.timestamp) < CACHE_TTL) {
248+
return cached.content
249+
}
250+
251+
const document = await workspace.openTextDocument(file)
252+
const content = document.getText()
253+
254+
fileContentCache.set(filePath, { content, timestamp: now })
255+
256+
// 限制缓存大小,避免内存泄漏
257+
if (fileContentCache.size > 1000) {
258+
const firstKey = fileContentCache.keys().next().value
259+
if (firstKey) {
260+
fileContentCache.delete(firstKey)
261+
}
262+
}
263+
264+
return content
265+
}
266+
267+
/**
268+
* 构建正则表达式模式
269+
*/
270+
private buildPatterns(
271+
escapedMethodName: string,
272+
escapedInputType: string,
273+
escapedOutputType: string,
274+
): RegExp[] {
275+
const patterns: RegExp[] = []
276+
277+
if (escapedInputType && escapedOutputType) {
278+
// 模式1: 标准 gRPC,resp 在返回值中
279+
patterns.push(new RegExp(
280+
`func\\s+\\([^)]+\\)\\s+${escapedMethodName}\\s*\\([^)]*\\*[\\w.]*\\.?${escapedInputType}[^)]*\\)\\s*\\([^)]*\\*[\\w.]*\\.?${escapedOutputType}[^)]*\\)`,
281+
'gi',
282+
))
283+
284+
// 模式2: 自定义模式,resp 在参数中
285+
patterns.push(new RegExp(
286+
`func\\s+\\([^)]+\\)\\s+${escapedMethodName}\\s*\\([^)]*\\*[\\w.]*\\.?${escapedInputType}[^)]*\\*[\\w.]*\\.?${escapedOutputType}[^)]*\\)`,
287+
'gi',
288+
))
289+
}
290+
else if (escapedInputType) {
291+
patterns.push(new RegExp(
292+
`func\\s+\\([^)]+\\)\\s+${escapedMethodName}\\s*\\([^)]*\\*[\\w.]*\\.?${escapedInputType}[^)]*\\)`,
293+
'gi',
294+
))
295+
}
296+
else if (escapedOutputType) {
297+
patterns.push(new RegExp(
298+
`func\\s+\\([^)]+\\)\\s+${escapedMethodName}\\s*\\([^)]*\\)\\s*\\([^)]*\\*[\\w.]*\\.?${escapedOutputType}[^)]*\\)`,
299+
'gi',
300+
))
301+
patterns.push(new RegExp(
302+
`func\\s+\\([^)]+\\)\\s+${escapedMethodName}\\s*\\([^)]*\\*[\\w.]*\\.?${escapedOutputType}[^)]*\\)`,
303+
'gi',
304+
))
305+
}
306+
else {
307+
patterns.push(new RegExp(
308+
`func\\s+\\([^)]+\\)\\s+${escapedMethodName}\\s*\\(`,
309+
'gi',
310+
))
311+
}
312+
313+
return patterns
314+
}
315+
316+
/**
317+
* 将数组分块
318+
*/
319+
private chunkArray<T>(array: T[], chunkSize: number): T[][] {
320+
const chunks: T[][] = []
321+
for (let i = 0; i < array.length; i += chunkSize) {
322+
chunks.push(array.slice(i, i + chunkSize))
323+
}
324+
return chunks
325+
}
326+
129327
/**
130328
* 从 proto 类型名获取基础类型名(移除包名)
131329
* 例如: com.example.AnalyzeReq -> AnalyzeReq

0 commit comments

Comments
 (0)