@hydranium/glsp-client-theia 1.0.0-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/lib/browser/action-dispatcher.d.ts +85 -0
- package/lib/browser/action-dispatcher.d.ts.map +1 -0
- package/lib/browser/action-dispatcher.js +238 -0
- package/lib/browser/action-dispatcher.js.map +1 -0
- package/lib/browser/client-contribution.d.ts +53 -0
- package/lib/browser/client-contribution.d.ts.map +1 -0
- package/lib/browser/client-contribution.js +111 -0
- package/lib/browser/client-contribution.js.map +1 -0
- package/lib/browser/diagram-loader.d.ts +123 -0
- package/lib/browser/diagram-loader.d.ts.map +1 -0
- package/lib/browser/diagram-loader.js +153 -0
- package/lib/browser/diagram-loader.js.map +1 -0
- package/lib/browser/diagram-only-marker-manager.d.ts +28 -0
- package/lib/browser/diagram-only-marker-manager.d.ts.map +1 -0
- package/lib/browser/diagram-only-marker-manager.js +44 -0
- package/lib/browser/diagram-only-marker-manager.js.map +1 -0
- package/lib/browser/diagram-widget.d.ts +97 -0
- package/lib/browser/diagram-widget.d.ts.map +1 -0
- package/lib/browser/diagram-widget.js +172 -0
- package/lib/browser/diagram-widget.js.map +1 -0
- package/lib/browser/glsp-client-theia-module.d.ts +40 -0
- package/lib/browser/glsp-client-theia-module.d.ts.map +1 -0
- package/lib/browser/glsp-client-theia-module.js +46 -0
- package/lib/browser/glsp-client-theia-module.js.map +1 -0
- package/lib/browser/glsp-diagram-manager.d.ts +46 -0
- package/lib/browser/glsp-diagram-manager.d.ts.map +1 -0
- package/lib/browser/glsp-diagram-manager.js +66 -0
- package/lib/browser/glsp-diagram-manager.js.map +1 -0
- package/lib/browser/glsp-message-service.d.ts +59 -0
- package/lib/browser/glsp-message-service.d.ts.map +1 -0
- package/lib/browser/glsp-message-service.js +97 -0
- package/lib/browser/glsp-message-service.js.map +1 -0
- package/lib/browser/glsp-theia-frontend-module.d.ts +110 -0
- package/lib/browser/glsp-theia-frontend-module.d.ts.map +1 -0
- package/lib/browser/glsp-theia-frontend-module.js +133 -0
- package/lib/browser/glsp-theia-frontend-module.js.map +1 -0
- package/lib/browser/hidden-bounds-updater.d.ts +122 -0
- package/lib/browser/hidden-bounds-updater.d.ts.map +1 -0
- package/lib/browser/hidden-bounds-updater.js +207 -0
- package/lib/browser/hidden-bounds-updater.js.map +1 -0
- package/lib/browser/hydranium-glsp-diagram-configuration.d.ts +52 -0
- package/lib/browser/hydranium-glsp-diagram-configuration.d.ts.map +1 -0
- package/lib/browser/hydranium-glsp-diagram-configuration.js +72 -0
- package/lib/browser/hydranium-glsp-diagram-configuration.js.map +1 -0
- package/lib/browser/index.d.ts +20 -0
- package/lib/browser/index.d.ts.map +1 -0
- package/lib/browser/index.js +38 -0
- package/lib/browser/index.js.map +1 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +39 -0
- package/lib/index.js.map +1 -0
- package/lib/node/connection-container-module.d.ts +22 -0
- package/lib/node/connection-container-module.d.ts.map +1 -0
- package/lib/node/connection-container-module.js +36 -0
- package/lib/node/connection-container-module.js.map +1 -0
- package/lib/node/glsp-server-connection-handler.d.ts +49 -0
- package/lib/node/glsp-server-connection-handler.d.ts.map +1 -0
- package/lib/node/glsp-server-connection-handler.js +64 -0
- package/lib/node/glsp-server-connection-handler.js.map +1 -0
- package/lib/node/index.d.ts +11 -0
- package/lib/node/index.d.ts.map +1 -0
- package/lib/node/index.js +28 -0
- package/lib/node/index.js.map +1 -0
- package/lib/testing/bind-recorder.d.ts +67 -0
- package/lib/testing/bind-recorder.d.ts.map +1 -0
- package/lib/testing/bind-recorder.js +72 -0
- package/lib/testing/bind-recorder.js.map +1 -0
- package/lib/testing/index.d.ts +10 -0
- package/lib/testing/index.d.ts.map +1 -0
- package/lib/testing/index.js +30 -0
- package/lib/testing/index.js.map +1 -0
- package/package.json +114 -0
- package/src/browser/action-dispatcher.ts +257 -0
- package/src/browser/client-contribution.ts +100 -0
- package/src/browser/diagram-loader.ts +173 -0
- package/src/browser/diagram-only-marker-manager.ts +33 -0
- package/src/browser/diagram-widget.ts +178 -0
- package/src/browser/glsp-client-theia-module.ts +54 -0
- package/src/browser/glsp-diagram-manager.ts +67 -0
- package/src/browser/glsp-message-service.ts +90 -0
- package/src/browser/glsp-theia-frontend-module.ts +157 -0
- package/src/browser/hidden-bounds-updater.ts +212 -0
- package/src/browser/hydranium-glsp-diagram-configuration.ts +63 -0
- package/src/browser/index.ts +22 -0
- package/src/index.ts +23 -0
- package/src/node/connection-container-module.ts +34 -0
- package/src/node/glsp-server-connection-handler.ts +69 -0
- package/src/node/index.ts +12 -0
- package/src/testing/bind-recorder.ts +126 -0
- package/src/testing/index.ts +15 -0
- package/style/diagram-loading.css +67 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/********************************************************************************
|
|
3
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
4
|
+
*
|
|
5
|
+
* This program and the accompanying materials are made available under the
|
|
6
|
+
* terms of the MIT License which is available in the project root.
|
|
7
|
+
*
|
|
8
|
+
* SPDX-License-Identifier: MIT
|
|
9
|
+
********************************************************************************/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
22
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
23
|
+
};
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
// Subpath barrel for `@hydranium/glsp-client-theia/testing` — the
|
|
26
|
+
// GLSP-module-specific Inversify test double. The cross-head doubles
|
|
27
|
+
// (`makeStubOutputChannelManager`, `makeStubInversifyContext`) live in
|
|
28
|
+
// `@hydranium/client-theia/testing`, alongside the logger they double for.
|
|
29
|
+
__exportStar(require("./bind-recorder"), exports);
|
|
30
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":";AAAA;;;;;;;kFAOkF;;;;;;;;;;;;;;;;AAElF,kEAAkE;AAClE,qEAAqE;AACrE,uEAAuE;AACvE,2EAA2E;AAE3E,kDAAgC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hydranium/glsp-client-theia",
|
|
3
|
+
"version": "1.0.0-next.10",
|
|
4
|
+
"description": "Theia-coupled client primitives for GLSP heads built on @hydranium/glsp-server. Provides the action dispatcher, channel logger, and connection-handler bases that Theia-based adopters extend; companion of @hydranium/glsp-server.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"hydranium",
|
|
7
|
+
"langium",
|
|
8
|
+
"language-server",
|
|
9
|
+
"lsp",
|
|
10
|
+
"glsp",
|
|
11
|
+
"diagram",
|
|
12
|
+
"theia"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/eclipse-emfcloud/hydranium/tree/main/packages/glsp-client-theia",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/eclipse-emfcloud/hydranium/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/eclipse-emfcloud/hydranium.git",
|
|
21
|
+
"directory": "packages/glsp-client-theia"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": {
|
|
25
|
+
"name": "Hydranium Team"
|
|
26
|
+
},
|
|
27
|
+
"sideEffects": [
|
|
28
|
+
"**/*.css"
|
|
29
|
+
],
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./lib/index.d.ts",
|
|
33
|
+
"default": "./lib/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./browser": {
|
|
36
|
+
"types": "./lib/browser/index.d.ts",
|
|
37
|
+
"default": "./lib/browser/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./node": {
|
|
40
|
+
"types": "./lib/node/index.d.ts",
|
|
41
|
+
"default": "./lib/node/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./testing": {
|
|
44
|
+
"types": "./lib/testing/index.d.ts",
|
|
45
|
+
"default": "./lib/testing/index.js"
|
|
46
|
+
},
|
|
47
|
+
"./lib/browser": {
|
|
48
|
+
"types": "./lib/browser/index.d.ts",
|
|
49
|
+
"default": "./lib/browser/index.js"
|
|
50
|
+
},
|
|
51
|
+
"./lib/node": {
|
|
52
|
+
"types": "./lib/node/index.d.ts",
|
|
53
|
+
"default": "./lib/node/index.js"
|
|
54
|
+
},
|
|
55
|
+
"./lib/testing": {
|
|
56
|
+
"types": "./lib/testing/index.d.ts",
|
|
57
|
+
"default": "./lib/testing/index.js"
|
|
58
|
+
},
|
|
59
|
+
"./style/diagram-loading.css": "./style/diagram-loading.css"
|
|
60
|
+
},
|
|
61
|
+
"main": "lib/index.js",
|
|
62
|
+
"types": "lib/index.d.ts",
|
|
63
|
+
"files": [
|
|
64
|
+
"lib",
|
|
65
|
+
"src",
|
|
66
|
+
"style",
|
|
67
|
+
"!lib/**/*.tsbuildinfo"
|
|
68
|
+
],
|
|
69
|
+
"scripts": {
|
|
70
|
+
"build": "tsc -b",
|
|
71
|
+
"clean": "rimraf lib tsconfig.tsbuildinfo",
|
|
72
|
+
"lint": "eslint src test --max-warnings 0",
|
|
73
|
+
"prepack": "node -e \"const m=require('./package.json'),fs=require('node:fs');const missing=[m.main,...Object.values(m.bin||{})].filter(entry=>entry&&!fs.existsSync(entry));if(missing.length){console.error('prepack '+m.name+': not built ('+missing.join(', ')+' missing). Run the build before packing: a files entry that matches nothing is skipped silently, so the tarball would ship src only.');process.exit(1);}\"",
|
|
74
|
+
"test": "npm run typecheck:test && vitest run",
|
|
75
|
+
"typecheck:test": "tsc --noEmit -p tsconfig.test.json",
|
|
76
|
+
"watch": "tsc -b -w --preserveWatchOutput"
|
|
77
|
+
},
|
|
78
|
+
"devDependencies": {
|
|
79
|
+
"@eclipse-glsp/client": "2.7.0",
|
|
80
|
+
"@eclipse-glsp/theia-integration": "2.7.0",
|
|
81
|
+
"@hydranium/client-theia": "1.0.0-next.10",
|
|
82
|
+
"@hydranium/protocol": "1.0.0-next.10",
|
|
83
|
+
"@theia/core": "^1.71.0",
|
|
84
|
+
"@theia/output": "^1.71.0",
|
|
85
|
+
"@theia/process": "^1.71.0",
|
|
86
|
+
"@theia/workspace": "^1.71.0",
|
|
87
|
+
"@types/node": "^22.0.0",
|
|
88
|
+
"inversify": "6.2.2",
|
|
89
|
+
"rimraf": "^5.0.0",
|
|
90
|
+
"snabbdom": "3.5.1",
|
|
91
|
+
"typescript": "^5.8.0"
|
|
92
|
+
},
|
|
93
|
+
"peerDependencies": {
|
|
94
|
+
"@eclipse-glsp/client": "^2.6.0",
|
|
95
|
+
"@eclipse-glsp/theia-integration": "^2.6.0",
|
|
96
|
+
"@hydranium/client-theia": "1.0.0-next.10",
|
|
97
|
+
"@hydranium/protocol": "1.0.0-next.10",
|
|
98
|
+
"@theia/core": "^1.71.0",
|
|
99
|
+
"@theia/output": "^1.71.0",
|
|
100
|
+
"@theia/process": "^1.71.0",
|
|
101
|
+
"@theia/workspace": "^1.71.0",
|
|
102
|
+
"inversify": "^6.0.0",
|
|
103
|
+
"snabbdom": "^3.5.1"
|
|
104
|
+
},
|
|
105
|
+
"engines": {
|
|
106
|
+
"node": ">=22.13"
|
|
107
|
+
},
|
|
108
|
+
"publishConfig": {
|
|
109
|
+
"access": "public"
|
|
110
|
+
},
|
|
111
|
+
"//exports": "The stylesheet key is an ASSET entry, not a module subpath, and it is spelled as the exact filename rather than `./style/*` so nothing becomes reachable automatically — the same no-wildcard property every other key here has. It exists because the widget's own import is package-RELATIVE, which `exports` does not gate, so the map's silence broke nobody inside the package while blocking every adopter who wants to `@import` the sheet, re-bundle it or override it by specifier; that failure reads as \"not exported under the conditions\" rather than as a missing entry. It has NO `./lib/style/...` twin, and must not grow one: the twin rule exists so a bare subpath is reachable from a node10 MODULE resolver, `tsc` emits no assets so there is no `lib/style/` to point at, and a bundler — which is what resolves a stylesheet — reads the map. `check:exports` carves out exactly this shape and asserts instead that the target file exists and is covered by `files`, which is the pair of mistakes an asset key can actually make.",
|
|
112
|
+
"//prepack": "The publish guard, and it deliberately is NOT a `prepare`: npm runs a workspace `prepare` BEFORE the root `postinstall` that applies patches/vscode-jsonrpc+9.0.1.patch, so building there fails on a cold clone and npm rolls the entire install back. `prepack` runs only when a tarball is made (`npm pack`, `npm publish`) and never on install, so it cannot break the install it has no business touching. It FAILS rather than rebuilds, because the rebuild is exactly the part that ordering defeats. What it defends against: `files` lists `lib`, `lib` is gitignored, and a `files` entry matching nothing is skipped SILENTLY — so `npm publish` from an unbuilt tree emits a tarball of `src` and nothing else, with no error.",
|
|
113
|
+
"//sideEffects": "A stylesheet IS the side effect: `style/diagram-loading.css` is pulled in by a bare import in `browser/diagram-widget.js`, and a CSS module exports nothing, so under a blanket `false` a bundler drops the import as unused and the diagram loading overlay renders unstyled. Nothing fails at build time. The glob rather than the importing module, so a second stylesheet is covered without another edit; the widget itself is reached through its exported class and needs no flag."
|
|
114
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
type Action,
|
|
12
|
+
ComputedBoundsAction,
|
|
13
|
+
GLSPActionDispatcher,
|
|
14
|
+
GLSPModelSource,
|
|
15
|
+
LocalComputedBoundsAction,
|
|
16
|
+
MessageAction,
|
|
17
|
+
RejectAction,
|
|
18
|
+
RequestAction,
|
|
19
|
+
RequestBoundsAction,
|
|
20
|
+
RequestContextActions,
|
|
21
|
+
RequestModelAction,
|
|
22
|
+
RequestTypeHintsAction,
|
|
23
|
+
ResponseAction,
|
|
24
|
+
ServerAction,
|
|
25
|
+
SetDirtyStateAction,
|
|
26
|
+
SetEditModeAction,
|
|
27
|
+
SetMarkersAction,
|
|
28
|
+
SetModelAction,
|
|
29
|
+
SetTypeHintsAction,
|
|
30
|
+
StatusAction,
|
|
31
|
+
UpdateModelAction
|
|
32
|
+
} from '@eclipse-glsp/client';
|
|
33
|
+
import { ChannelLogger, ChannelTracer } from '@hydranium/client-theia/lib/browser';
|
|
34
|
+
import { type Tracer } from '@hydranium/protocol';
|
|
35
|
+
import { inject, injectable, unmanaged } from '@theia/core/shared/inversify';
|
|
36
|
+
|
|
37
|
+
/** Default subset of action kinds the framework dispatcher will log: the GLSP
|
|
38
|
+
* traffic an observer reading the Output channel typically wants to see. All
|
|
39
|
+
* are standard `@eclipse-glsp/client` kinds; adopters with custom action kinds
|
|
40
|
+
* extend via `extraLoggedKinds`. */
|
|
41
|
+
export const HYDRANIUM_DEFAULT_LOGGED_KINDS: ReadonlySet<string> = new Set<string>([
|
|
42
|
+
SetModelAction.KIND,
|
|
43
|
+
UpdateModelAction.KIND,
|
|
44
|
+
RequestBoundsAction.KIND,
|
|
45
|
+
ComputedBoundsAction.KIND,
|
|
46
|
+
SetDirtyStateAction.KIND,
|
|
47
|
+
SetEditModeAction.KIND,
|
|
48
|
+
SetMarkersAction.KIND,
|
|
49
|
+
SetTypeHintsAction.KIND,
|
|
50
|
+
StatusAction.KIND,
|
|
51
|
+
MessageAction.KIND,
|
|
52
|
+
RejectAction.KIND,
|
|
53
|
+
RequestContextActions.KIND,
|
|
54
|
+
RequestModelAction.KIND,
|
|
55
|
+
RequestTypeHintsAction.KIND
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
/** Default request → response kind pairs for GLSP 'flow' actions that do NOT
|
|
59
|
+
* carry a requestId/responseId — so the dispatcher matches by kind (FIFO).
|
|
60
|
+
* Id-based matching still wins when a requestId is present. */
|
|
61
|
+
export const HYDRANIUM_DEFAULT_KIND_PAIRS: ReadonlyMap<string, string> = new Map<string, string>([
|
|
62
|
+
[RequestModelAction.KIND, SetModelAction.KIND],
|
|
63
|
+
[RequestTypeHintsAction.KIND, SetTypeHintsAction.KIND],
|
|
64
|
+
[RequestBoundsAction.KIND, ComputedBoundsAction.KIND]
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
/** Additive options. Adopters with custom action kinds pass extras here; the
|
|
68
|
+
* framework defaults always apply (you cannot un-log a kind the framework
|
|
69
|
+
* considers worth logging — subclass + override `dispatch` if you must). */
|
|
70
|
+
export interface HydraniumGlspActionDispatcherOptions {
|
|
71
|
+
readonly extraLoggedKinds?: Iterable<string>;
|
|
72
|
+
readonly extraKindPairs?: Iterable<readonly [string, string]>;
|
|
73
|
+
/** Replaces the default `summarize` body entirely. Use for adopter-domain
|
|
74
|
+
* action kinds where the framework default returns ''. */
|
|
75
|
+
readonly summarize?: (action: Action) => string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Wraps every dispatch so action traffic appears in the framework Output
|
|
80
|
+
* channel, interleaved with server log lines. Instrumenting the dispatcher
|
|
81
|
+
* (rather than per-kind handlers) catches ResponseActions that bypass the
|
|
82
|
+
* handler pipeline.
|
|
83
|
+
*
|
|
84
|
+
* The framework defaults cover the standard GLSP kinds worth logging, not every
|
|
85
|
+
* kind the protocol defines. Adopters with custom kinds either pass
|
|
86
|
+
* `extraLoggedKinds` / `extraKindPairs` to the constructor (via a
|
|
87
|
+
* `toDynamicValue` binding) or subclass and override.
|
|
88
|
+
*/
|
|
89
|
+
@injectable()
|
|
90
|
+
export class HydraniumGlspActionDispatcher extends GLSPActionDispatcher {
|
|
91
|
+
@inject(ChannelLogger) protected readonly channel!: ChannelLogger;
|
|
92
|
+
@inject(ChannelTracer) protected readonly tracer!: Tracer;
|
|
93
|
+
|
|
94
|
+
protected readonly loggedKinds: ReadonlySet<string>;
|
|
95
|
+
protected readonly kindPairs: ReadonlyMap<string, string>;
|
|
96
|
+
protected readonly kindPairsReverse: ReadonlyMap<string, string>;
|
|
97
|
+
|
|
98
|
+
protected pairCounter = 0;
|
|
99
|
+
/** Pending requests keyed by requestId (for actions that carry one). */
|
|
100
|
+
protected pendingById = new Map<string, { id: number; start: number }>();
|
|
101
|
+
/** FIFO queue per response kind, used for `flow` pairs without requestId/responseId. */
|
|
102
|
+
protected pendingByKind = new Map<string, Array<{ id: number; start: number }>>();
|
|
103
|
+
|
|
104
|
+
constructor(@unmanaged() options?: HydraniumGlspActionDispatcherOptions) {
|
|
105
|
+
super();
|
|
106
|
+
const extraLogged = options?.extraLoggedKinds ? Array.from(options.extraLoggedKinds) : [];
|
|
107
|
+
this.loggedKinds = new Set<string>([...HYDRANIUM_DEFAULT_LOGGED_KINDS, ...extraLogged]);
|
|
108
|
+
const extraPairs = options?.extraKindPairs ? Array.from(options.extraKindPairs) : [];
|
|
109
|
+
const pairs = new Map<string, string>([...HYDRANIUM_DEFAULT_KIND_PAIRS, ...extraPairs]);
|
|
110
|
+
this.kindPairs = pairs;
|
|
111
|
+
this.kindPairsReverse = new Map<string, string>(Array.from(pairs, ([req, res]) => [res, req]));
|
|
112
|
+
if (options?.summarize) {
|
|
113
|
+
this.summarize = options.summarize;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
override initialize(): Promise<void> {
|
|
118
|
+
// `initialize()` is called many times; super is idempotent, but our callback registration
|
|
119
|
+
// isn't. Register the timing log only on the first call (when super.initialized is unset).
|
|
120
|
+
if (!this.initialized) {
|
|
121
|
+
this.registerInitTiming();
|
|
122
|
+
}
|
|
123
|
+
return super.initialize();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Register the one-shot 'model initialized' timing line. Instrumentation
|
|
128
|
+
* only — guarded so a tracer failure degrades logging rather than aborting
|
|
129
|
+
* diagram init. `initialize()` runs inside `DiagramLoader.load()`, whose
|
|
130
|
+
* caller (`GLSPDiagramWidget.onAfterAttach`) neither awaits nor catches the
|
|
131
|
+
* returned promise; an error thrown here would therefore escape as an
|
|
132
|
+
* uncaught rejection that blanks the diagram with no server-side signal.
|
|
133
|
+
*/
|
|
134
|
+
protected registerInitTiming(): void {
|
|
135
|
+
try {
|
|
136
|
+
this.tracer.time('Model initialized', () => this.onceModelInitialized(), 'info', { logAfterMs: 0 });
|
|
137
|
+
} catch (err) {
|
|
138
|
+
this.channel.error('Failed to register model-initialized timing instrumentation', err);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
override dispatch(action: Action): Promise<void> {
|
|
143
|
+
if (this.loggedKinds.has(action.kind)) {
|
|
144
|
+
this.logAction(action);
|
|
145
|
+
}
|
|
146
|
+
return super.dispatch(action);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
protected logAction(action: Action): void {
|
|
150
|
+
const { verb, direction } = this.classify(action);
|
|
151
|
+
const summary = this.summarize(action);
|
|
152
|
+
const timing = this.trackPair(action);
|
|
153
|
+
const parts = [`${verb} action '${action.kind}'`, `[${direction}]`];
|
|
154
|
+
if (summary) {
|
|
155
|
+
parts.push(`[${summary}]`);
|
|
156
|
+
}
|
|
157
|
+
if (timing) {
|
|
158
|
+
parts.push(timing);
|
|
159
|
+
}
|
|
160
|
+
this.channel.trace(parts.join(' '));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
protected classify(action: Action): { verb: string; direction: string } {
|
|
164
|
+
// GLSP marks client-produced computedBounds as ServerAction to suppress forwarding; stays local.
|
|
165
|
+
if (LocalComputedBoundsAction.is(action)) {
|
|
166
|
+
return { verb: 'Process', direction: 'client → client' };
|
|
167
|
+
}
|
|
168
|
+
// Marked by GLSPModelSource.messageReceived — arrived from the server.
|
|
169
|
+
if (ServerAction.is(action)) {
|
|
170
|
+
return { verb: 'Receive', direction: 'server → client' };
|
|
171
|
+
}
|
|
172
|
+
// GLSPModelSource is registered as handler for every server-handled kind (configureServeActions),
|
|
173
|
+
// so its presence means the action will be forwarded to the server.
|
|
174
|
+
const handlers = this.actionHandlerRegistry.get(action.kind);
|
|
175
|
+
if (handlers.some(handler => handler instanceof GLSPModelSource)) {
|
|
176
|
+
return { verb: 'Send', direction: 'client → server' };
|
|
177
|
+
}
|
|
178
|
+
return { verb: 'Process', direction: 'client → client' };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Emit `[request #N]` on the request side, `[response #N, Xms]` on the matching response side.
|
|
182
|
+
* Match first by requestId/responseId, then by known kind pairs (FIFO). */
|
|
183
|
+
protected trackPair(action: Action): string {
|
|
184
|
+
// Response side.
|
|
185
|
+
if (ResponseAction.hasValidResponseId(action)) {
|
|
186
|
+
const entry = this.pendingById.get(action.responseId);
|
|
187
|
+
if (entry) {
|
|
188
|
+
this.pendingById.delete(action.responseId);
|
|
189
|
+
return this.formatResponse(entry);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (this.kindPairsReverse.has(action.kind)) {
|
|
193
|
+
const queue = this.pendingByKind.get(action.kind);
|
|
194
|
+
const entry = queue?.shift();
|
|
195
|
+
if (entry) {
|
|
196
|
+
return this.formatResponse(entry);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Request side.
|
|
200
|
+
if (RequestAction.is(action) && action.requestId) {
|
|
201
|
+
const entry = { id: ++this.pairCounter, start: performance.now() };
|
|
202
|
+
this.pendingById.set(action.requestId, entry);
|
|
203
|
+
return `[request #${entry.id}]`;
|
|
204
|
+
}
|
|
205
|
+
if (this.kindPairs.has(action.kind)) {
|
|
206
|
+
const responseKind = this.kindPairs.get(action.kind)!;
|
|
207
|
+
const entry = { id: ++this.pairCounter, start: performance.now() };
|
|
208
|
+
const queue = this.pendingByKind.get(responseKind) ?? [];
|
|
209
|
+
queue.push(entry);
|
|
210
|
+
this.pendingByKind.set(responseKind, queue);
|
|
211
|
+
return `[request #${entry.id}]`;
|
|
212
|
+
}
|
|
213
|
+
return '';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
protected formatResponse(entry: { id: number; start: number }): string {
|
|
217
|
+
const elapsed = Math.round(performance.now() - entry.start);
|
|
218
|
+
return `[response #${entry.id}, ${elapsed}ms]`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
protected summarize(action: Action): string {
|
|
222
|
+
if (SetModelAction.is(action) || UpdateModelAction.is(action)) {
|
|
223
|
+
const root = action.newRoot;
|
|
224
|
+
return `rootType=${root?.type ?? 'none'} children=${root?.children?.length ?? 0}`;
|
|
225
|
+
}
|
|
226
|
+
if (RequestBoundsAction.is(action)) {
|
|
227
|
+
return `rootType=${action.newRoot.type} children=${action.newRoot.children?.length ?? 0}`;
|
|
228
|
+
}
|
|
229
|
+
if (ComputedBoundsAction.is(action)) {
|
|
230
|
+
return `bounds=${action.bounds.length}`;
|
|
231
|
+
}
|
|
232
|
+
if (SetDirtyStateAction.is(action)) {
|
|
233
|
+
return `isDirty=${action.isDirty} reason=${action.reason ?? 'n/a'}`;
|
|
234
|
+
}
|
|
235
|
+
if (SetEditModeAction.is(action)) {
|
|
236
|
+
return `editMode=${action.editMode}`;
|
|
237
|
+
}
|
|
238
|
+
if (SetMarkersAction.is(action)) {
|
|
239
|
+
return `markers=${action.markers.length}`;
|
|
240
|
+
}
|
|
241
|
+
if (StatusAction.is(action) || MessageAction.is(action)) {
|
|
242
|
+
// severity=NONE with empty message is the conventional 'clear' signal.
|
|
243
|
+
const clear = action.severity === 'NONE' && !action.message ? ' (clear)' : '';
|
|
244
|
+
return `severity=${action.severity} message="${action.message.slice(0, 160)}"${clear}`;
|
|
245
|
+
}
|
|
246
|
+
if (RejectAction.is(action)) {
|
|
247
|
+
return `reason="${action.message.slice(0, 160)}"`;
|
|
248
|
+
}
|
|
249
|
+
if (SetTypeHintsAction.is(action)) {
|
|
250
|
+
return `shapes=${action.shapeHints.length} edges=${action.edgeHints.length}`;
|
|
251
|
+
}
|
|
252
|
+
if (RequestContextActions.is(action)) {
|
|
253
|
+
return `contextId=${action.contextId}`;
|
|
254
|
+
}
|
|
255
|
+
return '';
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
import { type GLSPClient } from '@eclipse-glsp/client';
|
|
11
|
+
import { BaseGLSPClientContribution } from '@eclipse-glsp/theia-integration';
|
|
12
|
+
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
13
|
+
import { inject, injectable, unmanaged } from '@theia/core/shared/inversify';
|
|
14
|
+
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
|
|
15
|
+
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
|
16
|
+
|
|
17
|
+
/** Options for `HydraniumGlspClientContribution`. The channel name is the
|
|
18
|
+
* Theia Output channel both the LSP and GLSP sides write to (so log lines
|
|
19
|
+
* interleave); the ready marker is a server-printed substring that signals
|
|
20
|
+
* "the GLSP server is accepting client connections". */
|
|
21
|
+
export interface ClientContributionOptions {
|
|
22
|
+
readonly languageContributionId: string;
|
|
23
|
+
readonly channelName: string;
|
|
24
|
+
readonly readyMarker: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Theia GLSP client contribution for adopters whose GLSP server runs in a
|
|
29
|
+
* sideloaded VS Code extension process (or any other deferred-start setup).
|
|
30
|
+
* The behaviours adopters reliably need are lifted:
|
|
31
|
+
*
|
|
32
|
+
* 1. `waitForBackendConnected`: tail the OutputChannel for a server-printed
|
|
33
|
+
* ready marker. While a socket connection might open earlier, the GLSP
|
|
34
|
+
* server typically does internal initialisation before it can accept
|
|
35
|
+
* client connections — we wait for it to say so.
|
|
36
|
+
* 2. `start`: defer until a workspace is opened. If Theia starts without a
|
|
37
|
+
* workspace, this prevents the frontend-backend connection (and its
|
|
38
|
+
* "connecting" progress spinner) from firing prematurely.
|
|
39
|
+
*
|
|
40
|
+
* A `GLSPClient` override filtering inbound messages by clientId is
|
|
41
|
+
* deliberately NOT provided: upstream's `BaseJsonrpcGLSPClient.onActionMessage`
|
|
42
|
+
* already filters by clientId, so the `TheiaJsonrpcGLSPClient` returned by
|
|
43
|
+
* `BaseGLSPClientContribution.createGLSPClient` is correct as-is.
|
|
44
|
+
*/
|
|
45
|
+
@injectable()
|
|
46
|
+
export class HydraniumGlspClientContribution extends BaseGLSPClientContribution {
|
|
47
|
+
@inject(OutputChannelManager) protected outputChannelManager!: OutputChannelManager;
|
|
48
|
+
@inject(WorkspaceService) protected readonly workspaceService!: WorkspaceService;
|
|
49
|
+
|
|
50
|
+
readonly id: string;
|
|
51
|
+
protected readonly channelName: string;
|
|
52
|
+
protected readonly readyMarker: string;
|
|
53
|
+
|
|
54
|
+
constructor(@unmanaged() options: ClientContributionOptions) {
|
|
55
|
+
super();
|
|
56
|
+
this.id = options.languageContributionId;
|
|
57
|
+
this.channelName = options.channelName;
|
|
58
|
+
this.readyMarker = options.readyMarker;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Wait for the server to print its ready marker into the OutputChannel.
|
|
62
|
+
* Resolves immediately if the marker is already present (server started
|
|
63
|
+
* before the client contribution mounted). */
|
|
64
|
+
protected async waitForBackendConnected(): Promise<void> {
|
|
65
|
+
const channel = this.outputChannelManager.getChannel(this.channelName);
|
|
66
|
+
const channelText =
|
|
67
|
+
(channel as unknown as { resource?: { textModel?: { getValue(): string } } }).resource?.textModel?.getValue() ?? '';
|
|
68
|
+
if (channelText.includes(this.readyMarker)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const deferred = new Deferred<void>();
|
|
72
|
+
const channelListener = channel.onContentChange(() => {
|
|
73
|
+
const text = (channel as unknown as { resource?: { textModel?: { getValue(): string } } }).resource?.textModel?.getValue() ?? '';
|
|
74
|
+
if (text.includes(this.readyMarker)) {
|
|
75
|
+
channelListener.dispose();
|
|
76
|
+
deferred.resolve();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
return deferred.promise;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
protected override async start(glspClient: GLSPClient): Promise<void> {
|
|
83
|
+
// Defer starting the GLSP client until a workspace is opened. If Theia
|
|
84
|
+
// starts without a workspace, this prevents creating the frontend-
|
|
85
|
+
// backend connection (and showing the "connecting" progress) prematurely.
|
|
86
|
+
const roots = this.workspaceService.tryGetRoots();
|
|
87
|
+
if (!roots || roots.length === 0) {
|
|
88
|
+
await new Promise<void>(resolve => {
|
|
89
|
+
const disposable = this.workspaceService.onWorkspaceChanged(changedRoots => {
|
|
90
|
+
if (changedRoots && changedRoots.length > 0) {
|
|
91
|
+
disposable.dispose();
|
|
92
|
+
resolve();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
await this.waitForBackendConnected();
|
|
98
|
+
return super.start(glspClient);
|
|
99
|
+
}
|
|
100
|
+
}
|