@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.
Files changed (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/lib/browser/action-dispatcher.d.ts +85 -0
  4. package/lib/browser/action-dispatcher.d.ts.map +1 -0
  5. package/lib/browser/action-dispatcher.js +238 -0
  6. package/lib/browser/action-dispatcher.js.map +1 -0
  7. package/lib/browser/client-contribution.d.ts +53 -0
  8. package/lib/browser/client-contribution.d.ts.map +1 -0
  9. package/lib/browser/client-contribution.js +111 -0
  10. package/lib/browser/client-contribution.js.map +1 -0
  11. package/lib/browser/diagram-loader.d.ts +123 -0
  12. package/lib/browser/diagram-loader.d.ts.map +1 -0
  13. package/lib/browser/diagram-loader.js +153 -0
  14. package/lib/browser/diagram-loader.js.map +1 -0
  15. package/lib/browser/diagram-only-marker-manager.d.ts +28 -0
  16. package/lib/browser/diagram-only-marker-manager.d.ts.map +1 -0
  17. package/lib/browser/diagram-only-marker-manager.js +44 -0
  18. package/lib/browser/diagram-only-marker-manager.js.map +1 -0
  19. package/lib/browser/diagram-widget.d.ts +97 -0
  20. package/lib/browser/diagram-widget.d.ts.map +1 -0
  21. package/lib/browser/diagram-widget.js +172 -0
  22. package/lib/browser/diagram-widget.js.map +1 -0
  23. package/lib/browser/glsp-client-theia-module.d.ts +40 -0
  24. package/lib/browser/glsp-client-theia-module.d.ts.map +1 -0
  25. package/lib/browser/glsp-client-theia-module.js +46 -0
  26. package/lib/browser/glsp-client-theia-module.js.map +1 -0
  27. package/lib/browser/glsp-diagram-manager.d.ts +46 -0
  28. package/lib/browser/glsp-diagram-manager.d.ts.map +1 -0
  29. package/lib/browser/glsp-diagram-manager.js +66 -0
  30. package/lib/browser/glsp-diagram-manager.js.map +1 -0
  31. package/lib/browser/glsp-message-service.d.ts +59 -0
  32. package/lib/browser/glsp-message-service.d.ts.map +1 -0
  33. package/lib/browser/glsp-message-service.js +97 -0
  34. package/lib/browser/glsp-message-service.js.map +1 -0
  35. package/lib/browser/glsp-theia-frontend-module.d.ts +110 -0
  36. package/lib/browser/glsp-theia-frontend-module.d.ts.map +1 -0
  37. package/lib/browser/glsp-theia-frontend-module.js +133 -0
  38. package/lib/browser/glsp-theia-frontend-module.js.map +1 -0
  39. package/lib/browser/hidden-bounds-updater.d.ts +122 -0
  40. package/lib/browser/hidden-bounds-updater.d.ts.map +1 -0
  41. package/lib/browser/hidden-bounds-updater.js +207 -0
  42. package/lib/browser/hidden-bounds-updater.js.map +1 -0
  43. package/lib/browser/hydranium-glsp-diagram-configuration.d.ts +52 -0
  44. package/lib/browser/hydranium-glsp-diagram-configuration.d.ts.map +1 -0
  45. package/lib/browser/hydranium-glsp-diagram-configuration.js +72 -0
  46. package/lib/browser/hydranium-glsp-diagram-configuration.js.map +1 -0
  47. package/lib/browser/index.d.ts +20 -0
  48. package/lib/browser/index.d.ts.map +1 -0
  49. package/lib/browser/index.js +38 -0
  50. package/lib/browser/index.js.map +1 -0
  51. package/lib/index.d.ts +10 -0
  52. package/lib/index.d.ts.map +1 -0
  53. package/lib/index.js +39 -0
  54. package/lib/index.js.map +1 -0
  55. package/lib/node/connection-container-module.d.ts +22 -0
  56. package/lib/node/connection-container-module.d.ts.map +1 -0
  57. package/lib/node/connection-container-module.js +36 -0
  58. package/lib/node/connection-container-module.js.map +1 -0
  59. package/lib/node/glsp-server-connection-handler.d.ts +49 -0
  60. package/lib/node/glsp-server-connection-handler.d.ts.map +1 -0
  61. package/lib/node/glsp-server-connection-handler.js +64 -0
  62. package/lib/node/glsp-server-connection-handler.js.map +1 -0
  63. package/lib/node/index.d.ts +11 -0
  64. package/lib/node/index.d.ts.map +1 -0
  65. package/lib/node/index.js +28 -0
  66. package/lib/node/index.js.map +1 -0
  67. package/lib/testing/bind-recorder.d.ts +67 -0
  68. package/lib/testing/bind-recorder.d.ts.map +1 -0
  69. package/lib/testing/bind-recorder.js +72 -0
  70. package/lib/testing/bind-recorder.js.map +1 -0
  71. package/lib/testing/index.d.ts +10 -0
  72. package/lib/testing/index.d.ts.map +1 -0
  73. package/lib/testing/index.js +30 -0
  74. package/lib/testing/index.js.map +1 -0
  75. package/package.json +114 -0
  76. package/src/browser/action-dispatcher.ts +257 -0
  77. package/src/browser/client-contribution.ts +100 -0
  78. package/src/browser/diagram-loader.ts +173 -0
  79. package/src/browser/diagram-only-marker-manager.ts +33 -0
  80. package/src/browser/diagram-widget.ts +178 -0
  81. package/src/browser/glsp-client-theia-module.ts +54 -0
  82. package/src/browser/glsp-diagram-manager.ts +67 -0
  83. package/src/browser/glsp-message-service.ts +90 -0
  84. package/src/browser/glsp-theia-frontend-module.ts +157 -0
  85. package/src/browser/hidden-bounds-updater.ts +212 -0
  86. package/src/browser/hydranium-glsp-diagram-configuration.ts +63 -0
  87. package/src/browser/index.ts +22 -0
  88. package/src/index.ts +23 -0
  89. package/src/node/connection-container-module.ts +34 -0
  90. package/src/node/glsp-server-connection-handler.ts +69 -0
  91. package/src/node/index.ts +12 -0
  92. package/src/testing/bind-recorder.ts +126 -0
  93. package/src/testing/index.ts +15 -0
  94. 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
+ }