Skip to content

Commit

Permalink
Improve update handling between editors
Browse files Browse the repository at this point in the history
- Ensure GLSP selection is forwarded again if active editor changes
- Only update properties if selection of active widget changes
- Add additional change reason for better update decision in GLSP
  • Loading branch information
martin-fleck-at committed Dec 3, 2023
1 parent 73aa596 commit 877dcf2
Show file tree
Hide file tree
Showing 12 changed files with 113 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export class CrossModelStorage implements SourceModelStorage, ClientSessionListe
this.state.setSemanticRoot(rootUri, root);
this.toDispose.push(
this.state.modelService.onUpdate<CrossModelRoot>(rootUri, async event => {
if (this.state.clientId !== event.sourceClientId) {
if (this.state.clientId !== event.sourceClientId || event.reason !== 'changed') {
this.state.setSemanticRoot(rootUri, event.model);
this.actionDispatcher.dispatchAll(await this.submissionHandler.submitModel('external'));
}
Expand Down
5 changes: 3 additions & 2 deletions extensions/crossmodel-lang/src/model-server/model-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export class ModelServer implements Disposable {
const entity: Entity | undefined = diagramNode?.entity?.ref;

if (!entity?.$container.$document) {
throw new Error('No node found with the given id: ' + id);
throw new Error('No node found with the given id: ' + id + ' (in ' + uri + ')');
}

const serializedEntity = toSerializable({
Expand Down Expand Up @@ -112,7 +112,8 @@ export class ModelServer implements Disposable {
this.connection.sendNotification(OnUpdated, {
uri: args.uri,
model: toSerializable(event.model) as CrossModelRoot,
sourceClientId: event.sourceClientId
sourceClientId: event.sourceClientId,
reason: event.reason
})
)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ import { CrossModelLanguageMetaData } from '../language-server/generated/module.
import { AddedSharedModelServices } from './model-module.js';
import { OpenableTextDocuments } from './openable-text-documents.js';

export interface UpdateInfo {
changed: URI[];
deleted: URI[];
}

/**
* A manager class that supports handling documents with a simple open-update-save/close lifecycle.
*
Expand All @@ -32,6 +37,8 @@ export class OpenTextDocumentManager {
protected langiumDocs: LangiumDocuments;
protected documentBuilder: DocumentBuilder;

protected lastUpdate?: UpdateInfo;

constructor(services: AddedSharedModelServices & LangiumDefaultSharedServices) {
this.textDocuments = services.workspace.TextDocuments;
this.fileSystemProvider = services.workspace.FileSystemProvider;
Expand All @@ -42,6 +49,9 @@ export class OpenTextDocumentManager {
this.open({ clientId: event.clientId, uri: event.document.uri, languageId: event.document.languageId })
);
this.textDocuments.onDidClose(event => this.close({ clientId: event.clientId, uri: event.document.uri }));
this.documentBuilder.onUpdate((changed, deleted) => {
this.lastUpdate = { changed, deleted };
});
}

/**
Expand Down Expand Up @@ -72,11 +82,17 @@ export class OpenTextDocumentManager {
const changedDocument = allChangedDocuments.find(document => document.uri.toString() === uri);
if (changedDocument) {
const sourceClientId = this.getSourceClientId(changedDocument, allChangedDocuments);
listener({
const event: ModelUpdatedEvent<T> = {
model: changedDocument.parseResult.value as T,
sourceClientId,
uri: changedDocument.textDocument.uri
});
uri: changedDocument.textDocument.uri,
reason: this.lastUpdate?.changed.includes(changedDocument.uri)
? 'changed'
: this.lastUpdate?.deleted.includes(changedDocument.uri)
? 'deleted'
: 'updated'
};
listener(event);
}
});
}
Expand Down Expand Up @@ -133,7 +149,7 @@ export class OpenTextDocumentManager {
}

protected createDummyDocument(uri: string): TextDocumentItem {
return TextDocumentItem.create(this.normalizedUri(uri), CrossModelLanguageMetaData.languageId, 1, '');
return TextDocumentItem.create(this.normalizedUri(uri), CrossModelLanguageMetaData.languageId, 0, '');
}

protected async createDocumentFromFileSystem(
Expand All @@ -142,7 +158,7 @@ export class OpenTextDocumentManager {
): Promise<TextDocumentItem> {
const vscUri = URI.parse(uri);
const content = this.fileSystemProvider.readFileSync(vscUri);
return TextDocumentItem.create(vscUri.toString(), languageId, 1, content.toString());
return TextDocumentItem.create(vscUri.toString(), languageId, 0, content.toString());
}

protected normalizedUri(uri: string): string {
Expand Down
1 change: 1 addition & 0 deletions packages/glsp-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"dependencies": {
"@crossbreeze/core": "0.0.0",
"@crossbreeze/protocol": "0.0.0",
"@eclipse-glsp/client": "2.0.0",
"@eclipse-glsp/theia-integration": "2.0.0",
"@theia/core": "1.43.1",
"@theia/filesystem": "1.43.1",
Expand Down
11 changes: 7 additions & 4 deletions packages/glsp-client/src/browser/crossmodel-diagram-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import {
configureModelElement,
initializeDiagramContainer
} from '@eclipse-glsp/client';
import { GlspSelectionDataService } from '@eclipse-glsp/theia-integration';
import { TheiaGLSPSelectionForwarder } from '@eclipse-glsp/theia-integration';
import { Container, ContainerModule } from '@theia/core/shared/inversify';
import { CrossModelGLSPSelectionDataService } from './crossmodel-selection-data-service';
import { CrossModelSelectionDataService, CrossModelTheiaGLSPSelectionForwarder } from './crossmodel-selection-forwarder';
import { ENTITY_NODE_TYPE, EntityNode } from './model';
import { EntityNodeView } from './views';

Expand All @@ -32,14 +33,16 @@ const crossModelDiagramModule = new ContainerModule((bind, unbind, isBound, rebi
// The view class shows how to draw the svg element given the properties of the model class
configureModelElement(context, ENTITY_NODE_TYPE, EntityNode, EntityNodeView);

bindAsService(bind, GlspSelectionDataService, CrossModelGLSPSelectionDataService);
bind(TYPES.ISelectionListener).toService(CrossModelGLSPSelectionDataService);
bindAsService(bind, CrossModelSelectionDataService, CrossModelGLSPSelectionDataService);

bind(CrossModelTheiaGLSPSelectionForwarder).toSelf().inSingletonScope();
rebind(TheiaGLSPSelectionForwarder).toService(CrossModelTheiaGLSPSelectionForwarder);
});

export function createCrossModelDiagramContainer(...containerConfiguration: ContainerConfiguration): Container {
return initializeCrossModelDiagramContainer(new Container(), ...containerConfiguration);
}

export function initializeCrossModelDiagramContainer(container: Container, ...containerConfiguration: ContainerConfiguration): Container {
return initializeDiagramContainer(container, crossModelDiagramModule, ...containerConfiguration);
return initializeDiagramContainer(container, ...containerConfiguration, crossModelDiagramModule);
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
/********************************************************************************
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/
import { GModelRoot, ISelectionListener } from '@eclipse-glsp/client';
import { GlspSelectionData, GlspSelectionDataService } from '@eclipse-glsp/theia-integration';
import { GModelElement, GModelRoot } from '@eclipse-glsp/client';
import { GlspSelectionData } from '@eclipse-glsp/theia-integration';
import { isDefined } from '@theia/core';
import { injectable } from '@theia/core/shared/inversify';
import { CrossModelSelectionDataService } from './crossmodel-selection-forwarder';

@injectable()
export class CrossModelGLSPSelectionDataService extends GlspSelectionDataService implements ISelectionListener {
protected root?: Readonly<GModelRoot>;

selectionChanged(root: Readonly<GModelRoot>, selectedElements: string[], deselectedElements?: string[] | undefined): void {
this.root = root;
export class CrossModelGLSPSelectionDataService extends CrossModelSelectionDataService {
async getSelectionData(root: Readonly<GModelRoot>, selectedElementIds: string[]): Promise<GlspSelectionData> {
return getSelectionDataFor(selectedElementIds.map(id => root.index.getById(id)).filter(isDefined));
}
}

async getSelectionData(selectedElementIds: string[]): Promise<GlspSelectionData> {
const selectionDataMap = new Map<string, string>();
selectedElementIds
.map(elementId => this.root?.index.getById(elementId))
.filter(isDefined)
.forEach(element => selectionDataMap.set(element.id, element.type));
return { selectionDataMap };
}
export function getSelectionDataFor(selection: GModelElement[]): GlspSelectionData {
const selectionDataMap = new Map<string, string>();
selection.forEach(element => selectionDataMap.set(element.id, element.type));
return { selectionDataMap };
}
60 changes: 60 additions & 0 deletions packages/glsp-client/src/browser/crossmodel-selection-forwarder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/********************************************************************************
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/

import { GModelRoot } from '@eclipse-glsp/client';
import { GlspSelection, GlspSelectionData, TheiaGLSPSelectionForwarder, getDiagramWidget } from '@eclipse-glsp/theia-integration';
import { SelectionService } from '@theia/core';

Check failure on line 7 in packages/glsp-client/src/browser/crossmodel-selection-forwarder.ts

View workflow job for this annotation

GitHub Actions / build-and-test / ubuntu-latest

'SelectionService' is defined but never used
import { ApplicationShell } from '@theia/core/lib/browser';
import { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify';

@injectable()
export abstract class CrossModelSelectionDataService {
abstract getSelectionData(root: Readonly<GModelRoot>, selectedElementIds: string[]): Promise<GlspSelectionData>;
}

/**
* Reacts to diagram selection changes and forwards the corresponding {@link GlspSelection}
* to Theia`s {@link SelectionService}
*
* (bound in Diagram child DI container)
*/
@injectable()
export class CrossModelTheiaGLSPSelectionForwarder extends TheiaGLSPSelectionForwarder {
@inject(CrossModelSelectionDataService)
@optional()
protected readonly dataService?: CrossModelSelectionDataService;

@inject(ApplicationShell)
protected shell: ApplicationShell;

@postConstruct()
protected init(): void {
this.shell.onDidChangeActiveWidget(() => {
const activeDiagramWidget = getDiagramWidget(this.shell);
if (activeDiagramWidget) {
// re-store selection from diagram to the global scope
this.selectionChanged(
activeDiagramWidget.editorContext.modelRoot,
activeDiagramWidget.editorContext.selectedElements.map(element => element.id)
);
}
});
}

override selectionChanged(root: Readonly<GModelRoot>, selectedElements: string[]): void {
this.handleSelectionChanged(selectedElements, root);
}

override async handleSelectionChanged(selectedElementsIDs: string[], root?: Readonly<GModelRoot>): Promise<void> {
const sourceUri = await this.getSourceUri();
const additionalSelectionData = (await this.dataService?.getSelectionData(root!, selectedElementsIDs)) ?? undefined;
const glspSelection: GlspSelection = {
selectedElementsIDs,
additionalSelectionData,
widgetId: this.viewerOptions.baseDiv,
sourceUri: sourceUri
};
this.theiaSelectionService.selection = glspSelection;
}
}
2 changes: 1 addition & 1 deletion packages/model-service/src/node/model-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export class ModelServiceImpl implements ModelService, BackendApplicationContrib

setUpListeners(): void {
this.connection.onNotification(OnSave, event => {
this.client?.updateModel(event);
this.client?.updateModel({ ...event, reason: 'saved' });
});
this.connection.onNotification(OnUpdated, event => {
this.client?.updateModel(event);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/

import { ModelService } from '@crossbreeze/model-service/lib/common';
import { GlspSelection } from '@eclipse-glsp/theia-integration';
import { inject, injectable } from '@theia/core/shared/inversify';
import { DefaultPropertyViewWidgetProvider } from '@theia/property-view/lib/browser/property-view-widget-provider';
Expand All @@ -16,11 +15,6 @@ export class ModelPropertyWidgetProvider extends DefaultPropertyViewWidgetProvid
currentNode = '';

@inject(ModelPropertyWidget) protected modelPropertyWidget: ModelPropertyWidget;
@inject(ModelService) protected modelService: ModelService;

constructor() {
super();
}

override canHandle(selection: GlspSelection | undefined): number {
// issue with how selection is determined, if the additionalSelectionData is empty we simply delete the property
Expand Down
7 changes: 6 additions & 1 deletion packages/property-view/src/browser/model-property-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { PropertyViewContentWidget } from '@theia/property-view/lib/browser/prop
import { ModelService } from '@crossbreeze/model-service/lib/common';
import { CrossModelRoot, isDiagramNodeEntity } from '@crossbreeze/protocol';
import { EntityPropertyView, withModelProvider } from '@crossbreeze/react-model-ui';
import { GLSPDiagramWidget, GlspSelection } from '@eclipse-glsp/theia-integration';
import { GLSPDiagramWidget, GlspSelection, getDiagramWidget } from '@eclipse-glsp/theia-integration';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { inject, injectable } from '@theia/core/shared/inversify';
import { PROPERTY_CLIENT_ID } from './model-data-service';
Expand Down Expand Up @@ -39,6 +39,11 @@ export class ModelPropertyWidget extends ReactWidget implements PropertyViewCont
}

async updatePropertyViewContent(propertyDataService?: PropertyDataService, selection?: GlspSelection | undefined): Promise<void> {
const activeWidget = getDiagramWidget(this.shell);
if (activeWidget?.options.uri === this.uri && this.uri !== selection?.sourceUri) {
// only react to selection of active widget
return;
}
this.model = undefined;

if (propertyDataService) {
Expand Down
1 change: 1 addition & 0 deletions packages/protocol/src/model-service/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export interface ModelUpdatedEvent<T> {
uri: string;
model: T;
sourceClientId: string;
reason: 'changed' | 'deleted' | 'updated' | 'saved';
}

export interface ModelSavedEvent<T> {
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1056,7 +1056,7 @@
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==

"@eclipse-glsp/client@~2.0.0":
"@eclipse-glsp/client@2.0.0", "@eclipse-glsp/client@~2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@eclipse-glsp/client/-/client-2.0.0.tgz#1100d7b2de0b4a92f4397417cc196fe7324af0f5"
integrity sha512-Bi3Pxv0jgr8uyKTtxivPEfD6nHYPB35a0+eaq5wjxymB3VlX3Fgguy+dAmO9P2FexRN/d4Wh8ZwWhtjC3XP0AQ==
Expand Down

0 comments on commit 877dcf2

Please sign in to comment.