xq-lsp 1.0.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/README.md +84 -0
- package/builtins/builtins-array.xq +160 -0
- package/builtins/builtins-fn.xq +1100 -0
- package/builtins/builtins-map.xq +86 -0
- package/builtins/builtins-math.xq +100 -0
- package/dist/analyzer.d.ts +14 -0
- package/dist/analyzer.js +363 -0
- package/dist/builtins.d.ts +2 -0
- package/dist/builtins.js +28 -0
- package/dist/completion.d.ts +9 -0
- package/dist/completion.js +152 -0
- package/dist/features.d.ts +8 -0
- package/dist/features.js +214 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +145 -0
- package/dist/types.d.ts +42 -0
- package/dist/types.js +0 -0
- package/package.json +29 -0
- package/src/analyzer.ts +402 -0
- package/src/builtins.ts +33 -0
- package/src/completion.ts +179 -0
- package/src/features.ts +260 -0
- package/src/server.ts +165 -0
- package/src/types.ts +47 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { CompletionItem, CompletionItemKind, InsertTextFormat, MarkupKind } from "vscode-languageserver/node.js";
|
|
2
|
+
import { resolvePrefix } from "./analyzer.js";
|
|
3
|
+
function parseToken(textBefore) {
|
|
4
|
+
const match = textBefore.match(/\$([\w:\-]*)$|(?:([\w\-]+):)([\w\-]*)$|([\w\-]+)$/);
|
|
5
|
+
if (!match)
|
|
6
|
+
return { kind: "name", prefix: "" };
|
|
7
|
+
if (match[0].startsWith("$")) {
|
|
8
|
+
return { kind: "variable", prefix: match[1] ?? "" };
|
|
9
|
+
}
|
|
10
|
+
if (match[2] !== undefined) {
|
|
11
|
+
return { kind: "qualified-name", nsPrefix: match[2], prefix: match[3] ?? "" };
|
|
12
|
+
}
|
|
13
|
+
return { kind: "name", prefix: match[4] ?? "" };
|
|
14
|
+
}
|
|
15
|
+
function functionDoc(fn) {
|
|
16
|
+
const paramStr = fn.params.map((p) => `$${p.name}${p.type ? " as " + p.type : ""}`).join(", ");
|
|
17
|
+
const ret = fn.returnType ? ` as ${fn.returnType}` : "";
|
|
18
|
+
const sig = `\`\`\`xquery\ndeclare function ${fn.name}(${paramStr})${ret}\n\`\`\``;
|
|
19
|
+
if (!fn.doc)
|
|
20
|
+
return sig;
|
|
21
|
+
const parts = [sig];
|
|
22
|
+
if (fn.doc.description)
|
|
23
|
+
parts.push(fn.doc.description);
|
|
24
|
+
const paramDocs = fn.params.filter((p) => p.description);
|
|
25
|
+
if (paramDocs.length > 0) {
|
|
26
|
+
parts.push(paramDocs.map((p) => `- \`$${p.name}\` — ${p.description}`).join("\n"));
|
|
27
|
+
}
|
|
28
|
+
if (fn.doc.returns)
|
|
29
|
+
parts.push(`**Returns:** ${fn.doc.returns}`);
|
|
30
|
+
return parts.join("\n\n");
|
|
31
|
+
}
|
|
32
|
+
function localInsertText(fn, snippets) {
|
|
33
|
+
if (fn.params.length === 0)
|
|
34
|
+
return fn.localName + "()";
|
|
35
|
+
const args = snippets
|
|
36
|
+
? fn.params.map((p, i) => `\${${i + 1}:\\$${p.name}}`).join(", ")
|
|
37
|
+
: fn.params.map((p) => `$${p.name}`).join(", ");
|
|
38
|
+
return `${fn.localName}(${args})`;
|
|
39
|
+
}
|
|
40
|
+
function buildVariableItem(v, filter) {
|
|
41
|
+
if (filter && !v.name.toLowerCase().startsWith(filter.toLowerCase()))
|
|
42
|
+
return null;
|
|
43
|
+
return {
|
|
44
|
+
label: `$${v.name}`,
|
|
45
|
+
kind: CompletionItemKind.Variable,
|
|
46
|
+
insertText: `$${v.name}`,
|
|
47
|
+
insertTextFormat: InsertTextFormat.PlainText,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function getCompletions(ctx, currentAnalysis, importedAnalyses, snippets = false) {
|
|
51
|
+
const token = parseToken(ctx.textBeforeCursor);
|
|
52
|
+
const items = [];
|
|
53
|
+
// ── Variable completions ─────────────────────────────────────────────────
|
|
54
|
+
if (token.kind === "variable") {
|
|
55
|
+
for (const v of currentAnalysis.moduleVariables) {
|
|
56
|
+
const item = buildVariableItem(v, token.prefix);
|
|
57
|
+
if (item)
|
|
58
|
+
items.push(item);
|
|
59
|
+
}
|
|
60
|
+
for (const v of currentAnalysis.localBindings) {
|
|
61
|
+
if (v.offset < ctx.cursorOffset) {
|
|
62
|
+
const item = buildVariableItem(v, token.prefix);
|
|
63
|
+
if (item)
|
|
64
|
+
items.push(item);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const analysis of importedAnalyses.values()) {
|
|
68
|
+
for (const v of analysis.moduleVariables) {
|
|
69
|
+
const item = buildVariableItem(v, token.prefix);
|
|
70
|
+
if (item)
|
|
71
|
+
items.push(item);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return items;
|
|
75
|
+
}
|
|
76
|
+
// ── Qualified-name completions (user typed "ns:partial") ─────────────────
|
|
77
|
+
if (token.kind === "qualified-name" && token.nsPrefix) {
|
|
78
|
+
const targetUri = resolvePrefix(token.nsPrefix, currentAnalysis);
|
|
79
|
+
const filter = token.prefix.toLowerCase();
|
|
80
|
+
const allFns = [...currentAnalysis.functions, ...[...importedAnalyses.values()].flatMap((a) => a.functions)];
|
|
81
|
+
for (const fn of allFns) {
|
|
82
|
+
if (fn.namespaceUri !== targetUri)
|
|
83
|
+
continue;
|
|
84
|
+
if (filter && !fn.localName.toLowerCase().includes(filter))
|
|
85
|
+
continue;
|
|
86
|
+
items.push({
|
|
87
|
+
label: fn.localName,
|
|
88
|
+
kind: CompletionItemKind.Function,
|
|
89
|
+
detail: `${fn.localName}#${fn.arity}`,
|
|
90
|
+
documentation: { kind: MarkupKind.Markdown, value: functionDoc(fn) },
|
|
91
|
+
insertText: localInsertText(fn, snippets),
|
|
92
|
+
insertTextFormat: snippets ? InsertTextFormat.Snippet : InsertTextFormat.PlainText,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return items;
|
|
96
|
+
}
|
|
97
|
+
// ── Plain-name completions ───────────────────────────────────────────────
|
|
98
|
+
const filter = token.prefix.toLowerCase();
|
|
99
|
+
const defaultUri = currentAnalysis.defaultFunctionNamespace;
|
|
100
|
+
// Functions declared in the current file (full qualified name)
|
|
101
|
+
for (const fn of currentAnalysis.functions) {
|
|
102
|
+
if (filter && !fn.name.toLowerCase().includes(filter))
|
|
103
|
+
continue;
|
|
104
|
+
items.push({
|
|
105
|
+
label: fn.name,
|
|
106
|
+
kind: CompletionItemKind.Function,
|
|
107
|
+
detail: `${fn.name}#${fn.arity}`,
|
|
108
|
+
documentation: { kind: MarkupKind.Markdown, value: functionDoc(fn) },
|
|
109
|
+
insertText: snippets
|
|
110
|
+
? fn.params.length === 0
|
|
111
|
+
? fn.name + "()"
|
|
112
|
+
: fn.name + "(" + fn.params.map((p, i) => `\${${i + 1}:\\$${p.name}}`).join(", ") + ")"
|
|
113
|
+
: fn.name + "(" + fn.params.map((p) => `$${p.name}`).join(", ") + ")",
|
|
114
|
+
insertTextFormat: snippets ? InsertTextFormat.Snippet : InsertTextFormat.PlainText,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
// Imported functions: those in the default function namespace are offered
|
|
118
|
+
// without prefix (since they can be called unqualified); others with full name.
|
|
119
|
+
for (const analysis of importedAnalyses.values()) {
|
|
120
|
+
for (const fn of analysis.functions) {
|
|
121
|
+
if (fn.namespaceUri === defaultUri) {
|
|
122
|
+
if (filter && !fn.localName.toLowerCase().includes(filter))
|
|
123
|
+
continue;
|
|
124
|
+
items.push({
|
|
125
|
+
label: fn.localName,
|
|
126
|
+
kind: CompletionItemKind.Function,
|
|
127
|
+
detail: `${fn.localName}#${fn.arity}`,
|
|
128
|
+
documentation: { kind: MarkupKind.Markdown, value: functionDoc(fn) },
|
|
129
|
+
insertText: localInsertText(fn, snippets),
|
|
130
|
+
insertTextFormat: snippets ? InsertTextFormat.Snippet : InsertTextFormat.PlainText,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
if (filter && !fn.name.toLowerCase().includes(filter))
|
|
135
|
+
continue;
|
|
136
|
+
items.push({
|
|
137
|
+
label: fn.name,
|
|
138
|
+
kind: CompletionItemKind.Function,
|
|
139
|
+
detail: `${fn.name}#${fn.arity}`,
|
|
140
|
+
documentation: { kind: MarkupKind.Markdown, value: functionDoc(fn) },
|
|
141
|
+
insertText: snippets
|
|
142
|
+
? fn.params.length === 0
|
|
143
|
+
? fn.name + "()"
|
|
144
|
+
: fn.name + "(" + fn.params.map((p, i) => `\${${i + 1}:\\$${p.name}}`).join(", ") + ")"
|
|
145
|
+
: fn.name + "(" + fn.params.map((p) => `$${p.name}`).join(", ") + ")",
|
|
146
|
+
insertTextFormat: snippets ? InsertTextFormat.Snippet : InsertTextFormat.PlainText,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return items;
|
|
152
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Location } from "vscode-languageserver/node.js";
|
|
2
|
+
import type { Hover, SignatureHelp, DocumentSymbol } from "vscode-languageserver/node.js";
|
|
3
|
+
import type { TextDocument } from "vscode-languageserver-textdocument";
|
|
4
|
+
import type { FileAnalysis } from "./types.ts";
|
|
5
|
+
export declare function getHover(doc: TextDocument, offset: number, current: FileAnalysis, imported: Map<string, FileAnalysis>): Hover | null;
|
|
6
|
+
export declare function getSignatureHelp(doc: TextDocument, offset: number, current: FileAnalysis, imported: Map<string, FileAnalysis>): SignatureHelp | null;
|
|
7
|
+
export declare function getDocumentSymbols(doc: TextDocument, current: FileAnalysis): DocumentSymbol[];
|
|
8
|
+
export declare function getDefinition(doc: TextDocument, offset: number, current: FileAnalysis, imported: Map<string, FileAnalysis>, resolveUri: (atPath: string) => string): Location | null;
|
package/dist/features.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { SignatureInformation, ParameterInformation, SymbolKind, Location, MarkupKind, } from "vscode-languageserver/node.js";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { resolvePrefix } from "./analyzer.js";
|
|
5
|
+
// ── Shared helpers ───────────────────────────────────────────────────────────
|
|
6
|
+
/**
|
|
7
|
+
* Resolve a qualified-name word (e.g. "fn:true", "m:sin", "true") to a FunctionSymbol
|
|
8
|
+
* using proper namespace URI resolution against the current file's namespace context.
|
|
9
|
+
*/
|
|
10
|
+
function resolveFunction(word, currentAnalysis, fns) {
|
|
11
|
+
const colonIdx = word.indexOf(":");
|
|
12
|
+
const prefix = colonIdx >= 0 ? word.slice(0, colonIdx) : "";
|
|
13
|
+
const localName = colonIdx >= 0 ? word.slice(colonIdx + 1) : word;
|
|
14
|
+
const uri = resolvePrefix(prefix, currentAnalysis);
|
|
15
|
+
return fns.find((f) => f.namespaceUri === uri && f.localName === localName);
|
|
16
|
+
}
|
|
17
|
+
function wordAt(text, offset) {
|
|
18
|
+
let start = offset;
|
|
19
|
+
let end = offset;
|
|
20
|
+
while (start > 0 && /[\w:\-]/.test(text[start - 1]))
|
|
21
|
+
start--;
|
|
22
|
+
while (end < text.length && /[\w:\-]/.test(text[end]))
|
|
23
|
+
end++;
|
|
24
|
+
return { word: text.slice(start, end), start, end };
|
|
25
|
+
}
|
|
26
|
+
function functionSignature(fn) {
|
|
27
|
+
const params = fn.params.map((p) => `$${p.name}${p.type ? " as " + p.type : ""}`).join(", ");
|
|
28
|
+
const ret = fn.returnType ? ` as ${fn.returnType}` : "";
|
|
29
|
+
return `declare function ${fn.name}(${params})${ret}`;
|
|
30
|
+
}
|
|
31
|
+
function allFunctions(current, imported) {
|
|
32
|
+
const fns = [...current.functions];
|
|
33
|
+
for (const a of imported.values())
|
|
34
|
+
fns.push(...a.functions);
|
|
35
|
+
return fns;
|
|
36
|
+
}
|
|
37
|
+
function positionInText(text, offset) {
|
|
38
|
+
const before = text.slice(0, offset);
|
|
39
|
+
const lines = before.split("\n");
|
|
40
|
+
return { line: lines.length - 1, character: lines[lines.length - 1].length };
|
|
41
|
+
}
|
|
42
|
+
function rangeFromOffset(doc, start, end) {
|
|
43
|
+
return { start: doc.positionAt(start), end: doc.positionAt(end) };
|
|
44
|
+
}
|
|
45
|
+
// ── Hover ────────────────────────────────────────────────────────────────────
|
|
46
|
+
export function getHover(doc, offset, current, imported) {
|
|
47
|
+
const text = doc.getText();
|
|
48
|
+
const { word, start, end } = wordAt(text, offset);
|
|
49
|
+
if (!word)
|
|
50
|
+
return null;
|
|
51
|
+
// Check if hovering over a $ variable
|
|
52
|
+
const hasDollar = start > 0 && text[start - 1] === "$";
|
|
53
|
+
if (hasDollar) {
|
|
54
|
+
const varName = word;
|
|
55
|
+
const allVars = [
|
|
56
|
+
...current.moduleVariables,
|
|
57
|
+
...current.localBindings,
|
|
58
|
+
...[...imported.values()].flatMap((a) => a.moduleVariables),
|
|
59
|
+
];
|
|
60
|
+
const v = allVars.find((v) => v.name === varName);
|
|
61
|
+
if (v) {
|
|
62
|
+
return {
|
|
63
|
+
contents: { kind: MarkupKind.Markdown, value: `\`$${v.name}\`` },
|
|
64
|
+
range: rangeFromOffset(doc, start - 1, end),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
// Check if hovering over a function name
|
|
70
|
+
const fn = resolveFunction(word, current, allFunctions(current, imported));
|
|
71
|
+
if (fn) {
|
|
72
|
+
return {
|
|
73
|
+
contents: {
|
|
74
|
+
kind: MarkupKind.Markdown,
|
|
75
|
+
value: "```xquery\n" + functionSignature(fn) + "\n```",
|
|
76
|
+
},
|
|
77
|
+
range: rangeFromOffset(doc, start, end),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
// ── Signature help ───────────────────────────────────────────────────────────
|
|
83
|
+
/**
|
|
84
|
+
* Scan left from cursor to find the function call we're inside.
|
|
85
|
+
* Returns { name, activeParam } or null.
|
|
86
|
+
*/
|
|
87
|
+
function findEnclosingCall(text, offset) {
|
|
88
|
+
let depth = 0;
|
|
89
|
+
let commas = 0;
|
|
90
|
+
for (let i = offset - 1; i >= 0; i--) {
|
|
91
|
+
const ch = text[i];
|
|
92
|
+
if (ch === ")") {
|
|
93
|
+
depth++;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (ch === "(") {
|
|
97
|
+
if (depth > 0) {
|
|
98
|
+
depth--;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
// Found the opening paren of our call — scan left for the function name
|
|
102
|
+
let nameEnd = i;
|
|
103
|
+
while (nameEnd > 0 && /\s/.test(text[nameEnd - 1]))
|
|
104
|
+
nameEnd--;
|
|
105
|
+
const { word, start } = wordAt(text, nameEnd - 1);
|
|
106
|
+
if (!word)
|
|
107
|
+
return null;
|
|
108
|
+
return { name: word, activeParam: commas };
|
|
109
|
+
}
|
|
110
|
+
if (ch === "," && depth === 0)
|
|
111
|
+
commas++;
|
|
112
|
+
// Stop at statement boundaries
|
|
113
|
+
if ((ch === ";" || ch === "{") && depth === 0)
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
export function getSignatureHelp(doc, offset, current, imported) {
|
|
119
|
+
const text = doc.getText();
|
|
120
|
+
const call = findEnclosingCall(text, offset);
|
|
121
|
+
if (!call)
|
|
122
|
+
return null;
|
|
123
|
+
const fn = resolveFunction(call.name, current, allFunctions(current, imported));
|
|
124
|
+
if (!fn)
|
|
125
|
+
return null;
|
|
126
|
+
const sig = {
|
|
127
|
+
label: functionSignature(fn),
|
|
128
|
+
parameters: fn.params.map((p) => {
|
|
129
|
+
const label = `$${p.name}${p.type ? " as " + p.type : ""}`;
|
|
130
|
+
return ParameterInformation.create(label);
|
|
131
|
+
}),
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
signatures: [sig],
|
|
135
|
+
activeSignature: 0,
|
|
136
|
+
activeParameter: Math.min(call.activeParam, fn.params.length - 1),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// ── Document symbols ─────────────────────────────────────────────────────────
|
|
140
|
+
export function getDocumentSymbols(doc, current) {
|
|
141
|
+
const symbols = [];
|
|
142
|
+
for (const fn of current.functions) {
|
|
143
|
+
const pos = doc.positionAt(fn.sourceOffset ?? 0);
|
|
144
|
+
const range = { start: pos, end: pos };
|
|
145
|
+
symbols.push({
|
|
146
|
+
name: fn.name,
|
|
147
|
+
kind: SymbolKind.Function,
|
|
148
|
+
range,
|
|
149
|
+
selectionRange: range,
|
|
150
|
+
detail: `#${fn.arity}`,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
for (const v of current.moduleVariables) {
|
|
154
|
+
const pos = doc.positionAt(v.offset);
|
|
155
|
+
const range = { start: pos, end: pos };
|
|
156
|
+
symbols.push({
|
|
157
|
+
name: `$${v.name}`,
|
|
158
|
+
kind: SymbolKind.Variable,
|
|
159
|
+
range,
|
|
160
|
+
selectionRange: range,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return symbols;
|
|
164
|
+
}
|
|
165
|
+
// ── Go to definition ─────────────────────────────────────────────────────────
|
|
166
|
+
export function getDefinition(doc, offset, current, imported, resolveUri) {
|
|
167
|
+
const text = doc.getText();
|
|
168
|
+
const { word, start } = wordAt(text, offset);
|
|
169
|
+
if (!word)
|
|
170
|
+
return null;
|
|
171
|
+
const hasDollar = start > 0 && text[start - 1] === "$";
|
|
172
|
+
if (hasDollar) {
|
|
173
|
+
// Variable definition — search current file only
|
|
174
|
+
const allVars = [...current.moduleVariables, ...current.localBindings];
|
|
175
|
+
const v = allVars.find((v) => v.name === word);
|
|
176
|
+
if (!v)
|
|
177
|
+
return null;
|
|
178
|
+
const pos = doc.positionAt(v.offset);
|
|
179
|
+
return Location.create(doc.uri, { start: pos, end: pos });
|
|
180
|
+
}
|
|
181
|
+
// Function definition — resolve by namespace URI, search current file then all imports
|
|
182
|
+
const colonIdx = word.indexOf(":");
|
|
183
|
+
const wordPrefix = colonIdx >= 0 ? word.slice(0, colonIdx) : "";
|
|
184
|
+
const wordLocalName = colonIdx >= 0 ? word.slice(colonIdx + 1) : word;
|
|
185
|
+
const targetUri = resolvePrefix(wordPrefix, current);
|
|
186
|
+
const localFn = current.functions.find((f) => f.namespaceUri === targetUri && f.localName === wordLocalName);
|
|
187
|
+
if (localFn) {
|
|
188
|
+
const pos = doc.positionAt(localFn.sourceOffset ?? 0);
|
|
189
|
+
return Location.create(doc.uri, { start: pos, end: pos });
|
|
190
|
+
}
|
|
191
|
+
// Search imported files: find which atPath contains the function, then navigate there
|
|
192
|
+
for (const imp of current.imports) {
|
|
193
|
+
const analysis = imported.get(imp.atPath);
|
|
194
|
+
if (!analysis)
|
|
195
|
+
continue;
|
|
196
|
+
const fn = analysis.functions.find((f) => f.namespaceUri === targetUri && f.localName === wordLocalName);
|
|
197
|
+
if (!fn)
|
|
198
|
+
continue;
|
|
199
|
+
const uri = resolveUri(imp.atPath);
|
|
200
|
+
let pos = { line: 0, character: 0 };
|
|
201
|
+
if (fn.sourceOffset !== undefined) {
|
|
202
|
+
try {
|
|
203
|
+
const filePath = fileURLToPath(uri);
|
|
204
|
+
const srcText = fs.readFileSync(filePath, "utf-8");
|
|
205
|
+
pos = positionInText(srcText, fn.sourceOffset);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
/* file not readable, use line 0 */
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return Location.create(uri, { start: pos, end: pos });
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createConnection, TextDocuments, ProposedFeatures, TextDocumentSyncKind, DiagnosticSeverity, } from 'vscode-languageserver/node.js';
|
|
3
|
+
import { TextDocument } from 'vscode-languageserver-textdocument';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
7
|
+
import { XQuery31Full } from 'xq-parser';
|
|
8
|
+
import { analyze } from "./analyzer.js";
|
|
9
|
+
import { getBuiltins } from "./builtins.js";
|
|
10
|
+
import { getCompletions } from "./completion.js";
|
|
11
|
+
import { getHover, getSignatureHelp, getDocumentSymbols, getDefinition } from "./features.js";
|
|
12
|
+
const connection = createConnection(ProposedFeatures.all);
|
|
13
|
+
const documents = new TextDocuments(TextDocument);
|
|
14
|
+
const analysisCache = new Map();
|
|
15
|
+
function analyzeDocument(doc) {
|
|
16
|
+
const analysis = analyze(doc.getText(), doc.uri);
|
|
17
|
+
analysisCache.set(doc.uri, analysis);
|
|
18
|
+
return analysis;
|
|
19
|
+
}
|
|
20
|
+
function uriToPath(uri) {
|
|
21
|
+
try {
|
|
22
|
+
return fileURLToPath(uri);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return uri.replace(/^file:\/\//, '');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function resolveImportUri(currentUri, atPath) {
|
|
29
|
+
const currentPath = uriToPath(currentUri);
|
|
30
|
+
const dir = path.dirname(currentPath);
|
|
31
|
+
const resolved = path.resolve(dir, atPath);
|
|
32
|
+
return pathToFileURL(resolved).toString();
|
|
33
|
+
}
|
|
34
|
+
function getImportedAnalysis(importUri) {
|
|
35
|
+
if (analysisCache.has(importUri))
|
|
36
|
+
return analysisCache.get(importUri);
|
|
37
|
+
try {
|
|
38
|
+
const text = fs.readFileSync(uriToPath(importUri), 'utf-8');
|
|
39
|
+
const analysis = analyze(text, importUri);
|
|
40
|
+
analysisCache.set(importUri, analysis);
|
|
41
|
+
return analysis;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function resolveImports(currentUri, analysis) {
|
|
48
|
+
const result = new Map();
|
|
49
|
+
result.set('builtin:fn', getBuiltins());
|
|
50
|
+
for (const imp of analysis.imports) {
|
|
51
|
+
const uri = resolveImportUri(currentUri, imp.atPath);
|
|
52
|
+
const imported = getImportedAnalysis(uri);
|
|
53
|
+
if (imported)
|
|
54
|
+
result.set(imp.atPath, imported);
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
function parseDiagnostics(text, uri) {
|
|
59
|
+
try {
|
|
60
|
+
XQuery31Full(text);
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
65
|
+
// "at line N, column M:" in the parser's error messages
|
|
66
|
+
const loc = msg.match(/at line (\d+), column (\d+)/);
|
|
67
|
+
const line = loc ? parseInt(loc[1]) - 1 : 0;
|
|
68
|
+
const col = loc ? parseInt(loc[2]) - 1 : 0;
|
|
69
|
+
return [{
|
|
70
|
+
severity: DiagnosticSeverity.Error,
|
|
71
|
+
range: { start: { line, character: col }, end: { line, character: col + 1 } },
|
|
72
|
+
message: msg.split('\n')[0],
|
|
73
|
+
source: 'xquery-lsp',
|
|
74
|
+
}];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// ── LSP lifecycle ────────────────────────────────────────────────────────────
|
|
78
|
+
let snippetSupport = false;
|
|
79
|
+
connection.onInitialize((params) => {
|
|
80
|
+
snippetSupport =
|
|
81
|
+
params.capabilities.textDocument?.completion?.completionItem?.snippetSupport ?? false;
|
|
82
|
+
return {
|
|
83
|
+
capabilities: {
|
|
84
|
+
textDocumentSync: TextDocumentSyncKind.Incremental,
|
|
85
|
+
completionProvider: {
|
|
86
|
+
triggerCharacters: ['$', ':'],
|
|
87
|
+
resolveProvider: false,
|
|
88
|
+
},
|
|
89
|
+
hoverProvider: true,
|
|
90
|
+
signatureHelpProvider: {
|
|
91
|
+
triggerCharacters: ['(', ','],
|
|
92
|
+
},
|
|
93
|
+
documentSymbolProvider: true,
|
|
94
|
+
definitionProvider: true,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
documents.onDidChangeContent(change => {
|
|
99
|
+
const doc = change.document;
|
|
100
|
+
analyzeDocument(doc);
|
|
101
|
+
const diagnostics = parseDiagnostics(doc.getText(), doc.uri);
|
|
102
|
+
connection.sendDiagnostics({ uri: doc.uri, diagnostics });
|
|
103
|
+
});
|
|
104
|
+
connection.onCompletion(params => {
|
|
105
|
+
const doc = documents.get(params.textDocument.uri);
|
|
106
|
+
if (!doc)
|
|
107
|
+
return [];
|
|
108
|
+
const analysis = analysisCache.get(doc.uri) ?? analyzeDocument(doc);
|
|
109
|
+
const imported = resolveImports(doc.uri, analysis);
|
|
110
|
+
const offset = doc.offsetAt(params.position);
|
|
111
|
+
return getCompletions({ textBeforeCursor: doc.getText().slice(0, offset), cursorOffset: offset }, analysis, imported, snippetSupport);
|
|
112
|
+
});
|
|
113
|
+
connection.onHover(params => {
|
|
114
|
+
const doc = documents.get(params.textDocument.uri);
|
|
115
|
+
if (!doc)
|
|
116
|
+
return null;
|
|
117
|
+
const analysis = analysisCache.get(doc.uri) ?? analyzeDocument(doc);
|
|
118
|
+
const imported = resolveImports(doc.uri, analysis);
|
|
119
|
+
return getHover(doc, doc.offsetAt(params.position), analysis, imported);
|
|
120
|
+
});
|
|
121
|
+
connection.onSignatureHelp(params => {
|
|
122
|
+
const doc = documents.get(params.textDocument.uri);
|
|
123
|
+
if (!doc)
|
|
124
|
+
return null;
|
|
125
|
+
const analysis = analysisCache.get(doc.uri) ?? analyzeDocument(doc);
|
|
126
|
+
const imported = resolveImports(doc.uri, analysis);
|
|
127
|
+
return getSignatureHelp(doc, doc.offsetAt(params.position), analysis, imported);
|
|
128
|
+
});
|
|
129
|
+
connection.onDocumentSymbol(params => {
|
|
130
|
+
const doc = documents.get(params.textDocument.uri);
|
|
131
|
+
if (!doc)
|
|
132
|
+
return [];
|
|
133
|
+
const analysis = analysisCache.get(doc.uri) ?? analyzeDocument(doc);
|
|
134
|
+
return getDocumentSymbols(doc, analysis);
|
|
135
|
+
});
|
|
136
|
+
connection.onDefinition(params => {
|
|
137
|
+
const doc = documents.get(params.textDocument.uri);
|
|
138
|
+
if (!doc)
|
|
139
|
+
return null;
|
|
140
|
+
const analysis = analysisCache.get(doc.uri) ?? analyzeDocument(doc);
|
|
141
|
+
const imported = resolveImports(doc.uri, analysis);
|
|
142
|
+
return getDefinition(doc, doc.offsetAt(params.position), analysis, imported, atPath => resolveImportUri(doc.uri, atPath));
|
|
143
|
+
});
|
|
144
|
+
documents.listen(connection);
|
|
145
|
+
connection.listen();
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface ParamInfo {
|
|
2
|
+
name: string;
|
|
3
|
+
type?: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface DocComment {
|
|
7
|
+
description: string;
|
|
8
|
+
params: Record<string, string>;
|
|
9
|
+
returns?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface FunctionSymbol {
|
|
12
|
+
name: string;
|
|
13
|
+
prefix: string;
|
|
14
|
+
localName: string;
|
|
15
|
+
namespaceUri: string;
|
|
16
|
+
arity: number;
|
|
17
|
+
params: ParamInfo[];
|
|
18
|
+
returnType?: string;
|
|
19
|
+
doc?: DocComment;
|
|
20
|
+
sourceUri: string;
|
|
21
|
+
sourceOffset?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface VariableSymbol {
|
|
24
|
+
name: string;
|
|
25
|
+
offset: number;
|
|
26
|
+
isModuleLevel: boolean;
|
|
27
|
+
sourceUri: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ImportInfo {
|
|
30
|
+
prefix: string;
|
|
31
|
+
namespaceUri: string;
|
|
32
|
+
atPath: string;
|
|
33
|
+
}
|
|
34
|
+
export interface FileAnalysis {
|
|
35
|
+
functions: FunctionSymbol[];
|
|
36
|
+
moduleVariables: VariableSymbol[];
|
|
37
|
+
localBindings: VariableSymbol[];
|
|
38
|
+
imports: ImportInfo[];
|
|
39
|
+
defaultFunctionNamespace: string;
|
|
40
|
+
moduleNamespaceUri?: string;
|
|
41
|
+
modulePrefix?: string;
|
|
42
|
+
}
|
package/dist/types.js
ADDED
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "xq-lsp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "XQuery Language Server Protocol implementation",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Martin Middel",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "dist/server.js",
|
|
9
|
+
"repository": "https://github.com/DrRataplan/xq-lsp.git",
|
|
10
|
+
"bin": {
|
|
11
|
+
"xq-lsp": "./src/server.ts"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"start": "node ./src/server.ts --stdio",
|
|
16
|
+
"test": "node --test src/test.ts",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"vscode-languageserver": "^9.0.1",
|
|
21
|
+
"vscode-languageserver-textdocument": "^1.0.12",
|
|
22
|
+
"xq-parser": "^0.4.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^25.6.0",
|
|
26
|
+
"prettier": "^3.8.3",
|
|
27
|
+
"typescript": "^6.0.3"
|
|
28
|
+
}
|
|
29
|
+
}
|