@lensmap/language-server 0.2.0 → 0.3.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/LICENSE +21 -0
- package/dist/main.js +1 -1
- package/dist/preview/assets/{playground-BCY9v6Ap.js → playground-B1_UXc9t.js} +2 -2
- package/dist/preview/assets/{preview-BRCvwAxB.js → preview-Bugh_5CI.js} +1 -1
- package/dist/preview/assets/{styles-9bHR-mQh.css → styles-BANxTO8w.css} +1 -1
- package/dist/preview/assets/styles-BEj3ZVHM.js +199 -0
- package/dist/preview/index.html +3 -3
- package/dist/preview/preview.html +3 -3
- package/dist/server.js +265 -14
- package/dist/vocabulary-project.d.ts +30 -0
- package/dist/vocabulary-project.js +146 -0
- package/dist/vocabulary-tooling.d.ts +11 -0
- package/dist/vocabulary-tooling.js +79 -0
- package/package.json +10 -10
- package/dist/preview/assets/styles-Bnie2-Dn.js +0 -190
package/dist/preview/index.html
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<meta name="theme-color" content="#10171f" />
|
|
7
7
|
<title>Lensmap · Spatial lens lab</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/playground-
|
|
9
|
-
<link rel="modulepreload" crossorigin href="/assets/styles-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/styles-
|
|
8
|
+
<script type="module" crossorigin src="/assets/playground-B1_UXc9t.js"></script>
|
|
9
|
+
<link rel="modulepreload" crossorigin href="/assets/styles-BEj3ZVHM.js">
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/styles-BANxTO8w.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
<meta name="theme-color" content="#10171f" />
|
|
7
7
|
<meta name="referrer" content="no-referrer" />
|
|
8
8
|
<title>Lensmap · Editor preview</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/preview-
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/styles-
|
|
11
|
-
<link rel="stylesheet" crossorigin href="/assets/styles-
|
|
9
|
+
<script type="module" crossorigin src="/assets/preview-Bugh_5CI.js"></script>
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/styles-BEj3ZVHM.js">
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/assets/styles-BANxTO8w.css">
|
|
12
12
|
<link rel="stylesheet" crossorigin href="/assets/preview-DDOSTiJj.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
package/dist/server.js
CHANGED
|
@@ -2,8 +2,10 @@ import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
|
2
2
|
import { readFile, realpath, stat } from "node:fs/promises";
|
|
3
3
|
import { createServer, } from "node:http";
|
|
4
4
|
import { extname, resolve, sep } from "node:path";
|
|
5
|
-
import { ConnectionTypeSchema, connectionWording, DocumentSession, positionToOffset, } from "@lensmap/document";
|
|
5
|
+
import { ConnectionTypeSchema, connectionWording, DocumentSession, offsetToPosition, parseVocabulary, positionToOffset, vocabularyLocations, } from "@lensmap/document";
|
|
6
6
|
import { CodeActionKind, CompletionItemKind, createConnection, StreamMessageReader, StreamMessageWriter, TextDocumentSyncKind, } from "vscode-languageserver/node";
|
|
7
|
+
import { ProjectVocabularies } from "./vocabulary-project.js";
|
|
8
|
+
import { analyzeVocabularyForTooling, declaresType, typeNameAt, typeValueRangeAt, } from "./vocabulary-tooling.js";
|
|
7
9
|
class HttpError extends Error {
|
|
8
10
|
status;
|
|
9
11
|
constructor(status, message) {
|
|
@@ -92,6 +94,24 @@ export async function startServer(options) {
|
|
|
92
94
|
let closePromise;
|
|
93
95
|
let origin = "";
|
|
94
96
|
let previewURL = "";
|
|
97
|
+
let workspaceUris = [];
|
|
98
|
+
const projects = new ProjectVocabularies((root, input) => {
|
|
99
|
+
if (closed)
|
|
100
|
+
return;
|
|
101
|
+
for (const [uri, entry] of entries) {
|
|
102
|
+
if (projects.rootFor(uri) !== root)
|
|
103
|
+
continue;
|
|
104
|
+
entry.vocabularyRequest = (entry.vocabularyRequest ?? 0) + 1;
|
|
105
|
+
entry.loading = false;
|
|
106
|
+
if (entry.timer)
|
|
107
|
+
clearTimeout(entry.timer);
|
|
108
|
+
entry.timer = undefined;
|
|
109
|
+
entry.session.setVocabulary(input);
|
|
110
|
+
publish(entry);
|
|
111
|
+
}
|
|
112
|
+
for (const uri of projects.resources(root))
|
|
113
|
+
void publishVocabulary(uri);
|
|
114
|
+
});
|
|
95
115
|
const envelope = () => ({
|
|
96
116
|
type: "session",
|
|
97
117
|
documents: [...entries.values()].map(({ session }) => session.snapshot()),
|
|
@@ -120,6 +140,48 @@ export async function startServer(options) {
|
|
|
120
140
|
throw new HttpError(409, "Current source has not produced valid bindings.");
|
|
121
141
|
return entry;
|
|
122
142
|
};
|
|
143
|
+
const reportedExternal = new Set();
|
|
144
|
+
const externalDiagnostics = (uri) => {
|
|
145
|
+
const result = [...entries.values()].flatMap(({ session }) => session.snapshot().diagnostics.filter((item) => item.uri === uri));
|
|
146
|
+
const buffer = projects.buffer(uri);
|
|
147
|
+
if (!result.length && buffer) {
|
|
148
|
+
const parsed = uri.endsWith(".lenstypes")
|
|
149
|
+
? parseVocabulary(buffer.source, uri)
|
|
150
|
+
: projects.cachedInput(uri);
|
|
151
|
+
if (parsed?.error && (!parsed.uri || parsed.uri === uri)) {
|
|
152
|
+
const at = offsetToPosition(buffer.source, parsed.offset ?? 0);
|
|
153
|
+
result.push({
|
|
154
|
+
message: parsed.error,
|
|
155
|
+
uri,
|
|
156
|
+
range: { start: at, end: at },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return [
|
|
161
|
+
...new Map(result.map((item) => [JSON.stringify(item), item])).values(),
|
|
162
|
+
];
|
|
163
|
+
};
|
|
164
|
+
const publishExternal = () => {
|
|
165
|
+
const current = new Set([
|
|
166
|
+
...projects.resources(),
|
|
167
|
+
...[...entries.values()].flatMap(({ session }) => session
|
|
168
|
+
.snapshot()
|
|
169
|
+
.diagnostics.flatMap((item) => item.uri && item.uri !== session.uri ? [item.uri] : [])),
|
|
170
|
+
]);
|
|
171
|
+
for (const uri of new Set([...reportedExternal, ...current])) {
|
|
172
|
+
void connection.sendDiagnostics({
|
|
173
|
+
uri,
|
|
174
|
+
diagnostics: externalDiagnostics(uri).map((item) => ({
|
|
175
|
+
...item,
|
|
176
|
+
severity: 1,
|
|
177
|
+
source: "lensmap",
|
|
178
|
+
})),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
reportedExternal.clear();
|
|
182
|
+
for (const uri of current)
|
|
183
|
+
reportedExternal.add(uri);
|
|
184
|
+
};
|
|
123
185
|
const publish = (entry) => {
|
|
124
186
|
const snapshot = entry.session.snapshot();
|
|
125
187
|
emit({ type: "snapshot", snapshot });
|
|
@@ -127,12 +189,15 @@ export async function startServer(options) {
|
|
|
127
189
|
void connection.sendDiagnostics({
|
|
128
190
|
uri: snapshot.uri,
|
|
129
191
|
version: snapshot.revision,
|
|
130
|
-
diagnostics: snapshot.diagnostics
|
|
192
|
+
diagnostics: snapshot.diagnostics
|
|
193
|
+
.filter((item) => !item.uri || item.uri === snapshot.uri)
|
|
194
|
+
.map((item) => ({
|
|
131
195
|
...item,
|
|
132
196
|
severity: 1,
|
|
133
197
|
source: "lensmap",
|
|
134
198
|
})),
|
|
135
199
|
});
|
|
200
|
+
publishExternal();
|
|
136
201
|
for (const observer of observers)
|
|
137
202
|
observer();
|
|
138
203
|
};
|
|
@@ -140,9 +205,57 @@ export async function startServer(options) {
|
|
|
140
205
|
if (entry.timer)
|
|
141
206
|
clearTimeout(entry.timer);
|
|
142
207
|
entry.timer = undefined;
|
|
208
|
+
if (entry.loading)
|
|
209
|
+
return;
|
|
143
210
|
entry.session.analyze();
|
|
144
211
|
publish(entry);
|
|
145
212
|
};
|
|
213
|
+
const loadVocabulary = async (entry) => {
|
|
214
|
+
const uri = entry.session.uri;
|
|
215
|
+
const request = (entry.vocabularyRequest ?? 0) + 1;
|
|
216
|
+
entry.vocabularyRequest = request;
|
|
217
|
+
entry.loading = true;
|
|
218
|
+
const input = await projects.forUri(uri);
|
|
219
|
+
if (closed ||
|
|
220
|
+
entries.get(uri) !== entry ||
|
|
221
|
+
entry.vocabularyRequest !== request)
|
|
222
|
+
return;
|
|
223
|
+
entry.loading = false;
|
|
224
|
+
if (entry.timer)
|
|
225
|
+
clearTimeout(entry.timer);
|
|
226
|
+
entry.timer = undefined;
|
|
227
|
+
entry.session.setVocabulary(input);
|
|
228
|
+
publish(entry);
|
|
229
|
+
};
|
|
230
|
+
const vocabularyContext = async (uri) => {
|
|
231
|
+
const input = await projects.forUri(uri);
|
|
232
|
+
const buffer = projects.buffer(uri);
|
|
233
|
+
const related = [...entries].filter(([mapUri]) => projects.rootFor(mapUri) === projects.rootFor(uri));
|
|
234
|
+
const contextualMap = input.uri === uri
|
|
235
|
+
? (related.find(([mapUri]) => mapUri === activeUri) ?? related[0])?.[1]
|
|
236
|
+
: undefined;
|
|
237
|
+
return {
|
|
238
|
+
...analyzeVocabularyForTooling(uri, input, entries.get(uri)?.session.source ?? contextualMap?.session.source, buffer && uri.endsWith(".lenstypes") ? buffer.source : undefined),
|
|
239
|
+
contextualUri: contextualMap?.session.uri,
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
const publishVocabulary = async (uri) => {
|
|
243
|
+
const buffer = projects.buffer(uri);
|
|
244
|
+
if (!buffer || closed)
|
|
245
|
+
return;
|
|
246
|
+
await projects.forUri(uri);
|
|
247
|
+
if (closed || projects.buffer(uri) !== buffer)
|
|
248
|
+
return;
|
|
249
|
+
void connection.sendDiagnostics({
|
|
250
|
+
uri,
|
|
251
|
+
version: buffer.version,
|
|
252
|
+
diagnostics: externalDiagnostics(uri).map((item) => ({
|
|
253
|
+
...item,
|
|
254
|
+
severity: 1,
|
|
255
|
+
source: "lensmap",
|
|
256
|
+
})),
|
|
257
|
+
});
|
|
258
|
+
};
|
|
146
259
|
const waitForReadback = (entry, base, expected) => new Promise((done) => {
|
|
147
260
|
const matches = () => !closed &&
|
|
148
261
|
entries.get(entry.session.snapshot().uri) === entry &&
|
|
@@ -444,6 +557,7 @@ export async function startServer(options) {
|
|
|
444
557
|
if (closePromise)
|
|
445
558
|
return closePromise;
|
|
446
559
|
closed = true;
|
|
560
|
+
projects.dispose();
|
|
447
561
|
for (const entry of entries.values())
|
|
448
562
|
if (entry.timer)
|
|
449
563
|
clearTimeout(entry.timer);
|
|
@@ -463,6 +577,10 @@ export async function startServer(options) {
|
|
|
463
577
|
connection.dispose();
|
|
464
578
|
};
|
|
465
579
|
connection.onInitialize((params) => {
|
|
580
|
+
workspaceUris =
|
|
581
|
+
params.workspaceFolders?.map((folder) => folder.uri) ??
|
|
582
|
+
(params.rootUri ? [params.rootUri] : []);
|
|
583
|
+
projects.setRoots(workspaceUris);
|
|
466
584
|
canApply = params.capabilities.workspace?.applyEdit === true;
|
|
467
585
|
canShowDocument =
|
|
468
586
|
params.capabilities.window?.showDocument?.support === true;
|
|
@@ -472,6 +590,10 @@ export async function startServer(options) {
|
|
|
472
590
|
textDocumentSync: TextDocumentSyncKind.Incremental,
|
|
473
591
|
documentHighlightProvider: true,
|
|
474
592
|
definitionProvider: true,
|
|
593
|
+
hoverProvider: true,
|
|
594
|
+
workspace: {
|
|
595
|
+
workspaceFolders: { supported: true, changeNotifications: true },
|
|
596
|
+
},
|
|
475
597
|
completionProvider: { triggerCharacters: [":", " "] },
|
|
476
598
|
codeActionProvider: true,
|
|
477
599
|
executeCommandProvider: {
|
|
@@ -485,7 +607,14 @@ export async function startServer(options) {
|
|
|
485
607
|
serverInfo: { name: "Lensmap", version: "0.0.0" },
|
|
486
608
|
};
|
|
487
609
|
});
|
|
488
|
-
connection.onDidOpenTextDocument(({ textDocument: document }) => {
|
|
610
|
+
connection.onDidOpenTextDocument(async ({ textDocument: document }) => {
|
|
611
|
+
if (shutdown || closed)
|
|
612
|
+
return;
|
|
613
|
+
if (projects.isResource(document.uri)) {
|
|
614
|
+
await projects.open(document.uri, document.text, document.version);
|
|
615
|
+
await publishVocabulary(document.uri);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
489
618
|
if (shutdown ||
|
|
490
619
|
closed ||
|
|
491
620
|
(!document.uri.endsWith(".lensmap") && document.languageId !== "lensmap"))
|
|
@@ -494,16 +623,38 @@ export async function startServer(options) {
|
|
|
494
623
|
if (previous?.timer)
|
|
495
624
|
clearTimeout(previous.timer);
|
|
496
625
|
const entry = {
|
|
497
|
-
session: new DocumentSession(document.uri, document.text, document.version),
|
|
626
|
+
session: new DocumentSession(document.uri, document.text, document.version, { error: "Project vocabulary is loading." }),
|
|
498
627
|
};
|
|
628
|
+
entry.session.receive(document.text, document.version);
|
|
499
629
|
entries.set(document.uri, entry);
|
|
500
630
|
activeUri = document.uri;
|
|
501
631
|
emit(envelope());
|
|
502
632
|
publish(entry);
|
|
633
|
+
await loadVocabulary(entry);
|
|
503
634
|
});
|
|
504
|
-
connection.onDidChangeTextDocument(({ textDocument: document, contentChanges }) => {
|
|
635
|
+
connection.onDidChangeTextDocument(async ({ textDocument: document, contentChanges }) => {
|
|
505
636
|
if (shutdown || closed)
|
|
506
637
|
return;
|
|
638
|
+
const vocabulary = projects.buffer(document.uri);
|
|
639
|
+
if (vocabulary) {
|
|
640
|
+
if (document.version <= vocabulary.version)
|
|
641
|
+
return;
|
|
642
|
+
let source = vocabulary.source;
|
|
643
|
+
for (const change of contentChanges) {
|
|
644
|
+
if ("range" in change) {
|
|
645
|
+
const start = positionToOffset(source, change.range.start);
|
|
646
|
+
const end = positionToOffset(source, change.range.end);
|
|
647
|
+
if (end < start)
|
|
648
|
+
return;
|
|
649
|
+
source = source.slice(0, start) + change.text + source.slice(end);
|
|
650
|
+
}
|
|
651
|
+
else
|
|
652
|
+
source = change.text;
|
|
653
|
+
}
|
|
654
|
+
await projects.open(document.uri, source, document.version);
|
|
655
|
+
await publishVocabulary(document.uri);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
507
658
|
const entry = entries.get(document.uri);
|
|
508
659
|
if (!entry || document.version <= entry.session.revision)
|
|
509
660
|
return;
|
|
@@ -547,11 +698,17 @@ export async function startServer(options) {
|
|
|
547
698
|
});
|
|
548
699
|
}
|
|
549
700
|
});
|
|
550
|
-
connection.onDidCloseTextDocument(({ textDocument }) => {
|
|
701
|
+
connection.onDidCloseTextDocument(async ({ textDocument }) => {
|
|
702
|
+
if (projects.buffer(textDocument.uri)) {
|
|
703
|
+
await projects.close(textDocument.uri);
|
|
704
|
+
publishExternal();
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
551
707
|
const entry = entries.get(textDocument.uri);
|
|
552
708
|
if (entry?.timer)
|
|
553
709
|
clearTimeout(entry.timer);
|
|
554
710
|
entries.delete(textDocument.uri);
|
|
711
|
+
publishExternal();
|
|
555
712
|
if (activeUri === textDocument.uri)
|
|
556
713
|
activeUri = entries.keys().next().value ?? null;
|
|
557
714
|
emit(envelope());
|
|
@@ -566,24 +723,78 @@ export async function startServer(options) {
|
|
|
566
723
|
focus(textDocument.uri, at, false);
|
|
567
724
|
return entry.session.highlights(at).map((range) => ({ range, kind: 1 }));
|
|
568
725
|
});
|
|
569
|
-
connection.onDefinition(({ textDocument, position: at }) => {
|
|
726
|
+
connection.onDefinition(async ({ textDocument, position: at }) => {
|
|
570
727
|
const session = entries.get(textDocument.uri)?.session;
|
|
728
|
+
const source = session?.source ?? projects.buffer(textDocument.uri)?.source;
|
|
729
|
+
const name = source === undefined ? undefined : typeNameAt(source, at);
|
|
730
|
+
if (name && source !== undefined) {
|
|
731
|
+
const { input, contextualUri } = await vocabularyContext(textDocument.uri);
|
|
732
|
+
const sources = [
|
|
733
|
+
{ uri: textDocument.uri, source },
|
|
734
|
+
...(contextualUri
|
|
735
|
+
? [
|
|
736
|
+
{
|
|
737
|
+
uri: contextualUri,
|
|
738
|
+
source: entries.get(contextualUri)?.session.source,
|
|
739
|
+
},
|
|
740
|
+
]
|
|
741
|
+
: []),
|
|
742
|
+
{ uri: input.uri, source: input.source },
|
|
743
|
+
];
|
|
744
|
+
for (const candidate of sources) {
|
|
745
|
+
if (!candidate.uri || candidate.source === undefined)
|
|
746
|
+
continue;
|
|
747
|
+
const locations = vocabularyLocations(candidate.source, candidate.uri);
|
|
748
|
+
if (!Object.hasOwn(locations, name))
|
|
749
|
+
continue;
|
|
750
|
+
const span = locations[name].name;
|
|
751
|
+
return {
|
|
752
|
+
uri: candidate.uri,
|
|
753
|
+
range: {
|
|
754
|
+
start: offsetToPosition(candidate.source, span.start),
|
|
755
|
+
end: offsetToPosition(candidate.source, span.end),
|
|
756
|
+
},
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
571
761
|
const key = session?.locate(at);
|
|
572
762
|
const range = key ? session?.definition(key) : undefined;
|
|
573
763
|
return range ? { uri: textDocument.uri, range } : null;
|
|
574
764
|
});
|
|
575
|
-
connection.onCompletion(({ textDocument, position: at }) => {
|
|
765
|
+
connection.onCompletion(async ({ textDocument, position: at }) => {
|
|
576
766
|
const session = entries.get(textDocument.uri)?.session;
|
|
577
|
-
|
|
767
|
+
const source = session?.source ?? projects.buffer(textDocument.uri)?.source;
|
|
768
|
+
if (source === undefined)
|
|
578
769
|
return [];
|
|
579
|
-
const line =
|
|
580
|
-
|
|
581
|
-
|
|
770
|
+
const line = source.split(/\r?\n/)[at.line]?.slice(0, at.character) ?? "";
|
|
771
|
+
const prefix = /^\s*(?:-\s*)?(?:type|extends)\s*:\s*/.exec(line);
|
|
772
|
+
const scalarRange = prefix ? typeValueRangeAt(source, at) : undefined;
|
|
773
|
+
if (prefix &&
|
|
774
|
+
(scalarRange || !line.slice(prefix[0].length).includes("#"))) {
|
|
775
|
+
const { snapshot, input } = await vocabularyContext(textDocument.uri);
|
|
776
|
+
const vocabulary = snapshot.vocabulary ?? session?.snapshot().vocabulary;
|
|
777
|
+
const names = new Set([
|
|
778
|
+
...ConnectionTypeSchema.options,
|
|
779
|
+
...Object.keys(vocabulary ?? {}),
|
|
780
|
+
...Object.keys(input.declarations ?? {}),
|
|
781
|
+
]);
|
|
782
|
+
return [...names].map((type) => ({
|
|
582
783
|
label: type,
|
|
583
784
|
kind: CompletionItemKind.EnumMember,
|
|
584
|
-
detail: connectionWording(type, "outgoing")
|
|
785
|
+
detail: connectionWording(type, "outgoing", vocabulary && Object.hasOwn(vocabulary, type)
|
|
786
|
+
? vocabulary[type]
|
|
787
|
+
: undefined),
|
|
788
|
+
textEdit: {
|
|
789
|
+
range: scalarRange ?? {
|
|
790
|
+
start: { line: at.line, character: prefix[0].length },
|
|
791
|
+
end: at,
|
|
792
|
+
},
|
|
793
|
+
newText: JSON.stringify(type),
|
|
794
|
+
},
|
|
585
795
|
}));
|
|
586
|
-
|
|
796
|
+
}
|
|
797
|
+
if (!session?.snapshot().valid || session.snapshot().pending)
|
|
587
798
|
return [];
|
|
588
799
|
if (!/^\s*(?:-\s*)?node_id:\s*[^#]*$/.test(line))
|
|
589
800
|
return [];
|
|
@@ -597,6 +808,46 @@ export async function startServer(options) {
|
|
|
597
808
|
},
|
|
598
809
|
]) ?? []);
|
|
599
810
|
});
|
|
811
|
+
connection.onHover(async ({ textDocument, position: at }) => {
|
|
812
|
+
const source = entries.get(textDocument.uri)?.session.source ??
|
|
813
|
+
projects.buffer(textDocument.uri)?.source;
|
|
814
|
+
if (source === undefined)
|
|
815
|
+
return null;
|
|
816
|
+
const name = typeNameAt(source, at);
|
|
817
|
+
if (!name)
|
|
818
|
+
return null;
|
|
819
|
+
const { snapshot, input, contextualUri } = await vocabularyContext(textDocument.uri);
|
|
820
|
+
const registry = snapshot.vocabulary ?? {};
|
|
821
|
+
const definition = Object.hasOwn(registry, name)
|
|
822
|
+
? registry[name]
|
|
823
|
+
: undefined;
|
|
824
|
+
if (!definition)
|
|
825
|
+
return null;
|
|
826
|
+
const origin = declaresType(source, name)
|
|
827
|
+
? textDocument.uri
|
|
828
|
+
: contextualUri &&
|
|
829
|
+
declaresType(entries.get(contextualUri)?.session.source ?? "", name)
|
|
830
|
+
? contextualUri
|
|
831
|
+
: Object.hasOwn(input.declarations ?? {}, name)
|
|
832
|
+
? input.uri
|
|
833
|
+
: "built-in";
|
|
834
|
+
return {
|
|
835
|
+
contents: {
|
|
836
|
+
kind: "plaintext",
|
|
837
|
+
value: `${name} (${definition.coreKind})\n${definition.text} / ${definition.reverseText}\nDefined in ${origin ?? "project vocabulary"}`,
|
|
838
|
+
},
|
|
839
|
+
};
|
|
840
|
+
});
|
|
841
|
+
connection.onNotification("workspace/didChangeWorkspaceFolders", (params) => {
|
|
842
|
+
const deleted = new Set(params.event.removed.map((folder) => folder.uri));
|
|
843
|
+
workspaceUris = [
|
|
844
|
+
...workspaceUris.filter((uri) => !deleted.has(uri)),
|
|
845
|
+
...params.event.added.map((folder) => folder.uri),
|
|
846
|
+
];
|
|
847
|
+
projects.setRoots(workspaceUris);
|
|
848
|
+
for (const entry of entries.values())
|
|
849
|
+
void loadVocabulary(entry);
|
|
850
|
+
});
|
|
600
851
|
connection.onCodeAction(({ textDocument, range }) => entries.has(textDocument.uri)
|
|
601
852
|
? [
|
|
602
853
|
{
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { VocabularyInput } from "@lensmap/document";
|
|
2
|
+
export interface VocabularyBuffer {
|
|
3
|
+
source: string;
|
|
4
|
+
version: number;
|
|
5
|
+
}
|
|
6
|
+
/** Only known workspace config and vocabulary files, never HTTP-supplied paths. */
|
|
7
|
+
export declare class ProjectVocabularies {
|
|
8
|
+
private readonly changed;
|
|
9
|
+
private roots;
|
|
10
|
+
private readonly projects;
|
|
11
|
+
private readonly buffers;
|
|
12
|
+
private readonly requests;
|
|
13
|
+
private readonly pending;
|
|
14
|
+
private serial;
|
|
15
|
+
private disposed;
|
|
16
|
+
constructor(changed: (root: string, input: VocabularyInput) => void);
|
|
17
|
+
setRoots(uris: readonly string[]): void;
|
|
18
|
+
rootFor(uri: string): string | undefined;
|
|
19
|
+
private refresh;
|
|
20
|
+
private load;
|
|
21
|
+
forUri(uri: string): Promise<VocabularyInput>;
|
|
22
|
+
isResource(uri: string): boolean;
|
|
23
|
+
buffer(uri: string): VocabularyBuffer | undefined;
|
|
24
|
+
cachedInput(uri: string): VocabularyInput | undefined;
|
|
25
|
+
open(uri: string, source: string, version: number): Promise<boolean>;
|
|
26
|
+
close(uri: string): Promise<boolean>;
|
|
27
|
+
resources(root?: string): string[];
|
|
28
|
+
private clearWatches;
|
|
29
|
+
dispose(): void;
|
|
30
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { unwatchFile, watchFile } from "node:fs";
|
|
2
|
+
import { resolve, sep } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { readProjectVocabulary } from "@lensmap/document/project";
|
|
5
|
+
const filePath = (uri) => {
|
|
6
|
+
try {
|
|
7
|
+
return resolve(fileURLToPath(uri));
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
/** Only known workspace config and vocabulary files, never HTTP-supplied paths. */
|
|
14
|
+
export class ProjectVocabularies {
|
|
15
|
+
changed;
|
|
16
|
+
roots = [];
|
|
17
|
+
projects = new Map();
|
|
18
|
+
buffers = new Map();
|
|
19
|
+
requests = new Map();
|
|
20
|
+
pending = new Map();
|
|
21
|
+
serial = 0;
|
|
22
|
+
disposed = false;
|
|
23
|
+
constructor(changed) {
|
|
24
|
+
this.changed = changed;
|
|
25
|
+
}
|
|
26
|
+
setRoots(uris) {
|
|
27
|
+
this.clearWatches();
|
|
28
|
+
this.roots = [...new Set(uris.flatMap((uri) => filePath(uri) ?? []))].sort((a, b) => b.length - a.length);
|
|
29
|
+
}
|
|
30
|
+
rootFor(uri) {
|
|
31
|
+
const path = filePath(uri);
|
|
32
|
+
return path
|
|
33
|
+
? this.roots.find((root) => path.startsWith(root.endsWith(sep) ? root : `${root}${sep}`))
|
|
34
|
+
: undefined;
|
|
35
|
+
}
|
|
36
|
+
refresh(root, notify) {
|
|
37
|
+
const request = ++this.serial;
|
|
38
|
+
this.requests.set(root, request);
|
|
39
|
+
const work = this.load(root, request, notify).finally(() => {
|
|
40
|
+
if (this.pending.get(root) === work)
|
|
41
|
+
this.pending.delete(root);
|
|
42
|
+
});
|
|
43
|
+
this.pending.set(root, work);
|
|
44
|
+
return work;
|
|
45
|
+
}
|
|
46
|
+
async load(root, request, notify) {
|
|
47
|
+
if (this.disposed)
|
|
48
|
+
return;
|
|
49
|
+
const overlays = new Map([...this.buffers].flatMap(([uri, buffer]) => {
|
|
50
|
+
const path = filePath(uri);
|
|
51
|
+
return path ? [[path, buffer.source]] : [];
|
|
52
|
+
}));
|
|
53
|
+
const result = await readProjectVocabulary(root, overlays);
|
|
54
|
+
if (this.disposed ||
|
|
55
|
+
this.requests.get(root) !== request ||
|
|
56
|
+
!this.roots.includes(root))
|
|
57
|
+
return;
|
|
58
|
+
const signature = JSON.stringify(result.input);
|
|
59
|
+
let project = this.projects.get(root);
|
|
60
|
+
const changed = project?.signature !== signature;
|
|
61
|
+
if (!project) {
|
|
62
|
+
project = { input: result.input, signature, watches: new Map() };
|
|
63
|
+
this.projects.set(root, project);
|
|
64
|
+
}
|
|
65
|
+
project.input = result.input;
|
|
66
|
+
project.signature = signature;
|
|
67
|
+
const files = new Set(result.files);
|
|
68
|
+
for (const [path, listener] of project.watches) {
|
|
69
|
+
if (files.has(path))
|
|
70
|
+
continue;
|
|
71
|
+
unwatchFile(path, listener);
|
|
72
|
+
project.watches.delete(path);
|
|
73
|
+
}
|
|
74
|
+
for (const path of files) {
|
|
75
|
+
if (project.watches.has(path))
|
|
76
|
+
continue;
|
|
77
|
+
const listener = () => {
|
|
78
|
+
void this.refresh(root, true);
|
|
79
|
+
};
|
|
80
|
+
project.watches.set(path, listener);
|
|
81
|
+
watchFile(path, { persistent: false, interval: 100 }, listener);
|
|
82
|
+
}
|
|
83
|
+
if (notify && changed)
|
|
84
|
+
this.changed(root, project.input);
|
|
85
|
+
}
|
|
86
|
+
async forUri(uri) {
|
|
87
|
+
const root = this.rootFor(uri);
|
|
88
|
+
if (!root)
|
|
89
|
+
return {};
|
|
90
|
+
if (!this.projects.has(root) && !this.pending.has(root))
|
|
91
|
+
void this.refresh(root, false);
|
|
92
|
+
for (;;) {
|
|
93
|
+
const work = this.pending.get(root);
|
|
94
|
+
if (!work)
|
|
95
|
+
break;
|
|
96
|
+
await work;
|
|
97
|
+
}
|
|
98
|
+
return this.projects.get(root)?.input ?? {};
|
|
99
|
+
}
|
|
100
|
+
isResource(uri) {
|
|
101
|
+
const path = filePath(uri), root = this.rootFor(uri);
|
|
102
|
+
return Boolean(path &&
|
|
103
|
+
(path.endsWith(".lenstypes") ||
|
|
104
|
+
(root && path === resolve(root, "lensmap.config.json"))));
|
|
105
|
+
}
|
|
106
|
+
buffer(uri) {
|
|
107
|
+
return this.buffers.get(uri);
|
|
108
|
+
}
|
|
109
|
+
cachedInput(uri) {
|
|
110
|
+
const root = this.rootFor(uri);
|
|
111
|
+
return root ? this.projects.get(root)?.input : undefined;
|
|
112
|
+
}
|
|
113
|
+
async open(uri, source, version) {
|
|
114
|
+
if (!this.isResource(uri))
|
|
115
|
+
return false;
|
|
116
|
+
this.buffers.set(uri, { source, version });
|
|
117
|
+
const root = this.rootFor(uri);
|
|
118
|
+
if (root)
|
|
119
|
+
await this.refresh(root, true);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
async close(uri) {
|
|
123
|
+
if (!this.buffers.delete(uri))
|
|
124
|
+
return false;
|
|
125
|
+
const root = this.rootFor(uri);
|
|
126
|
+
if (root)
|
|
127
|
+
await this.refresh(root, true);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
resources(root) {
|
|
131
|
+
return [...this.buffers.keys()].filter((uri) => !root || this.rootFor(uri) === root);
|
|
132
|
+
}
|
|
133
|
+
clearWatches() {
|
|
134
|
+
for (const project of this.projects.values())
|
|
135
|
+
for (const [path, listener] of project.watches)
|
|
136
|
+
unwatchFile(path, listener);
|
|
137
|
+
this.projects.clear();
|
|
138
|
+
this.requests.clear();
|
|
139
|
+
this.pending.clear();
|
|
140
|
+
}
|
|
141
|
+
dispose() {
|
|
142
|
+
this.disposed = true;
|
|
143
|
+
this.clearWatches();
|
|
144
|
+
this.buffers.clear();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type SourcePosition, type SourceRange, type VocabularyInput } from "@lensmap/document";
|
|
2
|
+
/** Reuse engine resolution for editor tooling, without publishing a second graph. */
|
|
3
|
+
export declare function analyzeVocabularyForTooling(uri: string, supplied: VocabularyInput, mapSource?: string, vocabularySource?: string): {
|
|
4
|
+
snapshot: import("@lensmap/document").DocumentSnapshot;
|
|
5
|
+
input: VocabularyInput;
|
|
6
|
+
};
|
|
7
|
+
/** Native YAML scalar decoding preserves quoted/Unicode names exactly. */
|
|
8
|
+
export declare function typeNameAt(source: string, at: SourcePosition): string | undefined;
|
|
9
|
+
/** Replace the native scalar, including existing quotes but excluding comments. */
|
|
10
|
+
export declare function typeValueRangeAt(source: string, at: SourcePosition): SourceRange | undefined;
|
|
11
|
+
export declare function declaresType(source: string, name: string): boolean;
|