From 74281a4950467d47d8b17ff1aada3248438bd015 Mon Sep 17 00:00:00 2001 From: Martin Fleck Date: Mon, 12 Feb 2024 10:13:35 +0100 Subject: [PATCH] Ensure implicit attributes are properly re-calculated --- .../common/cross-model-layout-configurator.ts | 20 ----------------- .../glsp-server/common/cross-model-storage.ts | 22 ++++++++++++++----- .../src/language-server/cross-model-module.ts | 5 ++++- .../references/cross-model-linker.ts | 17 ++++++++++++++ .../src/language-server/util/ast-util.ts | 11 +++++++--- .../src/model-server/model-service.ts | 2 +- .../open-text-document-manager.ts | 6 +++-- .../model-server/openable-text-documents.ts | 6 +++-- 8 files changed, 54 insertions(+), 35 deletions(-) delete mode 100644 extensions/crossmodel-lang/src/glsp-server/common/cross-model-layout-configurator.ts create mode 100644 extensions/crossmodel-lang/src/language-server/references/cross-model-linker.ts diff --git a/extensions/crossmodel-lang/src/glsp-server/common/cross-model-layout-configurator.ts b/extensions/crossmodel-lang/src/glsp-server/common/cross-model-layout-configurator.ts deleted file mode 100644 index e372269..0000000 --- a/extensions/crossmodel-lang/src/glsp-server/common/cross-model-layout-configurator.ts +++ /dev/null @@ -1,20 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2023 CrossBreeze. - ********************************************************************************/ -import { AbstractLayoutConfigurator, LayoutOptions, configureELKLayoutModule } from '@eclipse-glsp/layout-elk'; -import { GGraph } from '@eclipse-glsp/server'; -import { ContainerModule, injectable } from 'inversify'; - -@injectable() -export class CrossModelLayoutConfigurator extends AbstractLayoutConfigurator { - protected override graphOptions(graph: GGraph): LayoutOptions | undefined { - return { - 'elk.algorithm': 'layered' - }; - } -} - -export function createLayoutModule(): ContainerModule { - // use Eclipse Layout Kernel with our custom layered layout configuration - return configureELKLayoutModule({ algorithms: ['layered'], layoutConfigurator: CrossModelLayoutConfigurator }); -} diff --git a/extensions/crossmodel-lang/src/glsp-server/common/cross-model-storage.ts b/extensions/crossmodel-lang/src/glsp-server/common/cross-model-storage.ts index 4398dfb..976e84a 100644 --- a/extensions/crossmodel-lang/src/glsp-server/common/cross-model-storage.ts +++ b/extensions/crossmodel-lang/src/glsp-server/common/cross-model-storage.ts @@ -7,7 +7,6 @@ import { ClientSession, ClientSessionListener, ClientSessionManager, - Disposable, DisposableCollection, GLSPServerError, Logger, @@ -48,20 +47,31 @@ export class CrossModelStorage implements SourceModelStorage, ClientSessionListe // load semantic model from document in language model service const sourceUri = this.getSourceUri(action); const rootUri = URI.file(sourceUri).toString(); - await this.state.modelService.open({ uri: rootUri, clientId: this.state.clientId }); - this.toDispose.push(Disposable.create(() => this.state.modelService.close({ uri: rootUri, clientId: this.state.clientId }))); - const root = await this.state.modelService.request(rootUri, isCrossModelRoot); - this.state.setSemanticRoot(rootUri, root); + const root = await this.update(rootUri); + if (!root) { + return; + } + this.toDispose.push(await this.state.modelService.open({ uri: rootUri, clientId: this.state.clientId })); this.toDispose.push( this.state.modelService.onUpdate(rootUri, async event => { if (this.state.clientId !== event.sourceClientId || event.reason !== 'changed') { - this.state.setSemanticRoot(rootUri, event.model); + await this.update(rootUri, event.model); this.actionDispatcher.dispatchAll(await this.submissionHandler.submitModel('external')); } }) ); } + protected async update(uri: string, root?: CrossModelRoot): Promise { + const newRoot = root ?? (await this.state.modelService.request(uri, isCrossModelRoot)); + if (newRoot) { + this.state.setSemanticRoot(uri, newRoot); + } else { + this.logger.error('Could not find model for ' + uri); + } + return newRoot; + } + saveSourceModel(action: SaveModelAction): MaybePromise { const saveUri = this.getFileUri(action); diff --git a/extensions/crossmodel-lang/src/language-server/cross-model-module.ts b/extensions/crossmodel-lang/src/language-server/cross-model-module.ts index b5223e2..a6893b1 100644 --- a/extensions/crossmodel-lang/src/language-server/cross-model-module.ts +++ b/extensions/crossmodel-lang/src/language-server/cross-model-module.ts @@ -40,6 +40,7 @@ import { CrossModelGeneratedModule, CrossModelGeneratedSharedModule } from './ge import { createCrossModelCompletionParser } from './lexer/cross-model-completion-parser.js'; import { CrossModelLexer } from './lexer/cross-model-lexer.js'; import { CrossModelTokenBuilder } from './lexer/cross-model-token-generator.js'; +import { CrossModelLinker } from './references/cross-model-linker.js'; /*************************** * Shared Module @@ -128,6 +129,7 @@ export interface CrossModelModuleContext { export interface CrossModelAddedServices { references: { IdProvider: DefaultIdProvider; + Linker: CrossModelLinker; }; validation: { CrossModelValidator: CrossModelValidator; @@ -158,7 +160,8 @@ export function createCrossModelModule( ScopeComputation: services => new CrossModelScopeComputation(services), ScopeProvider: services => new CrossModelScopeProvider(services), IdProvider: services => new DefaultIdProvider(services), - NameProvider: services => services.references.IdProvider + NameProvider: services => services.references.IdProvider, + Linker: services => new CrossModelLinker(services) }, validation: { CrossModelValidator: services => new CrossModelValidator(services) diff --git a/extensions/crossmodel-lang/src/language-server/references/cross-model-linker.ts b/extensions/crossmodel-lang/src/language-server/references/cross-model-linker.ts new file mode 100644 index 0000000..8c37f75 --- /dev/null +++ b/extensions/crossmodel-lang/src/language-server/references/cross-model-linker.ts @@ -0,0 +1,17 @@ +/******************************************************************************** + * Copyright (c) 2024 CrossBreeze. + ********************************************************************************/ + +import { AstNode, DefaultLinker, DocumentState, LangiumDocument } from 'langium'; +import { isMapping, isSystemDiagram } from '../generated/ast.js'; +import { hasSemanticRoot } from '../util/ast-util.js'; + +export class CrossModelLinker extends DefaultLinker { + override unlink(document: LangiumDocument): void { + super.unlink(document); + if (hasSemanticRoot(document, isMapping) || hasSemanticRoot(document, isSystemDiagram)) { + // we want to re-compute the implicit attributes for our nodes + document.state = Math.min(document.state, DocumentState.IndexedContent); + } + } +} diff --git a/extensions/crossmodel-lang/src/language-server/util/ast-util.ts b/extensions/crossmodel-lang/src/language-server/util/ast-util.ts index c89e670..22f1774 100644 --- a/extensions/crossmodel-lang/src/language-server/util/ast-util.ts +++ b/extensions/crossmodel-lang/src/language-server/util/ast-util.ts @@ -174,10 +174,11 @@ export function createAttributeMappingTarget(container: AttributeMapping, target /** * Retrieve the document in which the given AST node is contained. A reference to the document is * usually held by the root node of the AST. - * - * @throws an error if the node is not contained in a document. */ -export function findDocument(node: AstNode): LangiumDocument | undefined { +export function findDocument(node?: AstNode): LangiumDocument | undefined { + if (!node) { + return undefined; + } const rootNode = findRootNode(node); const result = rootNode.$document; return result ? >result : undefined; @@ -198,3 +199,7 @@ export function findSemanticRoot(document: LangiumDocument): Ent const root = document.parseResult.value; return root.entity ?? root.mapping ?? root.relationship ?? root.systemDiagram; } + +export function hasSemanticRoot(document: LangiumDocument, guard: (item: unknown) => item is T): boolean { + return guard(findSemanticRoot(document)); +} diff --git a/extensions/crossmodel-lang/src/model-server/model-service.ts b/extensions/crossmodel-lang/src/model-server/model-service.ts index aa4efc4..1d2430b 100644 --- a/extensions/crossmodel-lang/src/model-server/model-service.ts +++ b/extensions/crossmodel-lang/src/model-server/model-service.ts @@ -61,7 +61,7 @@ export class ModelService { * * @param uri document URI */ - async open(args: OpenModelArgs): Promise { + async open(args: OpenModelArgs): Promise { return this.documentManager.open(args); } diff --git a/extensions/crossmodel-lang/src/model-server/open-text-document-manager.ts b/extensions/crossmodel-lang/src/model-server/open-text-document-manager.ts index 221011e..39579c6 100644 --- a/extensions/crossmodel-lang/src/model-server/open-text-document-manager.ts +++ b/extensions/crossmodel-lang/src/model-server/open-text-document-manager.ts @@ -81,7 +81,8 @@ export class OpenTextDocumentManager { return this.documentBuilder.onBuildPhase(DocumentState.Validated, (allChangedDocuments, _token) => { const changedDocument = allChangedDocuments.find(document => document.uri.toString() === uri); if (changedDocument) { - const sourceClientId = this.getSourceClientId(changedDocument, allChangedDocuments); + const buildTrigger = allChangedDocuments.find(document => document.uri.toString() === this.lastUpdate?.changed?.[0].toString()); + const sourceClientId = this.getSourceClientId(buildTrigger ?? changedDocument, allChangedDocuments); const event: ModelUpdatedEvent = { model: changedDocument.parseResult.value as T, sourceClientId, @@ -109,12 +110,13 @@ export class OpenTextDocumentManager { ); } - async open(args: OpenModelArgs): Promise { + async open(args: OpenModelArgs): Promise { // only create a dummy document if it is already open as we use the synced state anyway const textDocument = this.isOpen(args.uri) ? this.createDummyDocument(args.uri) : await this.createDocumentFromFileSystem(args.uri, args.languageId); this.textDocuments.notifyDidOpenTextDocument({ textDocument }, args.clientId); + return Disposable.create(() => this.close(args)); } async close(args: CloseModelArgs): Promise { diff --git a/extensions/crossmodel-lang/src/model-server/openable-text-documents.ts b/extensions/crossmodel-lang/src/model-server/openable-text-documents.ts index 820631b..6ae3e18 100644 --- a/extensions/crossmodel-lang/src/model-server/openable-text-documents.ts +++ b/extensions/crossmodel-lang/src/model-server/openable-text-documents.ts @@ -232,8 +232,10 @@ export class OpenableTextDocuments extends TextDocuments } } - getChangeSource(uri: string, version: number): string | undefined { - return this.__changeHistory.get(uri)?.[version]; + getChangeSource(uri: string, version?: number): string | undefined { + const history = this.__changeHistory.get(uri); + // given version or last entry + return version ? history?.[version] : history?.at(-1); } isOpen(uri: string): boolean {