forked from eclipse-langium/langium-website
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcommon.ts
317 lines (270 loc) · 10 KB
/
common.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
/******************************************************************************
* Copyright 2022 TypeFox GmbH
* This program and the accompanying materials are made available under the
* terms of the MIT License, which is available in the project root.
******************************************************************************/
import {
HelloWorldGrammar,
LangiumTextMateContent,
DSLInitialContent,
} from "./constants.js";
import { decompressFromEncodedURIComponent } from 'lz-string';
import { Disposable } from "vscode-languageserver";
import { render } from './Tree.js';
import { overlay, throttle } from "./utils.js";
import { addMonacoStyles, createUserConfig, MonacoEditorLanguageClientWrapper } from "langium-website-core/bundle";
import { DocumentChangeResponse } from "langium-ast-helper";
import { DefaultAstNodeLocator } from "langium";
import { createServicesForGrammar } from "langium/grammar";
import { generateTextMate } from "langium-cli/textmate";
export { share, overlay } from './utils.js';
export { addMonacoStyles, MonacoEditorLanguageClientWrapper };
export interface PlaygroundParameters {
grammar: string;
content: string;
}
/**
* Current langium grammar in the playground
*/
let currentGrammarContent = '';
/**
* Current DSL program in the playground
*/
let currentDSLContent = '';
/**
* DSL wrapper, allowing us to quickly access the current in-model code
*/
let dslWrapper: MonacoEditorLanguageClientWrapper | undefined = undefined;
/**
* Update delay for new grammars & DSL programs to be processed
* Any new updates occurring during this delay will cause an existing update to be cancelled,
* and will reset the delay again
*/
const languageUpdateDelay = 150;
/**
* Counter for language ids, which are incremented on each change
*/
let nextIdCounter = 0;
/**
* Helper for retrieving the next language id to use, to avoid conflicting with prior ones
*/
function nextId(): string {
return (nextIdCounter++).toString();
}
/**
* Helper to retrieve the current grammar & program in the playground.
* Typically used to generate a save link to this state
*/
export function getPlaygroundState(): PlaygroundParameters {
return {
grammar: currentGrammarContent,
content: currentDSLContent
};
}
/**
* Starts the playground
*
* @param leftEditor Left editor element
* @param rightEditor Right editor element
* @param encodedGrammar Encoded grammar to optionally use
* @param encodedContent Encoded content to optionally use
*/
export async function setupPlayground(
leftEditor: HTMLElement,
rightEditor: HTMLElement,
encodedGrammar?: string,
encodedContent?: string
): Promise<void> {
// setup initial contents for the grammar & dsl (Hello World)
currentGrammarContent = HelloWorldGrammar;
currentDSLContent = DSLInitialContent;
// handle to a Monaco language client instance for the DSL (program) editor
let dslClient;
// check to use existing grammar from URI
if (encodedGrammar) {
currentGrammarContent = decompressFromEncodedURIComponent(encodedGrammar) ?? currentGrammarContent;
}
// check to use existing content from URI
if (encodedContent) {
currentDSLContent = decompressFromEncodedURIComponent(encodedContent) ?? currentDSLContent;
}
// setup langium wrapper
const langiumWrapper = await getFreshLangiumWrapper(leftEditor);
// setup DSL wrapper
await setupDSLWrapper().catch((e) => {
// failed to setup, can happen with a bad Langium grammar, report it & discard
console.error('DSL editor setup error: ' + e);
overlay(true, true);
});
// retrieve the langium language client
const langiumClient = langiumWrapper.getLanguageClient();
if (!langiumClient) {
throw new Error('Unable to obtain language client for the Langium editor!');
}
// register to receive new grammars from langium, and send them to the DSL language client
langiumClient.onNotification('browser/DocumentChange', (resp: DocumentChangeResponse) => {
// verify the langium client is still running, and didn't crash due to a grammar issue
if (!langiumClient.isRunning()) {
throw new Error('Langium client is not running');
}
// extract & update current grammar
currentGrammarContent = resp.content;
if (resp.diagnostics.filter(d => d.severity === 1).length) {
// error in the grammar, report an error & stop here
overlay(true, true);
return;
}
// set a new timeout for updating our DSL grammar & editor, 200ms, to avoid intermediate states
throttle(1, languageUpdateDelay, async () => {
// display 'Loading...' while we regenerate the DSL editor
overlay(true, false);
if (!dslWrapper) {
// no dsl wrapper to start (or previously crashed), setup from scratch
// no exception handling here, as we're 'assuming' the Langium grammar is valid at this point
// or we already have a wrapper that crashed (2nd case here)
await setupDSLWrapper();
overlay(false, false);
} else {
// existing wrapper, attempt to first dispose
await dslWrapper?.dispose().then(async () => {
// disposed successfully, setup & clear overlay
await setupDSLWrapper();
overlay(false, false);
}).catch(async (e: any) => {
// failed to dispose, report & discard this error
// can happen when a previous editor was not started correctly
console.error('DSL editor disposal error: ' + e);
overlay(true, true);
});
}
});
});
/**
* Helper to configure & retrieve a fresh DSL wrapper
*/
async function setupDSLWrapper(): Promise<void> {
// get a fresh DSL wrapper
dslWrapper = await getFreshDSLWrapper(rightEditor, nextId(), currentDSLContent, currentGrammarContent);
// get a fresh client
dslClient = dslWrapper?.getLanguageClient();
if (!dslClient) {
throw new Error('Failed to retrieve fresh DSL LS client');
}
// re-register
registerForDocumentChanges(dslClient);
}
window.addEventListener("resize", () => {
dslWrapper?.updateLayout();
langiumWrapper.updateLayout();
});
// drop the overlay once done here
overlay(false, false);
}
/**
* Starts a fresh Monaco LC wrapper
*
* @param htmlElement Element to attach the editor to
* @param languageId ID of the language to use
* @param code Program to show in the editor
* @param grammarText Grammar text to use for the worker & monarch syntax
* @returns A promise resolving to a configured & started DSL wrapper
*/
async function getFreshDSLWrapper(
htmlElement: HTMLElement,
languageId: string,
code: string,
grammarText: string
): Promise<MonacoEditorLanguageClientWrapper | undefined> {
// construct and set a new monarch syntax onto the editor
const { Grammar } = await createServicesForGrammar({ grammar: grammarText });
const worker = await getLSWorkerForGrammar(grammarText);
const wrapper = new MonacoEditorLanguageClientWrapper();
const textmateGrammar = JSON.parse(generateTextMate(Grammar, { id: languageId, grammar: 'UserGrammar' }));
return wrapper.start(createUserConfig({
languageId,
code,
worker,
textmateGrammar
}), htmlElement).then(() => {
return wrapper;
}).catch(async (e: any) => {
console.error('Failed to start DSL wrapper: ' + e);
// don't leak the worker on failure to start
// normally we wouldn't need to manually terminate, but if the LC is stuck in the 'starting' state, the following dispose will fail prematurely
// and the worker will leak
worker.terminate();
// try to cleanup the existing wrapper (but don't fail if we can't complete this action)
// particularly due to a stuck LC, which can cause this to fail part-ways through
try {
await wrapper.dispose();
} catch (e) {}
return undefined as MonacoEditorLanguageClientWrapper|undefined;
});
}
/**
* Gets a fresh langium wrapper
*
* @param htmlElement Element to attach the wrapper to
* @returns A promise resolving to a configured & started Langium wrapper
*/
async function getFreshLangiumWrapper(htmlElement: HTMLElement): Promise<MonacoEditorLanguageClientWrapper> {
const langiumWrapper = new MonacoEditorLanguageClientWrapper();
await langiumWrapper.start(createUserConfig({
languageId: "langium",
code: currentGrammarContent,
worker: "./libs/worker/langiumServerWorker.js",
textmateGrammar: LangiumTextMateContent
}), htmlElement);
return langiumWrapper;
}
/**
* Document change listener for modified DSL programs
*/
let dslDocumentChangeListener: Disposable;
/**
* Helper for registering to receive new ASTs from parsed DSL programs
*/
function registerForDocumentChanges(dslClient: any | undefined) {
// dispose of any existing listener
if (dslDocumentChangeListener) {
dslDocumentChangeListener.dispose();
}
// register to receive new ASTs from parsed DSL programs
dslDocumentChangeListener = dslClient!.onNotification('browser/DocumentChange', (resp: DocumentChangeResponse) => {
// retrieve existing code from the model
currentDSLContent = dslWrapper?.getModel()?.getValue() as string;
// delay changes by 200ms, to avoid getting too many intermediate states
throttle(2, languageUpdateDelay, () => {
// render the AST in the far-right window
render(
JSON.parse(resp.content),
new DefaultAstNodeLocator()
);
});
});
}
/**
* Produce a new LS worker for a given grammar, which returns a Promise once it's finished starting
*
* @param grammar To setup LS for
* @returns Configured LS worker
*/
async function getLSWorkerForGrammar(grammar: string): Promise<Worker> {
return new Promise((resolve, reject) => {
// create & notify the worker to setup w/ this grammar
const worker = new Worker("./libs/worker/userServerWorker.js");
worker.postMessage({
type: "startWithGrammar",
grammar
});
// wait for the worker to finish starting
worker.onmessage = (event) => {
if (event.data.type === "lsStartedWithGrammar") {
resolve(worker);
}
};
worker.onerror = (event) => {
reject(event);
};
});
}