xq-lsp 1.7.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/demo/index.html +71 -2
  3. package/demo/main.ts +165 -12
  4. package/demo/package-lock.json +45 -0
  5. package/demo/package.json +2 -0
  6. package/demo/vite.config.ts +3 -0
  7. package/dist/analyzer.d.ts +1 -0
  8. package/dist/analyzer.js +29 -4
  9. package/dist/ast-nodes.d.ts +1 -0
  10. package/dist/ast-nodes.js +1 -1
  11. package/dist/braced-uri-diagnostics.d.ts +13 -0
  12. package/dist/braced-uri-diagnostics.js +27 -0
  13. package/dist/call-hierarchy.d.ts +20 -0
  14. package/dist/call-hierarchy.js +197 -0
  15. package/dist/code-lens.d.ts +23 -0
  16. package/dist/code-lens.js +69 -0
  17. package/dist/completion-core.js +1 -1
  18. package/dist/definition.js +1 -1
  19. package/dist/diagnostics.js +2 -1
  20. package/dist/document-links.d.ts +4 -0
  21. package/dist/document-links.js +79 -0
  22. package/dist/features.js +4 -1
  23. package/dist/hover-core.js +23 -0
  24. package/dist/inlay-hints.d.ts +5 -0
  25. package/dist/inlay-hints.js +121 -0
  26. package/dist/predeclared-namespaces.d.ts +25 -0
  27. package/dist/predeclared-namespaces.js +142 -0
  28. package/dist/refactor-actions.d.ts +39 -0
  29. package/dist/refactor-actions.js +322 -0
  30. package/dist/references.d.ts +17 -3
  31. package/dist/references.js +25 -15
  32. package/dist/runtimes/existdb/exist.xq +10 -0
  33. package/dist/runtimes.d.ts +2 -14
  34. package/dist/runtimes.js +1 -72
  35. package/dist/server.js +205 -35
  36. package/dist/typechecker.js +69 -4
  37. package/dist/types.d.ts +1 -0
  38. package/dist/variable-diagnostics.js +32 -21
  39. package/dist/workspace-symbol.d.ts +8 -0
  40. package/dist/workspace-symbol.js +29 -0
  41. package/package.json +1 -1
  42. package/src/analyzer.ts +35 -4
  43. package/src/ast-nodes.ts +2 -1
  44. package/src/braced-uri-diagnostics.ts +35 -0
  45. package/src/call-hierarchy.ts +232 -0
  46. package/src/code-lens.ts +90 -0
  47. package/src/completion-core.ts +1 -1
  48. package/src/definition.ts +1 -1
  49. package/src/diagnostics.ts +2 -1
  50. package/src/document-links.ts +81 -0
  51. package/src/features.ts +3 -1
  52. package/src/hover-core.ts +23 -0
  53. package/src/inlay-hints.ts +162 -0
  54. package/src/predeclared-namespaces.ts +149 -0
  55. package/src/refactor-actions.ts +369 -0
  56. package/src/references.ts +33 -10
  57. package/src/runtimes/existdb/exist.xq +10 -0
  58. package/src/runtimes.ts +8 -75
  59. package/src/server.ts +223 -54
  60. package/src/typechecker.ts +65 -3
  61. package/src/types.ts +1 -0
  62. package/src/variable-diagnostics.ts +24 -16
  63. package/src/workspace-symbol.ts +32 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
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
+
10
+ ## [1.8.0](https://github.com/DrRataplan/xq-lsp/compare/lsp-v1.7.0...lsp-v1.8.0) (2026-07-28)
11
+
12
+
13
+ ### Features
14
+
15
+ * add AST debug panel to demo and standalone dump-ast script ([dd42db4](https://github.com/DrRataplan/xq-lsp/commit/dd42db4866af25cd25732c3464089447e6a537ce))
16
+ * add call hierarchy support ([27cbe5d](https://github.com/DrRataplan/xq-lsp/commit/27cbe5d8c01e376f6e6dba3bc8903913ef4e0181))
17
+ * add code lens for reference counts ([b69300a](https://github.com/DrRataplan/xq-lsp/commit/b69300a3c036198d05eb24dacd7ecf000eed1d6e))
18
+ * add document links for module imports ([154bb04](https://github.com/DrRataplan/xq-lsp/commit/154bb0448758d49b26839ecff882381a4adaeb20)), closes [#118](https://github.com/DrRataplan/xq-lsp/issues/118)
19
+ * add inlay hints for parameter names and inferred types ([ee90295](https://github.com/DrRataplan/xq-lsp/commit/ee9029594df7a4faef6af7e34291aa77ae1a50a7))
20
+ * add refactor code actions ([34a9315](https://github.com/DrRataplan/xq-lsp/commit/34a93155f953f11d40579b38454b7614cba8e92c)), closes [#121](https://github.com/DrRataplan/xq-lsp/issues/121)
21
+ * add workspace symbol search ([fdeb48e](https://github.com/DrRataplan/xq-lsp/commit/fdeb48e2ab56478480847c8b3bb7b4dd7da09008))
22
+ * predeclare eXist-db install-script variables and surface variable doc comments ([67a8075](https://github.com/DrRataplan/xq-lsp/commit/67a807500cb723c0e8d6e8b229b06ccaf3e0fa04))
23
+ * show inlay hints in the demo playground ([169814d](https://github.com/DrRataplan/xq-lsp/commit/169814da37a47d141062946c255fd1ecb46a008f))
24
+
25
+
26
+ ### Bug Fixes
27
+
28
+ * add hint diagnostic for whitespace inside Q{uri} literals ([5f8d5d6](https://github.com/DrRataplan/xq-lsp/commit/5f8d5d6b4a517cf997b73a8c39d6d22621cfcf82))
29
+ * **ci:** pack xq-lsp before installing vscode extension deps ([527d824](https://github.com/DrRataplan/xq-lsp/commit/527d82486a26ea7742ad74d58c0ab18330a61405))
30
+ * **ci:** stop validating packed xq-lsp tarball against stale lockfile ([2369252](https://github.com/DrRataplan/xq-lsp/commit/23692529a2cf9d759665dee80c6e9c7aa1ac0968))
31
+ * correct qt4tests exclusion typo in .npmignore ([20080f1](https://github.com/DrRataplan/xq-lsp/commit/20080f1c242640bc9184a30949fe949b016398a4))
32
+ * declare vscode-languageserver-textdocument as a demo dependency ([013b04f](https://github.com/DrRataplan/xq-lsp/commit/013b04f9fd4da712bf56cb4afe731eed0f4b26f4))
33
+ * don't hover function signatures on lookalike element/attribute names ([1d87635](https://github.com/DrRataplan/xq-lsp/commit/1d87635e6c98b1bb1eb3af5e49b0ec6ae175e128))
34
+ * exclude version-exclusive spec dependencies from QT4 3.1 filter ([ad9bbe5](https://github.com/DrRataplan/xq-lsp/commit/ad9bbe5290ccb16f055da9df454d471f9ed5bec2))
35
+ * exclude XP-only spec dependencies from QT4 3.1 filter ([e5514b1](https://github.com/DrRataplan/xq-lsp/commit/e5514b1f6019ee694b7bb0fa6603a853e46cb55c))
36
+ * honor lib config for runtime function diagnostics in the playground demo ([cc29fd2](https://github.com/DrRataplan/xq-lsp/commit/cc29fd2f6edaccc655f9332e41db3ba9e3ab018a))
37
+ * infer arithmetic types and stop misinferring literal path exprs as nodes ([825a9ab](https://github.com/DrRataplan/xq-lsp/commit/825a9ab1b004c262c4d7cb0f585df67bc441b6f6))
38
+ * infer inlay hint types for for-bindings over range/typed sequences ([1a6d202](https://github.com/DrRataplan/xq-lsp/commit/1a6d202349e7d79372d79d5dd199e636de8926dc))
39
+ * predeclare xsi namespace prefix unconditionally ([ac6f47d](https://github.com/DrRataplan/xq-lsp/commit/ac6f47d59176cde7c2e0649cb4854d02c71811a4))
40
+ * resolve vscode-languageserver from src/inlay-hints.ts in the demo build ([db43ee1](https://github.com/DrRataplan/xq-lsp/commit/db43ee184a7005fb76e93a87dfcf6dd716acef33))
41
+ * resolve XPST0008 false positives from typeswitch scoping and EQName whitespace ([df0f89c](https://github.com/DrRataplan/xq-lsp/commit/df0f89c45fbe6ee2330e9311dc639f59bd4370a9))
42
+
3
43
  ## [1.7.0](https://github.com/DrRataplan/xq-lsp/compare/lsp-v1.6.0...lsp-v1.7.0) (2026-07-15)
4
44
 
5
45
 
package/demo/index.html CHANGED
@@ -30,8 +30,24 @@
30
30
  font-size: 15px;
31
31
  font-weight: 600;
32
32
  }
33
- #share-btn {
33
+ #ast-toggle {
34
34
  margin-left: auto;
35
+ background: transparent;
36
+ color: #cdd6f4;
37
+ border: 1px solid #45475a;
38
+ padding: 4px 12px;
39
+ border-radius: 4px;
40
+ cursor: pointer;
41
+ font-size: 13px;
42
+ font-weight: 600;
43
+ }
44
+ #ast-toggle:hover {
45
+ border-color: #89b4fa;
46
+ }
47
+ #ast-toggle.active {
48
+ background: #45475a;
49
+ }
50
+ #share-btn {
35
51
  background: #89b4fa;
36
52
  color: #1e1e2e;
37
53
  border: none;
@@ -44,11 +60,48 @@
44
60
  #share-btn:hover {
45
61
  background: #74c7ec;
46
62
  }
63
+ #main {
64
+ flex: 1;
65
+ display: flex;
66
+ overflow: hidden;
67
+ min-height: 0;
68
+ }
47
69
  #editor {
48
70
  flex: 1;
49
71
  overflow: hidden;
50
72
  min-height: 0;
51
73
  }
74
+ #ast-panel {
75
+ display: none;
76
+ width: 420px;
77
+ flex-shrink: 0;
78
+ flex-direction: column;
79
+ background: #181825;
80
+ border-left: 1px solid #45475a;
81
+ overflow: hidden;
82
+ }
83
+ #ast-panel.visible {
84
+ display: flex;
85
+ }
86
+ #ast-label {
87
+ padding: 3px 12px;
88
+ font-size: 11px;
89
+ color: #585b70;
90
+ font-family: monospace;
91
+ border-bottom: 1px solid #313244;
92
+ letter-spacing: 0.02em;
93
+ flex-shrink: 0;
94
+ }
95
+ #ast-output {
96
+ flex: 1;
97
+ margin: 0;
98
+ padding: 8px 12px;
99
+ overflow: auto;
100
+ font-family: monospace;
101
+ font-size: 11px;
102
+ color: #cdd6f4;
103
+ white-space: pre;
104
+ }
52
105
  .cm-editor {
53
106
  height: 100%;
54
107
  }
@@ -88,6 +141,15 @@
88
141
  color: #89b4fa;
89
142
  font-weight: bold;
90
143
  }
144
+ .xq-inlay-hint {
145
+ font-family: monospace;
146
+ font-size: 11px;
147
+ color: #9399b2;
148
+ background: #313244;
149
+ border-radius: 3px;
150
+ padding: 0 4px;
151
+ margin: 0 2px;
152
+ }
91
153
  #config-panel {
92
154
  flex-shrink: 0;
93
155
  background: #181825;
@@ -135,9 +197,16 @@
135
197
  <header>
136
198
  <img src="./favicon.svg" alt="" width="20" height="20" />
137
199
  <h1>xq-lsp playground</h1>
200
+ <button id="ast-toggle">Show AST</button>
138
201
  <button id="share-btn">Copy link</button>
139
202
  </header>
140
- <div id="editor"></div>
203
+ <div id="main">
204
+ <div id="editor"></div>
205
+ <div id="ast-panel">
206
+ <div id="ast-label">ast (xq-parser)</div>
207
+ <pre id="ast-output"></pre>
208
+ </div>
209
+ </div>
141
210
  <div id="config-panel">
142
211
  <div id="config-label">lsp-config.xq</div>
143
212
  <textarea
package/demo/main.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import { EditorView, basicSetup } from "codemirror";
2
- import { hoverTooltip, showTooltip } from "@codemirror/view";
3
- import { StateField } from "@codemirror/state";
2
+ import { hoverTooltip, showTooltip, Decoration, WidgetType, type DecorationSet } from "@codemirror/view";
3
+ import { StateField, StateEffect } from "@codemirror/state";
4
4
  import { StreamLanguage } from "@codemirror/language";
5
5
  import { xQuery } from "@codemirror/legacy-modes/mode/xquery";
6
6
  import { linter, lintGutter, forceLinting, type Diagnostic } from "@codemirror/lint";
7
7
  import { autocompletion, type CompletionContext as CMCompletionContext } from "@codemirror/autocomplete";
8
8
  import { analyzeWithAst, analyze, XMLNS_FN } from "../src/analyzer.ts";
9
+ import { getInlayHints } from "../src/inlay-hints.ts";
10
+ import { TextDocument } from "vscode-languageserver-textdocument";
9
11
  import builtinsFn from "../builtins/builtins-fn.xq?raw";
10
12
  import builtinsMath from "../builtins/builtins-math.xq?raw";
11
13
  import builtinsMap from "../builtins/builtins-map.xq?raw";
@@ -13,10 +15,45 @@ import builtinsArray from "../builtins/builtins-array.xq?raw";
13
15
  import { findUndeclaredPrefixUsages } from "../src/namespace-diagnostics.ts";
14
16
  import { runDiagnostics, runHints } from "../src/diagnostics.ts";
15
17
  import { type FileAnalysis } from "../src/types.ts";
18
+ import type { Node } from "xq-parser";
16
19
  import { resolveFunctionAtOffset, resolveSignatureAtOffset, functionSignature } from "../src/hover-core.ts";
17
20
  import { getCompletions } from "../src/completion-core.ts";
21
+ import { getRuntimePredeclaredNamespaces, withPredeclaredNs } from "../src/predeclared-namespaces.ts";
18
22
  import fontoxpath from "fontoxpath";
19
23
 
24
+ // Raw XQuery source for every runtime module file (src/runtimes/<lib>/*.xq), grouped
25
+ // by lib name below. Loaded eagerly via ?raw so this stays a plain string bundled
26
+ // into the browser build — src/runtimes.ts itself uses Node's fs and can't be
27
+ // imported here (see the computeRelativePath fix for the same class of issue).
28
+ const runtimeSources = import.meta.glob("../src/runtimes/*/*.xq", {
29
+ query: "?raw",
30
+ import: "default",
31
+ eager: true,
32
+ }) as Record<string, string>;
33
+
34
+ const runtimeFilesByLib = new Map<string, string[]>();
35
+ for (const [filePath, source] of Object.entries(runtimeSources)) {
36
+ const match = filePath.match(/\/runtimes\/([^/]+)\//);
37
+ if (!match) continue;
38
+ const list = runtimeFilesByLib.get(match[1]) ?? [];
39
+ list.push(source);
40
+ runtimeFilesByLib.set(match[1], list);
41
+ }
42
+
43
+ const runtimeAnalysisCache = new Map<string, FileAnalysis[]>();
44
+ function getRuntimeAnalyses(libs: string[]): FileAnalysis[] {
45
+ const results: FileAnalysis[] = [];
46
+ for (const lib of libs) {
47
+ let cached = runtimeAnalysisCache.get(lib);
48
+ if (!cached) {
49
+ cached = (runtimeFilesByLib.get(lib) ?? []).map((src, i) => analyze(src, `builtin:${lib}:${i}`));
50
+ runtimeAnalysisCache.set(lib, cached);
51
+ }
52
+ results.push(...cached);
53
+ }
54
+ return results;
55
+ }
56
+
20
57
  const DEFAULT_CODE = `(: xq-lsp playground — errors are annotated inline :)
21
58
  for $x in (1, 2, 3)
22
59
  return $x * 2
@@ -79,16 +116,40 @@ function getBuiltins() {
79
116
  return _builtins;
80
117
  }
81
118
 
82
- function getImported(analysis: FileAnalysis): Map<string, FileAnalysis> {
83
- return new Map([["builtin:fn", getBuiltins()]]);
119
+ function getImported(analysis: FileAnalysis, libs: string[]): Map<string, FileAnalysis> {
120
+ const imported = new Map<string, FileAnalysis>([["builtin:fn", getBuiltins()]]);
121
+
122
+ const runtimeByNamespace = new Map<string, FileAnalysis>();
123
+ for (const runtimeAnalysis of getRuntimeAnalyses(libs)) {
124
+ if (runtimeAnalysis.moduleNamespaceUri)
125
+ runtimeByNamespace.set(runtimeAnalysis.moduleNamespaceUri, runtimeAnalysis);
126
+ }
127
+
128
+ for (const imp of analysis.imports) {
129
+ const runtimeAnalysis = runtimeByNamespace.get(imp.namespaceUri);
130
+ if (runtimeAnalysis) imported.set(imp.namespaceUri, runtimeAnalysis);
131
+ }
132
+
133
+ // Runtime modules that are pre-declared (e.g. existdb's util/xmldb) are available
134
+ // without an explicit import.
135
+ for (const nd of getRuntimePredeclaredNamespaces(libs)) {
136
+ if (!imported.has(nd.namespaceUri)) {
137
+ const runtimeAnalysis = runtimeByNamespace.get(nd.namespaceUri);
138
+ if (runtimeAnalysis) imported.set(nd.namespaceUri, runtimeAnalysis);
139
+ }
140
+ }
141
+
142
+ return imported;
84
143
  }
85
144
 
86
145
  // ── Hover ────────────────────────────────────────────────────────────────────
87
146
 
88
147
  const xqueryHover = hoverTooltip((view, pos) => {
89
148
  const text = view.state.doc.toString();
90
- const { analysis } = analyzeWithAst(text, "playground.xq");
91
- const result = resolveFunctionAtOffset(text, pos, analysis, getImported(analysis));
149
+ const libs = getConfigLibs();
150
+ const { analysis: rawAnalysis } = analyzeWithAst(text, "playground.xq");
151
+ const analysis = withPredeclaredNs(rawAnalysis, getRuntimePredeclaredNamespaces(libs));
152
+ const result = resolveFunctionAtOffset(text, pos, analysis, getImported(analysis, libs));
92
153
  if (!result) return null;
93
154
 
94
155
  return {
@@ -118,11 +179,13 @@ function xqueryCompletions(ctx: CMCompletionContext) {
118
179
  if (!word && !ctx.explicit) return null;
119
180
 
120
181
  const text = ctx.state.doc.toString();
121
- const { analysis } = analyzeWithAst(text, "playground.xq");
182
+ const libs = getConfigLibs();
183
+ const { analysis: rawAnalysis } = analyzeWithAst(text, "playground.xq");
184
+ const analysis = withPredeclaredNs(rawAnalysis, getRuntimePredeclaredNamespaces(libs));
122
185
  const entries = getCompletions(
123
186
  { textBeforeCursor: text.slice(0, ctx.pos), cursorOffset: ctx.pos },
124
187
  analysis,
125
- getImported(analysis),
188
+ getImported(analysis, libs),
126
189
  );
127
190
  if (!entries.length) return null;
128
191
 
@@ -151,8 +214,10 @@ const signatureField = StateField.define({
151
214
  showTooltip.computeN([f], (state) => {
152
215
  const text = state.doc.toString();
153
216
  const pos = state.selection.main.head;
154
- const { analysis } = analyzeWithAst(text, "playground.xq");
155
- const sig = resolveSignatureAtOffset(text, pos, analysis, new Map([["builtin:fn", getBuiltins()]]));
217
+ const libs = getConfigLibs();
218
+ const { analysis: rawAnalysis } = analyzeWithAst(text, "playground.xq");
219
+ const analysis = withPredeclaredNs(rawAnalysis, getRuntimePredeclaredNamespaces(libs));
220
+ const sig = resolveSignatureAtOffset(text, pos, analysis, getImported(analysis, libs));
156
221
  if (!sig) return [];
157
222
  const name = sig.fn.qname.prefix
158
223
  ? `${sig.fn.qname.prefix}:${sig.fn.qname.localName}`
@@ -183,6 +248,78 @@ const signatureField = StateField.define({
183
248
  }),
184
249
  });
185
250
 
251
+ // ── Inlay hints ───────────────────────────────────────────────────────────────
252
+
253
+ class InlayHintWidget extends WidgetType {
254
+ constructor(private label: string) {
255
+ super();
256
+ }
257
+ eq(other: InlayHintWidget) {
258
+ return other.label === this.label;
259
+ }
260
+ toDOM() {
261
+ const span = document.createElement("span");
262
+ span.className = "xq-inlay-hint";
263
+ span.textContent = this.label;
264
+ return span;
265
+ }
266
+ }
267
+
268
+ const refreshInlayHints = StateEffect.define<null>();
269
+
270
+ function computeInlayDecorations(text: string): DecorationSet {
271
+ const { analysis: rawAnalysis, ast } = analyzeWithAst(text, "playground.xq");
272
+ if (!ast) return Decoration.none;
273
+ const libs = getConfigLibs();
274
+ const analysis = withPredeclaredNs(rawAnalysis, getRuntimePredeclaredNamespaces(libs));
275
+ const doc = TextDocument.create("playground.xq", "xquery", 0, text);
276
+ const range = { start: doc.positionAt(0), end: doc.positionAt(text.length) };
277
+ const hints = getInlayHints(doc, ast, analysis, getImported(analysis, libs), range);
278
+
279
+ const widgets = hints.map((h) =>
280
+ Decoration.widget({ widget: new InlayHintWidget(h.label as string), side: 1 }).range(doc.offsetAt(h.position)),
281
+ );
282
+ widgets.sort((a, b) => a.from - b.from);
283
+ return Decoration.set(widgets);
284
+ }
285
+
286
+ const inlayHintsField = StateField.define<DecorationSet>({
287
+ create(state) {
288
+ return computeInlayDecorations(state.doc.toString());
289
+ },
290
+ update(decos, tr) {
291
+ if (!tr.docChanged && !tr.effects.some((e) => e.is(refreshInlayHints))) return decos.map(tr.changes);
292
+ return computeInlayDecorations(tr.state.doc.toString());
293
+ },
294
+ provide: (f) => EditorView.decorations.from(f),
295
+ });
296
+
297
+ // ── AST panel ─────────────────────────────────────────────────────────────────
298
+ // Dumps the raw xq-parser tree for the current doc, for debugging analyzer/
299
+ // diagnostic logic against a specific parse shape. Runnable outside the
300
+ // browser too — see `node scripts/dump-ast.ts` (documented in CLAUDE.md).
301
+
302
+ const astPanel = document.getElementById("ast-panel")!;
303
+ const astOutput = document.getElementById("ast-output")!;
304
+ const astToggle = document.getElementById("ast-toggle")!;
305
+
306
+ function setAstVisible(visible: boolean) {
307
+ astPanel.classList.toggle("visible", visible);
308
+ astToggle.classList.toggle("active", visible);
309
+ astToggle.textContent = visible ? "Hide AST" : "Show AST";
310
+ const url = new URL(location.href);
311
+ if (visible) url.searchParams.set("ast", "1");
312
+ else url.searchParams.delete("ast");
313
+ history.replaceState(null, "", url);
314
+ }
315
+
316
+ function renderAst(ast: Node | null, parseError: Error | null) {
317
+ astOutput.textContent = parseError ? `Parse error: ${parseError.message}` : JSON.stringify(ast, null, 2);
318
+ }
319
+
320
+ astToggle.addEventListener("click", () => setAstVisible(!astPanel.classList.contains("visible")));
321
+ setAstVisible(new URLSearchParams(location.search).get("ast") === "1");
322
+
186
323
  // ── Config panel ─────────────────────────────────────────────────────────────
187
324
 
188
325
  const configEl = document.getElementById("config-editor") as HTMLTextAreaElement;
@@ -205,9 +342,23 @@ function getConfigPrefixes(): Record<string, string> {
205
342
  }
206
343
  }
207
344
 
345
+ function getConfigLibs(): string[] {
346
+ const text = configEl.value.trim();
347
+ if (!text) return [];
348
+ try {
349
+ return fontoxpath.evaluateXPathToStrings(`(${text})?lib`, null, null, {});
350
+ } catch {
351
+ return [];
352
+ }
353
+ }
354
+
208
355
  const xqueryLinter = linter((view): Diagnostic[] => {
209
356
  const code = view.state.doc.toString();
210
- const { analysis, ast, parseError } = analyzeWithAst(code, "playground.xq");
357
+ const libs = getConfigLibs();
358
+ const { analysis: rawAnalysis, ast, parseError } = analyzeWithAst(code, "playground.xq");
359
+ const analysis = withPredeclaredNs(rawAnalysis, getRuntimePredeclaredNamespaces(libs));
360
+ renderAst(ast, parseError);
361
+
211
362
  const configPrefixes = getConfigPrefixes();
212
363
  const diagnostics: Diagnostic[] = [];
213
364
 
@@ -234,7 +385,7 @@ const xqueryLinter = linter((view): Diagnostic[] => {
234
385
  }
235
386
 
236
387
  if (ast) {
237
- const imported = getImported(analysis);
388
+ const imported = getImported(analysis, libs);
238
389
  for (const d of runDiagnostics(ast, code, analysis, imported)) {
239
390
  diagnostics.push({ from: d.offset, to: d.offset + d.length, severity: "error", message: d.message });
240
391
  }
@@ -258,6 +409,7 @@ const view = new EditorView({
258
409
  xqueryHover,
259
410
  autocompletion({ override: [xqueryCompletions] }),
260
411
  signatureField,
412
+ inlayHintsField,
261
413
  EditorView.updateListener.of((update) => {
262
414
  if (!update.docChanged) return;
263
415
  const url = new URL(location.href);
@@ -289,6 +441,7 @@ function applyConfig() {
289
441
  }
290
442
  history.replaceState(null, "", url);
291
443
  forceLinting(view);
444
+ view.dispatch({ effects: refreshInlayHints.of(null) });
292
445
  }
293
446
 
294
447
  applyConfig();
@@ -12,6 +12,8 @@
12
12
  "@codemirror/lint": "^6.8.4",
13
13
  "codemirror": "^6.0.1",
14
14
  "fontoxpath": "^3.34.0",
15
+ "vscode-languageserver": "^9.0.1",
16
+ "vscode-languageserver-textdocument": "^1.0.12",
15
17
  "xq-parser": "^0.4.0"
16
18
  },
17
19
  "devDependencies": {
@@ -1270,6 +1272,49 @@
1270
1272
  }
1271
1273
  }
1272
1274
  },
1275
+ "node_modules/vscode-jsonrpc": {
1276
+ "version": "8.2.0",
1277
+ "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
1278
+ "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
1279
+ "license": "MIT",
1280
+ "engines": {
1281
+ "node": ">=14.0.0"
1282
+ }
1283
+ },
1284
+ "node_modules/vscode-languageserver": {
1285
+ "version": "9.0.1",
1286
+ "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz",
1287
+ "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==",
1288
+ "license": "MIT",
1289
+ "dependencies": {
1290
+ "vscode-languageserver-protocol": "3.17.5"
1291
+ },
1292
+ "bin": {
1293
+ "installServerIntoExtension": "bin/installServerIntoExtension"
1294
+ }
1295
+ },
1296
+ "node_modules/vscode-languageserver-protocol": {
1297
+ "version": "3.17.5",
1298
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
1299
+ "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
1300
+ "license": "MIT",
1301
+ "dependencies": {
1302
+ "vscode-jsonrpc": "8.2.0",
1303
+ "vscode-languageserver-types": "3.17.5"
1304
+ }
1305
+ },
1306
+ "node_modules/vscode-languageserver-textdocument": {
1307
+ "version": "1.0.12",
1308
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz",
1309
+ "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==",
1310
+ "license": "MIT"
1311
+ },
1312
+ "node_modules/vscode-languageserver-types": {
1313
+ "version": "3.17.5",
1314
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
1315
+ "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
1316
+ "license": "MIT"
1317
+ },
1273
1318
  "node_modules/w3c-keyname": {
1274
1319
  "version": "2.2.8",
1275
1320
  "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
package/demo/package.json CHANGED
@@ -12,6 +12,8 @@
12
12
  "@codemirror/lint": "^6.8.4",
13
13
  "codemirror": "^6.0.1",
14
14
  "fontoxpath": "^3.34.0",
15
+ "vscode-languageserver": "^9.0.1",
16
+ "vscode-languageserver-textdocument": "^1.0.12",
15
17
  "xq-parser": "^0.4.0"
16
18
  },
17
19
  "devDependencies": {
@@ -10,6 +10,9 @@ export default defineConfig({
10
10
  // xq-parser is imported by ../src/analyzer.ts; Rollup resolves it
11
11
  // from src/'s ancestor chain and misses demo/node_modules.
12
12
  "xq-parser": resolve(__dirname, "node_modules/xq-parser"),
13
+ // vscode-languageserver/node.js is imported by ../src/inlay-hints.ts for
14
+ // the InlayHintKind enum; same ancestor-chain resolution gap as xq-parser.
15
+ "vscode-languageserver/node.js": resolve(__dirname, "node_modules/vscode-languageserver/node.js"),
13
16
  },
14
17
  },
15
18
  });
@@ -3,6 +3,7 @@ import type { FileAnalysis } from "./types.ts";
3
3
  export declare const XMLNS_FN = "http://www.w3.org/2005/xpath-functions";
4
4
  export declare const XMLNS_LOCAL = "http://www.w3.org/2005/xquery-local-functions";
5
5
  export declare const XMLNS_XS = "http://www.w3.org/2001/XMLSchema";
6
+ export declare const XMLNS_XSI = "http://www.w3.org/2001/XMLSchema-instance";
6
7
  export declare const XMLNS_MATH = "http://www.w3.org/2005/xpath-functions/math";
7
8
  export declare const XMLNS_MAP = "http://www.w3.org/2005/xpath-functions/map";
8
9
  export declare const XMLNS_ARRAY = "http://www.w3.org/2005/xpath-functions/array";
package/dist/analyzer.js CHANGED
@@ -3,13 +3,20 @@ import { XQuery31Full } from "xq-parser";
3
3
  export const XMLNS_FN = "http://www.w3.org/2005/xpath-functions";
4
4
  export const XMLNS_LOCAL = "http://www.w3.org/2005/xquery-local-functions";
5
5
  export const XMLNS_XS = "http://www.w3.org/2001/XMLSchema";
6
+ export const XMLNS_XSI = "http://www.w3.org/2001/XMLSchema-instance";
6
7
  export const XMLNS_MATH = "http://www.w3.org/2005/xpath-functions/math";
7
8
  export const XMLNS_MAP = "http://www.w3.org/2005/xpath-functions/map";
8
9
  export const XMLNS_ARRAY = "http://www.w3.org/2005/xpath-functions/array";
10
+ // fn, local, xs, xsi, and xml are the five namespaces the XQuery 3.1 spec
11
+ // (§2.1.1 Static Context) mandates as pre-declared in every processor's static
12
+ // context, independent of any runtime/library configuration. math/map/array
13
+ // aren't spec-mandated but are universally available in practice (they back
14
+ // the 3.1 map/array/math core function libraries), so they're predeclared here too.
9
15
  const BUILTIN_PREFIXES = {
10
16
  fn: XMLNS_FN,
11
17
  local: XMLNS_LOCAL,
12
18
  xs: XMLNS_XS,
19
+ xsi: XMLNS_XSI,
13
20
  math: XMLNS_MATH,
14
21
  map: XMLNS_MAP,
15
22
  array: XMLNS_ARRAY,
@@ -173,6 +180,15 @@ function buildPrefixMap(moduleNs, imports, namespaceDecls) {
173
180
  function resolveNamespaceUri(prefix, prefixMap) {
174
181
  return prefixMap.get(prefix) ?? `urn:xq-lsp:undeclared:${prefix}`;
175
182
  }
183
+ /**
184
+ * Per the BracedURILiteral grammar, whitespace inside `Q{...}` is insignificant:
185
+ * leading/trailing whitespace is trimmed and internal runs are collapsed to a
186
+ * single space, so `Q{ urn:foo bar }` and `Q{urn:foo bar}` name the same URI.
187
+ * This does not decode percent-escapes — `Q{foo%20bar}` stays distinct from `Q{foo bar}`.
188
+ */
189
+ function normalizeBracedUri(uri) {
190
+ return uri.trim().replace(/\s+/g, " ");
191
+ }
176
192
  /**
177
193
  * Split a raw EQName terminal into its components, handling both the
178
194
  * conventional `prefix:local` form and the `Q{uri}local` URIQualifiedName form.
@@ -182,7 +198,7 @@ export function parseEQName(raw) {
182
198
  if (raw.startsWith("Q{")) {
183
199
  const close = raw.indexOf("}");
184
200
  if (close >= 0)
185
- return { prefix: "", localName: raw.slice(close + 1), uri: raw.slice(2, close) };
201
+ return { prefix: "", localName: raw.slice(close + 1), uri: normalizeBracedUri(raw.slice(2, close)) };
186
202
  }
187
203
  const ci = raw.indexOf(":");
188
204
  return ci >= 0 ? { prefix: raw.slice(0, ci), localName: raw.slice(ci + 1) } : { prefix: "", localName: raw };
@@ -249,18 +265,25 @@ function extractFunctions(ast, sourceUri, comments, text, prefixMap, defaultFnNs
249
265
  }
250
266
  return results;
251
267
  }
252
- function extractModuleVariables(ast, sourceUri, prefixMap) {
268
+ function extractModuleVariables(ast, sourceUri, prefixMap, comments, text) {
253
269
  const results = [];
254
- for (const varDecl of findAll(ast, "VarDecl")) {
270
+ // Use AnnotatedDecl (starts at 'declare') so the comment lookup clears the 'declare' keyword,
271
+ // matching how extractFunctions locates its doc comment.
272
+ for (const annotated of findAll(ast, "AnnotatedDecl")) {
273
+ const varDecl = directChildOf(annotated, "VarDecl");
274
+ if (!varDecl)
275
+ continue;
255
276
  const varName = directChildOf(varDecl, "VarName");
256
277
  const name = varName ? firstTerminalValue(varName) : null;
257
278
  if (!name)
258
279
  continue;
280
+ const doc = findPrecedingDoc(comments, text, annotated.start);
259
281
  results.push({
260
282
  qname: makeVarQName(name, prefixMap),
261
283
  offset: varDecl.start,
262
284
  isModuleLevel: true,
263
285
  sourceUri,
286
+ doc: doc?.description || undefined,
264
287
  });
265
288
  }
266
289
  return results;
@@ -323,7 +346,7 @@ function analyzeAst(ast, comments, text, sourceUri) {
323
346
  const prefixMap = buildPrefixMap(moduleNs, imports, namespaceDecls);
324
347
  return {
325
348
  functions: extractFunctions(ast, sourceUri, comments, text, prefixMap, defaultFunctionNamespace),
326
- moduleVariables: extractModuleVariables(ast, sourceUri, prefixMap),
349
+ moduleVariables: extractModuleVariables(ast, sourceUri, prefixMap, comments, text),
327
350
  localBindings: extractLocalBindings(ast, sourceUri, prefixMap),
328
351
  imports,
329
352
  namespaceDecls,
@@ -393,11 +416,13 @@ function analyzeRegex(text, sourceUri) {
393
416
  }
394
417
  RE_VAR_DECL.lastIndex = 0;
395
418
  while ((m = RE_VAR_DECL.exec(text)) !== null) {
419
+ const doc = findPrecedingDocInText(text, m.index);
396
420
  moduleVariables.push({
397
421
  qname: makeVarQName(m[1], prefixMap),
398
422
  offset: m.index,
399
423
  isModuleLevel: true,
400
424
  sourceUri,
425
+ doc: doc?.description || undefined,
401
426
  });
402
427
  }
403
428
  RE_LET.lastIndex = 0;
@@ -3,6 +3,7 @@ import type { QName, FileAnalysis } from "./types.ts";
3
3
  export interface FunctionCallShape {
4
4
  qname: QName;
5
5
  args: Node[];
6
+ eqnameNode: Node;
6
7
  }
7
8
  export declare function asFunctionCall(node: Node, analysis: FileAnalysis): FunctionCallShape | null;
8
9
  export interface NamedFunctionRefShape {
package/dist/ast-nodes.js CHANGED
@@ -51,7 +51,7 @@ export function asFunctionCall(node, analysis) {
51
51
  const namespaceUri = uri ?? resolvePrefix(prefix, analysis);
52
52
  const argList = directChildOf(node, "ArgumentList");
53
53
  const args = argList ? directChildrenOf(argList, "Argument") : [];
54
- return { qname: { prefix, localName, namespaceUri }, args };
54
+ return { qname: { prefix, localName, namespaceUri }, args, eqnameNode: fnEqname };
55
55
  }
56
56
  export function asNamedFunctionRef(node, analysis) {
57
57
  if (node.type !== "NamedFunctionRef")
@@ -0,0 +1,13 @@
1
+ import type { Node } from "xq-parser";
2
+ export interface BracedUriDiagnostic {
3
+ message: string;
4
+ code: "xq-lsp:braced-uri-whitespace";
5
+ offset: number;
6
+ length: number;
7
+ }
8
+ /**
9
+ * Flag whitespace inside a `Q{uri}local` URIQualifiedName. The grammar treats it
10
+ * as insignificant (trimmed and collapsed to single spaces), so two literals that
11
+ * look different can name the same URI, and vice versa — easy to typo unnoticed.
12
+ */
13
+ export declare function checkBracedUriWhitespace(ast: Node): BracedUriDiagnostic[];
@@ -0,0 +1,27 @@
1
+ import { findAll, firstTerminalValue } from "./analyzer.js";
2
+ /**
3
+ * Flag whitespace inside a `Q{uri}local` URIQualifiedName. The grammar treats it
4
+ * as insignificant (trimmed and collapsed to single spaces), so two literals that
5
+ * look different can name the same URI, and vice versa — easy to typo unnoticed.
6
+ */
7
+ export function checkBracedUriWhitespace(ast) {
8
+ const out = [];
9
+ for (const node of findAll(ast, "URIQualifiedName")) {
10
+ const raw = firstTerminalValue(node);
11
+ if (!raw)
12
+ continue;
13
+ const close = raw.indexOf("}");
14
+ if (close < 2)
15
+ continue;
16
+ const uri = raw.slice(2, close);
17
+ if (!/\s/.test(uri))
18
+ continue;
19
+ out.push({
20
+ message: "Whitespace inside 'Q{...}' is insignificant (trimmed/collapsed by the grammar) and easy to misread — remove it to avoid ambiguity.",
21
+ code: "xq-lsp:braced-uri-whitespace",
22
+ offset: (node.start ?? 0) + 2,
23
+ length: close - 2,
24
+ });
25
+ }
26
+ return out;
27
+ }