Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix auto-naming counter and align id with name when creating new entity #69

Merged
merged 2 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import { CrossModelSourceModel, CrossModelState } from './cross-model-state.js';
export class CrossModelCommand extends JsonRecordingCommand<CrossModelSourceModel> {
constructor(
protected state: CrossModelState,
protected runnable: () => MaybePromise<void>
protected runnable: () => MaybePromise<void>,
protected undoAction?: () => MaybePromise<void>,
protected redoAction?: () => MaybePromise<void>
) {
super(state, runnable);
}
Expand All @@ -33,13 +35,15 @@ export class CrossModelCommand extends JsonRecordingCommand<CrossModelSourceMode
override async undo(): Promise<void> {
if (this.undoPatch) {
const result = this.applyPatch(await this.getJsonObject(), this.undoPatch);
await this.undoAction?.();
await this.postChange?.(result.newDocument);
}
}

override async redo(): Promise<void> {
if (this.redoPatch) {
const result = this.applyPatch(await this.getJsonObject(), this.redoPatch);
await this.redoAction?.();
await this.postChange?.(result.newDocument);
}
}
Expand Down
6 changes: 3 additions & 3 deletions extensions/crossmodel-lang/src/glsp-server/common/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@
********************************************************************************/

import { ATTRIBUTE_COMPARTMENT_TYPE, createLeftPortId, createRightPortId } from '@crossbreeze/protocol';
import { GCompartment, GCompartmentBuilder, GLabel, GPort } from '@eclipse-glsp/server';
import { DefaultTypes, GCompartment, GCompartmentBuilder, GLabel, GPort } from '@eclipse-glsp/server';
import { Attribute } from '../../language-server/generated/ast.js';
import { CrossModelIndex } from './cross-model-index.js';

export function createHeader(text: string, containerId: string): GCompartment {
export function createHeader(text: string, containerId: string, labelType = DefaultTypes.LABEL): GCompartment {
return GCompartment.builder()
.id(`${containerId}_header`)
.layout('hbox')
.addLayoutOption('hAlign', 'center')
.addLayoutOption('vAlign', 'center')
.addLayoutOption('paddingTop', 3)
.addCssClass('header-compartment')
.add(GLabel.builder().text(text).id(`${containerId}_label`).addCssClass('header-label').build())
.add(GLabel.builder().type(labelType).text(text).id(`${containerId}_label`).addCssClass('header-label').build())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/********************************************************************************
* Copyright (c) 2024 CrossBreeze.
********************************************************************************/

import { toId } from '@crossbreeze/protocol';
import { ApplyLabelEditOperation, Command, getOrThrow, JsonOperationHandler, ModelState } from '@eclipse-glsp/server';
import { inject, injectable } from 'inversify';
import { CrossModelRoot, Entity, EntityNode } from '../../../language-server/generated/ast.js';
import { findDocument } from '../../../language-server/util/ast-util.js';
import { CrossModelCommand } from '../../common/cross-model-command.js';
import { SystemModelState } from '../model/system-model-state.js';

@injectable()
export class SystemDiagramApplyLabelEditOperationHandler extends JsonOperationHandler {
readonly operationType = ApplyLabelEditOperation.KIND;
@inject(ModelState) declare modelState: SystemModelState;

createCommand(operation: ApplyLabelEditOperation): Command {
const entityNode = getOrThrow(this.modelState.index.findEntityNode(operation.labelId), 'Entity node not found');
const entity = getOrThrow(entityNode.entity.ref, 'Entity not found');
const oldName = entity.name;
return new CrossModelCommand(
this.modelState,
() => this.renameEntity(entityNode, entity, operation.text),
() =>
this.renameEntity(
getOrThrow(this.modelState.index.findEntityNode(operation.labelId), 'Entity node not found'),
getOrThrow(entityNode.entity.ref, 'Entity not found'),
oldName ?? this.modelState.idProvider.findNextId(Entity, 'NewEntity')
),
() =>
this.renameEntity(
getOrThrow(this.modelState.index.findEntityNode(operation.labelId), 'Entity node not found'),
getOrThrow(entityNode.entity.ref, 'Entity not found'),
operation.text
)
);
}

protected async renameEntity(entityNode: EntityNode, entity: Entity, name: string): Promise<void> {
entity.name = name;
const document = findDocument<CrossModelRoot>(entity)!;
const references = Array.from(
this.modelState.services.language.references.References.findReferences(entity, { includeDeclaration: false })
);
if (references.length === 0 || (references.length === 1 && references[0].sourceUri.fsPath === this.modelState.sourceUri)) {
// if the diagram is the only reference to the entity, we can safely rename it
// otherwise we need to ensure to implement proper rename behavior
entity.id = this.modelState.idProvider.findNextGlobalId(Entity, toId(entity.name));
entityNode.entity = { $refText: entity.id, ref: entity };
}
await this.modelState.modelService.save({
uri: document.uri.toString(),
model: document.parseResult.value,
clientId: this.modelState.clientId
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
********************************************************************************/
import { ENTITY_NODE_TYPE } from '@crossbreeze/protocol';
import {
Action,
ActionDispatcher,
Command,
CreateNodeOperation,
Expand All @@ -23,7 +24,7 @@ export class SystemDiagramCreateEntityOperationHandler extends JsonCreateNodeOpe
override label = 'Create Entity';
elementTypeIds = [ENTITY_NODE_TYPE];

@inject(ModelState) protected override modelState!: SystemModelState;
@inject(ModelState) protected declare modelState: SystemModelState;
@inject(ActionDispatcher) protected actionDispatcher!: ActionDispatcher;

override createCommand(operation: CreateNodeOperation): MaybePromise<Command | undefined> {
Expand Down Expand Up @@ -52,6 +53,10 @@ export class SystemDiagramCreateEntityOperationHandler extends JsonCreateNodeOpe
customProperties: []
};
container.nodes.push(node);
this.actionDispatcher.dispatchAfterNextUpdate({
kind: 'EditLabel',
labelId: `${this.modelState.index.createId(node)}_label`
} as Action);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/********************************************************************************
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/
import { ENTITY_NODE_TYPE, REFERENCE_CONTAINER_TYPE, REFERENCE_PROPERTY, REFERENCE_VALUE } from '@crossbreeze/protocol';
import { ENTITY_NODE_TYPE, LABEL_ENTITY, REFERENCE_CONTAINER_TYPE, REFERENCE_PROPERTY, REFERENCE_VALUE } from '@crossbreeze/protocol';
import { ArgsUtil, GNode, GNodeBuilder } from '@eclipse-glsp/server';
import { EntityNode } from '../../../language-server/generated/ast.js';
import { getAttributes } from '../../../language-server/util/ast-util.js';
Expand Down Expand Up @@ -30,7 +30,7 @@ export class GEntityNodeBuilder extends GNodeBuilder<GEntityNode> {
this.addArg(REFERENCE_VALUE, node.entity.$refText);

// Add the label/name of the node
this.add(createHeader(entityRef?.name || entityRef?.id || 'unresolved', this.proxy.id));
this.add(createHeader(entityRef?.name || entityRef?.id || 'unresolved', this.proxy.id, LABEL_ENTITY));

// Add the children of the node
const attributes = getAttributes(node);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Copyright (c) 2024 CrossBreeze.
********************************************************************************/
import { injectable } from 'inversify';
import { AstNode } from 'langium';
import {
Entity,
EntityNode,
Expand Down Expand Up @@ -31,4 +32,11 @@ export class SystemModelIndex extends CrossModelIndex {
findRelationshipEdge(id: string): RelationshipEdge | undefined {
return this.findSemanticElement(id, isRelationshipEdge);
}

protected override indexAstNode(node: AstNode): void {
super.indexAstNode(node);
if (isEntityNode(node)) {
this.indexSemanticElement(`${this.createId(node)}_label`, node);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import { injectable } from 'inversify';
import { CrossModelIndex } from '../common/cross-model-index.js';
import { CrossModelState } from '../common/cross-model-state.js';
import { CrossModelStorage } from '../common/cross-model-storage.js';
import { CrossModelSubmissionHandler } from '../common/cross-model-submission-handler.js';
import { SystemDiagramAddEntityActionProvider } from './command-palette/add-entity-action-provider.js';
import { SystemDiagramAddEntityOperationHandler } from './handler/add-entity-operation-handler.js';
import { SystemDiagramApplyLabelEditOperationHandler } from './handler/apply-edit-operation-handler.js';
import { SystemDiagramChangeBoundsOperationHandler } from './handler/change-bounds-operation-handler.js';
import { SystemDiagramCreateEntityOperationHandler } from './handler/create-entity-operation-handler.js';
import { SystemDiagramCreateRelationshipOperationHandler } from './handler/create-relationship-operation-handler.js';
Expand All @@ -33,7 +35,6 @@ import { SystemModelIndex } from './model/system-model-index.js';
import { SystemModelState } from './model/system-model-state.js';
import { SystemDiagramConfiguration } from './system-diagram-configuration.js';
import { SystemToolPaletteProvider } from './tool-palette/system-tool-palette-provider.js';
import { CrossModelSubmissionHandler } from '../common/cross-model-submission-handler.js';

/**
* Provides configuration about our system diagrams.
Expand Down Expand Up @@ -62,6 +63,7 @@ export class SystemDiagramModule extends DiagramModule {
binding.add(SystemDiagramDropEntityOperationHandler);
binding.add(SystemDiagramAddEntityOperationHandler);
binding.add(SystemDiagramCreateEntityOperationHandler);
binding.add(SystemDiagramApplyLabelEditOperationHandler);
}

protected override configureContextActionProviders(binding: MultiBinding<ContextActionsProvider>): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ export class CrossModelDocumentBuilder extends DefaultDocumentBuilder {
}

override update(changed: URI[], deleted: URI[], cancelToken?: CancellationToken | undefined): Promise<void> {
return super.update(
changed.flatMap(uri => this.flattenAndAdaptURI(uri)),
deleted.flatMap(uri => this.collectDeletedURIs(uri)),
cancelToken
);
const changedURIs = changed.flatMap(uri => this.flattenAndAdaptURI(uri));
const deletedURIs = deleted.flatMap(uri => this.collectDeletedURIs(uri));
for (const deletedUri of deletedURIs) {
// ensure associated text documents are deleted as otherwise we face problems if documents with same URI are created
this.services.workspace.TextDocuments.delete(deletedUri);
}
return super.update(changedURIs, deletedURIs, cancelToken);
}

protected flattenAndAdaptURI(uri: URI): URI[] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,7 @@ export interface CrossModelAddedSharedServices {
export const CrossModelSharedServices = Symbol('CrossModelSharedServices');
export type CrossModelSharedServices = Omit<LangiumSharedServices, 'ServiceRegistry'> &
CrossModelAddedSharedServices &
AddedSharedModelServices & {
CrossModel: CrossModelServices;
};
AddedSharedModelServices;

export const CrossModelSharedModule: Module<
CrossModelSharedServices,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/

import { findNextUnique, identity } from '@crossbreeze/protocol';
import { AstNode, AstUtils, CstNode, GrammarUtils, isAstNode, NameProvider } from 'langium';
import { CrossModelServices } from './cross-model-module.js';
import { UNKNOWN_PROJECT_REFERENCE } from './cross-model-package-manager.js';
Expand Down Expand Up @@ -135,22 +136,13 @@ export class DefaultIdProvider implements NameProvider, IdProvider {
.map(this.getNodeId)
.nonNullable()
.toArray();
return this.countToNextId(knownIds, proposal);
return findNextUnique(proposal, knownIds, identity);
}

findNextGlobalId(type: string, proposal: string | undefined = 'Element'): string {
const knownIds = this.services.shared.workspace.IndexManager.allElements(type)
.map(element => element.name)
.toArray();
return this.countToNextId(knownIds, proposal);
}

protected countToNextId(knownIds: string[], proposal: string): string {
let nextId = proposal;
let counter = 1;
while (knownIds.includes(nextId)) {
nextId = proposal + counter++;
}
return nextId;
return findNextUnique(proposal, knownIds, identity);
}
}
5 changes: 3 additions & 2 deletions extensions/crossmodel-lang/src/model-server/model-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,12 @@ export class ModelService {
*/
async save(args: SaveModelArgs<CrossModelRoot>): Promise<void> {
// sync: implicit update of internal data structure to match file system (similar to workspace initialization)
const text = typeof args.model === 'string' ? args.model : this.serialize(URI.parse(args.uri), args.model);
if (this.documents.hasDocument(URI.parse(args.uri))) {
await this.update(args);
} else {
this.documents.createDocument(URI.parse(args.uri), text);
}

const text = typeof args.model === 'string' ? args.model : this.serialize(URI.parse(args.uri), args.model);
return this.documentManager.save(args.uri, text, args.clientId);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ export class OpenTextDocumentManager {
return this.documentBuilder.onBuildPhase(DocumentState.Validated, (allChangedDocuments, _token) => {
const changedDocument = allChangedDocuments.find(document => document.uri.toString() === uri);
if (changedDocument) {
const buildTrigger = allChangedDocuments.find(document => document.uri.toString() === this.lastUpdate?.changed?.[0].toString());
const buildTrigger = allChangedDocuments.find(
document => document.uri.toString() === this.lastUpdate?.changed?.[0]?.toString()
);
const sourceClientId = this.getSourceClientId(buildTrigger ?? changedDocument, allChangedDocuments);
const event: ModelUpdatedEvent<AstCrossModelDocument> = {
document: {
Expand Down
52 changes: 50 additions & 2 deletions packages/glsp-client/src/browser/system-diagram/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,56 @@
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/

import { GEdge, RectangularNode } from '@eclipse-glsp/client';
import {
BoundsAware,
Dimension,
EditableLabel,
GChildElement,
GEdge,
GLabel,
GModelElement,
GParentElement,
isEditableLabel,
isParent,
ModelFilterPredicate,
RectangularNode,
WithEditableLabel
} from '@eclipse-glsp/client';

export class EntityNode extends RectangularNode {}
export class EntityNode extends RectangularNode implements WithEditableLabel {
get editableLabel(): (GChildElement & EditableLabel) | undefined {
return findElementBy(this, isEditableLabel) as (GChildElement & EditableLabel) | undefined;
}
}

export class RelationshipEdge extends GEdge {}

export class GEditableLabel extends GLabel implements EditableLabel {
editControlPositionCorrection = {
x: -9,
y: -7
};

get editControlDimension(): Dimension {
const parentBounds = (this.parent as any as BoundsAware).bounds;
return {
width: parentBounds?.width ? parentBounds?.width + 5 : this.bounds.width - 10,
height: parentBounds?.height ? parentBounds.height + 3 : 100
};
}
}

export function findElementBy<T>(parent: GParentElement, predicate: ModelFilterPredicate<T>): (GModelElement & T) | undefined {
if (predicate(parent)) {
return parent;
}
if (isParent(parent)) {
for (const child of parent.children) {
const result = findElementBy(child, predicate);
if (result !== undefined) {
return result;
}
}
}
return undefined;
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
/********************************************************************************
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/
import { ATTRIBUTE_COMPARTMENT_TYPE, ENTITY_NODE_TYPE, RELATIONSHIP_EDGE_TYPE } from '@crossbreeze/protocol';
import { ATTRIBUTE_COMPARTMENT_TYPE, ENTITY_NODE_TYPE, LABEL_ENTITY, RELATIONSHIP_EDGE_TYPE } from '@crossbreeze/protocol';
import {
ContainerConfiguration,
GLabelView,
configureDefaultModelElements,
configureModelElement,
editLabelFeature,
gridModule,
initializeDiagramContainer
initializeDiagramContainer,
withEditLabelFeature
} from '@eclipse-glsp/client';
import { GLSPDiagramConfiguration } from '@eclipse-glsp/theia-integration';
import { Container } from '@theia/core/shared/inversify/index';
Expand All @@ -16,7 +19,7 @@ import { createCrossModelDiagramModule } from '../crossmodel-diagram-module';
import { AttributeCompartment } from '../model';
import { AttributeCompartmentView } from '../views';
import { systemEdgeCreationToolModule } from './edge-creation-tool/edge-creation-tool-module';
import { EntityNode, RelationshipEdge } from './model';
import { EntityNode, GEditableLabel, RelationshipEdge } from './model';
import { systemNodeCreationModule } from './node-creation-tool/node-creation-tool-module';
import { systemSelectModule } from './select-tool/select-tool-module';
import { EntityNodeView, RelationshipEdgeView } from './views';
Expand Down Expand Up @@ -50,7 +53,8 @@ const systemDiagramModule = createCrossModelDiagramModule((bind, unbind, isBound
// The glsp-server can send a request to render a specific view given a type, e.g. node:entity
// The model class holds the client-side model and properties
// The view class shows how to draw the svg element given the properties of the model class
configureModelElement(context, ENTITY_NODE_TYPE, EntityNode, EntityNodeView);
configureModelElement(context, ENTITY_NODE_TYPE, EntityNode, EntityNodeView, { enable: [withEditLabelFeature] });
configureModelElement(context, RELATIONSHIP_EDGE_TYPE, RelationshipEdge, RelationshipEdgeView);
configureModelElement(context, ATTRIBUTE_COMPARTMENT_TYPE, AttributeCompartment, AttributeCompartmentView);
configureModelElement(context, LABEL_ENTITY, GEditableLabel, GLabelView, { enable: [editLabelFeature] });
});
8 changes: 8 additions & 0 deletions packages/glsp-client/style/diagram.css
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,11 @@
.sprotty .palette-header {
flex-direction: row;
}

.label-edit input {
text-align: center;
background-color: transparent;
outline-color: transparent;
border: 0;
letter-spacing: 0.9px;
}
Loading
Loading