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 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
 
@@ -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[];
@@ -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
- const fileByUri = new Map(getOtherFiles().map((f) => [f.uri, f]));
153
- if (!fileByUri.has(item.uri))
154
- fileByUri.set(item.uri, { uri: item.uri, text: itemText, analysis: itemAnalysis });
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 record = fileByUri.get(loc.uri);
158
- if (!record?.analysis.ast)
162
+ const text = textByUri.get(loc.uri);
163
+ if (text === undefined)
159
164
  continue;
160
- const callOffset = toOffset(record.text, loc.range.start);
161
- const stack = nodeStackAtOffset(record.analysis.ast, callOffset);
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 = record.analysis.functions.find((f) => f.sourceOffset === enclosingDecl.start);
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, record.analysis, loc.uri, record.text);
188
+ const from = buildItemForFunction(enclosingFn, fileAnalysis, loc.uri, text);
174
189
  if (!from)
175
190
  continue;
176
191
  group = { from, ranges: [] };
@@ -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
  }
@@ -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
@@ -277,8 +277,7 @@ function getPrefixReferences(currentUri, currentText, ast, analysis, prefix, inc
277
277
  }
278
278
  return locs;
279
279
  }
280
- // ── Cross-file orchestration ──────────────────────────────────────────────────
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
- if (file.uri === currentUri || !file.analysis.ast)
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(file.analysis.ast, file.analysis, new ScopeStack(), fileOccs);
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.analysis.ast)
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(file.analysis.ast, file.analysis, fileOccs);
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 filePath of expandGlobs(config.globs, configDir)) {
121
- const fileUri = pathToFileURL(filePath).toString();
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(imported.moduleNamespaceUri);
126
- byNamespace.set(imported.moduleNamespaceUri, existing ? mergeAnalyses(existing, imported) : imported);
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
- // Per-file (not merged-by-namespace) glob analyses, for cross-file reference search.
132
- // Keyed by config directory; populated lazily and never invalidated, mirroring globAnalysesByConfigDir.
133
- const globFileRecordsByConfigDir = new Map();
134
- function getGlobFileRecords(currentUri) {
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
- return () => {
140
- if (!globFileRecordsByConfigDir.has(configDir)) {
141
- const predeclaredNs = getRuntimePredeclaredNamespaces(config.lib);
142
- const records = new Map();
143
- for (const filePath of expandGlobs(config.globs, configDir)) {
144
- const fileUri = pathToFileURL(filePath).toString();
145
- try {
146
- const text = fs.readFileSync(filePath, "utf-8");
147
- const { analysis } = analyzeWithAst(text, fileUri);
148
- records.set(fileUri, { uri: fileUri, text, analysis: withPredeclaredNs(analysis, predeclaredNs) });
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 getGlobFileRecords(doc.uri)())
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
- return getReferences(doc.uri, doc.getText(), doc.offsetAt(params.position), analysis, params.context.includeDeclaration, getGlobFileRecords(doc.uri));
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 locations = getRenameLocations(doc.uri, doc.getText(), doc.offsetAt(params.position), analysis, getGlobFileRecords(doc.uri));
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
- return resolveCodeLens(lens, doc.getText(), analysis, getGlobFileRecords(doc.uri));
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
- return getIncomingCalls(params.item, loaded.text, analysis, getGlobFileRecords(params.item.uri));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xq-lsp",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "description": "XQuery Language Server Protocol implementation",
5
5
  "license": "MIT",
6
6
  "author": "Martin Middel",
@@ -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
- const fileByUri = new Map<string, FileRecord>(getOtherFiles().map((f) => [f.uri, f]));
190
- if (!fileByUri.has(item.uri)) fileByUri.set(item.uri, { uri: item.uri, text: itemText, analysis: itemAnalysis });
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 record = fileByUri.get(loc.uri);
196
- if (!record?.analysis.ast) continue;
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(record.text, loc.range.start);
199
- const stack = nodeStackAtOffset(record.analysis.ast, callOffset);
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 = record.analysis.functions.find((f) => f.sourceOffset === enclosingDecl.start);
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, record.analysis, loc.uri, record.text);
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(lens: CodeLens, text: string, analysis: FileAnalysis, getOtherFiles: () => FileRecord[]): CodeLens {
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
- if (file.uri === currentUri || !file.analysis.ast) continue;
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(file.analysis.ast, file.analysis, new ScopeStack(), fileOccs);
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.analysis.ast) continue;
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(file.analysis.ast, file.analysis, fileOccs);
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") return getVariableReferences(currentUri, currentText, ast, analysis, offset, includeDeclaration, getOtherFiles);
487
- return getFunctionReferences(currentUri, currentText, ast, analysis, offset, includeDeclaration, getOtherFiles);
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 filePath of expandGlobs(config.globs, configDir)) {
154
- const fileUri = pathToFileURL(filePath).toString();
155
- const imported = getImportedAnalysis(fileUri);
156
- if (!imported?.moduleNamespaceUri) continue;
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
- // Per-file (not merged-by-namespace) glob analyses, for cross-file reference search.
165
- // Keyed by config directory; populated lazily and never invalidated, mirroring globAnalysesByConfigDir.
166
- const globFileRecordsByConfigDir = new Map<string, Map<string, FileRecord>>();
167
-
168
- function getGlobFileRecords(currentUri: string): () => FileRecord[] {
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
- if (!globFileRecordsByConfigDir.has(configDir)) {
175
- const predeclaredNs = getRuntimePredeclaredNamespaces(config.lib);
176
- const records = new Map<string, FileRecord>();
177
- for (const filePath of expandGlobs(config.globs, configDir)) {
178
- const fileUri = pathToFileURL(filePath).toString();
179
- try {
180
- const text = fs.readFileSync(filePath, "utf-8");
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 getGlobFileRecords(doc.uri)()) filesByUri.set(file.uri, file);
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
- return getReferences(
405
- doc.uri,
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 locations = getRenameLocations(
435
- doc.uri,
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
- return resolveCodeLens(lens, doc.getText(), analysis, getGlobFileRecords(doc.uri));
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
- return getIncomingCalls(params.item, loaded.text, analysis, getGlobFileRecords(params.item.uri));
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) => {