@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,157 @@
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 ContainerContext,
12
+ DiagramConfiguration,
13
+ GLSPClientContribution,
14
+ type GLSPDiagramManager,
15
+ GLSPDiagramWidget,
16
+ GLSPTheiaFrontendModule,
17
+ registerDiagramManager
18
+ } from '@eclipse-glsp/theia-integration';
19
+ import { type GLSPDiagramLanguage } from '@eclipse-glsp/theia-integration/lib/common';
20
+ import { bindLogLevelPreference } from '@hydranium/client-theia/lib/browser';
21
+ import type { interfaces } from '@theia/core/shared/inversify';
22
+ import { HydraniumGlspDiagramWidget } from './diagram-widget';
23
+
24
+ /**
25
+ * Sentinel returned from {@link AbstractHydraniumGlspTheiaFrontendModule.bindClientContribution}
26
+ * to mean "this module deliberately does not bind a GLSPClientContribution"
27
+ * — used for secondary diagram types that share a GLSP server with a
28
+ * primary diagram type whose module already bound the contribution. The
29
+ * override becomes a no-op rather than the framework default.
30
+ */
31
+ export const SkipClientContribution = Symbol.for('@hydranium/glsp-client-theia#SkipClientContribution');
32
+ export type SkipClientContribution = typeof SkipClientContribution;
33
+
34
+ /**
35
+ * `GLSPTheiaFrontendModule` subclass that factors out the method overrides
36
+ * every adopter writes verbatim (`bindDiagramConfiguration` /
37
+ * `bindGLSPClientContribution` / `configureDiagramManager`) into hook methods
38
+ * reading from abstract fields.
39
+ *
40
+ * Adopters subclass and provide:
41
+ * - {@link diagramLanguage} (declared `abstract` on the base class but
42
+ * `readonly` on the base type — `GLSPTheiaFrontendModule` requires it)
43
+ * - {@link diagramConfiguration} — the adopter's `DiagramConfiguration`
44
+ * - {@link diagramManager} — the adopter's `GLSPDiagramManager` subclass
45
+ *
46
+ * Optional override hook:
47
+ * - {@link bindClientContribution} — return a `Newable<GLSPClientContribution>`
48
+ * to bind it (the common case), {@link SkipClientContribution} to
49
+ * bypass binding entirely (secondary diagrams), or `undefined` to
50
+ * defer to `super.bindGLSPClientContribution(context)`. Default
51
+ * returns `undefined` (framework default).
52
+ *
53
+ * Adopter-specific customisation that stays as `override` on the subclass:
54
+ * - `bindDiagramWidgetFactory` — custom `GLSPDiagramWidget` rebinds
55
+ * - `configure` — LibAvoid initialiser, color contribution, startup hooks
56
+ * - `enableLayoutCommands` / `enableMarkerNavigationCommands` — flags
57
+ *
58
+ * Abstract base class + adopter subclass with concrete fields, following the
59
+ * GLSP module convention. No `Default*` concrete sibling because the abstract
60
+ * fields have no sane defaults — every adopter must provide them.
61
+ */
62
+ export abstract class AbstractHydraniumGlspTheiaFrontendModule extends GLSPTheiaFrontendModule {
63
+ /** Wire-format identifiers + file routing — required by the base. */
64
+ abstract override readonly diagramLanguage: GLSPDiagramLanguage;
65
+
66
+ /** Adopter's `DiagramConfiguration` class. Bound via `bindDiagramConfiguration`. */
67
+ protected abstract readonly diagramConfiguration: interfaces.Newable<DiagramConfiguration>;
68
+
69
+ /** Adopter's `GLSPDiagramManager` class. Bound + registered via `configureDiagramManager`. */
70
+ protected abstract readonly diagramManager: interfaces.Newable<GLSPDiagramManager>;
71
+
72
+ /**
73
+ * Theia preference id driving the framework log threshold, e.g.
74
+ * `'my-language.log.level'`. When set, {@link initialize} binds
75
+ * `LogLevelPreferenceContribution` so the level is applied at
76
+ * application start and kept in sync on change.
77
+ *
78
+ * It belongs here rather than beside the channel name in
79
+ * `createGlspClientTheiaModule`'s options because the two have different
80
+ * scopes: the channel is per diagram container, while the threshold is a
81
+ * process-global and must be applied once, from the frontend container.
82
+ */
83
+ protected readonly logLevelPreference?: string;
84
+
85
+ /**
86
+ * Hook returning the {@link GLSPClientContribution} class the framework
87
+ * should bind for this diagram module:
88
+ * - `undefined` (default) — invoke `super.bindGLSPClientContribution(context)`
89
+ * - a `Newable<GLSPClientContribution>` — bind it to-self singleton + bind
90
+ * `GLSPClientContribution` to the service
91
+ * - {@link SkipClientContribution} — no-op (secondary diagrams sharing
92
+ * a GLSP server with a primary diagram whose module already bound
93
+ * the contribution)
94
+ */
95
+ protected bindClientContribution(): interfaces.Newable<GLSPClientContribution> | SkipClientContribution | undefined {
96
+ return undefined;
97
+ }
98
+
99
+ /**
100
+ * Adds the framework's frontend-scoped bindings on top of the base wiring.
101
+ *
102
+ * Overrides `initialize` — the base's own orchestrator — rather than
103
+ * `configure`, which is documented as the *adopter's* hook: a subclass that
104
+ * overrides `configure` and forgets `super.configure(context)` would otherwise
105
+ * silently lose these bindings.
106
+ */
107
+ override initialize(context: ContainerContext): void {
108
+ super.initialize(context);
109
+ if (this.logLevelPreference) {
110
+ bindLogLevelPreference(context.bind, this.logLevelPreference);
111
+ }
112
+ }
113
+
114
+ override bindDiagramConfiguration(context: ContainerContext): void {
115
+ context.bind(DiagramConfiguration).to(this.diagramConfiguration);
116
+ }
117
+
118
+ override bindGLSPClientContribution(context: ContainerContext): void {
119
+ const contribution = this.bindClientContribution();
120
+ if (contribution === SkipClientContribution) {
121
+ return;
122
+ }
123
+ if (contribution === undefined) {
124
+ super.bindGLSPClientContribution(context);
125
+ return;
126
+ }
127
+ context.bind(contribution).toSelf().inSingletonScope();
128
+ context.bind(GLSPClientContribution).toService(contribution);
129
+ }
130
+
131
+ override configureDiagramManager(context: ContainerContext): void {
132
+ context.bind(this.diagramManager).toSelf().inSingletonScope();
133
+ registerDiagramManager(context.bind, this.diagramManager, false);
134
+ }
135
+
136
+ /**
137
+ * Binds the framework diagram widget on top of the base factory wiring, so
138
+ * every hydranium head gets the loading overlay.
139
+ *
140
+ * `super` binds `GLSPDiagramWidget` to itself (via `lazyBind`, a no-op when
141
+ * already bound) plus the `DiagramWidgetFactory`; the rebind then points the
142
+ * same token at the subclass, leaving the factory untouched.
143
+ *
144
+ * Unconditional on purpose. {@link HydraniumGlspDiagramWidget} differs from
145
+ * GLSP's widget only by the overlay, so there is no flag here: an adopter that
146
+ * wants different behaviour overrides the widget (opt out of the overlay by
147
+ * no-op'ing `showLoadingOverlay`), and one that wants its own subclass rebinds
148
+ * this token again after calling `super`. A module-level flag gating the
149
+ * rebind would be worse than useless: it decides which CLASS to bind, so an
150
+ * adopter subclassing the framework widget and setting the flag to `false`
151
+ * still gets the overlay, and the flag's name lies.
152
+ */
153
+ override bindDiagramWidgetFactory(context: ContainerContext): void {
154
+ super.bindDiagramWidgetFactory(context);
155
+ context.rebind(GLSPDiagramWidget).to(HydraniumGlspDiagramWidget);
156
+ }
157
+ }
@@ -0,0 +1,212 @@
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
+ type ElementAndRoutingPoints,
13
+ type GModelElement,
14
+ type GRoutableElement,
15
+ GLSPHiddenBoundsUpdater,
16
+ type LocalRequestBoundsAction,
17
+ Point,
18
+ RequestBoundsAction,
19
+ ServerAction,
20
+ isRoutable
21
+ } from '@eclipse-glsp/client';
22
+ import { ChannelLogger } from '@hydranium/client-theia/lib/browser';
23
+ import { type Clock, SystemClock } from '@hydranium/protocol';
24
+ import { inject, injectable } from '@theia/core/shared/inversify';
25
+ import { type VNode } from 'snabbdom';
26
+
27
+ /** How many scoped element ids the log line names before summarising the rest.
28
+ * A scoped request names one element in practice; the cap only stops a
29
+ * pathological caller from writing a thousand ids into the channel. */
30
+ const SCOPE_ID_CAP = 3;
31
+
32
+ /**
33
+ * Instruments sprotty's hidden-bounds pass and repairs degenerate routes before
34
+ * they go on the wire.
35
+ *
36
+ * **Why the framework owns this.** Every GLSP head runs the hidden-bounds pass on
37
+ * every model update, and it is the one client-side phase that scales with the
38
+ * *rendered element count* rather than the model size — labels, compartments and
39
+ * edge decorations expand a modest node count into orders of magnitude more DOM
40
+ * elements. Without a timing line there is no way to tell a slow measurement pass
41
+ * apart from slow edge routing or a slow server round trip; with one,
42
+ * `bounds-request` either exonerates the client in a sentence or names the
43
+ * dominant cost.
44
+ *
45
+ * Emits exactly one `trace` line per bounds request — `trace` because the
46
+ * frequency matches the per-action traffic `HydraniumGlspActionDispatcher` logs
47
+ * there (a bounds request is driven by those actions), so it belongs with them
48
+ * rather than above them. The line carries:
49
+ *
50
+ * - `server` / `local` — whether the request came from the server or from a
51
+ * client-local {@link LocalRequestBoundsAction}
52
+ * - `scope=<ids>` — the elements a scoped request named; absent when the request
53
+ * covers the whole diagram
54
+ * - `measured=N` — DOM elements whose bounds were read. Exceeds the scoped id
55
+ * count because the base class expands each requested element to its
56
+ * descendants
57
+ * - `getBBox=Nms` — {@link getBoundsFromDOM} **only**: the layout pass and the
58
+ * `ComputedBoundsAction` assembly stay outside it, so the number isolates raw
59
+ * measurement cost
60
+ *
61
+ * Composed from {@link Clock.measure} rather than `Tracer.time`, matching
62
+ * `HydraniumDocumentBuilder`'s phase instrumentation. `Tracer.time` is for "tell
63
+ * me when this is slow" and is deliberately silent on a fast success, which would
64
+ * drop the element count on every normal pass — and forcing it to emit costs a
65
+ * paired `[#N start]` line per request. This is a per-occurrence record, so it
66
+ * measures and composes its own line.
67
+ *
68
+ * Bound by `createGlspClientTheiaModule` via `rebind(GLSPHiddenBoundsUpdater)`.
69
+ */
70
+ @injectable()
71
+ export class HydraniumHiddenBoundsUpdater extends GLSPHiddenBoundsUpdater {
72
+ @inject(ChannelLogger) protected readonly channel!: ChannelLogger;
73
+
74
+ /** Not injected: the browser container binds no {@link Clock}, and this only
75
+ * measures — no logic is gated on time, so there is nothing to fake in
76
+ * production. Overridden in tests for a deterministic elapsed value. */
77
+ protected readonly clock: Clock = new SystemClock();
78
+
79
+ /**
80
+ * Scope description for the in-flight request, read from the `cause` in
81
+ * {@link postUpdate} and consumed by {@link getBoundsFromDOM} — which the base
82
+ * class calls with no arguments, so the cause cannot be threaded through.
83
+ */
84
+ protected currentScope?: string;
85
+
86
+ override decorate(vnode: VNode, element: GModelElement): VNode {
87
+ super.decorate(vnode, element);
88
+ if (isRoutable(element)) {
89
+ this.repairLastRoute(element);
90
+ }
91
+ return vnode;
92
+ }
93
+
94
+ /**
95
+ * Replace the routing entry the base class just pushed when it carries fewer
96
+ * than two points, which is not a valid route for any edge.
97
+ *
98
+ * `calcElementAndRoute` already guards the case where the registered router
99
+ * returns *nothing* — it falls back to source + own routing points + target.
100
+ * The hole is one step further in: `calcRoute` filters duplicate points at
101
+ * `Number.EPSILON`, so while obstacles still carry sprotty's unmeasured-bounds
102
+ * sentinel `(100, 100, -1, -1)` the source and target coincide and a valid
103
+ * two-point route collapses to a single point. That result is defined, so the
104
+ * upstream truthiness guard never fires and the single point ships.
105
+ *
106
+ * The substitute is deliberately upstream's own fallback shape rather than a
107
+ * new invention, so a repaired route is indistinguishable from a route the
108
+ * base class would have produced had it noticed. Only already-invalid payloads
109
+ * are touched, so a head with a working router is unaffected.
110
+ */
111
+ protected repairLastRoute(element: GRoutableElement): void {
112
+ const addedRoute = this.element2route.pop();
113
+ if (addedRoute === undefined) {
114
+ return;
115
+ }
116
+ this.element2route.push(this.hasValidRoute(addedRoute) ? addedRoute : this.toSourceTargetRoute(element));
117
+ }
118
+
119
+ /** A route needs at least a start and an end point to describe an edge. */
120
+ protected hasValidRoute(route: ElementAndRoutingPoints): boolean {
121
+ return route.newRoutingPoints !== undefined && route.newRoutingPoints.length >= 2;
122
+ }
123
+
124
+ /** Upstream's `calcElementAndRoute` no-router fallback: the element's own
125
+ * bendpoints framed by its source and target anchor positions. */
126
+ protected toSourceTargetRoute(element: GRoutableElement): ElementAndRoutingPoints {
127
+ return {
128
+ elementId: element.id,
129
+ newRoutingPoints: [element.source?.position ?? Point.ORIGIN, ...element.routingPoints, element.target?.position ?? Point.ORIGIN]
130
+ };
131
+ }
132
+
133
+ protected override getBoundsFromDOM(): void {
134
+ if (this.currentScope === undefined) {
135
+ // Not inside an instrumented `postUpdate` (a subclass calling directly);
136
+ // with no cause there is nothing to attribute a measurement to.
137
+ super.getBoundsFromDOM();
138
+ return;
139
+ }
140
+ // Read before the pass: the base class clears the map on its way out.
141
+ const measured = this.getElement2BoundsData().size;
142
+ const { elapsedMs } = this.clock.measure(() => super.getBoundsFromDOM());
143
+ this.channel.trace(this.formatBoundsRequest(this.currentScope, measured, elapsedMs));
144
+ }
145
+
146
+ /** Format the per-request line. Override to change the wording. */
147
+ protected formatBoundsRequest(scope: string, measured: number, elapsedMs: number): string {
148
+ return `bounds-request[${scope}] measured=${measured} getBBox=${elapsedMs.toFixed(0)}ms`;
149
+ }
150
+
151
+ override postUpdate(cause?: Action): void {
152
+ if (cause?.kind !== RequestBoundsAction.KIND) {
153
+ super.postUpdate(cause);
154
+ return;
155
+ }
156
+ this.currentScope = this.formatCause(cause);
157
+ try {
158
+ super.postUpdate(cause);
159
+ } finally {
160
+ this.currentScope = undefined;
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Scope facts for the log line: where the request came from, and which
166
+ * elements it asked for.
167
+ *
168
+ * Origin is read from {@link ServerAction}, the `__receivedFromServer` marker
169
+ * `GLSPModelSource` stamps on inbound actions, rather than from
170
+ * `LocalRequestBoundsAction.is`. The latter would answer the same today, but
171
+ * only incidentally: its `elementIDs` check is declared optional, so it is
172
+ * really just "a bounds request that did not come from the server". Keying on
173
+ * the marker states that directly, and keeps an *unscoped* local request from
174
+ * being mislabelled `server` if GLSP ever tightens that guard.
175
+ */
176
+ protected formatCause(cause: Action): string {
177
+ const origin = ServerAction.is(cause) ? 'server' : 'local';
178
+ const scope = this.formatScope(cause as RequestBoundsAction);
179
+ return scope ? `${origin} scope=${scope}` : origin;
180
+ }
181
+
182
+ /**
183
+ * Name the elements a scoped request asked for, or `undefined` when it covers
184
+ * the whole diagram.
185
+ *
186
+ * Deliberately the ids and not their count. A scoped request in practice names
187
+ * a single element, so a count is always `1` and says nothing, while the id
188
+ * says *which* element was re-measured — and it is the only part of the line
189
+ * that distinguishes one such request from the next. It also stops the line
190
+ * reading as a contradiction: `scope=node0, measured=6` is coherent (the base
191
+ * class expands a requested element to its descendants, so one id measures six
192
+ * DOM elements), where `scoped=1, measured=6` invites the reader to wonder
193
+ * which number is wrong.
194
+ *
195
+ * A request naming only the root is treated as unscoped: it measures
196
+ * everything, so the id adds nothing over the absence of a `scope` tag.
197
+ */
198
+ protected formatScope(cause: RequestBoundsAction): string | undefined {
199
+ const scopedIds = (cause as LocalRequestBoundsAction).elementIDs;
200
+ if (!scopedIds || scopedIds.length === 0) {
201
+ return undefined;
202
+ }
203
+ const named = scopedIds.filter(id => id !== cause.newRoot?.id);
204
+ if (named.length === 0) {
205
+ return undefined;
206
+ }
207
+ if (named.length > SCOPE_ID_CAP) {
208
+ return `${named.slice(0, SCOPE_ID_CAP).join(',')},+${named.length - SCOPE_ID_CAP} more`;
209
+ }
210
+ return named.join(',');
211
+ }
212
+ }
@@ -0,0 +1,63 @@
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 ExternalMarkerManager } from '@eclipse-glsp/client';
11
+ import { GLSPDiagramConfiguration, connectTheiaContextMenuService, connectTheiaMarkerManager } from '@eclipse-glsp/theia-integration';
12
+ import { type Container, injectable } from '@theia/core/shared/inversify';
13
+ import { NoOpExternalMarkerManager } from './diagram-only-marker-manager.js';
14
+
15
+ /**
16
+ * `GLSPDiagramConfiguration` base for hydranium adopters. By default GLSP
17
+ * validation markers propagate into Theia's Problems view (the stock
18
+ * `@eclipse-glsp/theia-integration` behaviour); a head that already publishes
19
+ * the same diagnostics over LSP suppresses that propagation by setting
20
+ * {@link propagateMarkersToProblemsView} to `false`.
21
+ *
22
+ * **When to suppress.** A head with a co-resident LSP that publishes its
23
+ * diagnostics over standard LSP `publishDiagnostics` would otherwise list each
24
+ * error twice: GLSP's `TheiaMarkerManager` pushes the *same* errors again under
25
+ * a different marker owner, and Theia shows the union across owners. The GLSP
26
+ * path is also open-diagram-scoped (markers clear when the diagram closes), so
27
+ * for such a head it cannot reproduce the LSP head's persistent, cross-file
28
+ * Problems entries — it is only ever a flickering duplicate. A graphical-only
29
+ * head (no LSP diagnostics, or markers the LSP never produces) keeps the
30
+ * default so its markers reach the Problems view. The on-diagram decoration is
31
+ * a separate client feedback path (`ApplyMarkersCommand`), unaffected either
32
+ * way.
33
+ *
34
+ * **How.** {@link initializeContainer} hands {@link connectTheiaMarkerManager}
35
+ * the Theia marker-manager factory by default; when suppression is enabled it
36
+ * hands a no-op {@link ExternalMarkerManager} factory instead, so the
37
+ * per-diagram container binds a manager that renders-but-does-not-forward and
38
+ * the real `TheiaMarkerManager` (with its `@postConstruct`
39
+ * `ProblemManager`/shell subscriptions) is never constructed.
40
+ *
41
+ * NB: this overrides `GLSPDiagramConfiguration.initializeContainer` rather than
42
+ * calling `super`. It mirrors the base body (context-menu + marker-manager
43
+ * wiring); revisit if a future `@eclipse-glsp/theia-integration` adds further
44
+ * setup there.
45
+ */
46
+ @injectable()
47
+ export abstract class AbstractHydraniumGlspDiagramConfiguration extends GLSPDiagramConfiguration {
48
+ /**
49
+ * Whether GLSP markers propagate into Theia's Problems view. `true` (the
50
+ * framework default) keeps the stock Theia propagation; set `false` for a
51
+ * head with a co-resident LSP that already publishes the same diagnostics,
52
+ * to render markers on the diagram only and avoid double-listing.
53
+ */
54
+ protected propagateMarkersToProblemsView = true;
55
+
56
+ protected override initializeContainer(container: Container): void {
57
+ connectTheiaContextMenuService(container, this.contextMenuServiceFactory);
58
+ const markerManagerFactory = this.propagateMarkersToProblemsView
59
+ ? this.theiaMarkerManager
60
+ : (): ExternalMarkerManager => new NoOpExternalMarkerManager();
61
+ connectTheiaMarkerManager(container, markerManagerFactory, this.diagramType);
62
+ }
63
+ }
@@ -0,0 +1,22 @@
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
+ // Browser-side framework primitives for GLSP-on-Theia adopters.
11
+ // The cross-head Output-channel logger lives in `@hydranium/client-theia/browser`.
12
+ export * from './action-dispatcher';
13
+ export * from './client-contribution';
14
+ export * from './diagram-loader';
15
+ export * from './diagram-only-marker-manager';
16
+ export * from './diagram-widget';
17
+ export * from './glsp-client-theia-module';
18
+ export * from './glsp-diagram-manager';
19
+ export * from './glsp-message-service';
20
+ export * from './glsp-theia-frontend-module';
21
+ export * from './hidden-bounds-updater';
22
+ export * from './hydranium-glsp-diagram-configuration';
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
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
+ // Root barrel re-exports the browser-side surface, mirroring the root entry of
11
+ // the package this one integrates: `@eclipse-glsp/theia-integration`'s own
12
+ // `main` is its browser tier, so an adopter importing the bare specifier
13
+ // expects the same surface here.
14
+ //
15
+ // That makes this root BROWSER-BOUND, not neutral, and not merely
16
+ // DOM-avoiding: the re-exported tier value-imports `@theia/workspace`'s and
17
+ // `@theia/output`'s browser modules, which touch `document` at module load, and
18
+ // its dependency graph imports stylesheets, so loading it needs a bundler with a
19
+ // CSS loader. It is a frontend entry and nothing else. Node-side primitives
20
+ // (connection-handler wiring) stay behind
21
+ // `@hydranium/glsp-client-theia/node` so the `@theia/core/lib/node/...`
22
+ // imports don't leak into a browser bundle.
23
+ export * from './browser/index';
@@ -0,0 +1,34 @@
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 { ConnectionHandler } from '@theia/core';
11
+ import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module';
12
+ import { ContainerModule, type interfaces } from '@theia/core/shared/inversify';
13
+
14
+ /**
15
+ * Creates the frontend-scoped Theia backend module that registers a GLSP
16
+ * `ConnectionHandler` per browser-frontend connection. Returns a ready-to-
17
+ * export `ContainerModule` so adopters can `export default createGlspConnectionContainerModule(MyHandler)`
18
+ * from their backend-module entry point.
19
+ *
20
+ * The wrapping `ContainerModule + ConnectionContainerModule.create` pattern
21
+ * mirrors the boilerplate every Theia GLSP adopter writes by hand; lifted so
22
+ * adopters declare only the handler class.
23
+ */
24
+ export function createGlspConnectionContainerModule(handlerClass: interfaces.Newable<ConnectionHandler>): ContainerModule {
25
+ const frontendScopedConnectionModule = ConnectionContainerModule.create(({ bind }) => {
26
+ bind(handlerClass).toSelf().inSingletonScope();
27
+ bind(ConnectionHandler)
28
+ .toDynamicValue(context => context.container.get(handlerClass))
29
+ .inSingletonScope();
30
+ });
31
+ return new ContainerModule(bind => {
32
+ bind(ConnectionContainerModule).toConstantValue(frontendScopedConnectionModule);
33
+ });
34
+ }
@@ -0,0 +1,69 @@
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 { GLSPContribution } from '@eclipse-glsp/theia-integration/lib/common';
11
+ import { SocketConnectionForwarder } from '@eclipse-glsp/theia-integration/lib/node';
12
+ import { AbstractSocketForwardingConnectionHandler } from '@hydranium/client-theia/lib/node';
13
+ import { type Channel, type Disposable } from '@theia/core';
14
+ import { injectable, unmanaged } from '@theia/core/shared/inversify';
15
+ import type * as net from 'net';
16
+
17
+ /** Options for `GlspServerConnectionHandler`. The language contribution
18
+ * id determines the per-language servicePath that Theia routes browser-frontend
19
+ * connections to; the port command is the Theia/VS Code command id whose return
20
+ * value is the GLSP server's listening port. */
21
+ export interface GlspServerConnectionHandlerOptions {
22
+ readonly languageContributionId: string;
23
+ readonly portCommand: string;
24
+ readonly findPortTimeout?: number;
25
+ readonly findPortAttempts?: number;
26
+ readonly connectTimeoutMs?: number;
27
+ /**
28
+ * Optional diagnostic hook called immediately after the outbound
29
+ * `net.Socket` to the GLSP server is created, BEFORE `socket.connect()`
30
+ * is invoked. Adopters use this to attach `'data'` / `'close'` listeners
31
+ * for byte-level observability when debugging wire-level handshake
32
+ * issues; attaching listeners before `connect()` is what guarantees the
33
+ * very first bytes are observed.
34
+ */
35
+ readonly onSocketCreated?: (socket: net.Socket) => void;
36
+ }
37
+
38
+ /**
39
+ * Bridges a Theia browser-frontend channel to a GLSP server's TCP socket.
40
+ *
41
+ * The port-discovery + buffer-and-replay race fix + connect orchestration live
42
+ * on the cross-head {@link AbstractSocketForwardingConnectionHandler} base; this
43
+ * subclass supplies only the GLSP specifics — the per-language servicePath
44
+ * (`GLSPContribution.servicePath + '/' + languageContributionId`), the port
45
+ * command, the log labels — and plugs in `@eclipse-glsp/theia-integration`'s
46
+ * `SocketConnectionForwarder` as the byte relay. Adopters with different
47
+ * language ids or command names subclass with a one-line `super({...})` call.
48
+ * Sibling of `@hydranium/data-client-theia`'s `DataServerConnectionHandler`,
49
+ * which subclasses the same base with its own GLSP-free forwarder.
50
+ */
51
+ @injectable()
52
+ export class GlspServerConnectionHandler extends AbstractSocketForwardingConnectionHandler {
53
+ constructor(@unmanaged() options: GlspServerConnectionHandlerOptions) {
54
+ super({
55
+ path: GLSPContribution.servicePath + '/' + options.languageContributionId,
56
+ portCommand: options.portCommand,
57
+ logComponent: 'GLSP',
58
+ serverName: 'Graphical Server',
59
+ findPortTimeout: options.findPortTimeout,
60
+ findPortAttempts: options.findPortAttempts,
61
+ connectTimeoutMs: options.connectTimeoutMs,
62
+ onSocketCreated: options.onSocketCreated
63
+ });
64
+ }
65
+
66
+ protected forwardToSocketConnection(clientChannel: Channel, socket: net.Socket): Disposable {
67
+ return new SocketConnectionForwarder(clientChannel, socket);
68
+ }
69
+ }
@@ -0,0 +1,12 @@
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
+ // Node-side framework primitives for GLSP-on-Theia adopters.
11
+ export * from './connection-container-module';
12
+ export * from './glsp-server-connection-handler';