@comfanion/usethis_search 3.0.0-dev.0 → 3.0.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/file-indexer.ts +13 -0
- package/index.ts +5 -1
- package/package.json +3 -1
- package/tools/codeindex.ts +155 -6
- package/tools/read-interceptor.ts +78 -5
- package/vectorizer/analyzers/lsp-analyzer.ts +225 -94
- package/vectorizer/analyzers/lsp-client.ts +369 -0
- package/vectorizer/graph-builder.ts +106 -3
- package/vectorizer/graph-db.ts +192 -0
- package/vectorizer/index.js +93 -9
- package/vectorizer/usage-tracker.ts +204 -0
|
@@ -1,4 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSPAnalyzer — extracts code relations via Language Server Protocol.
|
|
3
|
+
*
|
|
4
|
+
* Uses LSPClient (lsp-client.ts) to communicate with real language servers
|
|
5
|
+
* (typescript-language-server, pylsp, gopls, etc.).
|
|
6
|
+
*
|
|
7
|
+
* Strategy: LSP-first with graceful degradation.
|
|
8
|
+
* - If LSP server binary not found → isAvailable() returns false → GraphBuilder uses regex fallback
|
|
9
|
+
* - If LSP server crashes or times out during analysis → analyzeFile() returns [] → GraphBuilder uses regex
|
|
10
|
+
* - Timeout: 5 seconds per file (configurable)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import path from "path"
|
|
14
|
+
import fs from "fs/promises"
|
|
1
15
|
import { ChunkWithId } from "../graph-builder"
|
|
16
|
+
import { LSPClient, LSPSymbolInformation, SymbolKind } from "./lsp-client"
|
|
2
17
|
|
|
3
18
|
export interface Relation {
|
|
4
19
|
from: string
|
|
@@ -9,134 +24,248 @@ export interface Relation {
|
|
|
9
24
|
line?: number
|
|
10
25
|
}
|
|
11
26
|
|
|
27
|
+
/** Language ID mapping from file extension. */
|
|
28
|
+
const EXT_TO_LANGUAGE: Record<string, string> = {
|
|
29
|
+
ts: "typescript",
|
|
30
|
+
js: "javascript",
|
|
31
|
+
tsx: "typescriptreact",
|
|
32
|
+
jsx: "javascriptreact",
|
|
33
|
+
py: "python",
|
|
34
|
+
go: "go",
|
|
35
|
+
rs: "rust",
|
|
36
|
+
java: "java",
|
|
37
|
+
cpp: "cpp",
|
|
38
|
+
c: "c",
|
|
39
|
+
cs: "csharp",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Symbol kinds that represent types (classes, interfaces) — check implementations. */
|
|
43
|
+
const TYPE_SYMBOL_KINDS = new Set([
|
|
44
|
+
SymbolKind.Class,
|
|
45
|
+
SymbolKind.Interface,
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
/** Symbol kinds worth analyzing for references/definitions. */
|
|
49
|
+
const ANALYZABLE_SYMBOL_KINDS = new Set([
|
|
50
|
+
SymbolKind.Class,
|
|
51
|
+
SymbolKind.Interface,
|
|
52
|
+
SymbolKind.Function,
|
|
53
|
+
SymbolKind.Method,
|
|
54
|
+
SymbolKind.Variable,
|
|
55
|
+
SymbolKind.Constant,
|
|
56
|
+
SymbolKind.Enum,
|
|
57
|
+
])
|
|
58
|
+
|
|
12
59
|
export class LSPAnalyzer {
|
|
13
|
-
private
|
|
60
|
+
private client: LSPClient | null = null
|
|
61
|
+
private currentLanguage: string | null = null
|
|
62
|
+
private filesOpened = 0
|
|
63
|
+
private readonly timeoutMs: number
|
|
64
|
+
|
|
65
|
+
constructor(
|
|
66
|
+
private projectRoot?: string,
|
|
67
|
+
timeoutMs?: number,
|
|
68
|
+
) {
|
|
69
|
+
this.timeoutMs = timeoutMs ?? 5000
|
|
70
|
+
}
|
|
14
71
|
|
|
72
|
+
// ---- public API ---------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
/** Check if an LSP server is available for this file's language. */
|
|
15
75
|
async isAvailable(filePath: string): Promise<boolean> {
|
|
16
76
|
try {
|
|
17
|
-
const
|
|
18
|
-
if (!ext) return false
|
|
19
|
-
|
|
20
|
-
const language = this.getLanguage(ext)
|
|
77
|
+
const language = this.getLanguage(filePath)
|
|
21
78
|
if (!language) return false
|
|
22
|
-
|
|
23
|
-
return this.checkLSPServer(language)
|
|
79
|
+
return LSPClient.isAvailable(language)
|
|
24
80
|
} catch {
|
|
25
81
|
return false
|
|
26
82
|
}
|
|
27
83
|
}
|
|
28
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Analyze a file using LSP and return relations.
|
|
87
|
+
*
|
|
88
|
+
* Flow:
|
|
89
|
+
* 1. Start/reuse server for the file's language
|
|
90
|
+
* 2. Open document
|
|
91
|
+
* 3. Get document symbols (functions, classes, interfaces)
|
|
92
|
+
* 4. For each symbol: get definitions, references, implementations
|
|
93
|
+
* 5. Map LSP locations → chunk IDs → Relation objects
|
|
94
|
+
* 6. Close document
|
|
95
|
+
*/
|
|
29
96
|
async analyzeFile(filePath: string, chunks: ChunkWithId[]): Promise<Relation[]> {
|
|
97
|
+
const language = this.getLanguage(filePath)
|
|
98
|
+
if (!language) return []
|
|
99
|
+
|
|
100
|
+
const root = this.projectRoot || process.cwd()
|
|
101
|
+
const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(root, filePath)
|
|
102
|
+
|
|
103
|
+
let content: string
|
|
104
|
+
try {
|
|
105
|
+
content = await fs.readFile(absPath, "utf-8")
|
|
106
|
+
} catch {
|
|
107
|
+
return []
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Ensure client is started for this language
|
|
111
|
+
try {
|
|
112
|
+
await this.ensureClient(language, root)
|
|
113
|
+
} catch {
|
|
114
|
+
return []
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const client = this.client!
|
|
30
118
|
const relations: Relation[] = []
|
|
31
119
|
|
|
32
120
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
121
|
+
// Open the document
|
|
122
|
+
await client.openDocument(filePath, content, language)
|
|
123
|
+
this.filesOpened++
|
|
35
124
|
|
|
36
|
-
|
|
37
|
-
if (
|
|
125
|
+
// First file needs extra warmup for project loading / type graph build
|
|
126
|
+
if (this.filesOpened === 1) {
|
|
127
|
+
await new Promise(r => setTimeout(r, 2000))
|
|
128
|
+
} else {
|
|
129
|
+
await new Promise(r => setTimeout(r, 500))
|
|
130
|
+
}
|
|
38
131
|
|
|
39
|
-
|
|
132
|
+
// Get all symbols in the file
|
|
133
|
+
const rawSymbols = await client.documentSymbol(filePath)
|
|
134
|
+
const symbols = this.flattenSymbols(rawSymbols)
|
|
40
135
|
|
|
41
|
-
|
|
42
|
-
|
|
136
|
+
// Analyze each symbol
|
|
137
|
+
for (const sym of symbols) {
|
|
138
|
+
if (!ANALYZABLE_SYMBOL_KINDS.has(sym.kind)) continue
|
|
43
139
|
|
|
44
|
-
|
|
45
|
-
const fromChunkId = this.findChunkForPosition(chunks, symbol.line)
|
|
140
|
+
const fromChunkId = this.findChunkForPosition(chunks, sym.selectionRange.start.line)
|
|
46
141
|
if (!fromChunkId) continue
|
|
47
142
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
143
|
+
const selLine = sym.selectionRange.start.line
|
|
144
|
+
const selChar = sym.selectionRange.start.character
|
|
145
|
+
|
|
146
|
+
// --- implementations (for classes/interfaces) ---
|
|
147
|
+
if (TYPE_SYMBOL_KINDS.has(sym.kind)) {
|
|
148
|
+
try {
|
|
149
|
+
const impls = await client.implementation(filePath, selLine, selChar)
|
|
150
|
+
for (const impl of impls) {
|
|
151
|
+
const toChunkId = this.locationToChunkId(filePath, impl.uri, impl.range.start.line, root)
|
|
152
|
+
if (toChunkId && toChunkId !== fromChunkId) {
|
|
153
|
+
relations.push({
|
|
154
|
+
from: fromChunkId,
|
|
155
|
+
to: toChunkId,
|
|
156
|
+
predicate: "implements",
|
|
157
|
+
weight: 1.0,
|
|
158
|
+
source: "lsp",
|
|
159
|
+
line: selLine,
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch { /* server may not support implementation */ }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// --- references ---
|
|
167
|
+
try {
|
|
168
|
+
const refs = await client.references(filePath, selLine, selChar)
|
|
169
|
+
for (const ref of refs) {
|
|
170
|
+
const toChunkId = this.locationToChunkId(filePath, ref.uri, ref.range.start.line, root)
|
|
171
|
+
if (toChunkId && toChunkId !== fromChunkId) {
|
|
53
172
|
relations.push({
|
|
54
|
-
from:
|
|
55
|
-
to:
|
|
56
|
-
predicate: "
|
|
173
|
+
from: toChunkId,
|
|
174
|
+
to: fromChunkId,
|
|
175
|
+
predicate: "used_by",
|
|
57
176
|
weight: 1.0,
|
|
58
|
-
source: "lsp"
|
|
177
|
+
source: "lsp",
|
|
178
|
+
line: ref.range.start.line,
|
|
59
179
|
})
|
|
60
180
|
}
|
|
61
181
|
}
|
|
62
|
-
}
|
|
182
|
+
} catch { /* server may not support references */ }
|
|
63
183
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
for (const def of definitions) {
|
|
80
|
-
const toChunkId = this.resolveTargetChunk(filePath, def)
|
|
81
|
-
if (toChunkId && toChunkId !== fromChunkId) {
|
|
82
|
-
relations.push({
|
|
83
|
-
from: fromChunkId,
|
|
84
|
-
to: toChunkId,
|
|
85
|
-
predicate: "references",
|
|
86
|
-
weight: 1.0,
|
|
87
|
-
source: "lsp"
|
|
88
|
-
})
|
|
184
|
+
// --- definitions ---
|
|
185
|
+
try {
|
|
186
|
+
const defs = await client.definition(filePath, selLine, selChar)
|
|
187
|
+
for (const def of defs) {
|
|
188
|
+
const toChunkId = this.locationToChunkId(filePath, def.uri, def.range.start.line, root)
|
|
189
|
+
if (toChunkId && toChunkId !== fromChunkId) {
|
|
190
|
+
relations.push({
|
|
191
|
+
from: fromChunkId,
|
|
192
|
+
to: toChunkId,
|
|
193
|
+
predicate: "references",
|
|
194
|
+
weight: 1.0,
|
|
195
|
+
source: "lsp",
|
|
196
|
+
line: selLine,
|
|
197
|
+
})
|
|
198
|
+
}
|
|
89
199
|
}
|
|
90
|
-
}
|
|
200
|
+
} catch { /* server may not support definition */ }
|
|
91
201
|
}
|
|
92
|
-
|
|
93
|
-
|
|
202
|
+
|
|
203
|
+
await client.closeDocument(filePath)
|
|
204
|
+
} catch {
|
|
205
|
+
// Any error — return whatever we collected so far (or empty)
|
|
94
206
|
}
|
|
95
207
|
|
|
96
|
-
return relations
|
|
208
|
+
return this.deduplicateRelations(relations)
|
|
97
209
|
}
|
|
98
210
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
go: "go",
|
|
107
|
-
rs: "rust",
|
|
108
|
-
java: "java",
|
|
109
|
-
cpp: "cpp",
|
|
110
|
-
c: "c",
|
|
111
|
-
cs: "csharp"
|
|
211
|
+
/** Stop the LSP server. Call when done indexing. */
|
|
212
|
+
async shutdown(): Promise<void> {
|
|
213
|
+
if (this.client) {
|
|
214
|
+
try { await this.client.stop() } catch { /* best effort */ }
|
|
215
|
+
this.client = null
|
|
216
|
+
this.currentLanguage = null
|
|
217
|
+
this.filesOpened = 0
|
|
112
218
|
}
|
|
113
|
-
return map[ext] || null
|
|
114
219
|
}
|
|
115
220
|
|
|
116
|
-
|
|
117
|
-
return Promise.resolve(false)
|
|
118
|
-
}
|
|
221
|
+
// ---- internals ----------------------------------------------------------
|
|
119
222
|
|
|
120
|
-
private
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
return
|
|
223
|
+
private getLanguage(filePath: string): string | null {
|
|
224
|
+
const ext = filePath.split(".").pop()
|
|
225
|
+
if (!ext) return null
|
|
226
|
+
return EXT_TO_LANGUAGE[ext] || null
|
|
124
227
|
}
|
|
125
228
|
|
|
126
|
-
|
|
127
|
-
|
|
229
|
+
/** Start or reuse a client for the given language. */
|
|
230
|
+
private async ensureClient(language: string, root: string): Promise<void> {
|
|
231
|
+
// If we already have a client for a different language, shut it down
|
|
232
|
+
if (this.client && this.currentLanguage !== language) {
|
|
233
|
+
await this.shutdown()
|
|
234
|
+
}
|
|
235
|
+
if (!this.client) {
|
|
236
|
+
this.client = new LSPClient(root, this.timeoutMs)
|
|
237
|
+
await this.client.start(language)
|
|
238
|
+
this.currentLanguage = language
|
|
239
|
+
}
|
|
128
240
|
}
|
|
129
241
|
|
|
130
|
-
|
|
131
|
-
|
|
242
|
+
/** Flatten hierarchical DocumentSymbol tree into flat array. */
|
|
243
|
+
private flattenSymbols(symbols: LSPSymbolInformation[]): LSPSymbolInformation[] {
|
|
244
|
+
const result: LSPSymbolInformation[] = []
|
|
245
|
+
const walk = (list: LSPSymbolInformation[]) => {
|
|
246
|
+
for (const sym of list) {
|
|
247
|
+
result.push(sym)
|
|
248
|
+
if (sym.children?.length) walk(sym.children)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
walk(symbols)
|
|
252
|
+
return result
|
|
132
253
|
}
|
|
133
254
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
255
|
+
/** Convert LSP location URI + line → chunk_id. */
|
|
256
|
+
private locationToChunkId(currentFile: string, uri: string, line: number, root: string): string | null {
|
|
257
|
+
// uri = file:///absolute/path/to/file.ts
|
|
258
|
+
const filePath = uri.startsWith("file://") ? uri.slice(7) : uri
|
|
259
|
+
const relPath = path.relative(root, filePath)
|
|
260
|
+
|
|
261
|
+
// Skip external files (node_modules, etc.)
|
|
262
|
+
if (relPath.startsWith("..") || relPath.includes("node_modules")) return null
|
|
137
263
|
|
|
138
|
-
|
|
139
|
-
|
|
264
|
+
const withoutExt = relPath.replace(/\.[^/.]+$/, "")
|
|
265
|
+
const normalized = withoutExt.replace(/[^a-zA-Z0-9]/g, "_")
|
|
266
|
+
// For cross-file references, point to chunk 0 (first chunk of target file)
|
|
267
|
+
// For same-file, we could be more precise but chunk 0 is sufficient for graph
|
|
268
|
+
return `chunk_${normalized}_0`
|
|
140
269
|
}
|
|
141
270
|
|
|
142
271
|
private findChunkForPosition(chunks: ChunkWithId[], line: number): string | null {
|
|
@@ -147,16 +276,18 @@ export class LSPAnalyzer {
|
|
|
147
276
|
}
|
|
148
277
|
}
|
|
149
278
|
}
|
|
150
|
-
return
|
|
279
|
+
// Fallback: if no chunk matches by line, return first chunk
|
|
280
|
+
return chunks.length > 0 ? chunks[0].chunk_id : null
|
|
151
281
|
}
|
|
152
282
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
283
|
+
/** Remove duplicate relations (same from+to+predicate). */
|
|
284
|
+
private deduplicateRelations(relations: Relation[]): Relation[] {
|
|
285
|
+
const seen = new Set<string>()
|
|
286
|
+
return relations.filter(r => {
|
|
287
|
+
const key = `${r.from}|${r.to}|${r.predicate}`
|
|
288
|
+
if (seen.has(key)) return false
|
|
289
|
+
seen.add(key)
|
|
290
|
+
return true
|
|
291
|
+
})
|
|
161
292
|
}
|
|
162
293
|
}
|