@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,173 @@
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 { DiagramLoader, type DiagramLoadingOptions, StatusAction } from '@eclipse-glsp/client';
11
+ import { ChannelLogger } from '@hydranium/client-theia/lib/browser';
12
+ import { Deferred } from '@hydranium/protocol';
13
+ import { inject, injectable } from '@theia/core/shared/inversify';
14
+
15
+ /**
16
+ * How a diagram load ended. A discriminated union rather than a boolean or a
17
+ * string: a consumer showing a pending indicator has to distinguish "not settled
18
+ * yet" (`undefined`) from either terminal state, and the failed arm has to carry
19
+ * enough for that consumer to decide whether *it* is now the only thing that can
20
+ * report the failure — see {@link DiagramLoadFailure.surfaced}.
21
+ */
22
+ export type DiagramLoadOutcome = DiagramLoadSuccess | DiagramLoadFailure;
23
+
24
+ export interface DiagramLoadSuccess {
25
+ readonly status: 'loaded';
26
+ }
27
+
28
+ export interface DiagramLoadFailure {
29
+ readonly status: 'failed';
30
+ /** The thrown value, verbatim. `unknown` because a rejection can be anything. */
31
+ readonly error: unknown;
32
+ /**
33
+ * Whether the `severity: 'ERROR'` {@link StatusAction} reached the action
34
+ * dispatcher — i.e. whether GLSP's `StatusOverlay` is now displaying this
35
+ * failure.
36
+ *
37
+ * `false` means the report itself failed, which happens when the action
38
+ * dispatcher is the thing that could not initialize. A consumer that covers
39
+ * the canvas must then render the failure itself: it is the only surface
40
+ * left, and removing its overlay would leave a blank diagram whose only
41
+ * explanation is a line in the Output channel — the exact failure mode this
42
+ * loader exists to prevent.
43
+ */
44
+ readonly surfaced: boolean;
45
+ }
46
+
47
+ /**
48
+ * Diagram loader that never lets a load failure vanish.
49
+ *
50
+ * `DiagramLoader.load()` orchestrates the full diagram-init path — action
51
+ * dispatcher init, GLSP-client connect + server `initialize`, the first
52
+ * `RequestModelAction`, and the model-initialization constraint. Of these only
53
+ * the `IDiagramStartup` hooks are wrapped by the base loader (it logs them to
54
+ * `console.error`); the dispatcher-init / connection / request-model steps are
55
+ * not. The widget that drives loading
56
+ * (`@eclipse-glsp/theia-integration`'s `GLSPDiagramWidget.onAfterAttach`)
57
+ * calls `load()` *without awaiting or catching it*, so any rejection becomes an
58
+ * uncaught promise rejection visible only in the browser devtools console —
59
+ * nothing reaches the e2e output, the server log, or a Theia log sink, and the
60
+ * diagram just stays blank.
61
+ *
62
+ * This subclass closes that gap: it catches every load failure, routes it to
63
+ * the framework Output channel (the same {@link ChannelLogger} sink the server
64
+ * log and action traffic share, so it is discoverable where a developer already
65
+ * looks), and surfaces a persistent error to the user via a `severity: 'ERROR'`
66
+ * {@link StatusAction} on the diagram's status overlay. It deliberately does
67
+ * **not** rethrow — the caller drops the promise, so rethrowing would only
68
+ * reproduce the uncaught rejection this exists to prevent.
69
+ *
70
+ * It also publishes the load's terminal state ({@link loadOutcome} /
71
+ * {@link onceLoadSettled}) so a pending indicator has one authority to key on.
72
+ * `HydraniumGlspDiagramWidget` uses it to cover the canvas while loading; see
73
+ * {@link settle} for why the signal cannot be derived from the error
74
+ * `StatusAction` instead.
75
+ *
76
+ * Bound by `createGlspClientTheiaModule` via `rebind(DiagramLoader)`.
77
+ */
78
+ @injectable()
79
+ export class HydraniumDiagramLoader extends DiagramLoader {
80
+ @inject(ChannelLogger) protected readonly channel!: ChannelLogger;
81
+
82
+ protected readonly settled = new Deferred<DiagramLoadOutcome>();
83
+ protected outcome?: DiagramLoadOutcome;
84
+
85
+ /** Terminal state of the load, or `undefined` while it is still in flight.
86
+ * The synchronous companion of {@link onceLoadSettled}, for a consumer that
87
+ * must decide something in the same tick (e.g. whether to show an indicator
88
+ * at all when re-attaching an already-loaded diagram). */
89
+ get loadOutcome(): DiagramLoadOutcome | undefined {
90
+ return this.outcome;
91
+ }
92
+
93
+ /** Resolves once the load reaches a terminal state — on success **and** on
94
+ * failure, so a caller awaiting it cannot hang. Never rejects. */
95
+ onceLoadSettled(): Promise<DiagramLoadOutcome> {
96
+ return this.settled.promise;
97
+ }
98
+
99
+ override async load(options?: DiagramLoadingOptions): Promise<void> {
100
+ try {
101
+ await super.load(options);
102
+ this.settle({ status: 'loaded' });
103
+ } catch (err) {
104
+ // Report first, settle second, and carry whether the report landed: a
105
+ // consumer that uncovers the canvas on settle then finds the error
106
+ // already on the status overlay — or learns that it has to render the
107
+ // failure itself because nothing else can.
108
+ const surfaced = await this.reportLoadFailure(err);
109
+ this.settle({ status: 'failed', error: err, surfaced });
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Record the terminal state and release {@link onceLoadSettled}. Idempotent —
115
+ * the first outcome wins, so a reload cannot flip a settled indicator.
116
+ *
117
+ * This runs unconditionally after {@link reportLoadFailure}, *including* when
118
+ * the status dispatch inside it threw. That is the load-bearing property: when
119
+ * the action dispatcher is itself the thing that failed to initialize — the
120
+ * exact failure this class exists for — no `StatusAction` ever lands, so a
121
+ * consumer observing actions would wait forever. Settling here means a pending
122
+ * indicator always comes down, and `surfaced: false` tells that consumer it is
123
+ * now the only thing that can report the failure.
124
+ */
125
+ protected settle(outcome: DiagramLoadOutcome): void {
126
+ if (this.outcome !== undefined) {
127
+ return;
128
+ }
129
+ this.outcome = outcome;
130
+ this.settled.resolve(outcome);
131
+ }
132
+
133
+ /**
134
+ * Route a diagram-load failure to the Output channel and the status overlay.
135
+ * The status dispatch is itself guarded: when the action dispatcher is the
136
+ * thing that failed to initialize, dispatching may throw too, and a failure
137
+ * to *report* the failure must not re-escape as an uncaught rejection.
138
+ *
139
+ * @returns whether the `StatusAction` reached the dispatcher — i.e. whether the
140
+ * user can see this failure on the diagram. The channel line always happens, so
141
+ * the return value is specifically about the *user-visible* report; a `false`
142
+ * makes {@link DiagramLoadFailure.surfaced} false and hands the job to whoever
143
+ * is covering the canvas.
144
+ */
145
+ protected async reportLoadFailure(err: unknown): Promise<boolean> {
146
+ const label = this.loadFailureLabel(err);
147
+ this.channel.error(label, err);
148
+ try {
149
+ await this.actionDispatcher.dispatch(StatusAction.create(label, { severity: 'ERROR' }));
150
+ return true;
151
+ } catch (statusErr) {
152
+ this.channel.error('Failed to surface the diagram-load failure on the status overlay', statusErr);
153
+ return false;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * The sentence a failed load is reported with, on every surface that reports
159
+ * it, and the only seam an adopter rewords or localizes it at. Public
160
+ * because the canvas overlay is one of those surfaces and lives in another
161
+ * class: it renders this failure exactly when the `StatusAction` did not
162
+ * land, so a second copy of the sentence there could only ever drift unseen.
163
+ */
164
+ loadFailureLabel(err: unknown): string {
165
+ return `Diagram failed to load: ${this.formatError(err)}`;
166
+ }
167
+
168
+ /** Render a thrown value as a message. Shared so the channel line, the status
169
+ * overlay and any consumer rendering {@link DiagramLoadFailure.error} agree. */
170
+ protected formatError(err: unknown): string {
171
+ return err instanceof Error ? err.message : String(err);
172
+ }
173
+ }
@@ -0,0 +1,33 @@
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 { ExternalMarkerManager, type Marker } from '@eclipse-glsp/client';
11
+ import { injectable } from '@theia/core/shared/inversify';
12
+
13
+ /**
14
+ * {@link ExternalMarkerManager} that drops all marker propagation into Theia's
15
+ * Problems view. GLSP's `SetMarkersActionHandler` still applies the in-diagram
16
+ * decoration through the separate `ValidationFeedbackEmitter` path; only the
17
+ * forward into the `ProblemManager` is suppressed, so the LSP head stays the
18
+ * single source of Problems-view diagnostics.
19
+ *
20
+ * Bound per diagram by `AbstractHydraniumGlspDiagramConfiguration` when its
21
+ * `propagateMarkersToProblemsView` flag is `false`; see that flag for when a
22
+ * head should choose it.
23
+ *
24
+ * Kept in its own module — importing only `@eclipse-glsp/client`, not
25
+ * `@eclipse-glsp/theia-integration` — so it stays unit-testable without pulling
26
+ * the Theia integration into the test graph.
27
+ */
28
+ @injectable()
29
+ export class NoOpExternalMarkerManager extends ExternalMarkerManager {
30
+ setMarkers(_markers: Marker[], _reason?: string, _sourceUri?: string): void {
31
+ // Intentionally empty — the LSP head owns the Problems view.
32
+ }
33
+ }
@@ -0,0 +1,178 @@
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 { DiagramLoader } from '@eclipse-glsp/client';
11
+ import { GLSPDiagramWidget } from '@eclipse-glsp/theia-integration';
12
+ // Type-only: the `@theia/core/lib/browser` barrel touches DOM globals at module
13
+ // load, which the node-environment unit tests cannot provide.
14
+ import { type Message } from '@theia/core/lib/browser';
15
+ import { injectable } from '@theia/core/shared/inversify';
16
+ import { type DiagramLoadOutcome, HydraniumDiagramLoader } from './diagram-loader';
17
+ // Shipped by this package rather than left to adopters: an overlay whose
18
+ // stylesheet was forgotten is an unstyled div in normal flow, which is a silent
19
+ // failure. Adopters override individual rules from their own stylesheet.
20
+ import '../../style/diagram-loading.css';
21
+
22
+ /** Class on the overlay root. Adopters style / override via this contract. */
23
+ export const DIAGRAM_LOADING_CLASS = 'hydranium-diagram-loading';
24
+
25
+ /** Added to the overlay root when it switches from pending to reporting a
26
+ * failure, so the spinner can be hidden and the text restyled from CSS alone. */
27
+ export const DIAGRAM_LOADING_FAILED_CLASS = `${DIAGRAM_LOADING_CLASS}-failed`;
28
+
29
+ /**
30
+ * `GLSPDiagramWidget` that covers the canvas with a spinner until the diagram
31
+ * load reaches a terminal state.
32
+ *
33
+ * **Why.** GLSP renders nothing until the first model arrives, so opening a
34
+ * diagram shows a blank tab for the whole round trip — long enough on a large
35
+ * model to read as a broken editor rather than a slow one.
36
+ *
37
+ * **How the pending state is sourced.** From {@link HydraniumDiagramLoader}, not
38
+ * from `actionDispatcher.onceModelInitialized()`. The loader settles on failure
39
+ * as well as on success, and it does so even when its own error-reporting
40
+ * dispatch throws; `onceModelInitialized()` simply never settles on a failed
41
+ * load. That distinction is not academic here: this overlay is opaque and covers
42
+ * the widget node, while GLSP's `StatusOverlay` — where the loader reports the
43
+ * failure — mounts *inside* the diagram's base div. Keyed on model
44
+ * initialization, a failed load would leave a spinner turning on top of the
45
+ * error message explaining it. Keyed on the loader, exactly one component decides
46
+ * whether the canvas is pending and the two mechanisms compose instead of
47
+ * competing.
48
+ *
49
+ * **The one case where this overlay reports the failure itself.** When
50
+ * `DiagramLoadFailure.surfaced` is `false` the loader could not dispatch its
51
+ * `StatusAction` — the action dispatcher was what failed — so the status overlay
52
+ * shows nothing. Uncovering the canvas would then leave a blank diagram whose only
53
+ * explanation is a line in the Output channel. In that case, and only that case,
54
+ * the overlay stays up and swaps the spinner for the error text.
55
+ *
56
+ * Bound unconditionally by `AbstractHydraniumGlspTheiaFrontendModule`. To opt out,
57
+ * override {@link showLoadingOverlay} to a no-op; to change what is rendered,
58
+ * override {@link createLoadingOverlay} or {@link loadingLabel}.
59
+ */
60
+ @injectable()
61
+ export class HydraniumGlspDiagramWidget extends GLSPDiagramWidget {
62
+ protected loadingOverlay?: HTMLElement;
63
+
64
+ protected override onAfterAttach(msg: Message): void {
65
+ // Before `super`, which is what starts the load on first attach — so the
66
+ // overlay is already up when the round trip begins.
67
+ this.showLoadingOverlay();
68
+ super.onAfterAttach(msg);
69
+ }
70
+
71
+ override dispose(): void {
72
+ this.hideLoadingOverlay();
73
+ super.dispose();
74
+ }
75
+
76
+ /**
77
+ * Cover the canvas until the load settles.
78
+ *
79
+ * Nothing is created unless there is a signal to take it down again: no
80
+ * overlay without a framework loader, and none when the load has already
81
+ * settled — re-attaching a loaded diagram (a tab switch) skips it entirely
82
+ * rather than creating and immediately removing one, so there is no flash.
83
+ */
84
+ protected showLoadingOverlay(): void {
85
+ if (this.loadingOverlay) {
86
+ return;
87
+ }
88
+ const loader = this.hydraniumDiagramLoader;
89
+ if (!loader || loader.loadOutcome !== undefined) {
90
+ return;
91
+ }
92
+ this.loadingOverlay = this.createLoadingOverlay();
93
+ this.node.appendChild(this.loadingOverlay);
94
+ // Both arms: `onceLoadSettled` never rejects, but a widget must not be able
95
+ // to strand an overlay if that ever changes — a rejection is treated as an
96
+ // outcome it cannot report, which is the safe reading.
97
+ loader.onceLoadSettled().then(
98
+ outcome => this.onLoadSettled(outcome),
99
+ () => this.hideLoadingOverlay()
100
+ );
101
+ }
102
+
103
+ /**
104
+ * Take the overlay down, or keep it as the failure's only reporter.
105
+ *
106
+ * The overlay is retained ONLY for a failure the loader could not surface;
107
+ * anything else uncovers the canvas, so a load that failed *and was reported*
108
+ * reveals GLSP's status overlay rather than double-reporting on top of it.
109
+ */
110
+ protected onLoadSettled(outcome: DiagramLoadOutcome): void {
111
+ if (outcome.status === 'failed' && !outcome.surfaced) {
112
+ this.showLoadFailure(outcome.error);
113
+ return;
114
+ }
115
+ this.hideLoadingOverlay();
116
+ }
117
+
118
+ /**
119
+ * Repurpose the pending overlay in place as a failure report: same node, so
120
+ * there is no removal-then-insertion flicker, plus a marker class so the
121
+ * spinner can be hidden from CSS.
122
+ *
123
+ * The wording is {@link HydraniumDiagramLoader.loadFailureLabel}'s, so an
124
+ * adopter rewording a load failure reaches every surface that reports it.
125
+ * Without the framework loader there is no failure to render, so its absence
126
+ * is a no-op rather than a fallback wording.
127
+ */
128
+ protected showLoadFailure(error: unknown): void {
129
+ const overlay = this.loadingOverlay;
130
+ const loader = this.hydraniumDiagramLoader;
131
+ if (!overlay || !loader) {
132
+ return;
133
+ }
134
+ overlay.classList.add(DIAGRAM_LOADING_FAILED_CLASS);
135
+ const label = overlay.querySelector(`.${DIAGRAM_LOADING_CLASS}-label`);
136
+ if (label) {
137
+ label.textContent = loader.loadFailureLabel(error);
138
+ }
139
+ }
140
+
141
+ protected hideLoadingOverlay(): void {
142
+ this.loadingOverlay?.remove();
143
+ this.loadingOverlay = undefined;
144
+ }
145
+
146
+ /** Build the overlay DOM. Override to change the wording or add content; the
147
+ * root must carry {@link DIAGRAM_LOADING_CLASS} for the shipped styling, and
148
+ * the label element its `-label` class so {@link showLoadFailure} finds it. */
149
+ protected createLoadingOverlay(): HTMLElement {
150
+ const overlay = document.createElement('div');
151
+ overlay.className = DIAGRAM_LOADING_CLASS;
152
+ const spinner = document.createElement('div');
153
+ spinner.className = `${DIAGRAM_LOADING_CLASS}-spinner`;
154
+ const label = document.createElement('div');
155
+ label.className = `${DIAGRAM_LOADING_CLASS}-label`;
156
+ label.textContent = this.loadingLabel;
157
+ overlay.appendChild(spinner);
158
+ overlay.appendChild(label);
159
+ return overlay;
160
+ }
161
+
162
+ /** Text shown under the spinner. Override for a domain-specific wording; the
163
+ * failure text that replaces it is {@link showLoadFailure}'s. */
164
+ protected get loadingLabel(): string {
165
+ return 'Loading diagram...';
166
+ }
167
+
168
+ /**
169
+ * The framework loader for this diagram, or `undefined` when the container
170
+ * binds a plain `DiagramLoader`. Absence degrades to "no overlay" rather than
171
+ * an error: a head that opted out of the framework loader has no settle signal
172
+ * to key on, and an overlay that never comes down is worse than none.
173
+ */
174
+ protected get hydraniumDiagramLoader(): HydraniumDiagramLoader | undefined {
175
+ const loader = this.diContainer.get<DiagramLoader>(DiagramLoader);
176
+ return loader instanceof HydraniumDiagramLoader ? loader : undefined;
177
+ }
178
+ }
@@ -0,0 +1,54 @@
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 { bindChannelLogger, type ChannelLoggerOptions } from '@hydranium/client-theia/lib/browser';
11
+ import { type BindingContext, DiagramLoader, GLSPActionDispatcher, GLSPHiddenBoundsUpdater } from '@eclipse-glsp/client';
12
+ import { HydraniumGlspActionDispatcher } from './action-dispatcher';
13
+ import { HydraniumDiagramLoader } from './diagram-loader';
14
+ import { bindHydraniumGlspMessageService } from './glsp-message-service';
15
+ import { HydraniumHiddenBoundsUpdater } from './hidden-bounds-updater';
16
+
17
+ /** Options for `createGlspClientTheiaModule`. The adopter supplies the channel
18
+ * logger configuration ({@link ChannelLoggerOptions} — a `channelName`, and
19
+ * optionally a `component` label); the framework owns the dispatcher type and
20
+ * the `ChannelLogger` binding. The log threshold is not configured here — it
21
+ * is process-global, driven by `bindLogLevelPreference` or by
22
+ * `AbstractHydraniumGlspTheiaFrontendModule.logLevelPreference`. */
23
+ export interface GlspClientTheiaModuleOptions {
24
+ readonly channelLogger: ChannelLoggerOptions;
25
+ }
26
+
27
+ /**
28
+ * Composes the standard framework client bindings: the cross-head Output-channel
29
+ * logger, the instrumented action dispatcher, the failure-reporting diagram
30
+ * loader, the instrumented hidden-bounds updater, and the message service that
31
+ * drops the duplicate model-loading notification.
32
+ *
33
+ * Takes the whole {@link BindingContext} rather than `(bind, rebind)`: the
34
+ * message-service rebind has to know whether GLSP's `theiaNotificationModule`
35
+ * already bound its token, and a container-module body has the full context to
36
+ * hand anyway.
37
+ *
38
+ * Every binding is unconditional, with no per-feature flags. Each replaces a
39
+ * GLSP default with a strict superset of its behaviour, so a head that wants
40
+ * the original rebinds that one token back — the same one-line move this
41
+ * function makes.
42
+ *
43
+ * Adopter composition roots call this first; adopter-specific bindings
44
+ * (diagram-module rebinds, custom tools, etc.) layer on top.
45
+ */
46
+ export function createGlspClientTheiaModule(context: BindingContext, options: GlspClientTheiaModuleOptions): void {
47
+ const { bind, isBound, rebind } = context;
48
+ bindChannelLogger(bind, options.channelLogger);
49
+ bind(HydraniumGlspActionDispatcher).toSelf().inSingletonScope();
50
+ rebind(GLSPActionDispatcher).toService(HydraniumGlspActionDispatcher);
51
+ rebind(DiagramLoader).to(HydraniumDiagramLoader).inSingletonScope();
52
+ rebind(GLSPHiddenBoundsUpdater).to(HydraniumHiddenBoundsUpdater).inSingletonScope();
53
+ bindHydraniumGlspMessageService(bind, isBound, rebind);
54
+ }
@@ -0,0 +1,67 @@
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 { codiconCSSString } from '@eclipse-glsp/client';
11
+ import { GLSPDiagramManager } from '@eclipse-glsp/theia-integration';
12
+ import { type GLSPDiagramLanguage } from '@eclipse-glsp/theia-integration/lib/common';
13
+ import { injectable } from '@theia/core/shared/inversify';
14
+
15
+ /**
16
+ * `GLSPDiagramManager` subclass that derives the language-correlated getters
17
+ * (`fileExtensions`, `diagramType`, `contributionId`, `iconClass`) from a
18
+ * {@link GLSPDiagramLanguage} descriptor and the human-readable `label` from a
19
+ * separate adopter field — so adopters override two abstract members per
20
+ * manager rather than a getter apiece.
21
+ *
22
+ * Adopters subclass and provide:
23
+ * - {@link diagramLanguage} — wire-format identifiers + file routing
24
+ * - {@link managerLabel} — human-readable label shown in the open-with menu
25
+ *
26
+ * Optional override:
27
+ * - {@link customIconClass} — overrides the language's `iconClass`
28
+ * - `get id()` — manager id; defaults to nothing because manager id is
29
+ * typically `static readonly ID = '…'` referenced by external callers
30
+ * (Theia `WidgetManager` lookups). Adopter subclasses set `static ID`
31
+ * and override `get id()` to return it.
32
+ *
33
+ * Abstract base class with hook fields, adopter subclasses with concrete
34
+ * values, following the GLSP module convention. The container always
35
+ * constructs the adopter subclass, never the abstract base directly.
36
+ */
37
+ @injectable()
38
+ export abstract class AbstractHydraniumGlspDiagramManager extends GLSPDiagramManager {
39
+ /** Wire-format identifiers + file routing for this manager's diagram type. */
40
+ protected abstract readonly diagramLanguage: GLSPDiagramLanguage;
41
+
42
+ /** Human-readable label shown in the open-with menu and editor tabs. */
43
+ protected abstract readonly managerLabel: string;
44
+
45
+ /** Optional icon-class override; takes precedence over the language's `iconClass`. */
46
+ protected readonly customIconClass?: string;
47
+
48
+ override get fileExtensions(): string[] {
49
+ return [...this.diagramLanguage.fileExtensions];
50
+ }
51
+
52
+ override get diagramType(): string {
53
+ return this.diagramLanguage.diagramType;
54
+ }
55
+
56
+ override get contributionId(): string {
57
+ return this.diagramLanguage.contributionId;
58
+ }
59
+
60
+ override get iconClass(): string {
61
+ return this.customIconClass ?? this.diagramLanguage.iconClass ?? codiconCSSString('type-hierarchy-sub');
62
+ }
63
+
64
+ get label(): string {
65
+ return this.managerLabel;
66
+ }
67
+ }
@@ -0,0 +1,90 @@
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 EndProgressAction, type StartProgressAction, type UpdateProgressAction } from '@eclipse-glsp/client';
11
+ import { TheiaGLSPMessageService } from '@eclipse-glsp/theia-integration';
12
+ import { type interfaces, injectable } from '@theia/core/shared/inversify';
13
+
14
+ /**
15
+ * Title of the model-loading progress report. Emitted by
16
+ * `@eclipse-glsp/server`'s `RequestModelActionHandler`, so it is a GLSP-protocol
17
+ * constant every server shares, not an adopter string — which is what makes
18
+ * matching on it a framework-level concern.
19
+ *
20
+ * Matched by title because `StartProgressAction` carries no kind or category to
21
+ * key on; the `progressId` is generated per report, so it can only be correlated
22
+ * after the fact (see {@link HydraniumGlspMessageService.suppressedProgressIds}).
23
+ *
24
+ * Matching an English literal fails silently — no error, no log line, just the
25
+ * duplicate progress popup back — so this needs keeping in step with
26
+ * `RequestModelActionHandler` on every GLSP bump.
27
+ */
28
+ export const MODEL_LOADING_PROGRESS_TITLE = 'Model loading in progress';
29
+
30
+ /**
31
+ * Drops the Theia progress notification for diagram model loading, and only that
32
+ * one.
33
+ *
34
+ * `HydraniumGlspDiagramWidget`'s loading overlay already reports model loading,
35
+ * on the canvas the user is looking at; the notification says the same thing in a
36
+ * corner popup. Every other progress report is forwarded untouched, so a
37
+ * long-running server operation still surfaces normally.
38
+ *
39
+ * Bound unconditionally by `createGlspClientTheiaModule`, because the overlay it
40
+ * defers to is likewise unconditional. A head that no-ops the overlay and wants
41
+ * the notification back rebinds `TheiaGLSPMessageService` to GLSP's own.
42
+ */
43
+ @injectable()
44
+ export class HydraniumGlspMessageService extends TheiaGLSPMessageService {
45
+ /** Ids of reports that were swallowed, so their updates and completion are
46
+ * dropped as well — otherwise Theia sees an update for a progress it never
47
+ * started. */
48
+ protected suppressedProgressIds = new Set<string>();
49
+
50
+ protected override startProgress(action: StartProgressAction): void {
51
+ if (action.title === MODEL_LOADING_PROGRESS_TITLE) {
52
+ this.suppressedProgressIds.add(action.progressId);
53
+ return;
54
+ }
55
+ super.startProgress(action);
56
+ }
57
+
58
+ protected override updateProgress(action: UpdateProgressAction): void {
59
+ if (this.suppressedProgressIds.has(action.progressId)) {
60
+ return;
61
+ }
62
+ super.updateProgress(action);
63
+ }
64
+
65
+ protected override endProgress(action: EndProgressAction): void {
66
+ // `delete` reports whether the id was suppressed and cleans the entry up in
67
+ // one step, so the set cannot grow across reloads.
68
+ if (this.suppressedProgressIds.delete(action.progressId)) {
69
+ return;
70
+ }
71
+ super.endProgress(action);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Replace the message service bound by GLSP's `theiaNotificationModule`.
77
+ *
78
+ * Called by `createGlspClientTheiaModule`, whose module the adopter registers
79
+ * after the Theia default modules — so the token is normally already bound and
80
+ * this rebinds. The `isBound` branch keeps it correct regardless of module order
81
+ * and if GLSP's default composition changes: it degrades to "we bind it
82
+ * ourselves" instead of an inversify error at diagram-open time.
83
+ */
84
+ export function bindHydraniumGlspMessageService(bind: interfaces.Bind, isBound: interfaces.IsBound, rebind: interfaces.Rebind): void {
85
+ if (isBound(TheiaGLSPMessageService)) {
86
+ rebind(TheiaGLSPMessageService).to(HydraniumGlspMessageService).inSingletonScope();
87
+ } else {
88
+ bind(TheiaGLSPMessageService).to(HydraniumGlspMessageService).inSingletonScope();
89
+ }
90
+ }