Skip to content

Commit

Permalink
Improve stability of cross-updates and fix edge structure
Browse files Browse the repository at this point in the history
- Ensure edges (relationships) are created between nodes (entities)
-- Ensure node entity matches expected relationship entity

- Improve stability of cross-updates
-- Ensure 'save' does an implicit 'update' of the internal structure
-- Ensure update only updates text editor when it was opened
-- Only do full textual updates to avoid merging issues
-- Let UI react to updates, not only on save
-- Debounce model update for form editor

- Minors
-- Adapt grammar to better reflect semantic element (instead of 'for')
-- Always serialize properties in same order
  • Loading branch information
martin-fleck-at committed Oct 4, 2023
1 parent 03a1a0d commit c3f8a67
Show file tree
Hide file tree
Showing 23 changed files with 284 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class CrossModelAddEntityOperationHandler extends OperationHandler {
$type: DiagramNode,
$container: container,
name: findAvailableNodeName(container, entityDescription.name + 'Node'),
for: {
entity: {
$refText: entityDescription.name,
ref: entityDescription.node as Entity | undefined
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ export class CrossModelCreateEdgeOperationHandler extends OperationHandler imple
$type: DiagramEdge,
$container: this.state.diagramRoot,
name: relationship.name,
for: { ref: relationship, $refText: this.state.nameProvider.getName(relationship) || relationship.name || '' }
relationship: { ref: relationship, $refText: this.state.nameProvider.getName(relationship) || relationship.name || '' },
sourceNode: { ref: sourceNode, $refText: this.state.nameProvider.getLocalName(sourceNode) || sourceNode.name || '' },
targetNode: { ref: targetNode, $refText: this.state.nameProvider.getLocalName(targetNode) || targetNode.name || '' }
};
this.state.diagramRoot.edges.push(edge);
}
Expand All @@ -59,8 +61,8 @@ export class CrossModelCreateEdgeOperationHandler extends OperationHandler imple
* Creates a new relationship and stores it on a file on the file system.
*/
protected async createAndSaveRelationship(sourceNode: DiagramNode, targetNode: DiagramNode): Promise<Relationship | undefined> {
const source = sourceNode.for?.ref?.name || sourceNode.for?.$refText;
const target = targetNode.for?.ref?.name || targetNode.for?.$refText;
const source = sourceNode.entity?.ref?.name || sourceNode.entity?.$refText;
const target = targetNode.entity?.ref?.name || targetNode.entity?.$refText;

// search for unique file name for the relationship and use file base name as relationship name
// if the user doesn't rename any files we should end up with unique names ;-)
Expand All @@ -76,8 +78,8 @@ export class CrossModelCreateEdgeOperationHandler extends OperationHandler imple
$container: relationshipRoot,
name,
type: '1:1',
parent: { $refText: sourceNode.for?.$refText || '' },
child: { $refText: targetNode.for?.$refText || '' }
parent: { $refText: sourceNode.entity?.$refText || '' },
child: { $refText: targetNode.entity?.$refText || '' }
};
relationshipRoot.relationship = relationship;
const text = this.state.semanticSerializer.serialize(relationshipRoot);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class CrossModelDropEntityOperationHandler extends OperationHandler {
$type: DiagramNode,
$container: container,
name: findAvailableNodeName(container, root.entity.name + 'Node'),
for: {
entity: {
$refText: this.state.nameProvider.getFullyQualifiedName(root.entity) || root.entity.name || '',
ref: root.entity
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class GEntityNode extends GNode {
export class GEntityNodeBuilder extends GNodeBuilder {
addNode(node: DiagramNode): this {
// Get the reference that the DiagramNode holds to the Entity in the .langium file.
const entityRef = node.for?.ref;
const entityRef = node.entity?.ref;

// Options which are the same for every node
this.addCssClasses('diagram-node', 'entity').layout('vbox').addArgs(ArgsUtil.cornerRadius(3));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import { GEdge, GGraph, GModelFactory, GNode } from '@eclipse-glsp/server';
import { inject, injectable } from 'inversify';
import { DiagramEdge, DiagramNode } from '../../language-server/generated/ast';
import { CrossModelState } from './cross-model-state';
import { GEntityNode } from './builders/node-builder';
import { CrossModelState } from './cross-model-state';

/**
* Custom factory that translates the semantic diagram root from Langium to a GLSP graph.
Expand All @@ -29,7 +29,7 @@ export class CrossModelGModelFactory implements GModelFactory {
const graphBuilder = GGraph.builder().id(this.modelState.semanticUri);

diagramRoot.nodes.map(node => this.createDiagramNode(node)).forEach(node => graphBuilder.add(node));
diagramRoot.edges.map(edge => this.createDiagramEdge(edge, diagramRoot.nodes)).forEach(edge => graphBuilder.add(edge));
diagramRoot.edges.map(edge => this.createDiagramEdge(edge)).forEach(edge => graphBuilder.add(edge));

return graphBuilder.build();
}
Expand All @@ -41,14 +41,11 @@ export class CrossModelGModelFactory implements GModelFactory {
return GEntityNode.builder().id(id).addNode(node).build();
}

protected createDiagramEdge(edge: DiagramEdge, diagramNodes: DiagramNode[]): GEdge {
protected createDiagramEdge(edge: DiagramEdge): GEdge {
const id = this.modelState.index.createId(edge) ?? 'unknown';

const parentRef = edge.for?.ref?.parent?.$refText;
const childRef = edge.for?.ref?.child?.$refText;

const parentDiagramNode = diagramNodes.find(item => item.for?.ref?.name === parentRef)?.name;
const childDiagramNode = diagramNodes.find(item => item.for?.ref?.name === childRef)?.name;
const parentDiagramNode = edge.sourceNode?.ref?.name || edge.sourceNode?.$refText;
const childDiagramNode = edge.targetNode?.ref?.name || edge.targetNode?.$refText;

return GEdge.builder()
.id(id)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/********************************************************************************
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/
import { DefaultLanguageServer } from 'langium';
import { TextDocumentSyncKind, type InitializeParams, type InitializeResult } from 'vscode-languageserver-protocol';

export class CrossModelLanguageServer extends DefaultLanguageServer {
override async initialize(params: InitializeParams): Promise<InitializeResult> {
const result = await super.initialize(params);
result.capabilities.textDocumentSync = TextDocumentSyncKind.Full;
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { CrossModelCompletionProvider } from './cross-model-completion-provider'
import { CrossModelDocumentBuilder } from './cross-model-document-builder';
import { CrossModelModelFormatter } from './cross-model-formatter';
import { CrossModelLangiumDocuments } from './cross-model-langium-documents';
import { CrossModelLanguageServer } from './cross-model-language-server';
import { QualifiedNameProvider } from './cross-model-naming';
import { CrossModelPackageManager } from './cross-model-package-manager';
import { CrossModelScopeComputation } from './cross-model-scope';
Expand All @@ -36,8 +37,8 @@ import { CrossModelSerializer } from './cross-model-serializer';
import { CrossModelValidator, registerValidationChecks } from './cross-model-validator';
import { CrossModelWorkspaceManager } from './cross-model-workspace-manager';
import { CrossModelGeneratedModule, CrossModelGeneratedSharedModule } from './generated/module';
import { CrossModelTokenBuilder } from './lexer/cross-model-token-generator';
import { CrossModelLexer } from './lexer/cross-model-lexer';
import { CrossModelTokenBuilder } from './lexer/cross-model-token-generator';

/***************************
* Shared Module
Expand Down Expand Up @@ -101,6 +102,9 @@ export const CrossModelSharedModule: Module<
logger: {
ClientLogger: services => new ClientLogger(services)
},
lsp: {
LanguageServer: services => new CrossModelLanguageServer(services)
},
model: {
ModelService: services => new ModelService(services)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,27 @@ import { Serializer } from '../model-server/serializer';
import { CrossModelServices } from './cross-model-module';
import { CrossModelRoot, Entity, Relationship, SystemDiagram } from './generated/ast';

const PROPERTY_ORDER = [
'id',
'name',
'datatype',
'description',
'attributes',
'parent',
'child',
'type',
'nodes',
'edges',
'entity',
'x',
'y',
'width',
'height',
'relationship',
'sourceNode',
'targetNode'
];

/**
* Hand-written AST serializer as there is currently no out-of-the box serializer from Langium, but it is on the roadmap.
* cf. https://github.com/langium/langium/discussions/683
Expand Down Expand Up @@ -34,6 +55,7 @@ export class CrossModelSerializer implements Serializer<CrossModelRoot> {
const indentation = ' '.repeat(indentationLevel);

const serializedProperties = Object.entries(obj)
.sort((left, right) => PROPERTY_ORDER.indexOf(left[0]) - PROPERTY_ORDER.indexOf(right[0]))
.map(([key, value]) => {
if (Array.isArray(value) && value.length === 0) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
********************************************************************************/
import { ValidationAcceptor, ValidationChecks } from 'langium';
import type { CrossModelServices } from './cross-model-module';
import { CrossModelAstType, Entity, EntityAttribute, Relationship, SystemDiagram } from './generated/ast';
import { CrossModelAstType, DiagramEdge, Entity, EntityAttribute, Relationship, SystemDiagram } from './generated/ast';

/**
* Register custom validation checks.
Expand All @@ -16,7 +16,8 @@ export function registerValidationChecks(services: CrossModelServices): void {
Entity: validator.checkEntityHasNecessaryFields,
EntityAttribute: validator.checkAttributeHasNecessaryFields,
SystemDiagram: validator.checkSystemDiagramHasNecessaryFields,
Relationship: validator.checkRelationshipHasNecessaryFields
Relationship: validator.checkRelationshipHasNecessaryFields,
DiagramEdge: validator.checkDiagramEdge
};
registry.register(checks, validator);
}
Expand Down Expand Up @@ -48,4 +49,13 @@ export class CrossModelValidator {
accept('error', 'Attribute missing id field', { node: relationship, property: 'name' });
}
}

checkDiagramEdge(edge: DiagramEdge, accept: ValidationAcceptor): void {
if (edge.sourceNode?.ref?.entity?.ref?.$type !== edge.relationship?.ref?.parent?.ref?.$type) {
accept('error', 'Source must match type of parent', { node: edge, property: 'sourceNode' });
}
if (edge.targetNode?.ref?.entity?.ref?.$type !== edge.relationship?.ref?.child?.ref?.$type) {
accept('error', 'Target must match type of child', { node: edge, property: 'targetNode' });
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ DiagramNode:

DiagramNodeFields infers DiagramNode:
'id' ':' name=STRING |
'for' ':' for=[Entity:QualifiedName] |
'entity' ':' entity=[Entity:QualifiedName] |
'x' ':' x=NUMBER |
'y' ':' y=NUMBER |
'width' ':' width=NUMBER |
Expand All @@ -115,7 +115,9 @@ DiagramEdge:

DiagramEdgeFields infers DiagramEdge:
(
'for' ':' for=[Relationship:QualifiedName] |
'relationship' ':' relationship=[Relationship:QualifiedName] |
'sourceNode' ':' sourceNode=[DiagramNode:QualifiedName] |
'targetNode' ':' targetNode=[DiagramNode:QualifiedName] |
'id' ':' name=STRING
)
;
Expand Down
14 changes: 10 additions & 4 deletions extensions/crossmodel-lang/src/language-server/generated/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ export function isCrossModelRoot(item: unknown): item is CrossModelRoot {
export interface DiagramEdge extends AstNode {
readonly $container: SystemDiagram;
readonly $type: 'DiagramEdge';
for?: Reference<Relationship>
name?: string
relationship?: Reference<Relationship>
sourceNode?: Reference<DiagramNode>
targetNode?: Reference<DiagramNode>
}

export const DiagramEdge = 'DiagramEdge';
Expand All @@ -53,7 +55,7 @@ export interface DiagramNode extends AstNode {
readonly $container: SystemDiagram;
readonly $type: 'DiagramNode';
description?: string
for?: Reference<Entity>
entity?: Reference<Entity>
height?: number
name?: string
name_val?: string
Expand Down Expand Up @@ -158,10 +160,14 @@ export class CrossModelAstReflection extends AbstractAstReflection {
getReferenceType(refInfo: ReferenceInfo): string {
const referenceId = `${refInfo.container.$type}:${refInfo.property}`;
switch (referenceId) {
case 'DiagramEdge:for': {
case 'DiagramEdge:relationship': {
return Relationship;
}
case 'DiagramNode:for':
case 'DiagramEdge:sourceNode':
case 'DiagramEdge:targetNode': {
return DiagramNode;
}
case 'DiagramNode:entity':
case 'Relationship:child':
case 'Relationship:parent': {
return Entity;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -953,15 +953,15 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load
"elements": [
{
"$type": "Keyword",
"value": "for"
"value": "entity"
},
{
"$type": "Keyword",
"value": ":"
},
{
"$type": "Assignment",
"feature": "for",
"feature": "entity",
"operator": "=",
"terminal": {
"$type": "CrossReference",
Expand Down Expand Up @@ -1227,15 +1227,15 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load
"elements": [
{
"$type": "Keyword",
"value": "for"
"value": "relationship"
},
{
"$type": "Keyword",
"value": ":"
},
{
"$type": "Assignment",
"feature": "for",
"feature": "relationship",
"operator": "=",
"terminal": {
"$type": "CrossReference",
Expand All @@ -1254,6 +1254,70 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load
}
]
},
{
"$type": "Group",
"elements": [
{
"$type": "Keyword",
"value": "sourceNode"
},
{
"$type": "Keyword",
"value": ":"
},
{
"$type": "Assignment",
"feature": "sourceNode",
"operator": "=",
"terminal": {
"$type": "CrossReference",
"type": {
"$ref": "#/rules@11"
},
"terminal": {
"$type": "RuleCall",
"rule": {
"$ref": "#/rules@16"
},
"arguments": []
},
"deprecatedSyntax": false
}
}
]
},
{
"$type": "Group",
"elements": [
{
"$type": "Keyword",
"value": "targetNode"
},
{
"$type": "Keyword",
"value": ":"
},
{
"$type": "Assignment",
"feature": "targetNode",
"operator": "=",
"terminal": {
"$type": "CrossReference",
"type": {
"$ref": "#/rules@11"
},
"terminal": {
"$type": "RuleCall",
"rule": {
"$ref": "#/rules@16"
},
"arguments": []
},
"deprecatedSyntax": false
}
}
]
},
{
"$type": "Group",
"elements": [
Expand Down
Loading

0 comments on commit c3f8a67

Please sign in to comment.