xq-lsp 1.5.0 → 1.7.0

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 (84) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +23 -1
  3. package/demo/favicon.svg +6 -0
  4. package/demo/index.html +2 -0
  5. package/demo/main.ts +3 -12
  6. package/dist/analyzer.js +9 -6
  7. package/dist/ast-nodes.d.ts +29 -0
  8. package/dist/ast-nodes.js +63 -10
  9. package/dist/completion-core.d.ts +15 -2
  10. package/dist/completion-core.js +86 -13
  11. package/dist/completion.d.ts +8 -2
  12. package/dist/completion.js +36 -9
  13. package/dist/definition.d.ts +1 -1
  14. package/dist/definition.js +85 -9
  15. package/dist/diagnostics.d.ts +20 -0
  16. package/dist/diagnostics.js +32 -0
  17. package/dist/duplicate-function-diagnostics.d.ts +17 -0
  18. package/dist/duplicate-function-diagnostics.js +47 -0
  19. package/dist/module-import-diagnostics.d.ts +19 -0
  20. package/dist/module-import-diagnostics.js +35 -0
  21. package/dist/namespace-diagnostics.d.ts +7 -1
  22. package/dist/namespace-diagnostics.js +54 -13
  23. package/dist/qt4-worker.d.ts +8 -0
  24. package/dist/qt4-worker.js +37 -12
  25. package/dist/references.d.ts +56 -0
  26. package/dist/references.js +496 -0
  27. package/dist/runtimes/existdb/cache.xq +83 -0
  28. package/dist/runtimes/existdb/compression.xq +198 -0
  29. package/dist/runtimes/existdb/console.xq +24 -0
  30. package/dist/runtimes/existdb/contentextraction.xq +31 -0
  31. package/dist/runtimes/existdb/counter.xq +30 -0
  32. package/dist/runtimes/existdb/crypto.xq +97 -0
  33. package/dist/runtimes/existdb/datetime.xq +91 -0
  34. package/dist/runtimes/existdb/exist.xq +29 -0
  35. package/dist/runtimes/existdb/file.xq +129 -0
  36. package/dist/runtimes/existdb/ft.xq +325 -0
  37. package/dist/runtimes/existdb/httpclient.xq +118 -0
  38. package/dist/runtimes/existdb/image.xq +65 -0
  39. package/dist/runtimes/existdb/inspect.xq +49 -0
  40. package/dist/runtimes/existdb/kwic.xq +24 -0
  41. package/dist/runtimes/existdb/mail.xq +124 -0
  42. package/dist/runtimes/existdb/ngram.xq +57 -0
  43. package/dist/runtimes/existdb/process.xq +8 -0
  44. package/dist/runtimes/existdb/range.xq +171 -0
  45. package/dist/runtimes/existdb/repo.xq +125 -0
  46. package/dist/runtimes/existdb/request.xq +242 -0
  47. package/dist/runtimes/existdb/response.xq +50 -0
  48. package/dist/runtimes/existdb/scheduler.xq +35 -0
  49. package/dist/runtimes/existdb/session.xq +111 -0
  50. package/dist/runtimes/existdb/sm.xq +497 -0
  51. package/dist/runtimes/existdb/sort.xq +59 -0
  52. package/dist/runtimes/existdb/system.xq +267 -0
  53. package/dist/runtimes/existdb/transform.xq +88 -0
  54. package/dist/runtimes/existdb/util.xq +892 -0
  55. package/dist/runtimes/existdb/validation.xq +83 -0
  56. package/dist/runtimes/existdb/xmldb.xq +482 -0
  57. package/dist/runtimes/existdb/xmldiff.xq +22 -0
  58. package/dist/runtimes/fonto/fonto.xq +164 -0
  59. package/dist/runtimes.js +40 -4
  60. package/dist/server.js +139 -90
  61. package/dist/types.d.ts +3 -0
  62. package/dist/unused-diagnostics.d.ts +1 -1
  63. package/dist/unused-diagnostics.js +74 -1
  64. package/dist/variable-diagnostics.js +77 -7
  65. package/logo.svg +6 -0
  66. package/package.json +2 -2
  67. package/src/analyzer.ts +9 -6
  68. package/src/ast-nodes.ts +89 -10
  69. package/src/completion-core.ts +100 -12
  70. package/src/completion.ts +52 -9
  71. package/src/definition.ts +85 -8
  72. package/src/diagnostics.ts +47 -0
  73. package/src/duplicate-function-diagnostics.ts +55 -0
  74. package/src/module-import-diagnostics.ts +43 -0
  75. package/src/namespace-diagnostics.ts +53 -12
  76. package/src/qt4-worker.ts +53 -10
  77. package/src/references.ts +602 -0
  78. package/src/runtimes/existdb/exist.xq +29 -0
  79. package/src/runtimes.ts +41 -8
  80. package/src/server.ts +157 -91
  81. package/src/types.ts +3 -0
  82. package/src/unused-diagnostics.ts +75 -2
  83. package/src/variable-diagnostics.ts +65 -5
  84. package/src/runtimes/existdb/predeclared-namespaces.json +0 -34
package/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.7.0](https://github.com/DrRataplan/xq-lsp/compare/lsp-v1.6.0...lsp-v1.7.0) (2026-07-15)
4
+
5
+
6
+ ### Features
7
+
8
+ * add document highlight support (textDocument/documentHighlight) ([6d90ad7](https://github.com/DrRataplan/xq-lsp/commit/6d90ad7b63e50b10b68705e819320035318a56da))
9
+ * add find-references support via cross-file glob scan ([f0b0085](https://github.com/DrRataplan/xq-lsp/commit/f0b00852265c68ec95b96d86781eef41cb9dd940))
10
+ * add rename support (textDocument/rename, prepareRename) ([d0cb55d](https://github.com/DrRataplan/xq-lsp/commit/d0cb55dbcba1d5b8520f7a2080818cdc420e06e6))
11
+ * add XQST0034 diagnostic for double-declared functions ([6d44620](https://github.com/DrRataplan/xq-lsp/commit/6d44620e5b41e7de2c0a548086abec0b45ae22ff))
12
+ * add XQST0059 diagnostic for module import namespace mismatches ([d8111fc](https://github.com/DrRataplan/xq-lsp/commit/d8111fcbf034d23306a53cfe28bd7c3dd43ce3a7))
13
+ * auto-import and auto-declare-namespace via completion additionalTextEdits ([e1a9a93](https://github.com/DrRataplan/xq-lsp/commit/e1a9a935dea941bf35a3dc580ee77f7e88dd22c1))
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * don't blanket-skip XQUpdate-tagged QT4 tests ([f2e45f1](https://github.com/DrRataplan/xq-lsp/commit/f2e45f18022873cb3bb5105eb727b484bedfd518))
19
+ * don't count literal fn:error() code calls as expected static errors ([0bead1b](https://github.com/DrRataplan/xq-lsp/commit/0bead1b8044c52daa2d7f2a11a24f8512d6ee2a9))
20
+ * fall back to glob-resolved namespace when import location hint fails ([6ce03db](https://github.com/DrRataplan/xq-lsp/commit/6ce03db6d9c5deb1fa501617c29b6005f2540102))
21
+ * go-to-definition for function params and imported variables ([415cfdd](https://github.com/DrRataplan/xq-lsp/commit/415cfdd4717b71af66041a7727641f18d091175d))
22
+ * go-to-definition for inline function params and dashed variable names ([a4e7279](https://github.com/DrRataplan/xq-lsp/commit/a4e7279d4b27357bb27e7b67f2ed0551650ef66e))
23
+ * handle InlineFunctionExpr scope in variable and unused-variable walkers ([de19b18](https://github.com/DrRataplan/xq-lsp/commit/de19b186fd367dc1450de1554205e8c07d0e40fd))
24
+ * insert auto-import after last existing import, skip multi-line docblocks ([12227a9](https://github.com/DrRataplan/xq-lsp/commit/12227a916dcdafeb2ba5ddef21e82b4e2f6acbca))
25
+ * load external <test file> queries and skip unsupported QT4 features ([4cbdc58](https://github.com/DrRataplan/xq-lsp/commit/4cbdc589b9af65f9043e3c0de6b5ffe950f3c7fe))
26
+ * make computeRelativePath browser-safe and build the demo in CI ([033a057](https://github.com/DrRataplan/xq-lsp/commit/033a057849cbc492f8e243cf8f4e908200673033))
27
+ * model QT4 environment-bound variables to cut XPST0008 false positives ([5dcb404](https://github.com/DrRataplan/xq-lsp/commit/5dcb4040bd79c0b7dc8edac5086b2289b1582b9e))
28
+ * rewrite imported variable prefix to the importing module's prefix in completions ([f7c5ccc](https://github.com/DrRataplan/xq-lsp/commit/f7c5ccc337af75bae0c2d881210ba3373c90bb71))
29
+ * scope inline function params, window vars, and copy bindings for XPST0008 ([7391a94](https://github.com/DrRataplan/xq-lsp/commit/7391a94c2607a028b6636ad6fcdadb29ba1356e5))
30
+
31
+ ## [1.6.0](https://github.com/DrRataplan/xq-lsp/compare/lsp-v1.5.0...lsp-v1.6.0) (2026-07-09)
32
+
33
+
34
+ ### Features
35
+
36
+ * add diagnostics for unused imports and namespace declarations ([0ded7ba](https://github.com/DrRataplan/xq-lsp/commit/0ded7ba26b98bc308ca59cf7061f8ade197a4d13))
37
+ * **claude-code:** add xq-lsp plugin for Claude Code ([aa96a4d](https://github.com/DrRataplan/xq-lsp/commit/aa96a4d5fdd9192e33be862608674d71e020017f))
38
+ * **vscode:** add XQuery 4.0 syntax highlighting grammar ([f2ee489](https://github.com/DrRataplan/xq-lsp/commit/f2ee489ff1ec3ab67f9142d6c039c559d28d5e67))
39
+
40
+
41
+ ### Bug Fixes
42
+
43
+ * add the $exist:* variables to the exist runtime ([4798582](https://github.com/DrRataplan/xq-lsp/commit/4798582c28f036c959432dbcca56edc81f7cca93))
44
+ * copy runtime files to dist and inline existdb predeclared namespaces ([e98b6d6](https://github.com/DrRataplan/xq-lsp/commit/e98b6d672ba504bba5dda30980b7a58cbdfcf847))
45
+ * **vscode:** recognize .xquery file extension ([7afc049](https://github.com/DrRataplan/xq-lsp/commit/7afc04913c1b02419c1965c489c32d09df1834b6))
46
+
3
47
  ## [1.5.0](https://github.com/DrRataplan/xq-lsp/compare/lsp-v1.4.1...lsp-v1.5.0) (2026-06-18)
4
48
 
5
49
 
package/README.md CHANGED
@@ -1,9 +1,12 @@
1
+ <img src="./logo.svg" alt="xq-lsp" width="48" height="48">
2
+
1
3
  # xq-lsp
2
4
 
3
5
  Language Server Protocol implementation for XQuery, providing autocompletion, hover documentation, signature help, go-to-definition, and diagnostics.
4
6
 
5
7
  ## Features
6
8
 
9
+ - **Syntax highlighting** — keywords, types, variables, operators, comments, and XQuery 4.0 tokens (VS Code extension)
7
10
  - **Completion** — functions declared in the current file, imported modules, and variables in scope
8
11
  - **Hover** — function signatures on hover
9
12
  - **Signature help** — active parameter hints inside function calls
@@ -234,4 +237,23 @@ npx vsce package
234
237
  code --install-extension xquery-lsp-vscode-*.vsix
235
238
  ```
236
239
 
237
- The extension registers `.xq`, `.xql`, `.xqm`, and `.xqy` files as XQuery and starts `xq-lsp` automatically when you open one.
240
+ The extension registers `.xq`, `.xql`, `.xqm`, `.xqy`, and `.xquery` files as XQuery, provides syntax highlighting via a bundled TextMate grammar, and starts `xq-lsp` automatically when you open one.
241
+
242
+ ## Claude Code
243
+
244
+ A plugin lives in [`editors/claude-code/`](./editors/claude-code/) that registers `xq-lsp` as a native LSP server for Claude Code, giving diagnostics, go-to-definition, find-references, and hover for XQuery files.
245
+
246
+ Make sure the `xq-lsp` binary is on `PATH`:
247
+
248
+ ```sh
249
+ npm install -g xq-lsp
250
+ ```
251
+
252
+ Then, from within Claude Code:
253
+
254
+ ```
255
+ /plugin marketplace add DrRataplan/xq-lsp
256
+ /plugin install xquery-lsp@xq-lsp
257
+ ```
258
+
259
+ See [`editors/claude-code/README.md`](./editors/claude-code/README.md) for local-development setup and details on what the plugin configures.
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
+ <rect width="32" height="32" rx="7" fill="#1e1b4b"/>
3
+ <path d="M5 9 L11 16 L5 23" stroke="#a78bfa" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
4
+ <path d="M27 9 L21 16 L27 23" stroke="#a78bfa" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
5
+ <circle cx="16" cy="16" r="2.5" fill="#a78bfa"/>
6
+ </svg>
package/demo/index.html CHANGED
@@ -4,6 +4,7 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>xq-lsp playground</title>
7
+ <link rel="icon" type="image/svg+xml" href="./favicon.svg" />
7
8
  <style>
8
9
  * {
9
10
  box-sizing: border-box;
@@ -132,6 +133,7 @@
132
133
  </head>
133
134
  <body>
134
135
  <header>
136
+ <img src="./favicon.svg" alt="" width="20" height="20" />
135
137
  <h1>xq-lsp playground</h1>
136
138
  <button id="share-btn">Copy link</button>
137
139
  </header>
package/demo/main.ts CHANGED
@@ -11,10 +11,7 @@ import builtinsMath from "../builtins/builtins-math.xq?raw";
11
11
  import builtinsMap from "../builtins/builtins-map.xq?raw";
12
12
  import builtinsArray from "../builtins/builtins-array.xq?raw";
13
13
  import { findUndeclaredPrefixUsages } from "../src/namespace-diagnostics.ts";
14
- import { checkTypes } from "../src/typechecker.ts";
15
- import { checkFunctionCalls } from "../src/functioncall-diagnostics.ts";
16
- import { checkUnused } from "../src/unused-diagnostics.ts";
17
- import { checkContextItemUsage } from "../src/context-item-diagnostics.ts";
14
+ import { runDiagnostics, runHints } from "../src/diagnostics.ts";
18
15
  import { type FileAnalysis } from "../src/types.ts";
19
16
  import { resolveFunctionAtOffset, resolveSignatureAtOffset, functionSignature } from "../src/hover-core.ts";
20
17
  import { getCompletions } from "../src/completion-core.ts";
@@ -238,18 +235,12 @@ const xqueryLinter = linter((view): Diagnostic[] => {
238
235
 
239
236
  if (ast) {
240
237
  const imported = getImported(analysis);
241
- for (const d of checkTypes(ast, code, analysis, new Map())) {
242
- diagnostics.push({ from: d.offset, to: d.offset + d.length, severity: "warning", message: d.message });
243
- }
244
- for (const d of checkFunctionCalls(ast, analysis, imported)) {
238
+ for (const d of runDiagnostics(ast, code, analysis, imported)) {
245
239
  diagnostics.push({ from: d.offset, to: d.offset + d.length, severity: "error", message: d.message });
246
240
  }
247
- for (const d of checkUnused(ast, analysis)) {
241
+ for (const d of runHints(ast, analysis)) {
248
242
  diagnostics.push({ from: d.offset, to: d.offset + d.length, severity: "hint", message: d.message });
249
243
  }
250
- for (const d of checkContextItemUsage(ast)) {
251
- diagnostics.push({ from: d.offset, to: d.offset + d.length, severity: "error", message: d.message });
252
- }
253
244
  }
254
245
 
255
246
  return diagnostics;
package/dist/analyzer.js CHANGED
@@ -229,6 +229,7 @@ function extractFunctions(ast, sourceUri, comments, text, prefixMap, defaultFnNs
229
229
  name: paramName,
230
230
  type: paramType,
231
231
  description: doc?.params[paramName],
232
+ sourceOffset: param.start,
232
233
  });
233
234
  }
234
235
  }
@@ -291,7 +292,7 @@ function extractNamespaceDecls(ast) {
291
292
  const namespaceUri = uriNode ? stripQuotes(firstTerminalValue(uriNode) ?? "") : null;
292
293
  if (!namespaceUri)
293
294
  continue;
294
- results.push({ prefix, namespaceUri });
295
+ results.push({ prefix, namespaceUri, offset: ncname.start ?? nd.start ?? 0 });
295
296
  }
296
297
  return results;
297
298
  }
@@ -307,7 +308,7 @@ function extractImports(ast) {
307
308
  continue;
308
309
  const namespaceUri = stripQuotes(firstTerminalValue(uris[0]) ?? "");
309
310
  const atPath = uris.length >= 2 ? stripQuotes(firstTerminalValue(uris[1]) ?? "") : undefined;
310
- results.push({ prefix, namespaceUri, atPath });
311
+ results.push({ prefix, namespaceUri, atPath, offset: ncname.start ?? mi.start ?? 0 });
311
312
  }
312
313
  return results;
313
314
  }
@@ -338,8 +339,8 @@ const RE_FUNC = /declare\s+(?:%[\w:\-]+(?:\([^)]*\))?\s+)*function\s+([\w:\-]+)\
338
339
  const RE_VAR_DECL = /declare\s+(?:%[\w:\-]+(?:\([^)]*\))?\s+)*variable\s+\$([\w:\-]+)/g;
339
340
  const RE_LET = /\blet\s+\$([\w:\-]+)\s*:=/g;
340
341
  const RE_FOR = /\bfor\s+\$([\w:\-]+)\s+in\b/g;
341
- const RE_IMPORT = /import\s+module\s+namespace\s+([\w\-]+)\s*=\s*["']([^"']*)["'](?:\s+at\s+["']([^"']*)["'])?/g;
342
- const RE_NS_DECL = /declare\s+namespace\s+([\w\-]+)\s*=\s*["']([^"']*)["']/g;
342
+ const RE_IMPORT = /import\s+module\s+namespace\s+([\w\-]+)\s*=\s*["']([^"']*)["'](?:\s+at\s+["']([^"']*)["'])?/gd;
343
+ const RE_NS_DECL = /declare\s+namespace\s+([\w\-]+)\s*=\s*["']([^"']*)["']/gd;
343
344
  function analyzeRegex(text, sourceUri) {
344
345
  const moduleVariables = [];
345
346
  const localBindings = [];
@@ -350,7 +351,8 @@ function analyzeRegex(text, sourceUri) {
350
351
  const imports = [];
351
352
  RE_IMPORT.lastIndex = 0;
352
353
  while ((m = RE_IMPORT.exec(text)) !== null) {
353
- const imp = { prefix: m[1], namespaceUri: m[2] };
354
+ const indices = m.indices;
355
+ const imp = { prefix: m[1], namespaceUri: m[2], offset: indices[1][0] };
354
356
  if (m[3])
355
357
  imp.atPath = m[3];
356
358
  imports.push(imp);
@@ -358,7 +360,8 @@ function analyzeRegex(text, sourceUri) {
358
360
  const namespaceDecls = [];
359
361
  RE_NS_DECL.lastIndex = 0;
360
362
  while ((m = RE_NS_DECL.exec(text)) !== null) {
361
- namespaceDecls.push({ prefix: m[1], namespaceUri: m[2] });
363
+ const indices = m.indices;
364
+ namespaceDecls.push({ prefix: m[1], namespaceUri: m[2], offset: indices[1][0] });
362
365
  }
363
366
  const prefixMap = buildPrefixMap(moduleNs, imports, namespaceDecls);
364
367
  const functions = [];
@@ -27,6 +27,35 @@ export interface FunctionDeclShape {
27
27
  }
28
28
  /** Inner shape of a FunctionDecl node: name, resolved params, and function body. */
29
29
  export declare function asFunctionDecl(node: Node, analysis: FileAnalysis): FunctionDeclShape | null;
30
+ export interface InlineFunctionExprShape {
31
+ params: Array<{
32
+ nameNode: Node;
33
+ qname: QName;
34
+ }>;
35
+ body: Node | null;
36
+ }
37
+ export declare function asInlineFunctionExpr(node: Node, analysis: FileAnalysis): InlineFunctionExprShape | null;
38
+ export interface WindowVarsShape {
39
+ currentItem?: QName;
40
+ positionalVar?: {
41
+ nameNode: Node;
42
+ qname: QName;
43
+ };
44
+ previousItem?: QName;
45
+ nextItem?: QName;
46
+ }
47
+ /** Resolves the `$current at $pos previous $prev next $next` bindings of a WindowVars node. */
48
+ export declare function asWindowVars(node: Node, analysis: FileAnalysis): WindowVarsShape | null;
49
+ export interface CopyBindingShape {
50
+ qname: QName;
51
+ initExpr: Node;
52
+ }
53
+ export interface TransformExprShape {
54
+ copyBindings: CopyBindingShape[];
55
+ modifyExpr: Node | null;
56
+ returnExpr: Node | null;
57
+ }
58
+ export declare function asTransformExpr(node: Node, analysis: FileAnalysis): TransformExprShape | null;
30
59
  /** QName, name node, arity, and annotation local-names for an AnnotatedDecl containing a FunctionDecl, or null. */
31
60
  export declare function asFunctionDeclaration(node: Node, analysis: FileAnalysis): {
32
61
  qname: QName;
package/dist/ast-nodes.js CHANGED
@@ -18,6 +18,26 @@ function resolveQName(raw, analysis, defaultNs) {
18
18
  const namespaceUri = uri ?? resolvePrefix(prefix, analysis);
19
19
  return { prefix, localName, namespaceUri };
20
20
  }
21
+ /** Resolves a variable QName from a node whose first descendant terminal is the bare name (no "$"). */
22
+ function qnameFromRawTerminal(node, analysis) {
23
+ const rawName = firstTerminalValue(node);
24
+ if (!rawName)
25
+ return null;
26
+ const { prefix, localName, uri } = parseEQName(rawName);
27
+ return { prefix, localName, namespaceUri: uri ?? (prefix ? resolvePrefix(prefix, analysis) : "") };
28
+ }
29
+ function extractParamsFromParamList(paramList, analysis) {
30
+ const paramNodes = paramList ? directChildrenOf(paramList, "Param") : [];
31
+ return paramNodes.flatMap((p) => {
32
+ const pNameNode = directChildOf(p, "EQName");
33
+ if (!pNameNode)
34
+ return [];
35
+ const qname = qnameFromRawTerminal(pNameNode, analysis);
36
+ if (!qname)
37
+ return [];
38
+ return [{ nameNode: pNameNode, qname }];
39
+ });
40
+ }
21
41
  export function asFunctionCall(node, analysis) {
22
42
  if (node.type !== "FunctionCall")
23
43
  return null;
@@ -90,18 +110,51 @@ export function asFunctionDecl(node, analysis) {
90
110
  return null;
91
111
  const { prefix, localName, uri } = parseEQName(rawName);
92
112
  const namespaceUri = uri ?? resolvePrefix(prefix, analysis);
93
- const paramList = directChildOf(node, "ParamList");
94
- const paramNodes = paramList ? directChildrenOf(paramList, "Param") : [];
95
- const params = paramNodes.flatMap((p) => {
96
- const pNameNode = directChildOf(p, "EQName");
97
- const pName = pNameNode ? firstTerminalValue(pNameNode) : null;
98
- if (!pName || !pNameNode)
113
+ const params = extractParamsFromParamList(directChildOf(node, "ParamList"), analysis);
114
+ return { nameNode, qname: { prefix, localName, namespaceUri }, params, body: directChildOf(node, "FunctionBody") ?? null };
115
+ }
116
+ export function asInlineFunctionExpr(node, analysis) {
117
+ if (node.type !== "InlineFunctionExpr")
118
+ return null;
119
+ const params = extractParamsFromParamList(directChildOf(node, "ParamList"), analysis);
120
+ return { params, body: directChildOf(node, "FunctionBody") ?? null };
121
+ }
122
+ /** Resolves the `$current at $pos previous $prev next $next` bindings of a WindowVars node. */
123
+ export function asWindowVars(node, analysis) {
124
+ if (node.type !== "WindowVars")
125
+ return null;
126
+ const result = {};
127
+ const currentItemNode = directChildOf(node, "CurrentItem");
128
+ if (currentItemNode)
129
+ result.currentItem = qnameFromRawTerminal(currentItemNode, analysis) ?? undefined;
130
+ const posVarNode = directChildOf(node, "PositionalVar");
131
+ if (posVarNode)
132
+ result.positionalVar = asPositionalVar(posVarNode, analysis) ?? undefined;
133
+ const prevNode = directChildOf(node, "PreviousItem");
134
+ if (prevNode)
135
+ result.previousItem = qnameFromRawTerminal(prevNode, analysis) ?? undefined;
136
+ const nextNode = directChildOf(node, "NextItem");
137
+ if (nextNode)
138
+ result.nextItem = qnameFromRawTerminal(nextNode, analysis) ?? undefined;
139
+ return result;
140
+ }
141
+ export function asTransformExpr(node, analysis) {
142
+ if (node.type !== "XQUF_TransformExpr")
143
+ return null;
144
+ const bindingList = directChildOf(node, "XQUF_CopyBindingList");
145
+ const bindingNodes = bindingList ? directChildrenOf(bindingList, "XQUF_CopyBinding") : [];
146
+ const copyBindings = bindingNodes.flatMap((b) => {
147
+ const nameNode = directChildOf(b, "VarName");
148
+ const initExpr = directChildOf(b, "ExprSingle");
149
+ if (!nameNode || !initExpr)
99
150
  return [];
100
- const { prefix: pPrefix, localName: pLocalName, uri: pUri } = parseEQName(pName);
101
- const pNsUri = pUri ?? (pPrefix ? resolvePrefix(pPrefix, analysis) : "");
102
- return [{ nameNode: pNameNode, qname: { prefix: pPrefix, localName: pLocalName, namespaceUri: pNsUri } }];
151
+ const qname = qnameFromRawTerminal(nameNode, analysis);
152
+ if (!qname)
153
+ return [];
154
+ return [{ qname, initExpr }];
103
155
  });
104
- return { nameNode, qname: { prefix, localName, namespaceUri }, params, body: directChildOf(node, "FunctionBody") ?? null };
156
+ const [modifyExpr, returnExpr] = directChildrenOf(node, "ExprSingle");
157
+ return { copyBindings, modifyExpr: modifyExpr ?? null, returnExpr: returnExpr ?? null };
105
158
  }
106
159
  // ── AnnotatedDecl wrappers ────────────────────────────────────────────────────
107
160
  /** QName, name node, arity, and annotation local-names for an AnnotatedDecl containing a FunctionDecl, or null. */
@@ -5,13 +5,26 @@ export interface CompletionContext {
5
5
  /** Character offset of the cursor in the document */
6
6
  cursorOffset: number;
7
7
  }
8
+ /** Carries enough information for the LSP adapter to build the additionalTextEdit. */
9
+ export interface AdditionalEdit {
10
+ kind: "import-module" | "declare-namespace";
11
+ prefix: string;
12
+ namespaceUri: string;
13
+ /** Source URI of the module file, used to compute the relative `at` path. */
14
+ sourceUri?: string;
15
+ }
8
16
  export interface CompletionEntry {
9
17
  label: string;
10
- kind: "function" | "variable";
18
+ kind: "function" | "variable" | "namespace";
11
19
  detail?: string;
12
20
  documentation?: string;
13
21
  insertText: string;
14
22
  isSnippet: boolean;
23
+ additionalEdit?: AdditionalEdit;
15
24
  }
16
25
  export declare function functionDoc(fn: FunctionSymbol): string;
17
- export declare function getCompletions(ctx: CompletionContext, currentAnalysis: FileAnalysis, importedAnalyses: Map<string, FileAnalysis>, snippets?: boolean, lastValidAnalysis?: FileAnalysis): CompletionEntry[];
26
+ export declare function getCompletions(ctx: CompletionContext, currentAnalysis: FileAnalysis, importedAnalyses: Map<string, FileAnalysis>, snippets?: boolean, lastValidAnalysis?: FileAnalysis,
27
+ /** Modules available in the workspace but not yet imported in the current file. Keyed by namespace URI. */
28
+ availableAnalyses?: Map<string, FileAnalysis>,
29
+ /** Known namespace prefix → URI mappings (from config / glob scans) for pure XML namespaces. */
30
+ knownNamespaces?: Map<string, string>): CompletionEntry[];
@@ -76,7 +76,15 @@ function buildFunctionEntry(fn, label, snippets) {
76
76
  isSnippet: snippets && fn.params.length > 0,
77
77
  };
78
78
  }
79
- export function getCompletions(ctx, currentAnalysis, importedAnalyses, snippets = false, lastValidAnalysis) {
79
+ /** Return the source URI for a module's analysis (taken from the first known symbol). */
80
+ function moduleSourceUri(analysis) {
81
+ return analysis.functions[0]?.sourceUri ?? analysis.moduleVariables[0]?.sourceUri;
82
+ }
83
+ export function getCompletions(ctx, currentAnalysis, importedAnalyses, snippets = false, lastValidAnalysis,
84
+ /** Modules available in the workspace but not yet imported in the current file. Keyed by namespace URI. */
85
+ availableAnalyses,
86
+ /** Known namespace prefix → URI mappings (from config / glob scans) for pure XML namespaces. */
87
+ knownNamespaces) {
80
88
  const token = parseToken(ctx.textBeforeCursor);
81
89
  const items = [];
82
90
  if (token.kind === "variable") {
@@ -132,29 +140,94 @@ export function getCompletions(ctx, currentAnalysis, importedAnalyses, snippets
132
140
  }
133
141
  }
134
142
  }
135
- // Imported module-level variables are always visible
143
+ // Imported module-level variables — rewrite prefix to the one used in the current module
136
144
  for (const analysis of importedAnalyses.values()) {
137
145
  for (const v of analysis.moduleVariables) {
138
- const e = buildVariableEntry(v, filter);
146
+ const localPrefix = currentAnalysis.imports.find((i) => i.namespaceUri === v.qname.namespaceUri)?.prefix;
147
+ const displayVar = localPrefix !== undefined && localPrefix !== v.qname.prefix
148
+ ? { ...v, qname: { ...v.qname, prefix: localPrefix } }
149
+ : v;
150
+ const e = buildVariableEntry(displayVar, filter);
139
151
  if (e)
140
152
  items.push(e);
141
153
  }
142
154
  }
155
+ // Variables from modules available in the workspace but not yet imported
156
+ if (availableAnalyses) {
157
+ const colonIdx = filter.indexOf(":");
158
+ if (colonIdx >= 0) {
159
+ const nsPrefix = filter.slice(0, colonIdx);
160
+ const localFilter = filter.slice(colonIdx + 1).toLowerCase();
161
+ const nsUri = resolvePrefix(nsPrefix, currentAnalysis);
162
+ if (nsUri.startsWith("urn:xq-lsp:undeclared:")) {
163
+ for (const [moduleNsUri, analysis] of availableAnalyses) {
164
+ if (analysis.modulePrefix !== nsPrefix)
165
+ continue;
166
+ const sourceUri = moduleSourceUri(analysis);
167
+ for (const v of analysis.moduleVariables) {
168
+ if (localFilter && !v.qname.localName.toLowerCase().startsWith(localFilter))
169
+ continue;
170
+ const displayQName = { ...v.qname, prefix: nsPrefix };
171
+ items.push({
172
+ label: `$${formatQName(displayQName)}`,
173
+ kind: "variable",
174
+ insertText: `$${formatQName(displayQName)}`,
175
+ isSnippet: false,
176
+ additionalEdit: { kind: "import-module", prefix: nsPrefix, namespaceUri: moduleNsUri, sourceUri },
177
+ });
178
+ }
179
+ }
180
+ }
181
+ }
182
+ }
143
183
  return items;
144
184
  }
145
185
  if (token.kind === "qualified-name" && token.nsPrefix) {
146
186
  const targetUri = resolvePrefix(token.nsPrefix, currentAnalysis);
147
187
  const filter = token.prefix.toLowerCase();
148
- const allFns = [
149
- ...currentAnalysis.functions,
150
- ...[...importedAnalyses.values()].flatMap((a) => a.functions),
151
- ];
152
- for (const fn of allFns) {
153
- if (fn.qname.namespaceUri !== targetUri)
154
- continue;
155
- if (filter && !fn.qname.localName.toLowerCase().includes(filter))
156
- continue;
157
- items.push(buildFunctionEntry(fn, fn.qname.localName, snippets));
188
+ if (!targetUri.startsWith("urn:xq-lsp:undeclared:")) {
189
+ // Prefix is declared — show functions from current + imported analyses (no additionalEdit needed)
190
+ const allFns = [
191
+ ...currentAnalysis.functions,
192
+ ...[...importedAnalyses.values()].flatMap((a) => a.functions),
193
+ ];
194
+ for (const fn of allFns) {
195
+ if (fn.qname.namespaceUri !== targetUri)
196
+ continue;
197
+ if (filter && !fn.qname.localName.toLowerCase().includes(filter))
198
+ continue;
199
+ items.push(buildFunctionEntry(fn, fn.qname.localName, snippets));
200
+ }
201
+ return items;
202
+ }
203
+ // Prefix is undeclared — look for matching modules in the workspace
204
+ let foundModule = false;
205
+ if (availableAnalyses) {
206
+ for (const [moduleNsUri, analysis] of availableAnalyses) {
207
+ if (analysis.modulePrefix !== token.nsPrefix)
208
+ continue;
209
+ foundModule = true;
210
+ const sourceUri = moduleSourceUri(analysis);
211
+ for (const fn of analysis.functions) {
212
+ if (filter && !fn.qname.localName.toLowerCase().includes(filter))
213
+ continue;
214
+ items.push({
215
+ ...buildFunctionEntry(fn, fn.qname.localName, snippets),
216
+ additionalEdit: { kind: "import-module", prefix: token.nsPrefix, namespaceUri: moduleNsUri, sourceUri },
217
+ });
218
+ }
219
+ }
220
+ }
221
+ // Pure XML namespace (no module functions): offer a declare-namespace item if prefix is known
222
+ if (!foundModule && knownNamespaces?.has(token.nsPrefix)) {
223
+ const nsUri = knownNamespaces.get(token.nsPrefix);
224
+ items.push({
225
+ label: `declare namespace ${token.nsPrefix} = "${nsUri}"`,
226
+ kind: "namespace",
227
+ insertText: token.prefix,
228
+ isSnippet: false,
229
+ additionalEdit: { kind: "declare-namespace", prefix: token.nsPrefix, namespaceUri: nsUri },
230
+ });
158
231
  }
159
232
  return items;
160
233
  }
@@ -1,4 +1,10 @@
1
1
  import { CompletionItem } from "vscode-languageserver/node.js";
2
- import { getCompletions as getCoreCompletions } from "./completion-core.ts";
2
+ import type { FileAnalysis } from "./types.ts";
3
+ import type { CompletionContext } from "./completion-core.ts";
3
4
  export type { CompletionContext } from "./completion-core.ts";
4
- export declare function getCompletions(...args: Parameters<typeof getCoreCompletions>): CompletionItem[];
5
+ export interface EditContext {
6
+ docText: string;
7
+ docUri: string;
8
+ generateLocationHints: boolean;
9
+ }
10
+ export declare function getCompletions(ctx: CompletionContext, currentAnalysis: FileAnalysis, importedAnalyses: Map<string, FileAnalysis>, snippets?: boolean, lastValidAnalysis?: FileAnalysis, availableAnalyses?: Map<string, FileAnalysis>, knownNamespaces?: Map<string, string>, editContext?: EditContext): CompletionItem[];
@@ -1,12 +1,39 @@
1
1
  import { CompletionItem, CompletionItemKind, InsertTextFormat, MarkupKind } from "vscode-languageserver/node.js";
2
2
  import { getCompletions as getCoreCompletions } from "./completion-core.js";
3
- export function getCompletions(...args) {
4
- return getCoreCompletions(...args).map((e) => ({
5
- label: e.label,
6
- kind: e.kind === "function" ? CompletionItemKind.Function : CompletionItemKind.Variable,
7
- detail: e.detail,
8
- documentation: e.documentation ? { kind: MarkupKind.Markdown, value: e.documentation } : undefined,
9
- insertText: e.insertText,
10
- insertTextFormat: e.isSnippet ? InsertTextFormat.Snippet : InsertTextFormat.PlainText,
11
- }));
3
+ import { findImportInsertPosition, findDeclareNsInsertPosition, computeRelativePath } from "./namespace-diagnostics.js";
4
+ export function getCompletions(ctx, currentAnalysis, importedAnalyses, snippets, lastValidAnalysis, availableAnalyses, knownNamespaces, editContext) {
5
+ return getCoreCompletions(ctx, currentAnalysis, importedAnalyses, snippets, lastValidAnalysis, availableAnalyses, knownNamespaces).map((e) => {
6
+ const item = {
7
+ label: e.label,
8
+ kind: e.kind === "function"
9
+ ? CompletionItemKind.Function
10
+ : e.kind === "namespace"
11
+ ? CompletionItemKind.Module
12
+ : CompletionItemKind.Variable,
13
+ detail: e.detail,
14
+ documentation: e.documentation ? { kind: MarkupKind.Markdown, value: e.documentation } : undefined,
15
+ insertText: e.insertText,
16
+ insertTextFormat: e.isSnippet ? InsertTextFormat.Snippet : InsertTextFormat.PlainText,
17
+ };
18
+ if (e.additionalEdit && editContext) {
19
+ const { docText, docUri, generateLocationHints } = editContext;
20
+ const insertPos = e.additionalEdit.kind === "import-module"
21
+ ? findImportInsertPosition(docText)
22
+ : findDeclareNsInsertPosition(docText);
23
+ let newText;
24
+ if (e.additionalEdit.kind === "import-module") {
25
+ const atPath = generateLocationHints && e.additionalEdit.sourceUri
26
+ ? computeRelativePath(docUri, e.additionalEdit.sourceUri)
27
+ : undefined;
28
+ newText = atPath
29
+ ? `import module namespace ${e.additionalEdit.prefix} = "${e.additionalEdit.namespaceUri}" at "${atPath}";\n`
30
+ : `import module namespace ${e.additionalEdit.prefix} = "${e.additionalEdit.namespaceUri}";\n`;
31
+ }
32
+ else {
33
+ newText = `declare namespace ${e.additionalEdit.prefix} = "${e.additionalEdit.namespaceUri}";\n`;
34
+ }
35
+ item.additionalTextEdits = [{ range: { start: insertPos, end: insertPos }, newText }];
36
+ }
37
+ return item;
38
+ });
12
39
  }
@@ -1,4 +1,4 @@
1
1
  import { Location } from "vscode-languageserver/node.js";
2
2
  import type { TextDocument } from "vscode-languageserver-textdocument";
3
3
  import type { FileAnalysis } from "./types.ts";
4
- export declare function getDefinition(doc: TextDocument, offset: number, current: FileAnalysis, imported: Map<string, FileAnalysis>, resolveUri: (atPath: string) => string): Location | null;
4
+ export declare function getDefinition(doc: TextDocument, offset: number, current: FileAnalysis, imported: Map<string, FileAnalysis>): Location | null;