xq-lsp 1.8.0 → 1.8.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/CHANGELOG.md +7 -0
- package/dist/call-hierarchy.d.ts +2 -2
- package/dist/call-hierarchy.js +26 -11
- package/dist/code-lens.d.ts +2 -2
- package/dist/code-lens.js +2 -2
- package/dist/references.d.ts +13 -3
- package/dist/references.js +24 -14
- package/dist/server.js +66 -34
- package/package.json +1 -1
- package/src/call-hierarchy.ts +23 -10
- package/src/code-lens.ts +9 -3
- package/src/references.ts +32 -9
- package/src/server.ts +69 -48
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.8.1](https://github.com/DrRataplan/xq-lsp/compare/lsp-v1.8.0...lsp-v1.8.1) (2026-07-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* stop retaining full ASTs for every glob-matched file to prevent OOM ([b44460f](https://github.com/DrRataplan/xq-lsp/commit/b44460f1de449b48903ebddff4f56b7524b99234))
|
|
9
|
+
|
|
3
10
|
## [1.8.0](https://github.com/DrRataplan/xq-lsp/compare/lsp-v1.7.0...lsp-v1.8.0) (2026-07-28)
|
|
4
11
|
|
|
5
12
|
|
package/dist/call-hierarchy.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CallHierarchyItem, CallHierarchyIncomingCall, CallHierarchyOutgoingCall } from "vscode-languageserver/node.js";
|
|
2
2
|
import type { FileAnalysis } from "./types.ts";
|
|
3
|
-
import type { FileRecord } from "./references.ts";
|
|
3
|
+
import type { FileRecord, LoadAst } from "./references.ts";
|
|
4
4
|
/**
|
|
5
5
|
* Resolve the function under the cursor (declaration name or call-site name) to a
|
|
6
6
|
* `CallHierarchyItem[]`, the same way `getDefinition` resolves go-to-definition targets.
|
|
@@ -17,4 +17,4 @@ export declare function getOutgoingCalls(item: CallHierarchyItem, text: string,
|
|
|
17
17
|
* namespace + local name + arity), grouped by each call site's enclosing
|
|
18
18
|
* `declare function` — found by walking up the AST from the call-site offset.
|
|
19
19
|
*/
|
|
20
|
-
export declare function getIncomingCalls(item: CallHierarchyItem, itemText: string, itemAnalysis: FileAnalysis, getOtherFiles: () => FileRecord[]): CallHierarchyIncomingCall[];
|
|
20
|
+
export declare function getIncomingCalls(item: CallHierarchyItem, itemText: string, itemAnalysis: FileAnalysis, getOtherFiles: () => FileRecord[], loadAst: LoadAst): CallHierarchyIncomingCall[];
|
package/dist/call-hierarchy.js
CHANGED
|
@@ -143,34 +143,49 @@ export function getOutgoingCalls(item, text, analysis, imported) {
|
|
|
143
143
|
* namespace + local name + arity), grouped by each call site's enclosing
|
|
144
144
|
* `declare function` — found by walking up the AST from the call-site offset.
|
|
145
145
|
*/
|
|
146
|
-
export function getIncomingCalls(item, itemText, itemAnalysis, getOtherFiles) {
|
|
146
|
+
export function getIncomingCalls(item, itemText, itemAnalysis, getOtherFiles, loadAst) {
|
|
147
147
|
const offset = toOffset(itemText, item.selectionRange.start);
|
|
148
148
|
// includeDeclaration:false — we only want actual call sites, not the declaration itself.
|
|
149
|
-
const callSites = getReferences(item.uri, itemText, offset, itemAnalysis, false, getOtherFiles);
|
|
149
|
+
const callSites = getReferences(item.uri, itemText, offset, itemAnalysis, false, getOtherFiles, loadAst);
|
|
150
150
|
if (callSites.length === 0)
|
|
151
151
|
return [];
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
// getOtherFiles() returns cached, ast-free records — cheap to re-iterate for a uri->text
|
|
153
|
+
// lookup. The full (ast-bearing) analysis for each call site's file is fetched below, once
|
|
154
|
+
// per distinct uri, via loadAst.
|
|
155
|
+
const textByUri = new Map([[item.uri, itemText]]);
|
|
156
|
+
for (const file of getOtherFiles())
|
|
157
|
+
if (!textByUri.has(file.uri))
|
|
158
|
+
textByUri.set(file.uri, file.text);
|
|
159
|
+
const analysisByUri = new Map([[item.uri, itemAnalysis]]);
|
|
155
160
|
const groups = new Map();
|
|
156
161
|
for (const loc of callSites) {
|
|
157
|
-
const
|
|
158
|
-
if (
|
|
162
|
+
const text = textByUri.get(loc.uri);
|
|
163
|
+
if (text === undefined)
|
|
159
164
|
continue;
|
|
160
|
-
|
|
161
|
-
|
|
165
|
+
let fileAnalysis = analysisByUri.get(loc.uri);
|
|
166
|
+
if (!fileAnalysis) {
|
|
167
|
+
const loaded = loadAst(loc.uri, text);
|
|
168
|
+
if (!loaded)
|
|
169
|
+
continue;
|
|
170
|
+
fileAnalysis = loaded;
|
|
171
|
+
analysisByUri.set(loc.uri, fileAnalysis);
|
|
172
|
+
}
|
|
173
|
+
if (!fileAnalysis.ast)
|
|
174
|
+
continue;
|
|
175
|
+
const callOffset = toOffset(text, loc.range.start);
|
|
176
|
+
const stack = nodeStackAtOffset(fileAnalysis.ast, callOffset);
|
|
162
177
|
// Function declarations can't nest in XQuery, so at most one AnnotatedDecl/FunctionDecl
|
|
163
178
|
// pair appears in the stack; a call at module level (no enclosing function) has none.
|
|
164
179
|
const enclosingDecl = stack.find((n) => n.type === "AnnotatedDecl" && directChildOf(n, "FunctionDecl"));
|
|
165
180
|
if (!enclosingDecl)
|
|
166
181
|
continue;
|
|
167
|
-
const enclosingFn =
|
|
182
|
+
const enclosingFn = fileAnalysis.functions.find((f) => f.sourceOffset === enclosingDecl.start);
|
|
168
183
|
if (!enclosingFn)
|
|
169
184
|
continue;
|
|
170
185
|
const key = `${qnameKey(enclosingFn.qname)}#${enclosingFn.arity}@${loc.uri}`;
|
|
171
186
|
let group = groups.get(key);
|
|
172
187
|
if (!group) {
|
|
173
|
-
const from = buildItemForFunction(enclosingFn,
|
|
188
|
+
const from = buildItemForFunction(enclosingFn, fileAnalysis, loc.uri, text);
|
|
174
189
|
if (!from)
|
|
175
190
|
continue;
|
|
176
191
|
group = { from, ranges: [] };
|
package/dist/code-lens.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CodeLens } from "vscode-languageserver/node.js";
|
|
2
2
|
import type { TextDocument } from "vscode-languageserver-textdocument";
|
|
3
3
|
import type { FileAnalysis } from "./types.ts";
|
|
4
|
-
import type { FileRecord } from "./references.ts";
|
|
4
|
+
import type { FileRecord, LoadAst } from "./references.ts";
|
|
5
5
|
/** Carried on each unresolved lens so `resolveCodeLens` can find the declaration again later. */
|
|
6
6
|
export interface CodeLensData {
|
|
7
7
|
uri: string;
|
|
@@ -20,4 +20,4 @@ export declare function buildCodeLenses(doc: TextDocument, analysis: FileAnalysi
|
|
|
20
20
|
* reference search `textDocument/references` uses. Declarations aren't counted as
|
|
21
21
|
* references. 0 references is reported as-is — it's a useful signal, not hidden.
|
|
22
22
|
*/
|
|
23
|
-
export declare function resolveCodeLens(lens: CodeLens, text: string, analysis: FileAnalysis, getOtherFiles: () => FileRecord[]): CodeLens;
|
|
23
|
+
export declare function resolveCodeLens(lens: CodeLens, text: string, analysis: FileAnalysis, getOtherFiles: () => FileRecord[], loadAst: LoadAst): CodeLens;
|
package/dist/code-lens.js
CHANGED
|
@@ -60,10 +60,10 @@ function findNameOffset(ast, analysis, data) {
|
|
|
60
60
|
* reference search `textDocument/references` uses. Declarations aren't counted as
|
|
61
61
|
* references. 0 references is reported as-is — it's a useful signal, not hidden.
|
|
62
62
|
*/
|
|
63
|
-
export function resolveCodeLens(lens, text, analysis, getOtherFiles) {
|
|
63
|
+
export function resolveCodeLens(lens, text, analysis, getOtherFiles, loadAst) {
|
|
64
64
|
const data = lens.data;
|
|
65
65
|
const ast = analysis.ast;
|
|
66
66
|
const offset = data && ast ? findNameOffset(ast, analysis, data) : null;
|
|
67
|
-
const count = data && offset !== null ? getReferences(data.uri, text, offset, analysis, false, getOtherFiles).length : 0;
|
|
67
|
+
const count = data && offset !== null ? getReferences(data.uri, text, offset, analysis, false, getOtherFiles, loadAst).length : 0;
|
|
68
68
|
return { ...lens, command: { title: `${count} references`, command: "" } };
|
|
69
69
|
}
|
package/dist/references.d.ts
CHANGED
|
@@ -10,6 +10,14 @@ export declare function offsetToPosition(text: string, offset: number): {
|
|
|
10
10
|
line: number;
|
|
11
11
|
character: number;
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* Parses one glob-matched file's full AST on demand for a cross-file occurrence walk.
|
|
15
|
+
* Reuses `analysisCache` (open documents, explicit imports) when the file is already
|
|
16
|
+
* resident; otherwise parses fresh and returns without caching the result, so a
|
|
17
|
+
* cross-file search never retains more than the file currently being walked. See
|
|
18
|
+
* `getGlobFileAccess` in server.ts.
|
|
19
|
+
*/
|
|
20
|
+
export type LoadAst = (uri: string, text: string) => FileAnalysis | null;
|
|
13
21
|
/**
|
|
14
22
|
* Find all references to the symbol at `offset` in the current file.
|
|
15
23
|
*
|
|
@@ -26,9 +34,11 @@ export declare function offsetToPosition(text: string, offset: number): {
|
|
|
26
34
|
* Requires a valid AST (returns `[]` on the regex-fallback path, since parse
|
|
27
35
|
* failures don't produce reliable node positions).
|
|
28
36
|
* `getOtherFiles` is only invoked for symbol kinds that need a cross-file
|
|
29
|
-
* search, so callers can make it lazy.
|
|
37
|
+
* search, so callers can make it lazy. `getOtherFiles` returns lightweight,
|
|
38
|
+
* ast-free records for cheap candidate filtering; `loadAst` parses a specific
|
|
39
|
+
* candidate's full AST on demand once it survives that filter (see `LoadAst`).
|
|
30
40
|
*/
|
|
31
|
-
export declare function getReferences(currentUri: string, currentText: string, offset: number, analysis: FileAnalysis, includeDeclaration: boolean, getOtherFiles: () => FileRecord[]): Location[];
|
|
41
|
+
export declare function getReferences(currentUri: string, currentText: string, offset: number, analysis: FileAnalysis, includeDeclaration: boolean, getOtherFiles: () => FileRecord[], loadAst: LoadAst): Location[];
|
|
32
42
|
/**
|
|
33
43
|
* Range of the renameable identifier at `offset`, trimmed to the local-name part
|
|
34
44
|
* (namespace-prefix qualifiers are left untouched) or the bare prefix NCName for
|
|
@@ -48,7 +58,7 @@ export declare function getRenameRangeAtOffset(currentText: string, offset: numb
|
|
|
48
58
|
* recognizable at `offset`, or no resolvable local declaration (e.g. a builtin
|
|
49
59
|
* function or a predeclared runtime namespace prefix).
|
|
50
60
|
*/
|
|
51
|
-
export declare function getRenameLocations(currentUri: string, currentText: string, offset: number, analysis: FileAnalysis, getOtherFiles: () => FileRecord[]): Location[] | null;
|
|
61
|
+
export declare function getRenameLocations(currentUri: string, currentText: string, offset: number, analysis: FileAnalysis, getOtherFiles: () => FileRecord[], loadAst: LoadAst): Location[] | null;
|
|
52
62
|
/**
|
|
53
63
|
* Highlight every occurrence of the symbol at `offset` within the current
|
|
54
64
|
* document only (`textDocument/documentHighlight` is file-scoped by spec, even
|
package/dist/references.js
CHANGED
|
@@ -277,8 +277,7 @@ function getPrefixReferences(currentUri, currentText, ast, analysis, prefix, inc
|
|
|
277
277
|
}
|
|
278
278
|
return locs;
|
|
279
279
|
}
|
|
280
|
-
|
|
281
|
-
function getVariableReferences(currentUri, currentText, ast, analysis, offset, includeDeclaration, getOtherFiles, trimToLocalName = false) {
|
|
280
|
+
function getVariableReferences(currentUri, currentText, ast, analysis, offset, includeDeclaration, getOtherFiles, loadAst, trimToLocalName = false) {
|
|
282
281
|
const occurrences = [];
|
|
283
282
|
walkVariables(ast, analysis, new ScopeStack(), occurrences);
|
|
284
283
|
const target = occurrences.find((o) => offset >= o.start && offset <= o.end);
|
|
@@ -301,10 +300,16 @@ function getVariableReferences(currentUri, currentText, ast, analysis, offset, i
|
|
|
301
300
|
o,
|
|
302
301
|
}));
|
|
303
302
|
for (const file of getOtherFiles()) {
|
|
304
|
-
|
|
303
|
+
// Cheap pre-filter: localName always appears verbatim in source regardless of EQName
|
|
304
|
+
// syntax (prefixed, URIQualified, or unprefixed), so its absence rules out a match
|
|
305
|
+
// without needing to parse the file at all.
|
|
306
|
+
if (file.uri === currentUri || !file.text.includes(localName))
|
|
307
|
+
continue;
|
|
308
|
+
const fileAnalysis = loadAst(file.uri, file.text);
|
|
309
|
+
if (!fileAnalysis?.ast)
|
|
305
310
|
continue;
|
|
306
311
|
const fileOccs = [];
|
|
307
|
-
walkVariables(
|
|
312
|
+
walkVariables(fileAnalysis.ast, fileAnalysis, new ScopeStack(), fileOccs);
|
|
308
313
|
for (const o of matches(fileOccs))
|
|
309
314
|
all.push({ uri: file.uri, text: file.text, o });
|
|
310
315
|
}
|
|
@@ -313,7 +318,7 @@ function getVariableReferences(currentUri, currentText, ast, analysis, offset, i
|
|
|
313
318
|
return [];
|
|
314
319
|
return all.filter((m) => includeDeclaration || !m.o.isDeclaration).map((m) => toLoc(m.uri, m.text, m.o));
|
|
315
320
|
}
|
|
316
|
-
function getFunctionReferences(currentUri, currentText, ast, analysis, offset, includeDeclaration, getOtherFiles, trimToLocalName = false) {
|
|
321
|
+
function getFunctionReferences(currentUri, currentText, ast, analysis, offset, includeDeclaration, getOtherFiles, loadAst, trimToLocalName = false) {
|
|
317
322
|
const occurrences = [];
|
|
318
323
|
walkFunctions(ast, analysis, occurrences);
|
|
319
324
|
const target = occurrences.find((o) => offset >= o.start && offset <= o.end);
|
|
@@ -328,10 +333,13 @@ function getFunctionReferences(currentUri, currentText, ast, analysis, offset, i
|
|
|
328
333
|
o,
|
|
329
334
|
}));
|
|
330
335
|
for (const file of getOtherFiles()) {
|
|
331
|
-
if (file.uri === currentUri || !file.
|
|
336
|
+
if (file.uri === currentUri || !file.text.includes(localName))
|
|
337
|
+
continue;
|
|
338
|
+
const fileAnalysis = loadAst(file.uri, file.text);
|
|
339
|
+
if (!fileAnalysis?.ast)
|
|
332
340
|
continue;
|
|
333
341
|
const fileOccs = [];
|
|
334
|
-
walkFunctions(
|
|
342
|
+
walkFunctions(fileAnalysis.ast, fileAnalysis, fileOccs);
|
|
335
343
|
for (const o of matches(fileOccs))
|
|
336
344
|
all.push({ uri: file.uri, text: file.text, o });
|
|
337
345
|
}
|
|
@@ -385,9 +393,11 @@ function prefixHasDeclaration(analysis, prefix) {
|
|
|
385
393
|
* Requires a valid AST (returns `[]` on the regex-fallback path, since parse
|
|
386
394
|
* failures don't produce reliable node positions).
|
|
387
395
|
* `getOtherFiles` is only invoked for symbol kinds that need a cross-file
|
|
388
|
-
* search, so callers can make it lazy.
|
|
396
|
+
* search, so callers can make it lazy. `getOtherFiles` returns lightweight,
|
|
397
|
+
* ast-free records for cheap candidate filtering; `loadAst` parses a specific
|
|
398
|
+
* candidate's full AST on demand once it survives that filter (see `LoadAst`).
|
|
389
399
|
*/
|
|
390
|
-
export function getReferences(currentUri, currentText, offset, analysis, includeDeclaration, getOtherFiles) {
|
|
400
|
+
export function getReferences(currentUri, currentText, offset, analysis, includeDeclaration, getOtherFiles, loadAst) {
|
|
391
401
|
const ast = analysis.ast;
|
|
392
402
|
if (!ast)
|
|
393
403
|
return [];
|
|
@@ -397,8 +407,8 @@ export function getReferences(currentUri, currentText, offset, analysis, include
|
|
|
397
407
|
if (target.kind === "prefix")
|
|
398
408
|
return getPrefixReferences(currentUri, currentText, ast, analysis, target.prefix, includeDeclaration);
|
|
399
409
|
if (target.kind === "variable")
|
|
400
|
-
return getVariableReferences(currentUri, currentText, ast, analysis, offset, includeDeclaration, getOtherFiles);
|
|
401
|
-
return getFunctionReferences(currentUri, currentText, ast, analysis, offset, includeDeclaration, getOtherFiles);
|
|
410
|
+
return getVariableReferences(currentUri, currentText, ast, analysis, offset, includeDeclaration, getOtherFiles, loadAst);
|
|
411
|
+
return getFunctionReferences(currentUri, currentText, ast, analysis, offset, includeDeclaration, getOtherFiles, loadAst);
|
|
402
412
|
}
|
|
403
413
|
/**
|
|
404
414
|
* Range of the renameable identifier at `offset`, trimmed to the local-name part
|
|
@@ -430,7 +440,7 @@ export function getRenameRangeAtOffset(currentText, offset, analysis) {
|
|
|
430
440
|
* recognizable at `offset`, or no resolvable local declaration (e.g. a builtin
|
|
431
441
|
* function or a predeclared runtime namespace prefix).
|
|
432
442
|
*/
|
|
433
|
-
export function getRenameLocations(currentUri, currentText, offset, analysis, getOtherFiles) {
|
|
443
|
+
export function getRenameLocations(currentUri, currentText, offset, analysis, getOtherFiles, loadAst) {
|
|
434
444
|
const ast = analysis.ast;
|
|
435
445
|
if (!ast)
|
|
436
446
|
return null;
|
|
@@ -443,8 +453,8 @@ export function getRenameLocations(currentUri, currentText, offset, analysis, ge
|
|
|
443
453
|
return getPrefixReferences(currentUri, currentText, ast, analysis, target.prefix, true);
|
|
444
454
|
}
|
|
445
455
|
const locs = target.kind === "variable"
|
|
446
|
-
? getVariableReferences(currentUri, currentText, ast, analysis, offset, true, getOtherFiles, true)
|
|
447
|
-
: getFunctionReferences(currentUri, currentText, ast, analysis, offset, true, getOtherFiles, true);
|
|
456
|
+
? getVariableReferences(currentUri, currentText, ast, analysis, offset, true, getOtherFiles, loadAst, true)
|
|
457
|
+
: getFunctionReferences(currentUri, currentText, ast, analysis, offset, true, getOtherFiles, loadAst, true);
|
|
448
458
|
return locs.length > 0 ? locs : null;
|
|
449
459
|
}
|
|
450
460
|
/**
|
package/dist/server.js
CHANGED
|
@@ -107,6 +107,38 @@ function mergeAnalyses(a, b) {
|
|
|
107
107
|
moduleVariables: [...a.moduleVariables, ...b.moduleVariables],
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
|
+
// Lightweight (ast-free) per-file records for every glob-matched file: source text plus the
|
|
111
|
+
// *extracted* FileAnalysis (functions, module variables, imports, namespace decls, ...), with
|
|
112
|
+
// the AST itself dropped once those fields are pulled out. xq-parser's AST runs roughly two
|
|
113
|
+
// orders of magnitude larger than the source text — XQuery's operator-precedence grammar wraps
|
|
114
|
+
// even a bare `$x` variable reference in ~30 levels of single-child wrapper nodes, none of them
|
|
115
|
+
// collapsed — so retaining a full AST for every glob-matched file (often hundreds, only a
|
|
116
|
+
// handful of which anyone is actually looking at) is the dominant memory cost of any glob-backed
|
|
117
|
+
// feature. None of the FileAnalysis fields other than `.ast` hold node references (they're all
|
|
118
|
+
// plain offsets/strings — see types.ts), so dropping `.ast` after extraction is safe: nothing
|
|
119
|
+
// downstream keeps the tree reachable. Populated lazily per config dir; never invalidated.
|
|
120
|
+
const globFileSummariesByConfigDir = new Map();
|
|
121
|
+
function buildGlobFileSummaries(config, configDir) {
|
|
122
|
+
const records = new Map();
|
|
123
|
+
for (const filePath of expandGlobs(config.globs, configDir)) {
|
|
124
|
+
const fileUri = pathToFileURL(filePath).toString();
|
|
125
|
+
try {
|
|
126
|
+
const text = fs.readFileSync(filePath, "utf-8");
|
|
127
|
+
const { analysis } = analyzeWithAst(text, fileUri);
|
|
128
|
+
records.set(fileUri, { uri: fileUri, text, analysis: { ...analysis, ast: undefined } });
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
/* unreadable file, skip */
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return records;
|
|
135
|
+
}
|
|
136
|
+
function getGlobFileSummaries(configDir, config) {
|
|
137
|
+
if (!globFileSummariesByConfigDir.has(configDir)) {
|
|
138
|
+
globFileSummariesByConfigDir.set(configDir, buildGlobFileSummaries(config, configDir));
|
|
139
|
+
}
|
|
140
|
+
return globFileSummariesByConfigDir.get(configDir);
|
|
141
|
+
}
|
|
110
142
|
function getGlobAnalyses(currentUri) {
|
|
111
143
|
const found = findConfig(currentUri);
|
|
112
144
|
if (!found)
|
|
@@ -117,43 +149,39 @@ function getGlobAnalyses(currentUri) {
|
|
|
117
149
|
return { byNamespace: globAnalysesByConfigDir.get(configDir), lib };
|
|
118
150
|
}
|
|
119
151
|
const byNamespace = new Map();
|
|
120
|
-
for (const
|
|
121
|
-
|
|
122
|
-
const imported = getImportedAnalysis(fileUri);
|
|
123
|
-
if (!imported?.moduleNamespaceUri)
|
|
152
|
+
for (const { analysis } of getGlobFileSummaries(configDir, config).values()) {
|
|
153
|
+
if (!analysis.moduleNamespaceUri)
|
|
124
154
|
continue;
|
|
125
|
-
const existing = byNamespace.get(
|
|
126
|
-
byNamespace.set(
|
|
155
|
+
const existing = byNamespace.get(analysis.moduleNamespaceUri);
|
|
156
|
+
byNamespace.set(analysis.moduleNamespaceUri, existing ? mergeAnalyses(existing, analysis) : analysis);
|
|
127
157
|
}
|
|
128
158
|
globAnalysesByConfigDir.set(configDir, byNamespace);
|
|
129
159
|
return { byNamespace, lib };
|
|
130
160
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
161
|
+
/**
|
|
162
|
+
* Cross-file access for find-references / rename / call-hierarchy: `getOtherFiles` returns the
|
|
163
|
+
* cached, ast-free summaries above for cheap candidate filtering (see references.ts's text
|
|
164
|
+
* pre-filter); `loadAst` parses one specific candidate's full AST on demand, once it survives
|
|
165
|
+
* that filter, reusing `analysisCache` when the file is already resident (open document, or
|
|
166
|
+
* explicitly imported elsewhere) instead of re-parsing. The parsed result is intentionally NOT
|
|
167
|
+
* cached here — a cross-file search should never retain more than the handful of files it's
|
|
168
|
+
* actually walking.
|
|
169
|
+
*/
|
|
170
|
+
function getGlobFileAccess(currentUri) {
|
|
135
171
|
const found = findConfig(currentUri);
|
|
136
172
|
if (!found)
|
|
137
|
-
return () => [];
|
|
173
|
+
return { getOtherFiles: () => [], loadAst: () => null };
|
|
138
174
|
const { config, configDir } = found;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
/* unreadable file, skip */
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
globFileRecordsByConfigDir.set(configDir, records);
|
|
155
|
-
}
|
|
156
|
-
return [...globFileRecordsByConfigDir.get(configDir).values()];
|
|
175
|
+
const predeclaredNs = getRuntimePredeclaredNamespaces(config.lib);
|
|
176
|
+
return {
|
|
177
|
+
getOtherFiles: () => [...getGlobFileSummaries(configDir, config).values()].map((r) => ({ ...r, analysis: withPredeclaredNs(r.analysis, predeclaredNs) })),
|
|
178
|
+
loadAst: (uri, text) => {
|
|
179
|
+
const cached = analysisCache.get(uri);
|
|
180
|
+
if (cached?.ast)
|
|
181
|
+
return withPredeclaredNs(cached, predeclaredNs);
|
|
182
|
+
const { analysis } = analyzeWithAst(text, uri);
|
|
183
|
+
return withPredeclaredNs(analysis, predeclaredNs);
|
|
184
|
+
},
|
|
157
185
|
};
|
|
158
186
|
}
|
|
159
187
|
function resolveContext(currentUri, rawAnalysis) {
|
|
@@ -324,7 +352,7 @@ connection.onWorkspaceSymbol((params) => {
|
|
|
324
352
|
// No single document anchors a workspace-wide request, so gather config dirs from every open document.
|
|
325
353
|
const filesByUri = new Map();
|
|
326
354
|
for (const doc of documents.all()) {
|
|
327
|
-
for (const file of
|
|
355
|
+
for (const file of getGlobFileAccess(doc.uri).getOtherFiles())
|
|
328
356
|
filesByUri.set(file.uri, file);
|
|
329
357
|
}
|
|
330
358
|
return getWorkspaceSymbols([...filesByUri.values()], params.query);
|
|
@@ -343,7 +371,8 @@ connection.onReferences((params) => {
|
|
|
343
371
|
return [];
|
|
344
372
|
const rawAnalysis = analysisCache.get(doc.uri) ?? analyzeDocument(doc);
|
|
345
373
|
const { analysis } = resolveContext(doc.uri, rawAnalysis);
|
|
346
|
-
|
|
374
|
+
const { getOtherFiles, loadAst } = getGlobFileAccess(doc.uri);
|
|
375
|
+
return getReferences(doc.uri, doc.getText(), doc.offsetAt(params.position), analysis, params.context.includeDeclaration, getOtherFiles, loadAst);
|
|
347
376
|
});
|
|
348
377
|
const NCNAME_RE = /^[A-Za-z_][\w.-]*$/;
|
|
349
378
|
connection.onPrepareRename((params) => {
|
|
@@ -366,7 +395,8 @@ connection.onRenameRequest((params) => {
|
|
|
366
395
|
}
|
|
367
396
|
const rawAnalysis = analysisCache.get(doc.uri) ?? analyzeDocument(doc);
|
|
368
397
|
const { analysis } = resolveContext(doc.uri, rawAnalysis);
|
|
369
|
-
const
|
|
398
|
+
const { getOtherFiles, loadAst } = getGlobFileAccess(doc.uri);
|
|
399
|
+
const locations = getRenameLocations(doc.uri, doc.getText(), doc.offsetAt(params.position), analysis, getOtherFiles, loadAst);
|
|
370
400
|
if (!locations)
|
|
371
401
|
return null;
|
|
372
402
|
const changes = {};
|
|
@@ -408,7 +438,8 @@ connection.onCodeLensResolve((lens) => {
|
|
|
408
438
|
return { ...lens, command: { title: "0 references", command: "" } };
|
|
409
439
|
const rawAnalysis = analysisCache.get(doc.uri) ?? analyzeDocument(doc);
|
|
410
440
|
const { analysis } = resolveContext(doc.uri, rawAnalysis);
|
|
411
|
-
|
|
441
|
+
const { getOtherFiles, loadAst } = getGlobFileAccess(doc.uri);
|
|
442
|
+
return resolveCodeLens(lens, doc.getText(), analysis, getOtherFiles, loadAst);
|
|
412
443
|
});
|
|
413
444
|
connection.onDocumentLinks((params) => {
|
|
414
445
|
const doc = documents.get(params.textDocument.uri);
|
|
@@ -431,7 +462,8 @@ connection.languages.callHierarchy.onIncomingCalls((params) => {
|
|
|
431
462
|
if (!loaded)
|
|
432
463
|
return null;
|
|
433
464
|
const { analysis } = resolveContext(params.item.uri, loaded.analysis);
|
|
434
|
-
|
|
465
|
+
const { getOtherFiles, loadAst } = getGlobFileAccess(params.item.uri);
|
|
466
|
+
return getIncomingCalls(params.item, loaded.text, analysis, getOtherFiles, loadAst);
|
|
435
467
|
});
|
|
436
468
|
connection.languages.callHierarchy.onOutgoingCalls((params) => {
|
|
437
469
|
const loaded = loadAnalysisForUri(params.item.uri);
|
package/package.json
CHANGED
package/src/call-hierarchy.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { findAll, directChildOf, nodeStackAtOffset } from "./analyzer.ts";
|
|
|
10
10
|
import { asFunctionDecl, asFunctionCall } from "./ast-nodes.ts";
|
|
11
11
|
import { resolveFunctionAtOffset, allFunctions } from "./hover-core.ts";
|
|
12
12
|
import { getReferences } from "./references.ts";
|
|
13
|
-
import type { FileRecord } from "./references.ts";
|
|
13
|
+
import type { FileRecord, LoadAst } from "./references.ts";
|
|
14
14
|
|
|
15
15
|
/** Identifies the function a CallHierarchyItem refers to, round-tripped via `item.data`. */
|
|
16
16
|
interface CallHierarchyFunctionData {
|
|
@@ -180,34 +180,47 @@ export function getIncomingCalls(
|
|
|
180
180
|
itemText: string,
|
|
181
181
|
itemAnalysis: FileAnalysis,
|
|
182
182
|
getOtherFiles: () => FileRecord[],
|
|
183
|
+
loadAst: LoadAst,
|
|
183
184
|
): CallHierarchyIncomingCall[] {
|
|
184
185
|
const offset = toOffset(itemText, item.selectionRange.start);
|
|
185
186
|
// includeDeclaration:false — we only want actual call sites, not the declaration itself.
|
|
186
|
-
const callSites = getReferences(item.uri, itemText, offset, itemAnalysis, false, getOtherFiles);
|
|
187
|
+
const callSites = getReferences(item.uri, itemText, offset, itemAnalysis, false, getOtherFiles, loadAst);
|
|
187
188
|
if (callSites.length === 0) return [];
|
|
188
189
|
|
|
189
|
-
|
|
190
|
-
|
|
190
|
+
// getOtherFiles() returns cached, ast-free records — cheap to re-iterate for a uri->text
|
|
191
|
+
// lookup. The full (ast-bearing) analysis for each call site's file is fetched below, once
|
|
192
|
+
// per distinct uri, via loadAst.
|
|
193
|
+
const textByUri = new Map<string, string>([[item.uri, itemText]]);
|
|
194
|
+
for (const file of getOtherFiles()) if (!textByUri.has(file.uri)) textByUri.set(file.uri, file.text);
|
|
191
195
|
|
|
196
|
+
const analysisByUri = new Map<string, FileAnalysis>([[item.uri, itemAnalysis]]);
|
|
192
197
|
const groups = new Map<string, { from: CallHierarchyItem; ranges: Range[] }>();
|
|
193
198
|
|
|
194
199
|
for (const loc of callSites) {
|
|
195
|
-
const
|
|
196
|
-
if (
|
|
200
|
+
const text = textByUri.get(loc.uri);
|
|
201
|
+
if (text === undefined) continue;
|
|
202
|
+
let fileAnalysis = analysisByUri.get(loc.uri);
|
|
203
|
+
if (!fileAnalysis) {
|
|
204
|
+
const loaded = loadAst(loc.uri, text);
|
|
205
|
+
if (!loaded) continue;
|
|
206
|
+
fileAnalysis = loaded;
|
|
207
|
+
analysisByUri.set(loc.uri, fileAnalysis);
|
|
208
|
+
}
|
|
209
|
+
if (!fileAnalysis.ast) continue;
|
|
197
210
|
|
|
198
|
-
const callOffset = toOffset(
|
|
199
|
-
const stack = nodeStackAtOffset(
|
|
211
|
+
const callOffset = toOffset(text, loc.range.start);
|
|
212
|
+
const stack = nodeStackAtOffset(fileAnalysis.ast, callOffset);
|
|
200
213
|
// Function declarations can't nest in XQuery, so at most one AnnotatedDecl/FunctionDecl
|
|
201
214
|
// pair appears in the stack; a call at module level (no enclosing function) has none.
|
|
202
215
|
const enclosingDecl = stack.find((n) => n.type === "AnnotatedDecl" && directChildOf(n, "FunctionDecl"));
|
|
203
216
|
if (!enclosingDecl) continue;
|
|
204
|
-
const enclosingFn =
|
|
217
|
+
const enclosingFn = fileAnalysis.functions.find((f) => f.sourceOffset === enclosingDecl.start);
|
|
205
218
|
if (!enclosingFn) continue;
|
|
206
219
|
|
|
207
220
|
const key = `${qnameKey(enclosingFn.qname)}#${enclosingFn.arity}@${loc.uri}`;
|
|
208
221
|
let group = groups.get(key);
|
|
209
222
|
if (!group) {
|
|
210
|
-
const from = buildItemForFunction(enclosingFn,
|
|
223
|
+
const from = buildItemForFunction(enclosingFn, fileAnalysis, loc.uri, text);
|
|
211
224
|
if (!from) continue;
|
|
212
225
|
group = { from, ranges: [] };
|
|
213
226
|
groups.set(key, group);
|
package/src/code-lens.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { FileAnalysis } from "./types.ts";
|
|
|
5
5
|
import { findAll } from "./analyzer.ts";
|
|
6
6
|
import { asFunctionDeclaration, asVarDecl } from "./ast-nodes.ts";
|
|
7
7
|
import { getReferences } from "./references.ts";
|
|
8
|
-
import type { FileRecord } from "./references.ts";
|
|
8
|
+
import type { FileRecord, LoadAst } from "./references.ts";
|
|
9
9
|
|
|
10
10
|
/** Carried on each unresolved lens so `resolveCodeLens` can find the declaration again later. */
|
|
11
11
|
export interface CodeLensData {
|
|
@@ -74,11 +74,17 @@ function findNameOffset(ast: Node, analysis: FileAnalysis, data: CodeLensData):
|
|
|
74
74
|
* reference search `textDocument/references` uses. Declarations aren't counted as
|
|
75
75
|
* references. 0 references is reported as-is — it's a useful signal, not hidden.
|
|
76
76
|
*/
|
|
77
|
-
export function resolveCodeLens(
|
|
77
|
+
export function resolveCodeLens(
|
|
78
|
+
lens: CodeLens,
|
|
79
|
+
text: string,
|
|
80
|
+
analysis: FileAnalysis,
|
|
81
|
+
getOtherFiles: () => FileRecord[],
|
|
82
|
+
loadAst: LoadAst,
|
|
83
|
+
): CodeLens {
|
|
78
84
|
const data = lens.data as CodeLensData | undefined;
|
|
79
85
|
const ast = analysis.ast;
|
|
80
86
|
const offset = data && ast ? findNameOffset(ast, analysis, data) : null;
|
|
81
|
-
const count = data && offset !== null ? getReferences(data.uri, text, offset, analysis, false, getOtherFiles).length : 0;
|
|
87
|
+
const count = data && offset !== null ? getReferences(data.uri, text, offset, analysis, false, getOtherFiles, loadAst).length : 0;
|
|
82
88
|
|
|
83
89
|
return { ...lens, command: { title: `${count} references`, command: "" } };
|
|
84
90
|
}
|
package/src/references.ts
CHANGED
|
@@ -320,6 +320,15 @@ function getPrefixReferences(
|
|
|
320
320
|
|
|
321
321
|
// ── Cross-file orchestration ──────────────────────────────────────────────────
|
|
322
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Parses one glob-matched file's full AST on demand for a cross-file occurrence walk.
|
|
325
|
+
* Reuses `analysisCache` (open documents, explicit imports) when the file is already
|
|
326
|
+
* resident; otherwise parses fresh and returns without caching the result, so a
|
|
327
|
+
* cross-file search never retains more than the file currently being walked. See
|
|
328
|
+
* `getGlobFileAccess` in server.ts.
|
|
329
|
+
*/
|
|
330
|
+
export type LoadAst = (uri: string, text: string) => FileAnalysis | null;
|
|
331
|
+
|
|
323
332
|
function getVariableReferences(
|
|
324
333
|
currentUri: string,
|
|
325
334
|
currentText: string,
|
|
@@ -328,6 +337,7 @@ function getVariableReferences(
|
|
|
328
337
|
offset: number,
|
|
329
338
|
includeDeclaration: boolean,
|
|
330
339
|
getOtherFiles: () => FileRecord[],
|
|
340
|
+
loadAst: LoadAst,
|
|
331
341
|
trimToLocalName = false,
|
|
332
342
|
): Location[] {
|
|
333
343
|
const occurrences: VarOccurrence[] = [];
|
|
@@ -359,9 +369,14 @@ function getVariableReferences(
|
|
|
359
369
|
}));
|
|
360
370
|
|
|
361
371
|
for (const file of getOtherFiles()) {
|
|
362
|
-
|
|
372
|
+
// Cheap pre-filter: localName always appears verbatim in source regardless of EQName
|
|
373
|
+
// syntax (prefixed, URIQualified, or unprefixed), so its absence rules out a match
|
|
374
|
+
// without needing to parse the file at all.
|
|
375
|
+
if (file.uri === currentUri || !file.text.includes(localName)) continue;
|
|
376
|
+
const fileAnalysis = loadAst(file.uri, file.text);
|
|
377
|
+
if (!fileAnalysis?.ast) continue;
|
|
363
378
|
const fileOccs: VarOccurrence[] = [];
|
|
364
|
-
walkVariables(
|
|
379
|
+
walkVariables(fileAnalysis.ast, fileAnalysis, new ScopeStack(), fileOccs);
|
|
365
380
|
for (const o of matches(fileOccs)) all.push({ uri: file.uri, text: file.text, o });
|
|
366
381
|
}
|
|
367
382
|
|
|
@@ -379,6 +394,7 @@ function getFunctionReferences(
|
|
|
379
394
|
offset: number,
|
|
380
395
|
includeDeclaration: boolean,
|
|
381
396
|
getOtherFiles: () => FileRecord[],
|
|
397
|
+
loadAst: LoadAst,
|
|
382
398
|
trimToLocalName = false,
|
|
383
399
|
): Location[] {
|
|
384
400
|
const occurrences: FnOccurrence[] = [];
|
|
@@ -399,9 +415,11 @@ function getFunctionReferences(
|
|
|
399
415
|
}));
|
|
400
416
|
|
|
401
417
|
for (const file of getOtherFiles()) {
|
|
402
|
-
if (file.uri === currentUri || !file.
|
|
418
|
+
if (file.uri === currentUri || !file.text.includes(localName)) continue;
|
|
419
|
+
const fileAnalysis = loadAst(file.uri, file.text);
|
|
420
|
+
if (!fileAnalysis?.ast) continue;
|
|
403
421
|
const fileOccs: FnOccurrence[] = [];
|
|
404
|
-
walkFunctions(
|
|
422
|
+
walkFunctions(fileAnalysis.ast, fileAnalysis, fileOccs);
|
|
405
423
|
for (const o of matches(fileOccs)) all.push({ uri: file.uri, text: file.text, o });
|
|
406
424
|
}
|
|
407
425
|
|
|
@@ -466,7 +484,9 @@ function prefixHasDeclaration(analysis: FileAnalysis, prefix: string): boolean {
|
|
|
466
484
|
* Requires a valid AST (returns `[]` on the regex-fallback path, since parse
|
|
467
485
|
* failures don't produce reliable node positions).
|
|
468
486
|
* `getOtherFiles` is only invoked for symbol kinds that need a cross-file
|
|
469
|
-
* search, so callers can make it lazy.
|
|
487
|
+
* search, so callers can make it lazy. `getOtherFiles` returns lightweight,
|
|
488
|
+
* ast-free records for cheap candidate filtering; `loadAst` parses a specific
|
|
489
|
+
* candidate's full AST on demand once it survives that filter (see `LoadAst`).
|
|
470
490
|
*/
|
|
471
491
|
export function getReferences(
|
|
472
492
|
currentUri: string,
|
|
@@ -475,6 +495,7 @@ export function getReferences(
|
|
|
475
495
|
analysis: FileAnalysis,
|
|
476
496
|
includeDeclaration: boolean,
|
|
477
497
|
getOtherFiles: () => FileRecord[],
|
|
498
|
+
loadAst: LoadAst,
|
|
478
499
|
): Location[] {
|
|
479
500
|
const ast = analysis.ast;
|
|
480
501
|
if (!ast) return [];
|
|
@@ -483,8 +504,9 @@ export function getReferences(
|
|
|
483
504
|
if (!target) return [];
|
|
484
505
|
|
|
485
506
|
if (target.kind === "prefix") return getPrefixReferences(currentUri, currentText, ast, analysis, target.prefix, includeDeclaration);
|
|
486
|
-
if (target.kind === "variable")
|
|
487
|
-
|
|
507
|
+
if (target.kind === "variable")
|
|
508
|
+
return getVariableReferences(currentUri, currentText, ast, analysis, offset, includeDeclaration, getOtherFiles, loadAst);
|
|
509
|
+
return getFunctionReferences(currentUri, currentText, ast, analysis, offset, includeDeclaration, getOtherFiles, loadAst);
|
|
488
510
|
}
|
|
489
511
|
|
|
490
512
|
/**
|
|
@@ -523,6 +545,7 @@ export function getRenameLocations(
|
|
|
523
545
|
offset: number,
|
|
524
546
|
analysis: FileAnalysis,
|
|
525
547
|
getOtherFiles: () => FileRecord[],
|
|
548
|
+
loadAst: LoadAst,
|
|
526
549
|
): Location[] | null {
|
|
527
550
|
const ast = analysis.ast;
|
|
528
551
|
if (!ast) return null;
|
|
@@ -537,8 +560,8 @@ export function getRenameLocations(
|
|
|
537
560
|
|
|
538
561
|
const locs =
|
|
539
562
|
target.kind === "variable"
|
|
540
|
-
? getVariableReferences(currentUri, currentText, ast, analysis, offset, true, getOtherFiles, true)
|
|
541
|
-
: getFunctionReferences(currentUri, currentText, ast, analysis, offset, true, getOtherFiles, true);
|
|
563
|
+
? getVariableReferences(currentUri, currentText, ast, analysis, offset, true, getOtherFiles, loadAst, true)
|
|
564
|
+
: getFunctionReferences(currentUri, currentText, ast, analysis, offset, true, getOtherFiles, loadAst, true);
|
|
542
565
|
|
|
543
566
|
return locs.length > 0 ? locs : null;
|
|
544
567
|
}
|
package/src/server.ts
CHANGED
|
@@ -23,12 +23,13 @@ import { getDefinition } from "./definition.ts";
|
|
|
23
23
|
import { getDocumentLinks } from "./document-links.ts";
|
|
24
24
|
import { getInlayHints } from "./inlay-hints.ts";
|
|
25
25
|
import { getReferences, getRenameRangeAtOffset, getRenameLocations, getDocumentHighlights } from "./references.ts";
|
|
26
|
-
import type { FileRecord } from "./references.ts";
|
|
26
|
+
import type { FileRecord, LoadAst } from "./references.ts";
|
|
27
27
|
import { buildCodeLenses, resolveCodeLens } from "./code-lens.ts";
|
|
28
28
|
import type { CodeLensData } from "./code-lens.ts";
|
|
29
29
|
import { prepareCallHierarchy, getIncomingCalls, getOutgoingCalls } from "./call-hierarchy.ts";
|
|
30
30
|
import type { FileAnalysis, TypeDiagnostic } from "./types.ts";
|
|
31
31
|
import { findConfig, expandGlobs } from "./config.ts";
|
|
32
|
+
import type { LspConfig } from "./config.ts";
|
|
32
33
|
import { findImportInsertPosition, findDeclareNsInsertPosition, computeRelativePath } from "./namespace-diagnostics.ts";
|
|
33
34
|
import type { NamespaceUsageKind } from "./namespace-diagnostics.ts";
|
|
34
35
|
import { findUndeclaredPrefixUsages } from "./namespace-diagnostics.ts";
|
|
@@ -138,6 +139,40 @@ function mergeAnalyses(a: FileAnalysis, b: FileAnalysis): FileAnalysis {
|
|
|
138
139
|
};
|
|
139
140
|
}
|
|
140
141
|
|
|
142
|
+
// Lightweight (ast-free) per-file records for every glob-matched file: source text plus the
|
|
143
|
+
// *extracted* FileAnalysis (functions, module variables, imports, namespace decls, ...), with
|
|
144
|
+
// the AST itself dropped once those fields are pulled out. xq-parser's AST runs roughly two
|
|
145
|
+
// orders of magnitude larger than the source text — XQuery's operator-precedence grammar wraps
|
|
146
|
+
// even a bare `$x` variable reference in ~30 levels of single-child wrapper nodes, none of them
|
|
147
|
+
// collapsed — so retaining a full AST for every glob-matched file (often hundreds, only a
|
|
148
|
+
// handful of which anyone is actually looking at) is the dominant memory cost of any glob-backed
|
|
149
|
+
// feature. None of the FileAnalysis fields other than `.ast` hold node references (they're all
|
|
150
|
+
// plain offsets/strings — see types.ts), so dropping `.ast` after extraction is safe: nothing
|
|
151
|
+
// downstream keeps the tree reachable. Populated lazily per config dir; never invalidated.
|
|
152
|
+
const globFileSummariesByConfigDir = new Map<string, Map<string, FileRecord>>();
|
|
153
|
+
|
|
154
|
+
function buildGlobFileSummaries(config: LspConfig, configDir: string): Map<string, FileRecord> {
|
|
155
|
+
const records = new Map<string, FileRecord>();
|
|
156
|
+
for (const filePath of expandGlobs(config.globs, configDir)) {
|
|
157
|
+
const fileUri = pathToFileURL(filePath).toString();
|
|
158
|
+
try {
|
|
159
|
+
const text = fs.readFileSync(filePath, "utf-8");
|
|
160
|
+
const { analysis } = analyzeWithAst(text, fileUri);
|
|
161
|
+
records.set(fileUri, { uri: fileUri, text, analysis: { ...analysis, ast: undefined } });
|
|
162
|
+
} catch {
|
|
163
|
+
/* unreadable file, skip */
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return records;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function getGlobFileSummaries(configDir: string, config: LspConfig): Map<string, FileRecord> {
|
|
170
|
+
if (!globFileSummariesByConfigDir.has(configDir)) {
|
|
171
|
+
globFileSummariesByConfigDir.set(configDir, buildGlobFileSummaries(config, configDir));
|
|
172
|
+
}
|
|
173
|
+
return globFileSummariesByConfigDir.get(configDir)!;
|
|
174
|
+
}
|
|
175
|
+
|
|
141
176
|
function getGlobAnalyses(currentUri: string): { byNamespace: Map<string, FileAnalysis>; lib: string[] } {
|
|
142
177
|
const found = findConfig(currentUri);
|
|
143
178
|
if (!found) return { byNamespace: new Map(), lib: [] };
|
|
@@ -150,43 +185,38 @@ function getGlobAnalyses(currentUri: string): { byNamespace: Map<string, FileAna
|
|
|
150
185
|
}
|
|
151
186
|
|
|
152
187
|
const byNamespace = new Map<string, FileAnalysis>();
|
|
153
|
-
for (const
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
const existing = byNamespace.get(imported.moduleNamespaceUri);
|
|
158
|
-
byNamespace.set(imported.moduleNamespaceUri, existing ? mergeAnalyses(existing, imported) : imported);
|
|
188
|
+
for (const { analysis } of getGlobFileSummaries(configDir, config).values()) {
|
|
189
|
+
if (!analysis.moduleNamespaceUri) continue;
|
|
190
|
+
const existing = byNamespace.get(analysis.moduleNamespaceUri);
|
|
191
|
+
byNamespace.set(analysis.moduleNamespaceUri, existing ? mergeAnalyses(existing, analysis) : analysis);
|
|
159
192
|
}
|
|
160
193
|
globAnalysesByConfigDir.set(configDir, byNamespace);
|
|
161
194
|
return { byNamespace, lib };
|
|
162
195
|
}
|
|
163
196
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
197
|
+
/**
|
|
198
|
+
* Cross-file access for find-references / rename / call-hierarchy: `getOtherFiles` returns the
|
|
199
|
+
* cached, ast-free summaries above for cheap candidate filtering (see references.ts's text
|
|
200
|
+
* pre-filter); `loadAst` parses one specific candidate's full AST on demand, once it survives
|
|
201
|
+
* that filter, reusing `analysisCache` when the file is already resident (open document, or
|
|
202
|
+
* explicitly imported elsewhere) instead of re-parsing. The parsed result is intentionally NOT
|
|
203
|
+
* cached here — a cross-file search should never retain more than the handful of files it's
|
|
204
|
+
* actually walking.
|
|
205
|
+
*/
|
|
206
|
+
function getGlobFileAccess(currentUri: string): { getOtherFiles: () => FileRecord[]; loadAst: LoadAst } {
|
|
169
207
|
const found = findConfig(currentUri);
|
|
170
|
-
if (!found) return () => [];
|
|
208
|
+
if (!found) return { getOtherFiles: () => [], loadAst: () => null };
|
|
171
209
|
const { config, configDir } = found;
|
|
210
|
+
const predeclaredNs = getRuntimePredeclaredNamespaces(config.lib);
|
|
172
211
|
|
|
173
|
-
return
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const { analysis } = analyzeWithAst(text, fileUri);
|
|
182
|
-
records.set(fileUri, { uri: fileUri, text, analysis: withPredeclaredNs(analysis, predeclaredNs) });
|
|
183
|
-
} catch {
|
|
184
|
-
/* unreadable file, skip */
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
globFileRecordsByConfigDir.set(configDir, records);
|
|
188
|
-
}
|
|
189
|
-
return [...globFileRecordsByConfigDir.get(configDir)!.values()];
|
|
212
|
+
return {
|
|
213
|
+
getOtherFiles: () => [...getGlobFileSummaries(configDir, config).values()].map((r) => ({ ...r, analysis: withPredeclaredNs(r.analysis, predeclaredNs) })),
|
|
214
|
+
loadAst: (uri: string, text: string) => {
|
|
215
|
+
const cached = analysisCache.get(uri);
|
|
216
|
+
if (cached?.ast) return withPredeclaredNs(cached, predeclaredNs);
|
|
217
|
+
const { analysis } = analyzeWithAst(text, uri);
|
|
218
|
+
return withPredeclaredNs(analysis, predeclaredNs);
|
|
219
|
+
},
|
|
190
220
|
};
|
|
191
221
|
}
|
|
192
222
|
|
|
@@ -383,7 +413,7 @@ connection.onWorkspaceSymbol((params) => {
|
|
|
383
413
|
// No single document anchors a workspace-wide request, so gather config dirs from every open document.
|
|
384
414
|
const filesByUri = new Map<string, FileRecord>();
|
|
385
415
|
for (const doc of documents.all()) {
|
|
386
|
-
for (const file of
|
|
416
|
+
for (const file of getGlobFileAccess(doc.uri).getOtherFiles()) filesByUri.set(file.uri, file);
|
|
387
417
|
}
|
|
388
418
|
return getWorkspaceSymbols([...filesByUri.values()], params.query);
|
|
389
419
|
});
|
|
@@ -401,14 +431,8 @@ connection.onReferences((params) => {
|
|
|
401
431
|
if (!doc) return [];
|
|
402
432
|
const rawAnalysis = analysisCache.get(doc.uri) ?? analyzeDocument(doc);
|
|
403
433
|
const { analysis } = resolveContext(doc.uri, rawAnalysis);
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
doc.getText(),
|
|
407
|
-
doc.offsetAt(params.position),
|
|
408
|
-
analysis,
|
|
409
|
-
params.context.includeDeclaration,
|
|
410
|
-
getGlobFileRecords(doc.uri),
|
|
411
|
-
);
|
|
434
|
+
const { getOtherFiles, loadAst } = getGlobFileAccess(doc.uri);
|
|
435
|
+
return getReferences(doc.uri, doc.getText(), doc.offsetAt(params.position), analysis, params.context.includeDeclaration, getOtherFiles, loadAst);
|
|
412
436
|
});
|
|
413
437
|
|
|
414
438
|
const NCNAME_RE = /^[A-Za-z_][\w.-]*$/;
|
|
@@ -431,13 +455,8 @@ connection.onRenameRequest((params) => {
|
|
|
431
455
|
}
|
|
432
456
|
const rawAnalysis = analysisCache.get(doc.uri) ?? analyzeDocument(doc);
|
|
433
457
|
const { analysis } = resolveContext(doc.uri, rawAnalysis);
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
doc.getText(),
|
|
437
|
-
doc.offsetAt(params.position),
|
|
438
|
-
analysis,
|
|
439
|
-
getGlobFileRecords(doc.uri),
|
|
440
|
-
);
|
|
458
|
+
const { getOtherFiles, loadAst } = getGlobFileAccess(doc.uri);
|
|
459
|
+
const locations = getRenameLocations(doc.uri, doc.getText(), doc.offsetAt(params.position), analysis, getOtherFiles, loadAst);
|
|
441
460
|
if (!locations) return null;
|
|
442
461
|
|
|
443
462
|
const changes: Record<string, TextEdit[]> = {};
|
|
@@ -478,7 +497,8 @@ connection.onCodeLensResolve((lens) => {
|
|
|
478
497
|
if (!doc) return { ...lens, command: { title: "0 references", command: "" } };
|
|
479
498
|
const rawAnalysis = analysisCache.get(doc.uri) ?? analyzeDocument(doc);
|
|
480
499
|
const { analysis } = resolveContext(doc.uri, rawAnalysis);
|
|
481
|
-
|
|
500
|
+
const { getOtherFiles, loadAst } = getGlobFileAccess(doc.uri);
|
|
501
|
+
return resolveCodeLens(lens, doc.getText(), analysis, getOtherFiles, loadAst);
|
|
482
502
|
});
|
|
483
503
|
|
|
484
504
|
connection.onDocumentLinks((params) => {
|
|
@@ -501,7 +521,8 @@ connection.languages.callHierarchy.onIncomingCalls((params) => {
|
|
|
501
521
|
const loaded = loadAnalysisForUri(params.item.uri);
|
|
502
522
|
if (!loaded) return null;
|
|
503
523
|
const { analysis } = resolveContext(params.item.uri, loaded.analysis);
|
|
504
|
-
|
|
524
|
+
const { getOtherFiles, loadAst } = getGlobFileAccess(params.item.uri);
|
|
525
|
+
return getIncomingCalls(params.item, loaded.text, analysis, getOtherFiles, loadAst);
|
|
505
526
|
});
|
|
506
527
|
|
|
507
528
|
connection.languages.callHierarchy.onOutgoingCalls((params) => {
|