@kerebron/extension-lsp 0.7.9 → 0.8.2

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.
Files changed (46) hide show
  1. package/esm/ExtensionLsp.d.ts +2 -6
  2. package/esm/ExtensionLsp.d.ts.map +1 -1
  3. package/esm/ExtensionLsp.js +44 -36
  4. package/esm/ExtensionLsp.js.map +1 -1
  5. package/esm/LSPClient.d.ts +5 -4
  6. package/esm/LSPClient.d.ts.map +1 -1
  7. package/esm/LSPClient.js +14 -5
  8. package/esm/LSPClient.js.map +1 -1
  9. package/esm/LSPPlugin.d.ts +38 -0
  10. package/esm/LSPPlugin.d.ts.map +1 -0
  11. package/esm/LSPPlugin.js +190 -0
  12. package/esm/LSPPlugin.js.map +1 -0
  13. package/esm/{LspWebSocketTransport.d.ts → LSPWebSocketTransport.d.ts} +2 -2
  14. package/esm/{LspWebSocketTransport.d.ts.map → LSPWebSocketTransport.d.ts.map} +1 -1
  15. package/esm/{LspWebSocketTransport.js → LSPWebSocketTransport.js} +2 -2
  16. package/esm/{LspWebSocketTransport.js.map → LSPWebSocketTransport.js.map} +1 -1
  17. package/esm/LSPWorkspace.d.ts +39 -0
  18. package/esm/LSPWorkspace.d.ts.map +1 -0
  19. package/esm/LSPWorkspace.js +118 -0
  20. package/esm/LSPWorkspace.js.map +1 -0
  21. package/esm/createLspAutocomplete.d.ts +3 -9
  22. package/esm/createLspAutocomplete.d.ts.map +1 -1
  23. package/esm/createLspAutocomplete.js +4 -5
  24. package/esm/createLspAutocomplete.js.map +1 -1
  25. package/esm/mod.d.ts +1 -1
  26. package/esm/mod.d.ts.map +1 -1
  27. package/esm/mod.js +1 -1
  28. package/esm/mod.js.map +1 -1
  29. package/esm/workspace.d.ts +6 -35
  30. package/esm/workspace.d.ts.map +1 -1
  31. package/esm/workspace.js +3 -98
  32. package/esm/workspace.js.map +1 -1
  33. package/package.json +6 -6
  34. package/src/ExtensionLsp.ts +47 -47
  35. package/src/LSPClient.ts +21 -13
  36. package/src/LSPPlugin.ts +323 -0
  37. package/src/{LspWebSocketTransport.ts → LSPWebSocketTransport.ts} +1 -1
  38. package/src/LSPWorkspace.ts +172 -0
  39. package/src/createLspAutocomplete.ts +10 -7
  40. package/src/mod.ts +1 -1
  41. package/src/workspace.ts +8 -147
  42. package/esm/DiagnosticPlugin.d.ts +0 -32
  43. package/esm/DiagnosticPlugin.d.ts.map +0 -1
  44. package/esm/DiagnosticPlugin.js +0 -117
  45. package/esm/DiagnosticPlugin.js.map +0 -1
  46. package/src/DiagnosticPlugin.ts +0 -196
package/src/LSPClient.ts CHANGED
@@ -4,12 +4,9 @@ import {
4
4
  TextDocumentSyncKind,
5
5
  } from 'vscode-languageserver-protocol';
6
6
 
7
- import {
8
- DefaultWorkspace,
9
- LspSource,
10
- Workspace,
11
- WorkspaceFile,
12
- } from './workspace.js';
7
+ import { Workspace, WorkspaceFile } from './workspace.js';
8
+
9
+ import { LSPSource, LSPWorkspace } from './LSPWorkspace.js';
13
10
 
14
11
  const defaultNotificationHandlers: {
15
12
  [method: string]: (client: LSPClient, params: any) => void;
@@ -76,6 +73,11 @@ const clientCapabilities: lsp.ClientCapabilities = {
76
73
  parser: 'marked',
77
74
  },
78
75
  },
76
+ workspace: {
77
+ 'diagnostics': {
78
+ 'refreshSupport': true,
79
+ },
80
+ },
79
81
  textDocument: {
80
82
  publishDiagnostics: { versionSupport: true },
81
83
  completion: {
@@ -108,7 +110,7 @@ const clientCapabilities: lsp.ClientCapabilities = {
108
110
  implementation: {},
109
111
  typeDefinition: {},
110
112
  references: {},
111
- diagnostic: {},
113
+ // diagnostic: {},
112
114
  },
113
115
  window: {
114
116
  showMessage: {},
@@ -156,7 +158,7 @@ export class LSPError extends Error {
156
158
  }
157
159
 
158
160
  export class LSPClient extends EventTarget {
159
- sources: Record<string, LspSource> = {};
161
+ sources: Record<string, LSPSource> = {};
160
162
 
161
163
  workspace: Workspace;
162
164
  private nextReqID = 0;
@@ -209,7 +211,7 @@ export class LSPClient extends EventTarget {
209
211
 
210
212
  this.workspace = config.workspace
211
213
  ? config.workspace(this)
212
- : new DefaultWorkspace(this);
214
+ : new LSPWorkspace(this);
213
215
  }
214
216
 
215
217
  startInitializing() {
@@ -272,7 +274,7 @@ export class LSPClient extends EventTarget {
272
274
  this.workspace.connected();
273
275
  }
274
276
 
275
- connect(uri: string, source: LspSource) {
277
+ connect(uri: string, source: LSPSource) {
276
278
  if (this.sources[uri] && this.sources[uri] !== source) {
277
279
  throw new Error(`Source for ${uri} already connected`);
278
280
  }
@@ -311,6 +313,14 @@ export class LSPClient extends EventTarget {
311
313
  },
312
314
  },
313
315
  );
316
+ await this.notification<lsp.DocumentDiagnosticParams>(
317
+ 'textDocument/diagnostic',
318
+ {
319
+ textDocument: {
320
+ uri: file.uri,
321
+ },
322
+ },
323
+ );
314
324
  return true;
315
325
  }
316
326
 
@@ -464,7 +474,7 @@ export class LSPClient extends EventTarget {
464
474
  return this.serverCapabilities ? !!this.serverCapabilities[name] : null;
465
475
  }
466
476
 
467
- sync() {
477
+ syncFiles() {
468
478
  this.workspace.syncFiles();
469
479
  }
470
480
 
@@ -474,8 +484,6 @@ export class LSPClient extends EventTarget {
474
484
  id: number,
475
485
  params: any,
476
486
  ) {
477
- console.error('this.timeoutRequest', this.timeout, method, id, params);
478
-
479
487
  const index = this.requests.indexOf(req);
480
488
  if (index > -1) {
481
489
  req.reject(LSPError.createTimeout());
@@ -0,0 +1,323 @@
1
+ import { Plugin, PluginKey, Transaction } from 'prosemirror-state';
2
+ import { Decoration, DecorationSet } from 'prosemirror-view';
3
+ import { Diagnostic } from 'vscode-languageserver-protocol';
4
+
5
+ import { PositionMapper } from '@kerebron/extension-markdown/PositionMapper';
6
+ import { CoreEditor } from '@kerebron/editor';
7
+ import {
8
+ type ExtensionMarkdown,
9
+ type MarkdownResult,
10
+ } from '@kerebron/extension-markdown';
11
+
12
+ import { ExtensionLsp, LspConfig } from './ExtensionLsp.js';
13
+
14
+ export interface LspRange {
15
+ line: number;
16
+ character: number;
17
+ }
18
+
19
+ interface DiagnosticsItem {
20
+ severity: number;
21
+ range: {
22
+ start: LspRange;
23
+ end: LspRange;
24
+ };
25
+ message: string;
26
+ source: string;
27
+ code: string;
28
+ data: any; // "data":{"kind":"todo","keyword":"TODO"}},
29
+ }
30
+
31
+ interface LSPMeta {
32
+ }
33
+
34
+ interface WorkspaceMetaOpen {
35
+ operation: 'openFile';
36
+ uri: string;
37
+ lang: string;
38
+ }
39
+
40
+ interface WorkspaceMetaModify {
41
+ operation: 'modifyFile';
42
+ uri: string;
43
+ }
44
+
45
+ interface WorkspaceMetaClose {
46
+ operation: 'closeFile';
47
+ uri: string;
48
+ }
49
+
50
+ type WorkspaceMeta =
51
+ | WorkspaceMetaOpen
52
+ | WorkspaceMetaClose
53
+ | WorkspaceMetaModify;
54
+
55
+ class LSPPluginState {
56
+ diagnostics: DiagnosticsItem[] = [];
57
+
58
+ snapshot?: {
59
+ version: number;
60
+ mapper: PositionMapper;
61
+ markdownResult: MarkdownResult;
62
+ };
63
+
64
+ constructor(private editor: CoreEditor) {
65
+ }
66
+ }
67
+
68
+ const LSPPluginKey = new PluginKey('lsp');
69
+
70
+ export class LSPPlugin extends Plugin<LSPPluginState> {
71
+ listenerDiag: ((event: Event) => void) | undefined;
72
+ listenerDisconnect: ((event: Event) => void) | undefined;
73
+ listenerChange: ((event: Event) => void) | undefined;
74
+
75
+ constructor(config: LspConfig, extension: ExtensionLsp) {
76
+ super({
77
+ key: LSPPluginKey,
78
+ view(view) {
79
+ return {
80
+ destroy: () => {
81
+ const client = extension.getClient(extension.mainLang);
82
+
83
+ if (this.listenerDiag && client) {
84
+ client.removeEventListener(
85
+ 'textDocument/publishDiagnostics',
86
+ this.listenerDiag,
87
+ );
88
+ }
89
+ if (this.listenerDisconnect && client) {
90
+ client.removeEventListener(
91
+ 'close',
92
+ this.listenerDisconnect,
93
+ );
94
+ }
95
+ // if (this.listenerChange) {
96
+ // extension.getEditor().removeEventListener(
97
+ // 'change',
98
+ // this.listenerChange,
99
+ // );
100
+ // }
101
+ },
102
+ };
103
+ },
104
+ state: {
105
+ init() {
106
+ return new LSPPluginState(editor);
107
+ },
108
+ apply(
109
+ tr: Transaction,
110
+ value: LSPPluginState,
111
+ oldState,
112
+ newState,
113
+ ) {
114
+ const workspaceMeta: WorkspaceMeta | undefined = tr.getMeta(
115
+ 'workspace',
116
+ );
117
+
118
+ if (workspaceMeta) {
119
+ console.log(
120
+ 'workspaceMeta',
121
+ workspaceMeta.operation,
122
+ workspaceMeta,
123
+ );
124
+
125
+ const uri = workspaceMeta.uri;
126
+ // const lang = workspaceMeta.lang;
127
+
128
+ switch (workspaceMeta.operation) {
129
+ case 'openFile':
130
+ {
131
+ const client = extension.getClient(workspaceMeta.lang);
132
+ if (client) {
133
+ client.addEventListener(
134
+ 'textDocument/publishDiagnostics',
135
+ this.diagListener,
136
+ );
137
+
138
+ const go = async () => {
139
+ client.connect(uri, this.source);
140
+ if (uri === editor.config.uri) {
141
+ await client.restart();
142
+ }
143
+ client.workspace.openFile(
144
+ workspaceMeta.uri,
145
+ workspaceMeta.lang,
146
+ this.source,
147
+ );
148
+ };
149
+
150
+ go();
151
+ }
152
+ }
153
+ break;
154
+ case 'modifyFile':
155
+ break;
156
+ case 'closeFile':
157
+ // this.editor.view.dispatch(tr);
158
+ // const client = this.getClient(this.mainLang);
159
+ // if (client) {
160
+ // client.disconnect(this.uri);
161
+ // }
162
+
163
+ break;
164
+ }
165
+
166
+ return value;
167
+ }
168
+
169
+ const meta: LSPMeta | undefined = tr.getMeta(LSPPluginKey);
170
+ if (meta?.diagnostics) {
171
+ value.diagnostics = meta.diagnostics;
172
+ }
173
+
174
+ const markdownMeta = tr.getMeta('markdown');
175
+ if (markdownMeta?.snapshot) {
176
+ value.snapshot = markdownMeta.snapshot;
177
+ return value;
178
+ }
179
+ if (markdownMeta?.clearSnapshot) {
180
+ value.snapshot = undefined;
181
+ return value;
182
+ }
183
+
184
+ if (tr.docChanged) {
185
+ // value.snapshot = undefined;
186
+ }
187
+
188
+ // let codeBlockChanged = false;
189
+
190
+ // // Compare changed ranges only
191
+ // tr.mapping.maps.forEach((stepMap) => {
192
+ // stepMap.forEach((oldStart, oldEnd, newStart, newEnd) => {
193
+ // oldState.doc.nodesBetween(oldStart, oldEnd, (node) => {
194
+ // if (node.type.name === "code_block") {
195
+ // codeBlockChanged = true;
196
+ // }
197
+ // });
198
+
199
+ // newState.doc.nodesBetween(newStart, newEnd, (node) => {
200
+ // if (node.type.name === "code_block") {
201
+ // codeBlockChanged = true;
202
+ // }
203
+ // });
204
+ // });
205
+ // });
206
+
207
+ // if (codeBlockChanged) {
208
+ // console.log("Code block changed");
209
+ // // trigger your logic here
210
+ // }
211
+
212
+ return value;
213
+ },
214
+ },
215
+ props: {
216
+ decorations(state) {
217
+ const decorations: Decoration[] = [];
218
+
219
+ const pluginState = this.getState(state);
220
+ if (pluginState) {
221
+ const { diagnostics, snapshot } = pluginState;
222
+
223
+ if (snapshot?.version === editor.version) {
224
+ for (const diag of diagnostics) {
225
+ const from = snapshot.mapper.fromLineChar(
226
+ diag.range.start.line,
227
+ diag.range.start.character,
228
+ );
229
+ const end = snapshot.mapper.fromLineChar(
230
+ diag.range.end.line,
231
+ diag.range.end.character,
232
+ );
233
+
234
+ if (from > -1 && end > -1) {
235
+ const decoration = Decoration.inline(
236
+ from,
237
+ end,
238
+ { class: 'kb-lsp__error', title: diag.message },
239
+ );
240
+ decorations.push(decoration);
241
+ }
242
+ }
243
+ }
244
+ }
245
+
246
+ return DecorationSet.create(state.doc, decorations);
247
+ },
248
+ },
249
+ });
250
+
251
+ let lastDiag = 0;
252
+
253
+ const editor = extension.getEditor();
254
+
255
+ const uri = editor.config.uri;
256
+ if (uri) {
257
+ const client = extension.getClient(extension.mainLang);
258
+
259
+ this.listenerDiag = (event: Event) => {
260
+ const detail = (event as CustomEvent).detail;
261
+ if (detail.params.uri !== uri) {
262
+ return;
263
+ }
264
+
265
+ event.preventDefault();
266
+
267
+ lastDiag = +Date();
268
+
269
+ if (client) {
270
+ const file = client.workspace.getFile(uri);
271
+ if (file) {
272
+ const { mapper } = file;
273
+
274
+ const diagnostics: Array<Diagnostic> = detail.params.diagnostics;
275
+
276
+ // file.dia
277
+
278
+ const tr = editor.view.state.tr.setMeta(LSPPluginKey, {
279
+ diagnostics: detail.params.diagnostics,
280
+ mapper,
281
+ });
282
+ editor.view.dispatch(tr);
283
+ }
284
+ }
285
+ };
286
+
287
+ if (client) {
288
+ client.addEventListener(
289
+ 'textDocument/publishDiagnostics',
290
+ this.listenerDiag,
291
+ );
292
+ }
293
+
294
+ this.listenerDisconnect = (event: Event) => {
295
+ const tr = editor.view.state.tr.setMeta(LSPPluginKey, {
296
+ diagnostics: [],
297
+ mapper: undefined,
298
+ });
299
+ editor.view.dispatch(tr);
300
+ };
301
+
302
+ if (client) {
303
+ client.addEventListener(
304
+ 'close',
305
+ this.listenerDisconnect,
306
+ );
307
+ }
308
+
309
+ this.listenerChange = (event: Event) => {
310
+ if (lastDiag === 0 && +Date() - lastDiag > 10_1000) {
311
+ return;
312
+ }
313
+ const tr = editor.view.state.tr.setMeta(LSPPluginKey, {
314
+ diagnostics: [],
315
+ mapper: undefined,
316
+ });
317
+ editor.view.dispatch(tr);
318
+ };
319
+
320
+ // extension.getEditor().addEventListener('changed', this.listenerChange);
321
+ }
322
+ }
323
+ }
@@ -7,7 +7,7 @@ function shouldReconnectOnCode(code: number) {
7
7
  return reconnectCodes.includes(code);
8
8
  }
9
9
 
10
- export class LspWebSocketTransport extends EventTarget implements Transport {
10
+ export class LSPWebSocketTransport extends EventTarget implements Transport {
11
11
  socket: WebSocket | undefined;
12
12
  private reconnectAttempts = 0;
13
13
  private maxAttempts = 10;
@@ -0,0 +1,172 @@
1
+ import type * as lsp from 'vscode-languageserver-protocol';
2
+ import { TextDocumentSyncKind } from 'vscode-languageserver-protocol';
3
+
4
+ import type { EditorUi } from '@kerebron/editor';
5
+ import { Workspace, WorkspaceFile } from './workspace.js';
6
+
7
+ import { PositionMapper } from '@kerebron/extension-markdown/PositionMapper';
8
+
9
+ import { LSPClient } from './LSPClient.js';
10
+ import { computeIncrementalChanges } from './computeIncrementalChanges.js';
11
+
12
+ export interface LSPSource {
13
+ ui: EditorUi;
14
+ getMappedContent(): Promise<{ content: string; mapper: PositionMapper }>;
15
+ }
16
+
17
+ class LSPWorkspaceFile implements WorkspaceFile {
18
+ public syncedContent: string = '';
19
+ public diagnostics: Array<lsp.Diagnostic> = [];
20
+
21
+ constructor(
22
+ readonly uri: string,
23
+ readonly languageId: string,
24
+ public version: number,
25
+ public content: string,
26
+ public mapper: PositionMapper,
27
+ public source: LSPSource,
28
+ ) {
29
+ }
30
+
31
+ getUi() {
32
+ return this.source.ui;
33
+ }
34
+ }
35
+
36
+ export class LSPWorkspace extends Workspace {
37
+ files: LSPWorkspaceFile[] = [];
38
+ private fileVersions: { [uri: string]: number } = Object.create(null);
39
+
40
+ constructor(private client: LSPClient) {
41
+ super();
42
+ }
43
+
44
+ nextFileVersion(uri: string) {
45
+ return this.fileVersions[uri] = (this.fileVersions[uri] ?? -1) + 1;
46
+ }
47
+
48
+ override async connected(): Promise<void> {
49
+ super.connected();
50
+ for await (const file of this.files) {
51
+ const result = await this.client.didOpen(file);
52
+ }
53
+ }
54
+
55
+ override disconnected(): void {
56
+ for (const file of this.files) {
57
+ this.client.workspace.closeFile(file.uri);
58
+ }
59
+ super.disconnected();
60
+ }
61
+
62
+ syncFiles() {
63
+ if (!this.client.supportSync) {
64
+ return;
65
+ }
66
+
67
+ for (const file of this.files) {
68
+ this.changedFile(file.uri);
69
+ }
70
+ }
71
+
72
+ async changedFile(uri: string) {
73
+ const file = this.files.find((f) => f.uri == uri) || null;
74
+
75
+ if (file) {
76
+ const { content, mapper } = await file.source.getMappedContent();
77
+
78
+ if (!this.isConnected) {
79
+ file.content = content;
80
+ file.mapper = mapper;
81
+ return;
82
+ }
83
+
84
+ if (
85
+ await this.client.notification<lsp.DidChangeTextDocumentParams>(
86
+ 'textDocument/didChange',
87
+ {
88
+ textDocument: { uri: file.uri, version: file.version },
89
+ contentChanges: contentChangesFor(
90
+ file,
91
+ content,
92
+ mapper,
93
+ this.client.supportSync == TextDocumentSyncKind.Incremental,
94
+ ),
95
+ },
96
+ )
97
+ ) {
98
+ file.syncedContent = file.content;
99
+ file.content = content;
100
+ file.mapper = mapper;
101
+ file.version = this.nextFileVersion(file.uri);
102
+ file.diagnostics = [];
103
+
104
+ this.dispatchEvent(
105
+ new CustomEvent('fileChanged', { // TODO: clear diagnosrics
106
+ detail: {
107
+ uri: file.uri,
108
+ file,
109
+ },
110
+ }),
111
+ );
112
+ }
113
+
114
+ await this.client.notification<lsp.DocumentDiagnosticParams>(
115
+ 'textDocument/diagnostic',
116
+ {
117
+ textDocument: {
118
+ uri: file.uri,
119
+ },
120
+ },
121
+ );
122
+ }
123
+ }
124
+
125
+ async openFile(uri: string, languageId: string, source: LSPSource) {
126
+ // if (uri) {}
127
+
128
+ if (this.getFile(uri)) {
129
+ this.closeFile(uri);
130
+ }
131
+
132
+ const mappedContent = await source.getMappedContent();
133
+ const { content, mapper } = mappedContent;
134
+ const file = new LSPWorkspaceFile(
135
+ uri,
136
+ languageId,
137
+ this.nextFileVersion(uri),
138
+ content,
139
+ mapper,
140
+ source,
141
+ );
142
+ this.files.push(file);
143
+
144
+ this.client.didOpen(file);
145
+ }
146
+
147
+ closeFile(uri: string) {
148
+ const file = this.getFile(uri);
149
+ if (file) {
150
+ this.files = this.files.filter((f) => f.uri !== uri);
151
+ this.client.didClose(uri);
152
+ }
153
+ }
154
+ }
155
+
156
+ const enum Sync {
157
+ AlwaysIfSmaller = 1024,
158
+ }
159
+
160
+ function contentChangesFor(
161
+ file: WorkspaceFile,
162
+ newContent: string,
163
+ mapper: PositionMapper,
164
+ supportInc: boolean,
165
+ ): lsp.TextDocumentContentChangeEvent[] {
166
+ if (!supportInc || newContent.length < Sync.AlwaysIfSmaller) {
167
+ return [{ text: newContent }];
168
+ }
169
+
170
+ const changes = computeIncrementalChanges(file.content, newContent);
171
+ return changes.reverse();
172
+ }
@@ -1,10 +1,11 @@
1
- import { DefaultRenderer } from '@kerebron/extension-autocomplete/DefaultRenderer';
1
+ import { DefaultRenderer } from '@kerebron/extension-ui/autocomplete/DefaultRenderer';
2
2
  import { type CoreEditor, TextRange } from '@kerebron/editor';
3
3
 
4
4
  import {
5
+ AutocompleteConfig,
5
6
  type AutocompleteProps,
6
7
  createRegexMatcher,
7
- } from '@kerebron/extension-autocomplete';
8
+ } from '@kerebron/extension-ui/autocomplete';
8
9
 
9
10
  import { ExtensionLsp } from './ExtensionLsp.js';
10
11
 
@@ -35,13 +36,15 @@ export function cleanPlaceholders(input: string): string {
35
36
  return input.replace(regex, '$1');
36
37
  }
37
38
 
38
- export function createLspAutocomplete(extensionLsp: ExtensionLsp) {
39
+ export function createLspAutocomplete(
40
+ extensionLsp: ExtensionLsp,
41
+ ): { autocompleteConfig: AutocompleteConfig } {
39
42
  const editor: CoreEditor = extensionLsp.getEditor();
40
43
  const renderer = new CustomRenderer(editor);
41
44
 
42
- const config = {
43
- renderer,
44
- matchers: [createRegexMatcher([/\w+/, /(^|\s)@\w*/, /^#\w*/])],
45
+ const config: AutocompleteConfig = {
46
+ // renderer,
47
+ // matchers: [createRegexMatcher([/\w+/, /(^|\s)@\w*/, /^#\w*/])],
45
48
  getItems: async (query: string, props: AutocompleteProps) => {
46
49
  const { mapper } = await extensionLsp.source.getMappedContent();
47
50
 
@@ -49,7 +52,7 @@ export function createLspAutocomplete(extensionLsp: ExtensionLsp) {
49
52
 
50
53
  const client = extensionLsp.getClient(extensionLsp.mainLang);
51
54
  if (client) {
52
- client.sync();
55
+ client.syncFiles();
53
56
  try {
54
57
  const completions:
55
58
  | { items: CompletionItem[] }
package/src/mod.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export * from './ExtensionLsp.js';
2
2
  export * from './LSPClient.js';
3
- export * from './workspace.js';
3
+ export * from './LSPWorkspace.js';