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.
- package/CHANGELOG.md +40 -0
- package/demo/index.html +71 -2
- package/demo/main.ts +165 -12
- package/demo/package-lock.json +45 -0
- package/demo/package.json +2 -0
- package/demo/vite.config.ts +3 -0
- package/dist/analyzer.d.ts +1 -0
- package/dist/analyzer.js +29 -4
- package/dist/ast-nodes.d.ts +1 -0
- package/dist/ast-nodes.js +1 -1
- package/dist/braced-uri-diagnostics.d.ts +13 -0
- package/dist/braced-uri-diagnostics.js +27 -0
- package/dist/call-hierarchy.d.ts +20 -0
- package/dist/call-hierarchy.js +197 -0
- package/dist/code-lens.d.ts +23 -0
- package/dist/code-lens.js +69 -0
- package/dist/completion-core.js +1 -1
- package/dist/definition.js +1 -1
- package/dist/diagnostics.js +2 -1
- package/dist/document-links.d.ts +4 -0
- package/dist/document-links.js +79 -0
- package/dist/features.js +4 -1
- package/dist/hover-core.js +23 -0
- package/dist/inlay-hints.d.ts +5 -0
- package/dist/inlay-hints.js +121 -0
- package/dist/predeclared-namespaces.d.ts +25 -0
- package/dist/predeclared-namespaces.js +142 -0
- package/dist/refactor-actions.d.ts +39 -0
- package/dist/refactor-actions.js +322 -0
- package/dist/references.d.ts +17 -3
- package/dist/references.js +25 -15
- package/dist/runtimes/existdb/exist.xq +10 -0
- package/dist/runtimes.d.ts +2 -14
- package/dist/runtimes.js +1 -72
- package/dist/server.js +205 -35
- package/dist/typechecker.js +69 -4
- package/dist/types.d.ts +1 -0
- package/dist/variable-diagnostics.js +32 -21
- package/dist/workspace-symbol.d.ts +8 -0
- package/dist/workspace-symbol.js +29 -0
- package/package.json +1 -1
- package/src/analyzer.ts +35 -4
- package/src/ast-nodes.ts +2 -1
- package/src/braced-uri-diagnostics.ts +35 -0
- package/src/call-hierarchy.ts +232 -0
- package/src/code-lens.ts +90 -0
- package/src/completion-core.ts +1 -1
- package/src/definition.ts +1 -1
- package/src/diagnostics.ts +2 -1
- package/src/document-links.ts +81 -0
- package/src/features.ts +3 -1
- package/src/hover-core.ts +23 -0
- package/src/inlay-hints.ts +162 -0
- package/src/predeclared-namespaces.ts +149 -0
- package/src/refactor-actions.ts +369 -0
- package/src/references.ts +33 -10
- package/src/runtimes/existdb/exist.xq +10 -0
- package/src/runtimes.ts +8 -75
- package/src/server.ts +223 -54
- package/src/typechecker.ts +65 -3
- package/src/types.ts +1 -0
- package/src/variable-diagnostics.ts +24 -16
- 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
|
-
#
|
|
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="
|
|
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
|
-
|
|
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
|
|
91
|
-
const
|
|
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
|
|
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
|
|
155
|
-
const
|
|
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
|
|
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();
|
package/demo/package-lock.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": {
|
|
@@ -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
package/demo/vite.config.ts
CHANGED
|
@@ -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
|
});
|
package/dist/analyzer.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/ast-nodes.d.ts
CHANGED
|
@@ -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
|
+
}
|