@lensmap/language-server 0.2.1 → 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.
@@ -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-BCY9v6Ap.js"></script>
9
- <link rel="modulepreload" crossorigin href="/assets/styles-Bnie2-Dn.js">
10
- <link rel="stylesheet" crossorigin href="/assets/styles-9bHR-mQh.css">
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-BRCvwAxB.js"></script>
10
- <link rel="modulepreload" crossorigin href="/assets/styles-Bnie2-Dn.js">
11
- <link rel="stylesheet" crossorigin href="/assets/styles-9bHR-mQh.css">
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.map((item) => ({
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
- if (!session)
767
+ const source = session?.source ?? projects.buffer(textDocument.uri)?.source;
768
+ if (source === undefined)
578
769
  return [];
579
- const line = session.source.split(/\r?\n/)[at.line]?.slice(0, at.character) ?? "";
580
- if (/^\s*(?:-\s*)?type:\s*[^#]*$/.test(line))
581
- return ConnectionTypeSchema.options.map((type) => ({
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
- if (!session.snapshot().valid || session.snapshot().pending)
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;