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.
- package/CHANGELOG.md +44 -0
- package/README.md +23 -1
- package/demo/favicon.svg +6 -0
- package/demo/index.html +2 -0
- package/demo/main.ts +3 -12
- package/dist/analyzer.js +9 -6
- package/dist/ast-nodes.d.ts +29 -0
- package/dist/ast-nodes.js +63 -10
- package/dist/completion-core.d.ts +15 -2
- package/dist/completion-core.js +86 -13
- package/dist/completion.d.ts +8 -2
- package/dist/completion.js +36 -9
- package/dist/definition.d.ts +1 -1
- package/dist/definition.js +85 -9
- package/dist/diagnostics.d.ts +20 -0
- package/dist/diagnostics.js +32 -0
- package/dist/duplicate-function-diagnostics.d.ts +17 -0
- package/dist/duplicate-function-diagnostics.js +47 -0
- package/dist/module-import-diagnostics.d.ts +19 -0
- package/dist/module-import-diagnostics.js +35 -0
- package/dist/namespace-diagnostics.d.ts +7 -1
- package/dist/namespace-diagnostics.js +54 -13
- package/dist/qt4-worker.d.ts +8 -0
- package/dist/qt4-worker.js +37 -12
- package/dist/references.d.ts +56 -0
- package/dist/references.js +496 -0
- package/dist/runtimes/existdb/cache.xq +83 -0
- package/dist/runtimes/existdb/compression.xq +198 -0
- package/dist/runtimes/existdb/console.xq +24 -0
- package/dist/runtimes/existdb/contentextraction.xq +31 -0
- package/dist/runtimes/existdb/counter.xq +30 -0
- package/dist/runtimes/existdb/crypto.xq +97 -0
- package/dist/runtimes/existdb/datetime.xq +91 -0
- package/dist/runtimes/existdb/exist.xq +29 -0
- package/dist/runtimes/existdb/file.xq +129 -0
- package/dist/runtimes/existdb/ft.xq +325 -0
- package/dist/runtimes/existdb/httpclient.xq +118 -0
- package/dist/runtimes/existdb/image.xq +65 -0
- package/dist/runtimes/existdb/inspect.xq +49 -0
- package/dist/runtimes/existdb/kwic.xq +24 -0
- package/dist/runtimes/existdb/mail.xq +124 -0
- package/dist/runtimes/existdb/ngram.xq +57 -0
- package/dist/runtimes/existdb/process.xq +8 -0
- package/dist/runtimes/existdb/range.xq +171 -0
- package/dist/runtimes/existdb/repo.xq +125 -0
- package/dist/runtimes/existdb/request.xq +242 -0
- package/dist/runtimes/existdb/response.xq +50 -0
- package/dist/runtimes/existdb/scheduler.xq +35 -0
- package/dist/runtimes/existdb/session.xq +111 -0
- package/dist/runtimes/existdb/sm.xq +497 -0
- package/dist/runtimes/existdb/sort.xq +59 -0
- package/dist/runtimes/existdb/system.xq +267 -0
- package/dist/runtimes/existdb/transform.xq +88 -0
- package/dist/runtimes/existdb/util.xq +892 -0
- package/dist/runtimes/existdb/validation.xq +83 -0
- package/dist/runtimes/existdb/xmldb.xq +482 -0
- package/dist/runtimes/existdb/xmldiff.xq +22 -0
- package/dist/runtimes/fonto/fonto.xq +164 -0
- package/dist/runtimes.js +40 -4
- package/dist/server.js +139 -90
- package/dist/types.d.ts +3 -0
- package/dist/unused-diagnostics.d.ts +1 -1
- package/dist/unused-diagnostics.js +74 -1
- package/dist/variable-diagnostics.js +77 -7
- package/logo.svg +6 -0
- package/package.json +2 -2
- package/src/analyzer.ts +9 -6
- package/src/ast-nodes.ts +89 -10
- package/src/completion-core.ts +100 -12
- package/src/completion.ts +52 -9
- package/src/definition.ts +85 -8
- package/src/diagnostics.ts +47 -0
- package/src/duplicate-function-diagnostics.ts +55 -0
- package/src/module-import-diagnostics.ts +43 -0
- package/src/namespace-diagnostics.ts +53 -12
- package/src/qt4-worker.ts +53 -10
- package/src/references.ts +602 -0
- package/src/runtimes/existdb/exist.xq +29 -0
- package/src/runtimes.ts +41 -8
- package/src/server.ts +157 -91
- package/src/types.ts +3 -0
- package/src/unused-diagnostics.ts +75 -2
- package/src/variable-diagnostics.ts +65 -5
- 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 `.
|
|
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.
|
package/demo/favicon.svg
ADDED
|
@@ -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 {
|
|
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
|
|
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
|
|
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+["']([^"']*)["'])?/
|
|
342
|
-
const RE_NS_DECL = /declare\s+namespace\s+([\w\-]+)\s*=\s*["']([^"']*)["']/
|
|
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
|
|
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
|
-
|
|
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 = [];
|
package/dist/ast-nodes.d.ts
CHANGED
|
@@ -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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
151
|
+
const qname = qnameFromRawTerminal(nameNode, analysis);
|
|
152
|
+
if (!qname)
|
|
153
|
+
return [];
|
|
154
|
+
return [{ qname, initExpr }];
|
|
103
155
|
});
|
|
104
|
-
|
|
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
|
|
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[];
|
package/dist/completion-core.js
CHANGED
|
@@ -76,7 +76,15 @@ function buildFunctionEntry(fn, label, snippets) {
|
|
|
76
76
|
isSnippet: snippets && fn.params.length > 0,
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
}
|
package/dist/completion.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { CompletionItem } from "vscode-languageserver/node.js";
|
|
2
|
-
import {
|
|
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
|
|
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[];
|
package/dist/completion.js
CHANGED
|
@@ -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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
}
|
package/dist/definition.d.ts
CHANGED
|
@@ -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
|
|
4
|
+
export declare function getDefinition(doc: TextDocument, offset: number, current: FileAnalysis, imported: Map<string, FileAnalysis>): Location | null;
|