@hydranium/cli 1.0.0-next.27 → 1.0.0-next.270
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 +73 -204
- package/bin/hydranium-cli.js +13 -0
- package/lib/cli-version.js.map +1 -1
- package/lib/cli.js +2 -1
- package/lib/cli.js.map +1 -1
- package/lib/commands/analyze-heap-args.js.map +1 -1
- package/lib/commands/analyze-heap.d.ts +3 -0
- package/lib/commands/analyze-heap.d.ts.map +1 -1
- package/lib/commands/analyze-heap.js +2 -1
- package/lib/commands/analyze-heap.js.map +1 -1
- package/lib/commands/ast-ground-truth-args.d.ts +3 -2
- package/lib/commands/ast-ground-truth-args.d.ts.map +1 -1
- package/lib/commands/ast-ground-truth-args.js +11 -9
- package/lib/commands/ast-ground-truth-args.js.map +1 -1
- package/lib/commands/ast-ground-truth-driver.js +2 -1
- package/lib/commands/ast-ground-truth-driver.js.map +1 -1
- package/lib/commands/ast-ground-truth.js +1 -1
- package/lib/commands/ast-ground-truth.js.map +1 -1
- package/lib/commands/generate-ast-builder.d.ts +49 -0
- package/lib/commands/generate-ast-builder.d.ts.map +1 -0
- package/lib/commands/generate-ast-builder.js +200 -0
- package/lib/commands/generate-ast-builder.js.map +1 -0
- package/lib/commands/generate-transfer-model-args.d.ts.map +1 -1
- package/lib/commands/generate-transfer-model-args.js +11 -1
- package/lib/commands/generate-transfer-model-args.js.map +1 -1
- package/lib/commands/generate-transfer-model-config.d.ts.map +1 -1
- package/lib/commands/generate-transfer-model-config.js +6 -3
- package/lib/commands/generate-transfer-model-config.js.map +1 -1
- package/lib/commands/generate-transfer-model.d.ts +9 -0
- package/lib/commands/generate-transfer-model.d.ts.map +1 -1
- package/lib/commands/generate-transfer-model.js +18 -4
- package/lib/commands/generate-transfer-model.js.map +1 -1
- package/lib/commands/harness-args.d.ts +37 -42
- package/lib/commands/harness-args.d.ts.map +1 -1
- package/lib/commands/harness-args.js +91 -27
- package/lib/commands/harness-args.js.map +1 -1
- package/lib/commands/headless-harness.d.ts +37 -20
- package/lib/commands/headless-harness.d.ts.map +1 -1
- package/lib/commands/headless-harness.js +68 -17
- package/lib/commands/headless-harness.js.map +1 -1
- package/lib/commands/init-args.js +6 -6
- package/lib/commands/init-args.js.map +1 -1
- package/lib/commands/init-prompt.d.ts.map +1 -1
- package/lib/commands/init-templates.d.ts +8 -10
- package/lib/commands/init-templates.d.ts.map +1 -1
- package/lib/commands/init-templates.js +260 -152
- package/lib/commands/init-templates.js.map +1 -1
- package/lib/commands/init-wizard.js +4 -4
- package/lib/commands/init-wizard.js.map +1 -1
- package/lib/commands/init-workspace.d.ts +13 -3
- package/lib/commands/init-workspace.d.ts.map +1 -1
- package/lib/commands/init-workspace.js +21 -6
- package/lib/commands/init-workspace.js.map +1 -1
- package/lib/commands/init.d.ts +16 -10
- package/lib/commands/init.d.ts.map +1 -1
- package/lib/commands/init.js +17 -15
- package/lib/commands/init.js.map +1 -1
- package/lib/commands/lint-grammar-args.d.ts +3 -2
- package/lib/commands/lint-grammar-args.d.ts.map +1 -1
- package/lib/commands/lint-grammar-args.js +13 -10
- package/lib/commands/lint-grammar-args.js.map +1 -1
- package/lib/commands/lint-grammar-driver.js +2 -1
- package/lib/commands/lint-grammar-driver.js.map +1 -1
- package/lib/commands/lint-grammar-report.js.map +1 -1
- package/lib/commands/lint-grammar.js +1 -1
- package/lib/commands/lint-grammar.js.map +1 -1
- package/lib/commands/measure-memory-args.d.ts +3 -2
- package/lib/commands/measure-memory-args.d.ts.map +1 -1
- package/lib/commands/measure-memory-args.js +22 -11
- package/lib/commands/measure-memory-args.js.map +1 -1
- package/lib/commands/measure-memory-driver.js +5 -3
- package/lib/commands/measure-memory-driver.js.map +1 -1
- package/lib/commands/measure-memory.js +1 -1
- package/lib/commands/measure-memory.js.map +1 -1
- package/lib/commands/model-docs-args.d.ts +4 -3
- package/lib/commands/model-docs-args.d.ts.map +1 -1
- package/lib/commands/model-docs-args.js +12 -10
- package/lib/commands/model-docs-args.js.map +1 -1
- package/lib/commands/model-docs-driver.js +2 -1
- package/lib/commands/model-docs-driver.js.map +1 -1
- package/lib/commands/model-docs.js +1 -1
- package/lib/commands/model-docs.js.map +1 -1
- package/lib/commands/projects-args.d.ts.map +1 -1
- package/lib/commands/projects-args.js +5 -5
- package/lib/commands/projects-args.js.map +1 -1
- package/lib/commands/projects.d.ts +3 -4
- package/lib/commands/projects.d.ts.map +1 -1
- package/lib/commands/projects.js +2 -2
- package/lib/commands/projects.js.map +1 -1
- package/lib/commands/query-args.d.ts.map +1 -1
- package/lib/commands/query-args.js +5 -5
- package/lib/commands/query-args.js.map +1 -1
- package/lib/commands/query.d.ts +3 -4
- package/lib/commands/query.d.ts.map +1 -1
- package/lib/commands/query.js +2 -2
- package/lib/commands/query.js.map +1 -1
- package/lib/commands/reflect-args.d.ts +3 -2
- package/lib/commands/reflect-args.d.ts.map +1 -1
- package/lib/commands/reflect-args.js +13 -10
- package/lib/commands/reflect-args.js.map +1 -1
- package/lib/commands/reflect-driver.js +2 -1
- package/lib/commands/reflect-driver.js.map +1 -1
- package/lib/commands/reflect-report.js.map +1 -1
- package/lib/commands/reflect.js +1 -1
- package/lib/commands/reflect.js.map +1 -1
- package/lib/commands/save-args.d.ts.map +1 -1
- package/lib/commands/save-args.js +6 -4
- package/lib/commands/save-args.js.map +1 -1
- package/lib/commands/save.d.ts +8 -4
- package/lib/commands/save.d.ts.map +1 -1
- package/lib/commands/save.js +31 -6
- package/lib/commands/save.js.map +1 -1
- package/lib/commands/validate-args.d.ts +3 -2
- package/lib/commands/validate-args.d.ts.map +1 -1
- package/lib/commands/validate-args.js +13 -10
- package/lib/commands/validate-args.js.map +1 -1
- package/lib/commands/validate-driver.js +2 -1
- package/lib/commands/validate-driver.js.map +1 -1
- package/lib/commands/validate-report.js.map +1 -1
- package/lib/commands/validate.js +1 -1
- package/lib/commands/validate.js.map +1 -1
- package/lib/commands/watch-args.d.ts.map +1 -1
- package/lib/commands/watch-args.js +6 -4
- package/lib/commands/watch-args.js.map +1 -1
- package/lib/commands/watch.d.ts +3 -4
- package/lib/commands/watch.d.ts.map +1 -1
- package/lib/commands/watch.js +20 -2
- package/lib/commands/watch.js.map +1 -1
- package/lib/driver-heap.d.ts +52 -0
- package/lib/driver-heap.d.ts.map +1 -0
- package/lib/driver-heap.js +51 -0
- package/lib/driver-heap.js.map +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/log-level.d.ts +21 -11
- package/lib/log-level.d.ts.map +1 -1
- package/lib/log-level.js +25 -14
- package/lib/log-level.js.map +1 -1
- package/lib/spawn-data-server.d.ts +6 -1
- package/lib/spawn-data-server.d.ts.map +1 -1
- package/lib/spawn-data-server.js +2 -1
- package/lib/spawn-data-server.js.map +1 -1
- package/lib/stderr-connection-logger.d.ts +12 -0
- package/lib/stderr-connection-logger.d.ts.map +1 -0
- package/lib/stderr-connection-logger.js +16 -0
- package/lib/stderr-connection-logger.js.map +1 -0
- package/lib/testing/echo-server.js +9 -1
- package/lib/testing/echo-server.js.map +1 -1
- package/package.json +50 -39
- package/src/cli.ts +2 -1
- package/src/commands/analyze-heap.ts +4 -1
- package/src/commands/ast-ground-truth-args.ts +15 -12
- package/src/commands/ast-ground-truth-driver.ts +2 -1
- package/src/commands/ast-ground-truth.ts +1 -1
- package/src/commands/generate-ast-builder.ts +246 -0
- package/src/commands/generate-transfer-model-args.ts +11 -1
- package/src/commands/generate-transfer-model-config.ts +6 -3
- package/src/commands/generate-transfer-model.ts +27 -4
- package/src/commands/harness-args.ts +76 -31
- package/src/commands/headless-harness.ts +86 -26
- package/src/commands/init-args.ts +6 -6
- package/src/commands/init-templates.ts +270 -154
- package/src/commands/init-wizard.ts +4 -4
- package/src/commands/init-workspace.ts +31 -7
- package/src/commands/init.ts +36 -26
- package/src/commands/lint-grammar-args.ts +17 -13
- package/src/commands/lint-grammar-driver.ts +2 -1
- package/src/commands/lint-grammar.ts +1 -1
- package/src/commands/measure-memory-args.ts +27 -14
- package/src/commands/measure-memory-driver.ts +5 -3
- package/src/commands/measure-memory.ts +1 -1
- package/src/commands/model-docs-args.ts +16 -13
- package/src/commands/model-docs-driver.ts +2 -1
- package/src/commands/model-docs.ts +1 -1
- package/src/commands/projects-args.ts +5 -4
- package/src/commands/projects.ts +4 -6
- package/src/commands/query-args.ts +5 -4
- package/src/commands/query.ts +4 -6
- package/src/commands/reflect-args.ts +17 -13
- package/src/commands/reflect-driver.ts +2 -1
- package/src/commands/reflect.ts +1 -1
- package/src/commands/save-args.ts +6 -3
- package/src/commands/save.ts +47 -10
- package/src/commands/validate-args.ts +17 -13
- package/src/commands/validate-driver.ts +2 -1
- package/src/commands/validate.ts +1 -1
- package/src/commands/watch-args.ts +6 -3
- package/src/commands/watch.ts +28 -7
- package/src/driver-heap.ts +65 -0
- package/src/index.ts +1 -0
- package/src/log-level.ts +42 -14
- package/src/spawn-data-server.ts +14 -2
- package/src/stderr-connection-logger.ts +18 -0
- package/src/testing/echo-server.ts +11 -2
|
@@ -82,16 +82,6 @@ export interface InitTemplate {
|
|
|
82
82
|
content: string;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
/**
|
|
86
|
-
* The version every framework package carries before the first release.
|
|
87
|
-
*
|
|
88
|
-
* A scaffold made by a CLI still at this version pins a range the registry
|
|
89
|
-
* cannot serve, so the emitted README keeps its yalc note and `init` keeps its
|
|
90
|
-
* install warning while — and only while — it holds: from a published CLI the
|
|
91
|
-
* derived pins resolve and either note would be false as printed.
|
|
92
|
-
*/
|
|
93
|
-
export const UNPUBLISHED_FRAMEWORK_VERSION = '0.0.0';
|
|
94
|
-
|
|
95
85
|
/**
|
|
96
86
|
* The version `init` pins every `@hydranium/*` dependency at: the scaffolding
|
|
97
87
|
* CLI's own.
|
|
@@ -177,7 +167,7 @@ function columnsFor(composition: InitComposition): number {
|
|
|
177
167
|
|
|
178
168
|
/**
|
|
179
169
|
* A named import on one line while it fits the column budget, else one symbol
|
|
180
|
-
* per line. Emitted files are supposed to pass a
|
|
170
|
+
* per line. Emitted files are supposed to pass a format check as-is, and the
|
|
181
171
|
* generated-module import grows by one symbol per grammar.
|
|
182
172
|
*/
|
|
183
173
|
function importList(symbols: readonly string[], from: string, columns: number, typeOnly = false): string {
|
|
@@ -228,7 +218,12 @@ const HEAD_FRAMEWORK_DEPENDENCIES: Record<InitHead, readonly string[]> = {
|
|
|
228
218
|
const HEAD_THIRD_PARTY_DEPENDENCIES: Record<InitHead, Readonly<Record<string, string>>> = {
|
|
229
219
|
lsp: {
|
|
230
220
|
langium: '4.3.1',
|
|
231
|
-
|
|
221
|
+
// Exact, as are the protocol and transport it pins, so Langium and the
|
|
222
|
+
// framework share their copies; across copies, an error a handler throws
|
|
223
|
+
// or returns loses its code, and a later patch can pin another protocol.
|
|
224
|
+
'vscode-languageserver': '10.0.1',
|
|
225
|
+
'vscode-jsonrpc': '9.0.0',
|
|
226
|
+
'vscode-languageserver-protocol': '3.18.1'
|
|
232
227
|
},
|
|
233
228
|
data: {},
|
|
234
229
|
glsp: {
|
|
@@ -241,6 +236,37 @@ const HEAD_THIRD_PARTY_DEPENDENCIES: Record<InitHead, Readonly<Record<string, st
|
|
|
241
236
|
}
|
|
242
237
|
};
|
|
243
238
|
|
|
239
|
+
/**
|
|
240
|
+
* The exact pins of `heads`, for a workspace root to declare too. npm installs a
|
|
241
|
+
* root's own dependencies at the top of the tree, so another member's version of
|
|
242
|
+
* one nests beneath that member instead of taking the place of the copy this
|
|
243
|
+
* package shares with the framework.
|
|
244
|
+
*/
|
|
245
|
+
export function workspaceRootPins(heads: readonly InitHead[]): ReadonlyArray<readonly [string, string]> {
|
|
246
|
+
return heads
|
|
247
|
+
.flatMap(head => Object.entries(HEAD_THIRD_PARTY_DEPENDENCIES[head]))
|
|
248
|
+
.filter(([, version]) => /^\d+\.\d+\.\d+$/.test(version))
|
|
249
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Whether an npm version is 11.6.0 or later, the first that installs `vitest` 4.1's peer set; `11.6.0-rc.1` is earlier. */
|
|
253
|
+
function installsVitest41(npmVersion: string | undefined): boolean {
|
|
254
|
+
const [release, prerelease] = (npmVersion ?? '0.0.0').split('-', 2);
|
|
255
|
+
const [major, minor, patch] = release.split('.').map(Number);
|
|
256
|
+
return major > 11 || (major === 11 && (minor > 6 || (minor === 6 && (patch > 0 || prerelease === undefined))));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Whether the scaffold holds `vitest` below 4.1, whose peer set crashes npm
|
|
261
|
+
* before 11.6 (every npm a Node 22 release bundles) on the first install. Only a
|
|
262
|
+
* workspace root declaring a newer npm lifts it, and only when `init` runs under
|
|
263
|
+
* an npm known to be newer: npm does not enforce `packageManager`.
|
|
264
|
+
*/
|
|
265
|
+
function pinsVitest(composition: InitComposition): boolean {
|
|
266
|
+
const workspace = composition.packaging.workspace;
|
|
267
|
+
return !(installsVitest41(workspace?.packageManagerNpm) && installsVitest41(workspace?.npmVersion));
|
|
268
|
+
}
|
|
269
|
+
|
|
244
270
|
/** The dependency block for a head set, merged and sorted as npm writes it. */
|
|
245
271
|
function dependencyBlock(composition: InitComposition, indent: string): string {
|
|
246
272
|
const merged: Record<string, string> = {};
|
|
@@ -260,7 +286,7 @@ function dependencyBlock(composition: InitComposition, indent: string): string {
|
|
|
260
286
|
// npm convention, and `npm install` rewrites `package.json` at two spaces
|
|
261
287
|
// regardless — so three would not survive the adopter's first install.
|
|
262
288
|
//
|
|
263
|
-
// The KEY ORDER is
|
|
289
|
+
// The KEY ORDER is the one Oxfmt's `sortPackageJson` canonicalises to, `private`
|
|
264
290
|
// before `license` included. Any other order is rewritten by the scaffolded
|
|
265
291
|
// project's own first `format` run — a diff on a file the adopter never touched
|
|
266
292
|
// — and it makes a byte-compare against a formatted copy of this emission
|
|
@@ -291,16 +317,9 @@ const PACKAGE_JSON = `{
|
|
|
291
317
|
"version": "0.0.0",
|
|
292
318
|
__PRIVATE__ "description": "__NAME__ language server, built with the Hydranium framework.",
|
|
293
319
|
"keywords": [
|
|
294
|
-
|
|
295
|
-
"langium",
|
|
296
|
-
"language-server",
|
|
297
|
-
"__PROJECT_ID__"
|
|
298
|
-
],
|
|
320
|
+
__KEYWORDS__ ],
|
|
299
321
|
"license": "UNLICENSED",
|
|
300
322
|
"author": "",
|
|
301
|
-
"type": "module",
|
|
302
|
-
"main": "lib/index.js",
|
|
303
|
-
"types": "lib/index.d.ts",
|
|
304
323
|
"bin": {
|
|
305
324
|
__BIN__ },
|
|
306
325
|
"files": [
|
|
@@ -308,11 +327,14 @@ __BIN__ },
|
|
|
308
327
|
"src",
|
|
309
328
|
"syntaxes"
|
|
310
329
|
],
|
|
330
|
+
"type": "module",
|
|
331
|
+
"main": "lib/index.js",
|
|
332
|
+
"types": "lib/index.d.ts",
|
|
311
333
|
"scripts": {
|
|
312
334
|
"build": "npm run generate && tsc",
|
|
313
|
-
"clean": "rimraf lib syntaxes src/language-server/generated src/language-server/generated-
|
|
335
|
+
"clean": "rimraf lib syntaxes src/language-server/generated src/language-server/generated-hydranium tsconfig.tsbuildinfo",
|
|
314
336
|
"generate": "npm run langium:generate && npm run generate:transfer-model",
|
|
315
|
-
"generate:transfer-model": "hydranium-cli generate-transfer-model --ast-file src/language-server/generated/ast.ts --augmentation-file src/language-server/ast.ts --out-file src/language-server/generated-
|
|
337
|
+
"generate:transfer-model": "hydranium-cli generate-transfer-model --ast-file src/language-server/generated/ast.ts --augmentation-file src/language-server/ast.ts --out-file src/language-server/generated-hydranium/transfer-model.ts --ast-builder-file src/language-server/generated-hydranium/ast-builder.ts --element-type-name __NAME__Element --terminals-name __NAME__Terminals --regen-command \\"Run: __NPM_RUN__ generate:transfer-model\\"",
|
|
316
338
|
"langium:generate": "langium generate",
|
|
317
339
|
"langium:watch": "langium generate --watch",
|
|
318
340
|
__LINT__ "start": "node lib/main.js --stdio",
|
|
@@ -328,8 +350,8 @@ __DEPENDENCIES__
|
|
|
328
350
|
"@types/node": "^22.0.0",
|
|
329
351
|
"langium-cli": "4.3.0",
|
|
330
352
|
"rimraf": "^5.0.0",
|
|
331
|
-
"typescript": "^
|
|
332
|
-
"vitest": "
|
|
353
|
+
"typescript": "^7.0.2",
|
|
354
|
+
"vitest": "__VITEST__"
|
|
333
355
|
},
|
|
334
356
|
"engines": {
|
|
335
357
|
"node": ">=22.13"
|
|
@@ -359,7 +381,7 @@ function npmRun(composition: InitComposition): string {
|
|
|
359
381
|
* name. Without the second key a scaffolded project has no command line that
|
|
360
382
|
* reaches its own data head at all.
|
|
361
383
|
*
|
|
362
|
-
* Alphabetical, because `
|
|
384
|
+
* Alphabetical, because Oxfmt's `sortPackageJson` sorts `bin` and any other
|
|
363
385
|
* order is rewritten by the scaffolded project's first `format` run.
|
|
364
386
|
*/
|
|
365
387
|
function binBlock(composition: InitComposition): string {
|
|
@@ -374,15 +396,32 @@ function binBlock(composition: InitComposition): string {
|
|
|
374
396
|
function packageJson(composition: InitComposition): string {
|
|
375
397
|
const { scope } = composition.packaging;
|
|
376
398
|
const packageName = scope === undefined ? composition.projectId : `${scope}/${composition.projectId}`;
|
|
377
|
-
|
|
399
|
+
const keywords =
|
|
400
|
+
['hydranium', 'langium', 'language-server', composition.projectId]
|
|
401
|
+
.sort()
|
|
402
|
+
.map(keyword => ` ${JSON.stringify(keyword)}`)
|
|
403
|
+
.join(',\n') + '\n';
|
|
404
|
+
// Emitted only where a root lint config says the repo lints. The absence is
|
|
378
405
|
// the dangerous direction: a task runner runs a script only where one is
|
|
379
406
|
// declared, so a package with no `lint` is SKIPPED rather than reported, and
|
|
380
407
|
// that reads as a clean lint. A wrong invocation is one visible line to edit.
|
|
381
|
-
const
|
|
408
|
+
const workspace = composition.packaging.workspace;
|
|
409
|
+
const oxlintConfig =
|
|
410
|
+
workspace?.oxlintConfig === undefined
|
|
411
|
+
? undefined
|
|
412
|
+
: `${'../'.repeat(workspace.targetPath.split('/').length)}${workspace.oxlintConfig}`;
|
|
413
|
+
const lint =
|
|
414
|
+
workspace?.oxlintConfig !== undefined
|
|
415
|
+
? ` "lint": "oxlint --report-unused-disable-directives --format stylish --config ${oxlintConfig} src test --max-warnings 0",\n`
|
|
416
|
+
: workspace?.eslintConfig !== undefined
|
|
417
|
+
? ' "lint": "eslint src test --max-warnings 0",\n'
|
|
418
|
+
: '';
|
|
382
419
|
return project(PACKAGE_JSON, composition)
|
|
383
420
|
.replace('__PACKAGE_NAME__', packageName)
|
|
421
|
+
.replace('__KEYWORDS__', keywords)
|
|
384
422
|
.replace('__BIN__', binBlock(composition))
|
|
385
423
|
.replace('__LINT__', lint)
|
|
424
|
+
.replace('__VITEST__', pinsVitest(composition) ? '~4.0.18' : '^4.0.0')
|
|
386
425
|
.replace('__PRIVATE__', composition.packaging.private ? ' "private": true,\n' : '')
|
|
387
426
|
.replace('__NPM_RUN__', npmRun(composition))
|
|
388
427
|
.replace('__FRAMEWORK_PIN__', frameworkPin(composition))
|
|
@@ -439,6 +478,11 @@ const TSCONFIG_COMPILER_OPTIONS: ReadonlyArray<readonly [string, JsonValue]> = [
|
|
|
439
478
|
['outDir', 'lib'],
|
|
440
479
|
['strict', true],
|
|
441
480
|
['esModuleInterop', true],
|
|
481
|
+
// A server that renders its own user-facing messages loads a catalogue, and
|
|
482
|
+
// a catalogue is JSON. Without this the import does not resolve at all, and
|
|
483
|
+
// without the `include` glob below a composite project rejects it with
|
|
484
|
+
// TS6307 naming neither cause.
|
|
485
|
+
['resolveJsonModule', true],
|
|
442
486
|
['skipLibCheck', true],
|
|
443
487
|
['declaration', true],
|
|
444
488
|
['experimentalDecorators', true],
|
|
@@ -455,9 +499,20 @@ const TSCONFIG_COMPILER_OPTIONS: ReadonlyArray<readonly [string, JsonValue]> = [
|
|
|
455
499
|
*/
|
|
456
500
|
const PACKAGE_LOCAL_OPTIONS: readonly string[] = ['rootDir', 'outDir'];
|
|
457
501
|
|
|
458
|
-
/**
|
|
459
|
-
|
|
460
|
-
|
|
502
|
+
/**
|
|
503
|
+
* The enum-valued options of {@link TSCONFIG_COMPILER_OPTIONS}, whose values
|
|
504
|
+
* `tsc` reads case-insensitively, so `nodenext` and `NodeNext` are one value.
|
|
505
|
+
* An enum-valued option added there belongs here too.
|
|
506
|
+
*/
|
|
507
|
+
const CASE_INSENSITIVE_OPTIONS: readonly string[] = ['target', 'lib', 'module', 'moduleResolution'];
|
|
508
|
+
|
|
509
|
+
/** Compare two option values. Sound here because compiler options are scalars and flat arrays. */
|
|
510
|
+
function optionEquals(key: string, left: JsonValue | undefined, right: JsonValue): boolean {
|
|
511
|
+
const normalise = (value: JsonValue | undefined) => {
|
|
512
|
+
const text = JSON.stringify(value);
|
|
513
|
+
return CASE_INSENSITIVE_OPTIONS.includes(key) ? text?.toLowerCase() : text;
|
|
514
|
+
};
|
|
515
|
+
return normalise(left) === normalise(right);
|
|
461
516
|
}
|
|
462
517
|
|
|
463
518
|
/**
|
|
@@ -476,11 +531,13 @@ function tsconfigJson(composition: InitComposition): string {
|
|
|
476
531
|
? TSCONFIG_COMPILER_OPTIONS
|
|
477
532
|
: [
|
|
478
533
|
...TSCONFIG_COMPILER_OPTIONS.filter(([key]) => PACKAGE_LOCAL_OPTIONS.includes(key)),
|
|
479
|
-
...TSCONFIG_COMPILER_OPTIONS.filter(
|
|
534
|
+
...TSCONFIG_COMPILER_OPTIONS.filter(
|
|
535
|
+
([key, value]) => !PACKAGE_LOCAL_OPTIONS.includes(key) && !optionEquals(key, base[key], value)
|
|
536
|
+
)
|
|
480
537
|
];
|
|
481
538
|
const extendsLine = workspace?.baseTsconfig === undefined ? '' : ` "extends": "${workspace.baseTsconfig}",\n`;
|
|
482
539
|
const body = options.map(([key, value]) => ` "${key}": ${JSON.stringify(value)}`).join(',\n');
|
|
483
|
-
return `{\n${extendsLine} "compilerOptions": {\n${body}\n },\n "include": ["src"]\n}\n`;
|
|
540
|
+
return `{\n${extendsLine} "compilerOptions": {\n${body}\n },\n "include": ["src", "src/**/*.json"]\n}\n`;
|
|
484
541
|
}
|
|
485
542
|
|
|
486
543
|
// `isolatedModules` is what makes this check agree with the transform that
|
|
@@ -498,7 +555,7 @@ const TSCONFIG_TEST = `{
|
|
|
498
555
|
"isolatedModules": true,
|
|
499
556
|
"types": ["node"]
|
|
500
557
|
},
|
|
501
|
-
"include": ["src", "test"]
|
|
558
|
+
"include": ["src", "test", "src/**/*.json"]
|
|
502
559
|
}
|
|
503
560
|
`;
|
|
504
561
|
|
|
@@ -518,6 +575,11 @@ export default defineConfig({
|
|
|
518
575
|
});
|
|
519
576
|
`;
|
|
520
577
|
|
|
578
|
+
/**
|
|
579
|
+
* The `.gitignore`, written for a workspace member too: its root may not ignore
|
|
580
|
+
* `lib/` (a `yo langium` root ignores `out/`), and a rule the root repeats
|
|
581
|
+
* costs nothing.
|
|
582
|
+
*/
|
|
521
583
|
const GITIGNORE = `node_modules/
|
|
522
584
|
lib/
|
|
523
585
|
*.tsbuildinfo
|
|
@@ -526,27 +588,6 @@ lib/
|
|
|
526
588
|
syntaxes/
|
|
527
589
|
`;
|
|
528
590
|
|
|
529
|
-
/**
|
|
530
|
-
* The workspace-member `.gitignore` — one entry, and the one entry a root
|
|
531
|
-
* cannot be assumed to have.
|
|
532
|
-
*
|
|
533
|
-
* A member inherits the root's rules, and `node_modules/`, `lib/` and
|
|
534
|
-
* `*.tsbuildinfo` are in every monorepo root already, so repeating them here
|
|
535
|
-
* would be three lines to keep in step for no coverage. `syntaxes/` is not like
|
|
536
|
-
* them: it is a Langium artefact, so a root that has never held a Langium
|
|
537
|
-
* package has no rule for it, and the first `langium generate` then offers
|
|
538
|
-
* generated output up for commit with nothing to warn the adopter. Dropping the
|
|
539
|
-
* whole file rather than this subset is the mistake this template exists to
|
|
540
|
-
* undo.
|
|
541
|
-
*/
|
|
542
|
-
const GITIGNORE_WORKSPACE_MEMBER = `# The workspace root already covers \`node_modules/\`, \`lib/\` and \`*.tsbuildinfo\`.
|
|
543
|
-
# It has no reason to know about this one: the TextMate grammar is a Langium
|
|
544
|
-
# artefact, rewritten by every \`langium generate\`, so a root that has never held
|
|
545
|
-
# a Langium package ignores nothing here and the generated file is offered for
|
|
546
|
-
# commit.
|
|
547
|
-
syntaxes/
|
|
548
|
-
`;
|
|
549
|
-
|
|
550
591
|
/** The terminals every grammar needs, shared verbatim by the fragment and the lone-grammar case. */
|
|
551
592
|
const TERMINALS = `hidden terminal WS: /\\s+/;
|
|
552
593
|
terminal ID: /[_a-zA-Z][\\w_]*/;
|
|
@@ -708,13 +749,35 @@ export type __NAME__Services = LangiumServices &
|
|
|
708
749
|
LspServerAddedServices & {
|
|
709
750
|
shared: __NAME__SharedServices;
|
|
710
751
|
};
|
|
752
|
+
|
|
753
|
+
/** What a host or a test may vary about this composition. */
|
|
754
|
+
export interface __NAME__Options {
|
|
755
|
+
/**
|
|
756
|
+
* Shared modules layered in after the framework's own bindings.
|
|
757
|
+
*
|
|
758
|
+
* The framework constructs most shared services with no options —
|
|
759
|
+
* \`DocumentBuilder: services => new HydraniumDocumentBuilder(services)\` — so
|
|
760
|
+
* rebinding the slot is the only way to boot one configured differently, and
|
|
761
|
+
* a factory that hard-codes its composition leaves a test nowhere to do it.
|
|
762
|
+
* That is what this is for: pass a module binding \`workspace.DocumentBuilder\`
|
|
763
|
+
* to exercise a builder option, or to substitute a subclass.
|
|
764
|
+
*
|
|
765
|
+
* Composed LAST, after \`__NAME__SharedModule\`, so it wins over every other
|
|
766
|
+
* tier including this file's own bindings — which is what makes it usable for
|
|
767
|
+
* a slot the adopter overrides. Production code should not reach for it.
|
|
768
|
+
*/
|
|
769
|
+
readonly extraSharedModules?: ReadonlyArray<Module<__NAME__SharedServices, DeepPartial<__NAME__SharedServices>>>;
|
|
770
|
+
}
|
|
711
771
|
${configurationRoot}
|
|
712
772
|
${sharedModule}
|
|
713
773
|
|
|
714
774
|
${languageModules}
|
|
715
775
|
|
|
716
776
|
/** Compose the Langium DI tree for __NAME__ — returns the shared + language services. */
|
|
717
|
-
export function create__NAME__Services(
|
|
777
|
+
export function create__NAME__Services(
|
|
778
|
+
context: Partial<ServerModuleContext> = EmptyFileSystem,
|
|
779
|
+
options: __NAME__Options = {}
|
|
780
|
+
): {
|
|
718
781
|
shared: __NAME__SharedServices;
|
|
719
782
|
${returnType}
|
|
720
783
|
} {
|
|
@@ -724,7 +787,8 @@ ${returnType}
|
|
|
724
787
|
sharedModules: {
|
|
725
788
|
generated: __NAME__GeneratedSharedModule,
|
|
726
789
|
adopter: __NAME__SharedModule,
|
|
727
|
-
extra: [createLspServerSharedModule(fullContext)]
|
|
790
|
+
extra: [createLspServerSharedModule(fullContext)],
|
|
791
|
+
overrides: options.extraSharedModules
|
|
728
792
|
},
|
|
729
793
|
languageModules: {
|
|
730
794
|
generated: ${primary.grammar}GeneratedModule,
|
|
@@ -791,9 +855,9 @@ const SERIALIZER = `// Concrete-syntax emitter for __GRAMMAR__ — the parser's
|
|
|
791
855
|
//
|
|
792
856
|
// Without it every structured write fails: the framework's default binding at
|
|
793
857
|
// \`services.serializer.Serializer\` THROWS, because a concrete syntax is
|
|
794
|
-
// language knowledge no framework can derive. \`
|
|
795
|
-
// the data head's \`saveModelDocument\` and a GLSP \`SaveModelAction\`
|
|
796
|
-
// it.
|
|
858
|
+
// language knowledge no framework can derive. A client session's \`update\` /
|
|
859
|
+
// \`save\`, the data head's \`saveModelDocument\` and a GLSP \`SaveModelAction\`
|
|
860
|
+
// all reach it.
|
|
797
861
|
//
|
|
798
862
|
// \`AbstractSerializer\`'s generic property walk lays out FORMAT-structured
|
|
799
863
|
// output — its YAML and JSON subclasses are what it exists for — and cannot
|
|
@@ -980,7 +1044,7 @@ import { startGlspServer } from '@hydranium/glsp-server/node';
|
|
|
980
1044
|
// \`TransferElement\` structurally, so naming the AST type here compiles fine and
|
|
981
1045
|
// silently tells every typed client that a reference is a resolvable object
|
|
982
1046
|
// rather than a name.
|
|
983
|
-
${importList(roots, './language-server/generated-
|
|
1047
|
+
${importList(roots, './language-server/generated-hydranium/transfer-model.js', columns, true)}
|
|
984
1048
|
`
|
|
985
1049
|
: '';
|
|
986
1050
|
|
|
@@ -1004,19 +1068,22 @@ const dataServer = startSocketServer({ port: 0, logTag: 'ModelServer', logger: s
|
|
|
1004
1068
|
return { dispose: () => undefined };
|
|
1005
1069
|
});
|
|
1006
1070
|
dataServer.started
|
|
1007
|
-
.then(
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1071
|
+
.then(
|
|
1072
|
+
() => {
|
|
1073
|
+
const { port } = dataServer;
|
|
1074
|
+
if (port === undefined) {
|
|
1075
|
+
// \`started\` resolves only once the address is resolved, so this is
|
|
1076
|
+
// unreachable; a non-null assertion in its place would publish
|
|
1077
|
+
// \`undefined\`, which the host cannot tell from an unreachable port.
|
|
1078
|
+
throw new Error('the data head started without a resolved port');
|
|
1079
|
+
}
|
|
1080
|
+
publishPortOnLspConnection(shared.lsp.Connection, ${upper}_DATA_SERVER_PORT_COMMAND, port);
|
|
1081
|
+
},
|
|
1082
|
+
// The launcher logged its own bind failure; the rejection is control flow.
|
|
1083
|
+
() => undefined
|
|
1084
|
+
)
|
|
1017
1085
|
.catch((error: unknown) => {
|
|
1018
|
-
|
|
1019
|
-
shared.Logger.error(\`[ModelServer] Could not publish the data-server port: \${reason}\`);
|
|
1086
|
+
shared.Logger.error(\`[ModelServer] Could not publish the data-server port: \${describeErrorWithStack(error)}\`);
|
|
1020
1087
|
});
|
|
1021
1088
|
`
|
|
1022
1089
|
: '';
|
|
@@ -1063,11 +1130,17 @@ void glspServer;
|
|
|
1063
1130
|
// than a library entry — import \`./index.js\` instead to compose the language.
|
|
1064
1131
|
|
|
1065
1132
|
${reflectImport}${glspImports}${importList(coreNodeSymbols, '@hydranium/core/node', columns)}
|
|
1066
|
-
${data ? "import { DataServer } from '@hydranium/data-server';\n" : ''}
|
|
1133
|
+
${data ? "import { DataServer } from '@hydranium/data-server';\nimport { describeErrorWithStack } from '@hydranium/protocol';\n" : ''}// The framework's entry point, NOT Langium's \`@hydranium/langium/lsp\` one. It is
|
|
1134
|
+
// signature-compatible and delegates straight through; what it adds first is
|
|
1135
|
+
// \`assertLspHeadComposed\`, which fails the start if the LSP head's SHARED module
|
|
1136
|
+
// was never composed. Reaching for Langium's is the natural mistake and it is
|
|
1137
|
+
// silent: the server boots, links and completes, while echo suppression, the
|
|
1138
|
+
// didChangeContent debounce and the last-client-close rebuild are all inert.
|
|
1139
|
+
import { startLanguageServer, withHydraniumLspFeatures } from '@hydranium/core/lsp';
|
|
1067
1140
|
import { ProposedFeatures, createConnection } from 'vscode-languageserver/node';
|
|
1068
1141
|
${diagramImports}${transferImports}import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
|
|
1069
1142
|
${portCommands === '' ? '' : '\n' + portCommands}
|
|
1070
|
-
const connection = createConnection(ProposedFeatures.all);
|
|
1143
|
+
const connection = createConnection(withHydraniumLspFeatures(ProposedFeatures.all));
|
|
1071
1144
|
const { shared } = create__NAME__Services({ connection, ...NodeFileSystem });
|
|
1072
1145
|
startLanguageServer(shared);
|
|
1073
1146
|
${dataBlock}${glspBlock}`;
|
|
@@ -1117,7 +1190,7 @@ function dataServerMainFile(composition: InitComposition): string {
|
|
|
1117
1190
|
${importList(['NodeFileSystem', 'startStdioServer'], '@hydranium/core/node', columns)}
|
|
1118
1191
|
import { DataServer } from '@hydranium/data-server';
|
|
1119
1192
|
// The TRANSFER root${plural}, not the AST one${plural} — same reasoning as \`main.ts\`.
|
|
1120
|
-
${importList(roots, './language-server/generated-
|
|
1193
|
+
${importList(roots, './language-server/generated-hydranium/transfer-model.js', columns, true)}
|
|
1121
1194
|
import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
|
|
1122
1195
|
|
|
1123
1196
|
const { shared } = create__NAME__Services({ ...NodeFileSystem });
|
|
@@ -1236,8 +1309,8 @@ function serializationTest(composition: InitComposition): string {
|
|
|
1236
1309
|
const transferTypes = composition.grammars.map(grammar => `${grammar.entryRule} as Transfer${grammar.entryRule}`);
|
|
1237
1310
|
const text = `// Round-trips each grammar through its serializer: parse the source, serialize
|
|
1238
1311
|
// the model, compare the text. The serializer is the parser's inverse, so this
|
|
1239
|
-
// is the assertion that keeps \`
|
|
1240
|
-
// language server can read back.
|
|
1312
|
+
// is the assertion that keeps a client session's \`update\` / \`save\` writing
|
|
1313
|
+
// files the language server can read back.
|
|
1241
1314
|
//
|
|
1242
1315
|
// The transfer case is the one that catches the mistake worth catching.
|
|
1243
1316
|
// \`ModelService.modelToText\` short-circuits only a RAW STRING, so a typed model
|
|
@@ -1247,7 +1320,7 @@ function serializationTest(composition: InitComposition): string {
|
|
|
1247
1320
|
import { parseHelper } from '@hydranium/core/testing';
|
|
1248
1321
|
import { describe, expect, it } from 'vitest';
|
|
1249
1322
|
${importList(astTypes, '../src/language-server/ast.js', columns, true)}
|
|
1250
|
-
${importList(transferTypes, '../src/language-server/generated-
|
|
1323
|
+
${importList(transferTypes, '../src/language-server/generated-hydranium/transfer-model.js', columns, true)}
|
|
1251
1324
|
import { createServices } from '../src/services.js';
|
|
1252
1325
|
|
|
1253
1326
|
${composition.grammars.map(grammar => render(SERIALIZATION_SUITE, composition, grammar)).join('\n\n')}
|
|
@@ -1416,20 +1489,6 @@ function readme(composition: InitComposition): string {
|
|
|
1416
1489
|
'',
|
|
1417
1490
|
'## Getting started',
|
|
1418
1491
|
'',
|
|
1419
|
-
// Conditional on the pin this scaffold actually carries — see
|
|
1420
|
-
// `UNPUBLISHED_FRAMEWORK_VERSION`.
|
|
1421
|
-
...(composition.frameworkVersion === UNPUBLISHED_FRAMEWORK_VERSION
|
|
1422
|
-
? [
|
|
1423
|
-
'> **Pre-publish note.** `@hydranium/*` is not on npm yet, so the `0.0.0`',
|
|
1424
|
-
'> pins below are placeholders and `npm install` will fail with a 404 until',
|
|
1425
|
-
'> the framework is released. Until then, supply the packages from a local',
|
|
1426
|
-
'> framework checkout with [yalc](https://github.com/wclr/yalc) — a plain',
|
|
1427
|
-
'> `file:` path or `npm link` is not enough, because the framework packages',
|
|
1428
|
-
'> depend on each other by version and npm would try to fetch those from the',
|
|
1429
|
-
'> registry too.',
|
|
1430
|
-
''
|
|
1431
|
-
]
|
|
1432
|
-
: []),
|
|
1433
1492
|
'```bash',
|
|
1434
1493
|
'npm install',
|
|
1435
1494
|
'npm run langium:generate # generate the AST from the grammar',
|
|
@@ -1543,6 +1602,24 @@ function readme(composition: InitComposition): string {
|
|
|
1543
1602
|
'atomic set and depends on a single physical copy, so a floating range can',
|
|
1544
1603
|
'silently resolve a second one.',
|
|
1545
1604
|
'',
|
|
1605
|
+
'`vscode-languageserver` is pinned to `10.0.1`, and',
|
|
1606
|
+
'`vscode-languageserver-protocol` and `vscode-jsonrpc` to the `3.18.1` and',
|
|
1607
|
+
'`9.0.0` it pins, so Langium, the LSP connection and the framework share one',
|
|
1608
|
+
'copy of each. With two, an error a Langium handler returns, such as a request',
|
|
1609
|
+
'for a document the server does not have, reaches the client as a result, and a',
|
|
1610
|
+
'framework error your own LSP handler throws loses its code. The Langium 4.4',
|
|
1611
|
+
'upgrade',
|
|
1612
|
+
'([eclipse-emfcloud/hydranium#142](https://github.com/eclipse-emfcloud/hydranium/issues/142))',
|
|
1613
|
+
'replaces these pins.',
|
|
1614
|
+
'',
|
|
1615
|
+
...(pinsVitest(composition)
|
|
1616
|
+
? [
|
|
1617
|
+
'`vitest` is held below 4.1, whose peer set crashes the npm every Node 22',
|
|
1618
|
+
'release bundles. Raise it once every npm that installs this project is',
|
|
1619
|
+
'11.6 or later.',
|
|
1620
|
+
''
|
|
1621
|
+
]
|
|
1622
|
+
: []),
|
|
1546
1623
|
'## If your repo gates license headers',
|
|
1547
1624
|
'',
|
|
1548
1625
|
'The emitted `.ts` files carry no copyright header — the scaffold cannot know',
|
|
@@ -1562,10 +1639,9 @@ function readme(composition: InitComposition): string {
|
|
|
1562
1639
|
/**
|
|
1563
1640
|
* The GLSP files for ONE grammar's diagram.
|
|
1564
1641
|
*
|
|
1565
|
-
* Per grammar rather than per project because
|
|
1566
|
-
* grammar
|
|
1567
|
-
*
|
|
1568
|
-
* grammar with no diagram can sit on the same server.
|
|
1642
|
+
* Per grammar rather than per project because the state, storage and GModel
|
|
1643
|
+
* factory are typed over that grammar's model, and a grammar with no diagram
|
|
1644
|
+
* can sit on the same server.
|
|
1569
1645
|
*
|
|
1570
1646
|
* The index and the computed-bounds handler are framework classes used
|
|
1571
1647
|
* unmodified; storage and the submission handler are thin typed subclasses,
|
|
@@ -1633,8 +1709,11 @@ export class __GRAMMAR__GlspState extends FullTextHydraniumGlspState<__ENTRY_RUL
|
|
|
1633
1709
|
|
|
1634
1710
|
const storage = `// Source-model storage for the __GRAMMAR__ diagram, inheriting both framework
|
|
1635
1711
|
// defaults: \`loadSourceModel\` (open + settle + \`setSourceRoot\`) and
|
|
1636
|
-
// \`saveSourceModel\` (
|
|
1637
|
-
//
|
|
1712
|
+
// \`saveSourceModel\` (flush the store's text for the primary and every tracked
|
|
1713
|
+
// secondary through the diagram's client session).
|
|
1714
|
+
//
|
|
1715
|
+
// No serializer runs on the save path — a save persists what the diagram's
|
|
1716
|
+
// operations already wrote to the store.
|
|
1638
1717
|
//
|
|
1639
1718
|
// The subclass exists so a bespoke load or save has a stable place to land.
|
|
1640
1719
|
|
|
@@ -1798,27 +1877,28 @@ export class __GRAMMAR__DiagramConfiguration implements DiagramConfiguration {
|
|
|
1798
1877
|
// read-only is a deletion the compiler checks, while read-only → editable is
|
|
1799
1878
|
// authoring against a seam you have not used yet.
|
|
1800
1879
|
//
|
|
1801
|
-
// **It
|
|
1802
|
-
// \`
|
|
1803
|
-
//
|
|
1804
|
-
//
|
|
1805
|
-
//
|
|
1806
|
-
//
|
|
1807
|
-
//
|
|
1808
|
-
// \`HydraniumGlspRecordingCommand\` over \`state.sourceModel\` instead — the same
|
|
1809
|
-
// binding the diagram's own save action needs.
|
|
1880
|
+
// **It edits the operation's working copy.** A handler runs inside an operation,
|
|
1881
|
+
// where \`sourceRoot\` is a copy of the built root: the recording command's
|
|
1882
|
+
// runnable appends to the copy, and the operation writes the copy's text through
|
|
1883
|
+
// the grammar's \`Serializer\` once, gated on the version the copy was made at,
|
|
1884
|
+
// with undo and redo applying that one change. A command that writes the
|
|
1885
|
+
// document itself instead reads no text off the copy, which has no \`$document\`,
|
|
1886
|
+
// and the operation's own write then overwrites it.
|
|
1810
1887
|
//
|
|
1811
1888
|
// **The drop location is discarded.** \`needsClientLayout\` is \`true\` and the
|
|
1812
1889
|
// starter grammar persists no bounds, so there is nowhere to put a coordinate: the
|
|
1813
1890
|
// node is appended at the end of the document and the client places it.
|
|
1814
1891
|
//
|
|
1815
1892
|
// Like \`types.ts\`, \`gmodel-factory.ts\` and \`diagram-configuration.ts\`, this file
|
|
1816
|
-
// knows the starter grammar's
|
|
1817
|
-
//
|
|
1893
|
+
// knows the starter grammar's shape — the \`nodes\` list and the \`__NODE_RULE__\`
|
|
1894
|
+
// it holds. Replacing the grammar means replacing these four together.
|
|
1818
1895
|
|
|
1819
1896
|
import { type Command, type CreateNodeOperation, JsonCreateNodeOperationHandler, type MaybePromise } from '@eclipse-glsp/server';
|
|
1897
|
+
import { appendChild } from '@hydranium/core';
|
|
1898
|
+
import { HydraniumGlspRecordingCommand } from '@hydranium/glsp-server';
|
|
1820
1899
|
import { findNextUnique } from '@hydranium/protocol';
|
|
1821
1900
|
import { injectable } from 'inversify';
|
|
1901
|
+
import type { __NODE_RULE__ } from '../../language-server/ast.js';
|
|
1822
1902
|
import { ${upper}_NODE_TYPE } from './types.js';
|
|
1823
1903
|
import type { __GRAMMAR__GlspState } from './state.js';
|
|
1824
1904
|
|
|
@@ -1837,46 +1917,20 @@ export class __GRAMMAR__CreateNodeOperationHandler extends JsonCreateNodeOperati
|
|
|
1837
1917
|
if (!this.elementTypeIds.includes(operation.elementTypeId)) {
|
|
1838
1918
|
return undefined;
|
|
1839
1919
|
}
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
before,
|
|
1844
|
-
findNextUnique(
|
|
1920
|
+
return new HydraniumGlspRecordingCommand(this.modelState, this.label, () => {
|
|
1921
|
+
const root = this.modelState.sourceRoot;
|
|
1922
|
+
const name = findNextUnique(
|
|
1845
1923
|
NODE_NAME_STEM,
|
|
1846
|
-
|
|
1847
|
-
)
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
// has exactly one field, so there is nothing to merge a concurrent edit into.
|
|
1851
|
-
return {
|
|
1852
|
-
execute: () => state.updateSourceModel({ text: after }),
|
|
1853
|
-
undo: () => state.updateSourceModel({ text: before }),
|
|
1854
|
-
redo: () => state.updateSourceModel({ text: after })
|
|
1855
|
-
};
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
|
-
/** The text the captured source root was parsed from — the baseline an edit appends to. */
|
|
1859
|
-
protected documentText(): string {
|
|
1860
|
-
return this.modelState.sourceRoot.$document?.textDocument.getText() ?? '';
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
/** \`text\` with one more node declaration, under exactly one trailing newline. */
|
|
1864
|
-
protected withNode(text: string, name: string): string {
|
|
1865
|
-
const body = text.trimEnd();
|
|
1866
|
-
const declaration = \`node \${name}\`;
|
|
1867
|
-
return body.length === 0 ? \`\${declaration}\\n\` : \`\${body}\\n\${declaration}\\n\`;
|
|
1924
|
+
root.nodes.map(node => node.name)
|
|
1925
|
+
);
|
|
1926
|
+
appendChild(root, 'nodes', root.nodes, { $type: '__NODE_RULE__', name } as __NODE_RULE__);
|
|
1927
|
+
});
|
|
1868
1928
|
}
|
|
1869
1929
|
}
|
|
1870
1930
|
`;
|
|
1871
1931
|
|
|
1872
1932
|
const module = `// GLSP diagram module for __GRAMMAR__ — the DI wiring of one diagram type.
|
|
1873
1933
|
//
|
|
1874
|
-
// \`declareLanguage\` is the multi-grammar seam: it binds the grammar this diagram
|
|
1875
|
-
// edits on the SESSION container, so \`modelState.diagramLanguage\` and the
|
|
1876
|
-
// per-language lookups resolve to __LANGUAGE_ID__. That is what stops one
|
|
1877
|
-
// diagram type from fighting over a process-wide binding when a project holds
|
|
1878
|
-
// several grammars — a grammar with no diagram is registered on the same server.
|
|
1879
|
-
//
|
|
1880
1934
|
// \`configureActionHandlers\` REBINDS rather than adds: GLSP's own
|
|
1881
1935
|
// \`DiagramModule\` already registers \`ComputedBoundsActionHandler\`, and two
|
|
1882
1936
|
// handlers for one action would both run.
|
|
@@ -1903,13 +1957,11 @@ ${importList(
|
|
|
1903
1957
|
true
|
|
1904
1958
|
)}
|
|
1905
1959
|
import { ComputedBoundsActionHandler } from '@eclipse-glsp/server';
|
|
1906
|
-
import type { LanguageMetaData } from '@hydranium/langium';
|
|
1907
1960
|
${importList(
|
|
1908
1961
|
['HydraniumGlspComputedBoundsActionHandler', 'AbstractHydraniumGlspDiagramModule', 'HydraniumGlspIndex'],
|
|
1909
1962
|
'@hydranium/glsp-server',
|
|
1910
1963
|
columns
|
|
1911
1964
|
)}
|
|
1912
|
-
import { __GRAMMAR__LanguageMetaData } from '../../language-server/generated/module.js';
|
|
1913
1965
|
import { __GRAMMAR__CreateNodeOperationHandler } from './create-node-operation-handler.js';
|
|
1914
1966
|
import { __GRAMMAR__DiagramConfiguration } from './diagram-configuration.js';
|
|
1915
1967
|
import { __GRAMMAR__GModelFactory } from './gmodel-factory.js';
|
|
@@ -1921,10 +1973,6 @@ import { ${upper}_DIAGRAM_TYPE } from './types.js';
|
|
|
1921
1973
|
export class __GRAMMAR__DiagramModule extends AbstractHydraniumGlspDiagramModule {
|
|
1922
1974
|
readonly diagramType = ${upper}_DIAGRAM_TYPE;
|
|
1923
1975
|
|
|
1924
|
-
protected override declareLanguage(): LanguageMetaData {
|
|
1925
|
-
return __GRAMMAR__LanguageMetaData;
|
|
1926
|
-
}
|
|
1927
|
-
|
|
1928
1976
|
protected override bindModelState(): BindingTarget<ModelState> {
|
|
1929
1977
|
return { service: __GRAMMAR__GlspState };
|
|
1930
1978
|
}
|
|
@@ -1960,6 +2008,76 @@ export class __GRAMMAR__DiagramModule extends AbstractHydraniumGlspDiagramModule
|
|
|
1960
2008
|
binding.add(__GRAMMAR__CreateNodeOperationHandler);
|
|
1961
2009
|
}
|
|
1962
2010
|
}
|
|
2011
|
+
`;
|
|
2012
|
+
|
|
2013
|
+
const diagramTest = `// The starter create-node handler, run the way a client runs it: an in-process
|
|
2014
|
+
// GLSP server opens a document, a create-node operation adds a node, and an undo
|
|
2015
|
+
// and a redo take it out and put it back. Every step must keep the nodes the
|
|
2016
|
+
// document already had, and its comments: the diagram writes the document from
|
|
2017
|
+
// its AST, which carries none.
|
|
2018
|
+
|
|
2019
|
+
import 'reflect-metadata';
|
|
2020
|
+
import { type Action, CreateNodeOperation, RedoAction, ServerModule, SetDirtyStateAction, UndoAction } from '@eclipse-glsp/server';
|
|
2021
|
+
import { initializeWorkspaceProgrammatically } from '@hydranium/core';
|
|
2022
|
+
import { makeScratchWorkspace, type ScratchWorkspace } from '@hydranium/core/testing/node';
|
|
2023
|
+
import { HydraniumGlspAppModule } from '@hydranium/glsp-server';
|
|
2024
|
+
import { type GlspHarness, makeGlspHarness } from '@hydranium/glsp-server/testing';
|
|
2025
|
+
import { waitFor } from '@hydranium/protocol/testing';
|
|
2026
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2027
|
+
import { __GRAMMAR__DiagramModule } from '../src/glsp/__GRAMMAR_ID__/diagram-module.js';
|
|
2028
|
+
import type { __GRAMMAR__GlspState } from '../src/glsp/__GRAMMAR_ID__/state.js';
|
|
2029
|
+
import { ${upper}_DIAGRAM_TYPE, ${upper}_NODE_TYPE } from '../src/glsp/__GRAMMAR_ID__/types.js';
|
|
2030
|
+
import { createServices } from '../src/services.js';
|
|
2031
|
+
|
|
2032
|
+
let workspace: ScratchWorkspace | undefined;
|
|
2033
|
+
let diagram: GlspHarness<__GRAMMAR__GlspState> | undefined;
|
|
2034
|
+
|
|
2035
|
+
afterEach(() => {
|
|
2036
|
+
diagram?.dispose();
|
|
2037
|
+
workspace?.dispose();
|
|
2038
|
+
diagram = undefined;
|
|
2039
|
+
workspace = undefined;
|
|
2040
|
+
});
|
|
2041
|
+
|
|
2042
|
+
/** Send \`action\` and wait for the dirty state the server answers it with, which it sends once the edit is written. */
|
|
2043
|
+
async function send(harness: GlspHarness<__GRAMMAR__GlspState>, action: Action, reason: string): Promise<void> {
|
|
2044
|
+
const before = harness.actions.length;
|
|
2045
|
+
harness.dispatch(action);
|
|
2046
|
+
await waitFor(() => harness.actions.slice(before).some(sent => SetDirtyStateAction.is(sent) && sent.reason === reason), {
|
|
2047
|
+
message: \`no '\${reason}' dirty state\`
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
describe('__GRAMMAR__ diagram', () => {
|
|
2052
|
+
it('creates a node after the existing ones, keeping their comments, and undoes and redoes it', async () => {
|
|
2053
|
+
workspace = makeScratchWorkspace({ prefix: '__GRAMMAR_ID__-diagram-' });
|
|
2054
|
+
const original = '// keep this comment\\nnode first -> second\\n// about second\\nnode second';
|
|
2055
|
+
const file = workspace.write('model.__EXTENSION__', original);
|
|
2056
|
+
const { shared } = createServices();
|
|
2057
|
+
await initializeWorkspaceProgrammatically(shared, workspace.root);
|
|
2058
|
+
diagram = makeGlspHarness<__GRAMMAR__GlspState>({
|
|
2059
|
+
serverModule: new ServerModule().configureDiagramModule(new __GRAMMAR__DiagramModule()),
|
|
2060
|
+
diagramType: ${upper}_DIAGRAM_TYPE,
|
|
2061
|
+
appModules: [new HydraniumGlspAppModule({ shared })]
|
|
2062
|
+
});
|
|
2063
|
+
await diagram.start();
|
|
2064
|
+
await diagram.openDocument(file);
|
|
2065
|
+
const uri = workspace.uri('model.__EXTENSION__');
|
|
2066
|
+
const text = (): string | undefined => shared.workspace.TextDocuments.get(uri)?.getText();
|
|
2067
|
+
|
|
2068
|
+
await send(diagram, CreateNodeOperation.create(${upper}_NODE_TYPE), 'operation');
|
|
2069
|
+
const created = text();
|
|
2070
|
+
await send(diagram, UndoAction.create(), 'undo');
|
|
2071
|
+
const undone = text();
|
|
2072
|
+
await send(diagram, RedoAction.create(), 'redo');
|
|
2073
|
+
|
|
2074
|
+
expect({ created, undone, redone: text() }).toEqual({
|
|
2075
|
+
created: \`\${original}\\nnode Node\`,
|
|
2076
|
+
undone: original,
|
|
2077
|
+
redone: \`\${original}\\nnode Node\`
|
|
2078
|
+
});
|
|
2079
|
+
});
|
|
2080
|
+
});
|
|
1963
2081
|
`;
|
|
1964
2082
|
|
|
1965
2083
|
return [
|
|
@@ -1970,7 +2088,8 @@ export class __GRAMMAR__DiagramModule extends AbstractHydraniumGlspDiagramModule
|
|
|
1970
2088
|
{ path: `${dir}/gmodel-factory.ts`, content: render(factory) },
|
|
1971
2089
|
{ path: `${dir}/diagram-configuration.ts`, content: render(configuration) },
|
|
1972
2090
|
{ path: `${dir}/create-node-operation-handler.ts`, content: render(createHandler) },
|
|
1973
|
-
{ path: `${dir}/diagram-module.ts`, content: render(module) }
|
|
2091
|
+
{ path: `${dir}/diagram-module.ts`, content: render(module) },
|
|
2092
|
+
{ path: `test/${grammar.grammarId}-diagram.test.ts`, content: render(diagramTest) }
|
|
1974
2093
|
];
|
|
1975
2094
|
}
|
|
1976
2095
|
|
|
@@ -1999,10 +2118,7 @@ export function buildInitTemplates(composition: InitComposition): InitFile[] {
|
|
|
1999
2118
|
{ path: 'tsconfig.json', content: tsconfigJson(composition) },
|
|
2000
2119
|
{ path: 'tsconfig.test.json', content: TSCONFIG_TEST },
|
|
2001
2120
|
{ path: 'vitest.config.ts', content: VITEST_CONFIG },
|
|
2002
|
-
{
|
|
2003
|
-
path: '.gitignore',
|
|
2004
|
-
content: composition.packaging.workspace === undefined ? GITIGNORE : GITIGNORE_WORKSPACE_MEMBER
|
|
2005
|
-
},
|
|
2121
|
+
{ path: '.gitignore', content: GITIGNORE },
|
|
2006
2122
|
{ path: 'README.md', content: readme(composition) },
|
|
2007
2123
|
...grammarFiles,
|
|
2008
2124
|
{ path: `src/language-server/${composition.projectId}-module.ts`, content: moduleFile(composition) },
|