@hydranium/glsp-client-theia 1.0.0-next.32 → 1.0.0-next.321
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -98
- package/lib/browser/client-contribution.d.ts +87 -25
- package/lib/browser/client-contribution.d.ts.map +1 -1
- package/lib/browser/client-contribution.js +233 -53
- package/lib/browser/client-contribution.js.map +1 -1
- package/lib/browser/diagram-only-marker-manager.d.ts.map +1 -1
- package/lib/browser/diagram-only-marker-manager.js.map +1 -1
- package/lib/browser/diagram-widget.d.ts +43 -25
- package/lib/browser/diagram-widget.d.ts.map +1 -1
- package/lib/browser/diagram-widget.js +76 -26
- package/lib/browser/diagram-widget.js.map +1 -1
- package/lib/browser/glsp-client-theia-module.d.ts +12 -5
- package/lib/browser/glsp-client-theia-module.d.ts.map +1 -1
- package/lib/browser/glsp-client-theia-module.js +27 -12
- package/lib/browser/glsp-client-theia-module.js.map +1 -1
- package/lib/browser/glsp-diagram-manager.d.ts +40 -1
- package/lib/browser/glsp-diagram-manager.d.ts.map +1 -1
- package/lib/browser/glsp-diagram-manager.js +100 -0
- package/lib/browser/glsp-diagram-manager.js.map +1 -1
- package/lib/browser/glsp-message-service.d.ts.map +1 -1
- package/lib/browser/glsp-message-service.js.map +1 -1
- package/lib/browser/glsp-saveable.d.ts +67 -0
- package/lib/browser/glsp-saveable.d.ts.map +1 -0
- package/lib/browser/glsp-saveable.js +110 -0
- package/lib/browser/glsp-saveable.js.map +1 -0
- package/lib/browser/glsp-theia-frontend-module.d.ts.map +1 -1
- package/lib/browser/glsp-theia-frontend-module.js +4 -1
- package/lib/browser/glsp-theia-frontend-module.js.map +1 -1
- package/lib/browser/hydranium-glsp-diagram-configuration.d.ts +3 -1
- package/lib/browser/hydranium-glsp-diagram-configuration.d.ts.map +1 -1
- package/lib/browser/hydranium-glsp-diagram-configuration.js +3 -1
- package/lib/browser/hydranium-glsp-diagram-configuration.js.map +1 -1
- package/lib/browser/index.d.ts +4 -3
- package/lib/browser/index.d.ts.map +1 -1
- package/lib/browser/index.js +7 -5
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/theia-diagram-loader.d.ts +25 -0
- package/lib/browser/theia-diagram-loader.d.ts.map +1 -0
- package/lib/browser/theia-diagram-loader.js +42 -0
- package/lib/browser/theia-diagram-loader.js.map +1 -0
- package/lib/browser/window-session.d.ts +57 -0
- package/lib/browser/window-session.d.ts.map +1 -0
- package/lib/browser/window-session.js +102 -0
- package/lib/browser/window-session.js.map +1 -0
- package/lib/node/connection-container-module.js.map +1 -1
- package/lib/node/glsp-server-connection-handler.d.ts +11 -20
- package/lib/node/glsp-server-connection-handler.d.ts.map +1 -1
- package/lib/node/glsp-server-connection-handler.js +12 -15
- package/lib/node/glsp-server-connection-handler.js.map +1 -1
- package/package.json +37 -46
- package/src/browser/client-contribution.ts +254 -55
- package/src/browser/diagram-widget.ts +80 -28
- package/src/browser/glsp-client-theia-module.ts +34 -13
- package/src/browser/glsp-diagram-manager.ts +112 -3
- package/src/browser/glsp-saveable.ts +111 -0
- package/src/browser/glsp-theia-frontend-module.ts +4 -1
- package/src/browser/hydranium-glsp-diagram-configuration.ts +3 -1
- package/src/browser/index.ts +12 -5
- package/src/browser/theia-diagram-loader.ts +31 -0
- package/src/browser/window-session.ts +114 -0
- package/src/node/glsp-server-connection-handler.ts +12 -24
- package/style/diagram-loading.css +2 -5
- package/lib/browser/action-dispatcher.d.ts +0 -85
- package/lib/browser/action-dispatcher.d.ts.map +0 -1
- package/lib/browser/action-dispatcher.js +0 -238
- package/lib/browser/action-dispatcher.js.map +0 -1
- package/lib/browser/diagram-loader.d.ts +0 -123
- package/lib/browser/diagram-loader.d.ts.map +0 -1
- package/lib/browser/diagram-loader.js +0 -153
- package/lib/browser/diagram-loader.js.map +0 -1
- package/lib/browser/hidden-bounds-updater.d.ts +0 -122
- package/lib/browser/hidden-bounds-updater.d.ts.map +0 -1
- package/lib/browser/hidden-bounds-updater.js +0 -207
- package/lib/browser/hidden-bounds-updater.js.map +0 -1
- package/lib/testing/bind-recorder.d.ts +0 -67
- package/lib/testing/bind-recorder.d.ts.map +0 -1
- package/lib/testing/bind-recorder.js +0 -72
- package/lib/testing/bind-recorder.js.map +0 -1
- package/lib/testing/index.d.ts +0 -10
- package/lib/testing/index.d.ts.map +0 -1
- package/lib/testing/index.js +0 -30
- package/lib/testing/index.js.map +0 -1
- package/src/browser/action-dispatcher.ts +0 -257
- package/src/browser/diagram-loader.ts +0 -173
- package/src/browser/hidden-bounds-updater.ts +0 -212
- package/src/testing/bind-recorder.ts +0 -126
- package/src/testing/index.ts +0 -15
|
@@ -7,13 +7,17 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
-
import { DiagramLoader } from '@eclipse-glsp/client';
|
|
11
|
-
import {
|
|
10
|
+
import { type Args, DiagramLoader, EditorContextService, GLSPActionDispatcher, SetViewportAction } from '@eclipse-glsp/client';
|
|
11
|
+
import { RESUME_TOKEN_ARG } from '@hydranium/protocol';
|
|
12
|
+
import { GLSPDiagramWidget, type GLSPDiagramWidgetOptions } from '@eclipse-glsp/theia-integration';
|
|
13
|
+
import { type DiagramLoadOutcome, HydraniumDiagramLoader } from '@hydranium/glsp-client';
|
|
12
14
|
// Type-only: the `@theia/core/lib/browser` barrel touches DOM globals at module
|
|
13
15
|
// load, which the node-environment unit tests cannot provide.
|
|
14
16
|
import { type Message } from '@theia/core/lib/browser';
|
|
15
|
-
import {
|
|
16
|
-
import { type
|
|
17
|
+
import { Emitter, type Event, nls } from '@theia/core';
|
|
18
|
+
import { type Container, inject, injectable } from '@theia/core/shared/inversify';
|
|
19
|
+
import { HydraniumGlspSaveable } from './glsp-saveable';
|
|
20
|
+
import { WindowSessionService } from './window-session';
|
|
17
21
|
// Shipped by this package rather than left to adopters: an overlay whose
|
|
18
22
|
// stylesheet was forgotten is an unstyled div in normal flow, which is a silent
|
|
19
23
|
// failure. Adopters override individual rules from their own stylesheet.
|
|
@@ -35,31 +39,55 @@ export const DIAGRAM_LOADING_FAILED_CLASS = `${DIAGRAM_LOADING_CLASS}-failed`;
|
|
|
35
39
|
* model to read as a broken editor rather than a slow one.
|
|
36
40
|
*
|
|
37
41
|
* **How the pending state is sourced.** From {@link HydraniumDiagramLoader}, not
|
|
38
|
-
* from `actionDispatcher.onceModelInitialized()
|
|
39
|
-
* as well as on success,
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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.
|
|
42
|
+
* from `actionDispatcher.onceModelInitialized()`: the loader settles on failure
|
|
43
|
+
* as well as on success, even when its own error report throws, while
|
|
44
|
+
* `onceModelInitialized()` never settles on a failed load and would leave the
|
|
45
|
+
* spinner turning for good.
|
|
48
46
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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.
|
|
47
|
+
* A failed load keeps the overlay up with the error and a Retry, which asks for
|
|
48
|
+
* a fresh widget through {@link onDidRequestReopen}.
|
|
55
49
|
*
|
|
56
50
|
* Bound unconditionally by `AbstractHydraniumGlspTheiaFrontendModule`. To opt out,
|
|
57
51
|
* override {@link showLoadingOverlay} to a no-op; to change what is rendered,
|
|
58
52
|
* override {@link createLoadingOverlay} or {@link loadingLabel}.
|
|
53
|
+
*
|
|
54
|
+
* Its saveable is a {@link HydraniumGlspSaveable}, which keeps a pending save
|
|
55
|
+
* dirty, so Theia's exit check still counts it; see {@link createSaveable}.
|
|
59
56
|
*/
|
|
60
57
|
@injectable()
|
|
61
58
|
export class HydraniumGlspDiagramWidget extends GLSPDiagramWidget {
|
|
59
|
+
@inject(WindowSessionService) protected readonly windowSessions!: WindowSessionService;
|
|
62
60
|
protected loadingOverlay?: HTMLElement;
|
|
61
|
+
protected readonly reopenRequestEmitter = new Emitter<void>();
|
|
62
|
+
|
|
63
|
+
/** Fires when this diagram asks to be replaced by a fresh widget, as its
|
|
64
|
+
* Retry does; `AbstractHydraniumGlspDiagramManager` reopens it. A load is
|
|
65
|
+
* not repeated in place, since GLSP's model source registers its handlers
|
|
66
|
+
* once per load. */
|
|
67
|
+
readonly onDidRequestReopen: Event<void> = this.reopenRequestEmitter.event;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Replaces the saveable GLSP's `configure` builds inline, with no factory
|
|
71
|
+
* seam. The widget factory calls `configure` before the widget reaches the
|
|
72
|
+
* shell, so Theia only ever tracks this one.
|
|
73
|
+
*/
|
|
74
|
+
override configure(options: GLSPDiagramWidgetOptions, diContainer: Container): void {
|
|
75
|
+
super.configure(options, diContainer);
|
|
76
|
+
this.saveable.dispose();
|
|
77
|
+
this.saveable = this.createSaveable();
|
|
78
|
+
this.toDispose.push(this.saveable);
|
|
79
|
+
this.toDispose.push(this.reopenRequestEmitter);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The widget's saveable. Override to change how saves and dirty state behave. */
|
|
83
|
+
protected createSaveable(): GLSPDiagramWidget['saveable'] {
|
|
84
|
+
return new HydraniumGlspSaveable(this.actionDispatcher, this.diContainer.get(EditorContextService));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Adds the window's resume token, so a reload or reconnect takes this diagram's session over. */
|
|
88
|
+
protected override getRequestModelOptions(): Args {
|
|
89
|
+
return { ...super.getRequestModelOptions(), [RESUME_TOKEN_ARG]: this.windowSessions.current().resumeToken };
|
|
90
|
+
}
|
|
63
91
|
|
|
64
92
|
protected override onAfterAttach(msg: Message): void {
|
|
65
93
|
// Before `super`, which is what starts the load on first attach — so the
|
|
@@ -73,6 +101,19 @@ export class HydraniumGlspDiagramWidget extends GLSPDiagramWidget {
|
|
|
73
101
|
super.dispose();
|
|
74
102
|
}
|
|
75
103
|
|
|
104
|
+
/**
|
|
105
|
+
* GLSP's restore with `animate: false`; compare the two on a GLSP upgrade.
|
|
106
|
+
* An animated restore holds back every later command, so a Delete right
|
|
107
|
+
* after a select-all typed as the diagram opens finds nothing selected.
|
|
108
|
+
*/
|
|
109
|
+
protected override async setViewportData(viewportData: Parameters<GLSPDiagramWidget['setViewportData']>[0]): Promise<void> {
|
|
110
|
+
if (this.actionDispatcher instanceof GLSPActionDispatcher) {
|
|
111
|
+
this.actionDispatcher.dispatchOnceModelInitialized(
|
|
112
|
+
SetViewportAction.create(viewportData.elementId, viewportData.viewportData, { animate: false })
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
76
117
|
/**
|
|
77
118
|
* Cover the canvas until the load settles.
|
|
78
119
|
*
|
|
@@ -100,15 +141,9 @@ export class HydraniumGlspDiagramWidget extends GLSPDiagramWidget {
|
|
|
100
141
|
);
|
|
101
142
|
}
|
|
102
143
|
|
|
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
|
-
*/
|
|
144
|
+
/** Take the overlay down, or keep it as the failure's report. */
|
|
110
145
|
protected onLoadSettled(outcome: DiagramLoadOutcome): void {
|
|
111
|
-
if (outcome.status === 'failed'
|
|
146
|
+
if (outcome.status === 'failed') {
|
|
112
147
|
this.showLoadFailure(outcome.error);
|
|
113
148
|
return;
|
|
114
149
|
}
|
|
@@ -136,6 +171,23 @@ export class HydraniumGlspDiagramWidget extends GLSPDiagramWidget {
|
|
|
136
171
|
if (label) {
|
|
137
172
|
label.textContent = loader.loadFailureLabel(error);
|
|
138
173
|
}
|
|
174
|
+
overlay.appendChild(this.createRetryButton());
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Ask for a fresh widget, after a failed load. */
|
|
178
|
+
protected retryLoad(): void {
|
|
179
|
+
if (this.hydraniumDiagramLoader?.loadOutcome?.status === 'failed') {
|
|
180
|
+
this.reopenRequestEmitter.fire();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Build the failure overlay's Retry button, which calls {@link retryLoad}. */
|
|
185
|
+
protected createRetryButton(): HTMLElement {
|
|
186
|
+
const button = document.createElement('button');
|
|
187
|
+
button.className = `theia-button ${DIAGRAM_LOADING_CLASS}-retry`;
|
|
188
|
+
button.textContent = nls.localize('hydranium/glsp-client-theia/diagram-load-retry', 'Retry');
|
|
189
|
+
button.addEventListener('click', () => this.retryLoad());
|
|
190
|
+
return button;
|
|
139
191
|
}
|
|
140
192
|
|
|
141
193
|
protected hideLoadingOverlay(): void {
|
|
@@ -7,12 +7,19 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
import { type BindingContext, DiagramLoader } from '@eclipse-glsp/client';
|
|
11
|
+
import {
|
|
12
|
+
bindChannelLogger,
|
|
13
|
+
ChannelLogger,
|
|
14
|
+
type ChannelLoggerOptions,
|
|
15
|
+
ChannelTracer,
|
|
16
|
+
getRequestParentName
|
|
17
|
+
} from '@hydranium/client-theia/browser';
|
|
18
|
+
import { Clock } from '@hydranium/client-theia/common';
|
|
19
|
+
import { createGlspClientModule, HydraniumClientTypes } from '@hydranium/glsp-client';
|
|
20
|
+
import { SystemClock } from '@hydranium/protocol';
|
|
14
21
|
import { bindHydraniumGlspMessageService } from './glsp-message-service';
|
|
15
|
-
import {
|
|
22
|
+
import { HydraniumGlspTheiaDiagramLoader } from './theia-diagram-loader';
|
|
16
23
|
|
|
17
24
|
/** Options for `createGlspClientTheiaModule`. The adopter supplies the channel
|
|
18
25
|
* logger configuration ({@link ChannelLoggerOptions} — a `channelName`, and
|
|
@@ -25,10 +32,17 @@ export interface GlspClientTheiaModuleOptions {
|
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
/**
|
|
28
|
-
* Composes the standard framework client bindings: the
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
35
|
+
* Composes the standard framework client bindings for a Theia frontend: the
|
|
36
|
+
* cross-head Output-channel logger, the host-neutral classes
|
|
37
|
+
* `createGlspClientModule` binds, fed from that channel, the loader that renders
|
|
38
|
+
* its failure through Theia's catalogue, and the message service that drops
|
|
39
|
+
* the duplicate model-loading notification.
|
|
40
|
+
*
|
|
41
|
+
* The neutral tokens are bound BEFORE the neutral binder runs, so it binds no
|
|
42
|
+
* console defaults. The logger binding tags each line with the requesting
|
|
43
|
+
* class, as `bindChannelLogger`'s own does; a plain `toService` alias would
|
|
44
|
+
* start a fresh request and lose the tag. The clock is resolved lazily, so a
|
|
45
|
+
* clock an adopter module binds after this one is still the one used.
|
|
32
46
|
*
|
|
33
47
|
* Takes the whole {@link BindingContext} rather than `(bind, rebind)`: the
|
|
34
48
|
* message-service rebind has to know whether GLSP's `theiaNotificationModule`
|
|
@@ -46,9 +60,16 @@ export interface GlspClientTheiaModuleOptions {
|
|
|
46
60
|
export function createGlspClientTheiaModule(context: BindingContext, options: GlspClientTheiaModuleOptions): void {
|
|
47
61
|
const { bind, isBound, rebind } = context;
|
|
48
62
|
bindChannelLogger(bind, options.channelLogger);
|
|
49
|
-
bind(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
63
|
+
bind(HydraniumClientTypes.Logger).toDynamicValue(ctx => {
|
|
64
|
+
const base = ctx.container.get(ChannelLogger);
|
|
65
|
+
const parentName = getRequestParentName(ctx);
|
|
66
|
+
return parentName ? base.for(parentName) : base;
|
|
67
|
+
});
|
|
68
|
+
bind(HydraniumClientTypes.Tracer).toService(ChannelTracer);
|
|
69
|
+
bind(HydraniumClientTypes.Clock)
|
|
70
|
+
.toDynamicValue(ctx => (ctx.container.isBound(Clock) ? ctx.container.get<Clock>(Clock) : new SystemClock()))
|
|
71
|
+
.inSingletonScope();
|
|
72
|
+
createGlspClientModule(context);
|
|
73
|
+
rebind(DiagramLoader).to(HydraniumGlspTheiaDiagramLoader).inSingletonScope();
|
|
53
74
|
bindHydraniumGlspMessageService(bind, isBound, rebind);
|
|
54
75
|
}
|
|
@@ -7,12 +7,22 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
-
import { codiconCSSString } from '@eclipse-glsp/client';
|
|
10
|
+
import { codiconCSSString, DiagramLoader, type IDiagramOptions } from '@eclipse-glsp/client';
|
|
11
11
|
import { GLSPDiagramManager } from '@eclipse-glsp/theia-integration';
|
|
12
12
|
import { type GLSPDiagramLanguage } from '@eclipse-glsp/theia-integration/lib/common';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
type GLSPDiagramWidget,
|
|
15
|
+
type GLSPDiagramWidgetOptions,
|
|
16
|
+
type GLSPWidgetOpenerOptions
|
|
17
|
+
} from '@eclipse-glsp/theia-integration/lib/browser';
|
|
14
18
|
import { type WidgetOpenerOptions } from '@theia/core/lib/browser';
|
|
15
|
-
import {
|
|
19
|
+
import { type Disposable, DisposableCollection } from '@theia/core';
|
|
20
|
+
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
21
|
+
import { textHash } from '@hydranium/protocol';
|
|
22
|
+
import { HydraniumDiagramLoader } from '@hydranium/glsp-client';
|
|
23
|
+
import { HydraniumGlspClientContribution } from './client-contribution';
|
|
24
|
+
import { HydraniumGlspDiagramWidget } from './diagram-widget';
|
|
25
|
+
import { WindowSessionService } from './window-session';
|
|
16
26
|
|
|
17
27
|
/**
|
|
18
28
|
* `GLSPDiagramManager` subclass that derives the language-correlated getters
|
|
@@ -47,6 +57,12 @@ export abstract class AbstractHydraniumGlspDiagramManager extends GLSPDiagramMan
|
|
|
47
57
|
/** Optional icon-class override; takes precedence over the language's `iconClass`. */
|
|
48
58
|
protected readonly customIconClass?: string;
|
|
49
59
|
|
|
60
|
+
@inject(WindowSessionService) protected readonly windowSessions!: WindowSessionService;
|
|
61
|
+
|
|
62
|
+
protected clientListeners?: Disposable;
|
|
63
|
+
/** The reopens asked for so far, which run one at a time. */
|
|
64
|
+
protected reopening: Promise<void> = Promise.resolve();
|
|
65
|
+
|
|
50
66
|
override get fileExtensions(): string[] {
|
|
51
67
|
return [...this.diagramLanguage.fileExtensions];
|
|
52
68
|
}
|
|
@@ -67,6 +83,99 @@ export abstract class AbstractHydraniumGlspDiagramManager extends GLSPDiagramMan
|
|
|
67
83
|
return this.managerLabel;
|
|
68
84
|
}
|
|
69
85
|
|
|
86
|
+
override async createWidget(options?: unknown): Promise<GLSPDiagramWidget> {
|
|
87
|
+
this.listenToClient();
|
|
88
|
+
const widget = await super.createWidget(options);
|
|
89
|
+
if (widget instanceof HydraniumGlspDiagramWidget) {
|
|
90
|
+
widget.onDidRequestReopen(() => void this.reopen(widget));
|
|
91
|
+
}
|
|
92
|
+
return widget;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The same client id for the same diagram in this window, across a reload
|
|
97
|
+
* and a reopen after a lost client, so its load takes over the session the
|
|
98
|
+
* old connection left and keeps its unsaved text. A diagram closed by the
|
|
99
|
+
* user ends its session, and opening it again starts afresh.
|
|
100
|
+
*
|
|
101
|
+
* The widget options enter as a hash: GLSP makes the id an element id and
|
|
102
|
+
* finds it with an `#id` selector, which a URI's `:` and `/` break. The
|
|
103
|
+
* options identify the widget, so two widgets of one document, in two edit
|
|
104
|
+
* modes or set apart by an option an adopter adds, do not take each other's
|
|
105
|
+
* session over.
|
|
106
|
+
*/
|
|
107
|
+
protected override createDiagramOptions(options: GLSPDiagramWidgetOptions): IDiagramOptions {
|
|
108
|
+
const clientId = `${this.diagramType}_${this.windowSessions.current().id}_${textHash(JSON.stringify(options))}`;
|
|
109
|
+
return { ...super.createDiagramOptions(options), clientId };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Reopen every diagram once its client is lost, since none of them has a
|
|
114
|
+
* server behind it any more, and a failed one once a client starts.
|
|
115
|
+
*/
|
|
116
|
+
protected listenToClient(): void {
|
|
117
|
+
if (this.clientListeners) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const contribution = this.diagramServiceProvider.getGLSPClientContribution(this.contributionId);
|
|
121
|
+
this.clientListeners =
|
|
122
|
+
contribution instanceof HydraniumGlspClientContribution
|
|
123
|
+
? new DisposableCollection(
|
|
124
|
+
contribution.onDidLoseClient(() => this.reopenAll(() => true)),
|
|
125
|
+
contribution.onDidStartClient(() => this.reopenAll(widget => this.loadFailed(widget)))
|
|
126
|
+
)
|
|
127
|
+
: new DisposableCollection();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
protected reopenAll(which: (widget: GLSPDiagramWidget) => boolean): void {
|
|
131
|
+
for (const widget of this.all.filter(which)) {
|
|
132
|
+
void this.reopen(widget);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
protected loadFailed(widget: GLSPDiagramWidget): boolean {
|
|
137
|
+
const loader = widget.diContainer.get<DiagramLoader>(DiagramLoader);
|
|
138
|
+
return loader instanceof HydraniumDiagramLoader && loader.loadOutcome?.status === 'failed';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Replace `widget` with a fresh one for the same diagram, in the same tab
|
|
143
|
+
* position and with its viewport: a new container and load under the same
|
|
144
|
+
* client id, which takes over the session the old connection left.
|
|
145
|
+
* Reopens run one at a time, since each places its replacement next to a
|
|
146
|
+
* neighbour that a reopen running alongside could take out of the layout.
|
|
147
|
+
*/
|
|
148
|
+
reopen(widget: GLSPDiagramWidget): Promise<void> {
|
|
149
|
+
const reopened = this.reopening.then(() => this.replaceWidget(widget));
|
|
150
|
+
this.reopening = reopened.catch(() => undefined);
|
|
151
|
+
return reopened;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Taken down as a close does, but without its save prompt, since a dirty
|
|
155
|
+
* diagram whose server is gone has nothing to save to. */
|
|
156
|
+
protected async replaceWidget(widget: GLSPDiagramWidget): Promise<void> {
|
|
157
|
+
if (widget.isDisposed) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const tabBar = this.shell.getTabBarFor(widget);
|
|
161
|
+
const titles = tabBar?.titles ?? [];
|
|
162
|
+
const index = titles.indexOf(widget.title);
|
|
163
|
+
// ponytail: a diagram alone in its tab bar reopens where a new one opens,
|
|
164
|
+
// since its split closes with it; restore the dock layout if that matters.
|
|
165
|
+
const neighbour = index > 0 ? titles[index - 1].owner : titles[index + 1]?.owner;
|
|
166
|
+
const mode = this.shell.activeWidget === widget ? 'activate' : tabBar?.currentTitle === widget.title ? 'reveal' : 'open';
|
|
167
|
+
// Detached before the dispose, as a close does: the widget stores its
|
|
168
|
+
// viewport on detach, and its dispose empties the container it reads.
|
|
169
|
+
widget.parent = null;
|
|
170
|
+
widget.dispose();
|
|
171
|
+
const options: GLSPWidgetOpenerOptions = {
|
|
172
|
+
mode,
|
|
173
|
+
editMode: widget.options.editMode,
|
|
174
|
+
widgetOptions: neighbour ? { ref: neighbour, mode: index > 0 ? 'tab-after' : 'tab-before' } : undefined
|
|
175
|
+
};
|
|
176
|
+
await this.open(widget.uri, options);
|
|
177
|
+
}
|
|
178
|
+
|
|
70
179
|
/**
|
|
71
180
|
* Drops a `selection` key that is present but `undefined` before the base
|
|
72
181
|
* class reads it.
|
|
@@ -0,0 +1,111 @@
|
|
|
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 { RequestSaveModelAction } from '@hydranium/protocol';
|
|
11
|
+
// The module path, not the package root: the root also loads GLSP's diagram
|
|
12
|
+
// widget, which touches DOM globals at module load, and this module's unit tests
|
|
13
|
+
// load it in Node.
|
|
14
|
+
import { GLSPSaveable } from '@eclipse-glsp/theia-integration/lib/browser/diagram/glsp-saveable';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A `GLSPSaveable` whose every save is a request the server answers, and which
|
|
18
|
+
* stays dirty until that answer arrives.
|
|
19
|
+
*
|
|
20
|
+
* GLSP's sends a `SaveModelAction`, which the server does not answer. It takes
|
|
21
|
+
* the next dirty-state change to clean as the answer, but the editor context
|
|
22
|
+
* reports a change only when the dirty flag changes: the second of two saves
|
|
23
|
+
* gets no answer, and a failed save gets none either. Here each save sends a
|
|
24
|
+
* `RequestSaveModelAction` and settles on the response to that request only:
|
|
25
|
+
* it resolves when the server has saved, rejects when the server rejects it,
|
|
26
|
+
* and rejects when {@link saveTimeout} runs out. A save sent while another is
|
|
27
|
+
* pending is a request of its own, so an edit made between the two is saved.
|
|
28
|
+
*
|
|
29
|
+
* GLSP's also reports not dirty from the moment it sends a save. Theia's exit
|
|
30
|
+
* check asks only whether anything is dirty, so the window closes over a save
|
|
31
|
+
* the server has not finished, and a page reloaded right after Ctrl+S gives no
|
|
32
|
+
* warning. Here the saveable is dirty while any save is pending. GLSP's also
|
|
33
|
+
* gives up after 2 s, and Theia's Save All then counts the save as failed and
|
|
34
|
+
* closes the window anyway, so a slow save of a large model is cut off.
|
|
35
|
+
*
|
|
36
|
+
* The dirty flag still comes from the server. An edit whose dirty-state change
|
|
37
|
+
* has not arrived when Save All runs does not make the saveable dirty, so that
|
|
38
|
+
* edit is not saved.
|
|
39
|
+
*
|
|
40
|
+
* The server answers only where the diagram module registers the request's
|
|
41
|
+
* handler; `AbstractHydraniumGlspDiagramModule` does. A server without it does
|
|
42
|
+
* not advertise the request, and against such a server the saveable is GLSP's:
|
|
43
|
+
* it sends a `SaveModelAction` and reports dirty as GLSP's does. It does not
|
|
44
|
+
* keep only the dirty-while-pending half there, because GLSP's tracks one
|
|
45
|
+
* pending save and a second save overwrites it, so the first would never
|
|
46
|
+
* report clean.
|
|
47
|
+
*
|
|
48
|
+
* The answer means the server has done what its save does, which is not always
|
|
49
|
+
* the disk write: under the `fire-and-forget` `SaveDeliveryPolicy` the server
|
|
50
|
+
* answers before the write finishes and answers a failed write as saved, so a
|
|
51
|
+
* window closed on that answer can still lose the write.
|
|
52
|
+
*/
|
|
53
|
+
export class HydraniumGlspSaveable extends GLSPSaveable {
|
|
54
|
+
/**
|
|
55
|
+
* How long a save waits for the server's response before it rejects. The
|
|
56
|
+
* server answers up to its language-client settle bound after the write, so
|
|
57
|
+
* this exceeds that bound with room for the write itself; a save that
|
|
58
|
+
* rejects in Theia's Save All lets the window close over it.
|
|
59
|
+
*/
|
|
60
|
+
protected override saveTimeout = 20_000;
|
|
61
|
+
/** The saves sent and not yet settled. */
|
|
62
|
+
protected readonly pendingSaves = new Set<Promise<void>>();
|
|
63
|
+
|
|
64
|
+
override get dirty(): boolean {
|
|
65
|
+
if (!this.serverAnswersSaves()) {
|
|
66
|
+
return super.dirty;
|
|
67
|
+
}
|
|
68
|
+
return this.editorContextService.isDirty || this.pendingSaves.size > 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override save(): Promise<void> {
|
|
72
|
+
if (!this.serverAnswersSaves()) {
|
|
73
|
+
return super.save();
|
|
74
|
+
}
|
|
75
|
+
if (this.editorContextService.isDirty) {
|
|
76
|
+
return this.sendSave();
|
|
77
|
+
}
|
|
78
|
+
// Nothing new to save, but a save still pending has not finished.
|
|
79
|
+
return Promise.all(this.pendingSaves).then(() => undefined);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Whether the server answers a {@link RequestSaveModelAction}. GLSP's model
|
|
84
|
+
* source registers a handler for each kind the server advertises before it
|
|
85
|
+
* requests the model, so the dispatcher knows it from the first save on.
|
|
86
|
+
*/
|
|
87
|
+
protected serverAnswersSaves(): boolean {
|
|
88
|
+
return this.actionDispatcher.hasHandler(RequestSaveModelAction.create());
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
protected sendSave(): Promise<void> {
|
|
92
|
+
// `request` with a bound of our own rather than GLSP's `requestUntil`:
|
|
93
|
+
// that one forgets a request when it times out, so a late response is
|
|
94
|
+
// dispatched as an action no client handler takes, which rejects.
|
|
95
|
+
// Here a late response settles the forgotten request and nothing else.
|
|
96
|
+
const response = this.actionDispatcher.request(RequestSaveModelAction.create());
|
|
97
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
98
|
+
const timedOut = new Promise<never>((_resolve, reject) => {
|
|
99
|
+
timer = setTimeout(() => reject(new Error('Save operation timed out')), this.saveTimeout);
|
|
100
|
+
});
|
|
101
|
+
const saving: Promise<void> = Promise.race([response, timedOut])
|
|
102
|
+
.then(() => undefined)
|
|
103
|
+
.finally(() => {
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
this.pendingSaves.delete(saving);
|
|
106
|
+
this.onDirtyChangedEmitter.fire(undefined);
|
|
107
|
+
});
|
|
108
|
+
this.pendingSaves.add(saving);
|
|
109
|
+
return saving;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -17,9 +17,10 @@ import {
|
|
|
17
17
|
registerDiagramManager
|
|
18
18
|
} from '@eclipse-glsp/theia-integration';
|
|
19
19
|
import { type GLSPDiagramLanguage } from '@eclipse-glsp/theia-integration/lib/common';
|
|
20
|
-
import { bindLogLevelPreference } from '@hydranium/client-theia/
|
|
20
|
+
import { bindConnectionReporter, bindLogLevelPreference } from '@hydranium/client-theia/browser';
|
|
21
21
|
import type { interfaces } from '@theia/core/shared/inversify';
|
|
22
22
|
import { HydraniumGlspDiagramWidget } from './diagram-widget';
|
|
23
|
+
import { bindWindowSessionService } from './window-session';
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Sentinel returned from {@link AbstractHydraniumGlspTheiaFrontendModule.bindClientContribution}
|
|
@@ -106,6 +107,8 @@ export abstract class AbstractHydraniumGlspTheiaFrontendModule extends GLSPTheia
|
|
|
106
107
|
*/
|
|
107
108
|
override initialize(context: ContainerContext): void {
|
|
108
109
|
super.initialize(context);
|
|
110
|
+
bindConnectionReporter(context.bind, context.isBound);
|
|
111
|
+
bindWindowSessionService(context.bind, context.isBound);
|
|
109
112
|
if (this.logLevelPreference) {
|
|
110
113
|
bindLogLevelPreference(context.bind, this.logLevelPreference);
|
|
111
114
|
}
|
|
@@ -22,7 +22,9 @@ import { NoOpExternalMarkerManager } from './diagram-only-marker-manager.js';
|
|
|
22
22
|
* **When to suppress.** A head with a co-resident LSP that publishes its
|
|
23
23
|
* diagnostics over standard LSP `publishDiagnostics` would otherwise list each
|
|
24
24
|
* error twice: GLSP's `TheiaMarkerManager` pushes the *same* errors again under
|
|
25
|
-
* a different marker owner, and Theia shows the union across owners.
|
|
25
|
+
* a different marker owner, and Theia shows the union across owners. One shared
|
|
26
|
+
* owner would not merge them either: each write replaces that owner's markers
|
|
27
|
+
* for the file, so the two heads would overwrite each other's. The GLSP
|
|
26
28
|
* path is also open-diagram-scoped (markers clear when the diagram closes), so
|
|
27
29
|
* for such a head it cannot reproduce the LSP head's persistent, cross-file
|
|
28
30
|
* Problems entries — it is only ever a flickering duplicate. A graphical-only
|
package/src/browser/index.ts
CHANGED
|
@@ -7,16 +7,23 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
-
// Browser-side framework primitives for GLSP-on-Theia adopters.
|
|
11
|
-
//
|
|
12
|
-
|
|
10
|
+
// Browser-side framework primitives for GLSP-on-Theia adopters. The
|
|
11
|
+
// host-neutral classes (the dispatcher, loader, updater, overlay and client)
|
|
12
|
+
// are `@hydranium/glsp-client`'s; the cross-head Output-channel logger lives
|
|
13
|
+
// in `@hydranium/client-theia/browser`.
|
|
13
14
|
export * from './client-contribution';
|
|
14
|
-
export * from './diagram-loader';
|
|
15
15
|
export * from './diagram-only-marker-manager';
|
|
16
16
|
export * from './diagram-widget';
|
|
17
17
|
export * from './glsp-client-theia-module';
|
|
18
18
|
export * from './glsp-diagram-manager';
|
|
19
19
|
export * from './glsp-message-service';
|
|
20
|
+
export * from './glsp-saveable';
|
|
20
21
|
export * from './glsp-theia-frontend-module';
|
|
21
|
-
export * from './hidden-bounds-updater';
|
|
22
22
|
export * from './hydranium-glsp-diagram-configuration';
|
|
23
|
+
export * from './theia-diagram-loader';
|
|
24
|
+
export * from './window-session';
|
|
25
|
+
|
|
26
|
+
// Upstream's root entry is its browser tier and re-exports no `lib/common`, so the
|
|
27
|
+
// type our abstract `diagramLanguage` members are declared with has no bare-specifier
|
|
28
|
+
// path — without this line every adopter implementing one deep-imports it.
|
|
29
|
+
export type { GLSPDiagramLanguage } from '@eclipse-glsp/theia-integration/lib/common';
|
|
@@ -0,0 +1,31 @@
|
|
|
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 { HydraniumDiagramLoader } from '@hydranium/glsp-client';
|
|
11
|
+
import { nls } from '@theia/core';
|
|
12
|
+
import { injectable } from '@theia/core/shared/inversify';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The framework loader rendering its failure through Theia's catalogue, which
|
|
16
|
+
* knows the reading user's locale where the neutral loader does not.
|
|
17
|
+
*
|
|
18
|
+
* The key and the English are inline literals, because `theia nls-extract`
|
|
19
|
+
* drops a key it cannot resolve. The English must match
|
|
20
|
+
* `DIAGRAM_LOAD_FAILED`'s text in `@hydranium/glsp-client`; a test holds them
|
|
21
|
+
* equal.
|
|
22
|
+
*
|
|
23
|
+
* Bound by `createGlspClientTheiaModule` via `rebind(DiagramLoader)`, after
|
|
24
|
+
* `createGlspClientModule` bound the neutral loader.
|
|
25
|
+
*/
|
|
26
|
+
@injectable()
|
|
27
|
+
export class HydraniumGlspTheiaDiagramLoader extends HydraniumDiagramLoader {
|
|
28
|
+
override loadFailureLabel(err: unknown): string {
|
|
29
|
+
return nls.localize('hydranium/glsp-client-theia/diagram-load-failed', 'Diagram failed to load: {0}', this.formatError(err));
|
|
30
|
+
}
|
|
31
|
+
}
|