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

Allow multiple diagrams to be opened #44

Merged
merged 8 commits into from
Dec 8, 2023
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { Command, JsonOperationHandler, ModelState } from '@eclipse-glsp/server'
import { inject, injectable } from 'inversify';
import { DiagramNode, Entity } from '../../language-server/generated/ast.js';
import { createNodeToEntityReference } from '../../language-server/util/ast-util.js';
import { findNextId } from '../../language-server/util/name-util.js';
import { CrossModelState } from '../model/cross-model-state.js';
import { CrossModelCommand } from './cross-model-command.js';

Expand All @@ -34,7 +33,7 @@ export class CrossModelAddEntityOperationHandler extends JsonOperationHandler {
const node: DiagramNode = {
$type: DiagramNode,
$container: container,
id: findNextId(container, entityDescription.name + 'Node'),
id: this.modelState.idProvider.findNextId(DiagramNode, entityDescription.name + 'Node', container),
entity: {
$refText: entityDescription.name,
ref: entityDescription.node as Entity | undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class CrossModelCreateEdgeOperationHandler extends JsonCreateEdgeOperatio
const edge: DiagramEdge = {
$type: DiagramEdge,
$container: this.modelState.diagramRoot,
id: relationship.id,
id: this.modelState.idProvider.findNextId(DiagramEdge, relationship.id, this.modelState.diagramRoot),
relationship: {
ref: relationship,
$refText: this.modelState.idProvider.getExternalId(relationship) || relationship.id || ''
Expand All @@ -58,23 +58,23 @@ export class CrossModelCreateEdgeOperationHandler extends JsonCreateEdgeOperatio
const source = sourceNode.entity?.ref?.id || sourceNode.entity?.$refText;
const target = targetNode.entity?.ref?.id || 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 ;-)
const dirName = UriUtils.dirname(URI.parse(this.modelState.semanticUri));
const targetUri = UriUtils.joinPath(dirName, source + 'To' + target + '.relationship.cm');
const uri = Utils.findNewUri(targetUri);
const id = UriUtils.basename(uri).split('.')[0];

// create relationship, serialize and re-read to ensure everything is up to date and linked properly
const relationshipRoot: CrossModelRoot = { $type: 'CrossModelRoot' };
const relationship: Relationship = {
$type: Relationship,
$container: relationshipRoot,
id,
id: this.modelState.idProvider.findNextId(Relationship, source + 'To' + target),
type: '1:1',
parent: { $refText: sourceNode.entity?.$refText || '' },
child: { $refText: 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 ;-)
const dirName = UriUtils.dirname(URI.parse(this.modelState.semanticUri));
const targetUri = UriUtils.joinPath(dirName, relationship.id + '.relationship.cm');
const uri = Utils.findNewUri(targetUri);

relationshipRoot.relationship = relationship;
const text = this.modelState.semanticSerializer.serialize(relationshipRoot);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { Command, JsonOperationHandler } from '@eclipse-glsp/server';
import { inject, injectable } from 'inversify';
import { URI } from 'vscode-uri';
import { CrossModelRoot, DiagramNode, isCrossModelRoot } from '../../language-server/generated/ast.js';
import { findNextId } from '../../language-server/util/name-util.js';
import { CrossModelState } from '../model/cross-model-state.js';
import { CrossModelCommand } from './cross-model-command.js';

Expand Down Expand Up @@ -37,7 +36,7 @@ export class CrossModelDropEntityOperationHandler extends JsonOperationHandler {
const node: DiagramNode = {
$type: DiagramNode,
$container: container,
id: findNextId(container, root.entity.id + 'Node'),
id: this.modelState.idProvider.findNextId(DiagramNode, root.entity.id + 'Node', this.modelState.diagramRoot),
entity: {
$refText: this.modelState.idProvider.getExternalId(root.entity) || root.entity.id || '',
ref: root.entity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/

import { AstNode, CstNode, findNodeForProperty, NameProvider } from 'langium';
import { AstNode, CstNode, findNodeForProperty, isAstNode, NameProvider, streamAst } from 'langium';
import { CrossModelServices } from './cross-model-module.js';
import { UNKNOWN_PROJECT_REFERENCE } from './cross-model-package-manager.js';
import { findDocument } from './util/ast-util.js';
Expand All @@ -25,6 +25,9 @@ export interface IdProvider extends NameProvider {
getNodeId(node?: AstNode): string | undefined;
getLocalId(node?: AstNode): string | undefined;
getExternalId(node?: AstNode): string | undefined;

findNextId(type: string, proposal: string | undefined): string;
findNextId(type: string, proposal: string | undefined, container: AstNode): string;
}

/**
Expand Down Expand Up @@ -103,4 +106,38 @@ export class DefaultIdProvider implements NameProvider, IdProvider {
getNameNode(node: AstNode): CstNode | undefined {
return findNodeForProperty(node.$cstNode, ID_PROPERTY);
}

findNextId(type: string, proposal: string | undefined): string;
findNextId(type: string, proposal: string | undefined, container: AstNode): string;
findNextId(type: string, proposal: string | undefined, container?: AstNode): string {
if (isAstNode(container)) {
return this.findNextIdInContainer(type, proposal ?? 'Element', container);
}
return this.findNextIdInIndex(type, proposal ?? 'Element');
}

protected findNextIdInContainer(type: string, proposal: string, container: AstNode): string {
const knownIds = streamAst(container)
.filter(node => node.$type === type)
.map(this.getNodeId)
.nonNullable()
.toArray();
return this.countToNextId(knownIds, proposal);
}

protected findNextIdInIndex(type: string, proposal: string): 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ID_PROPERTY } from './cross-model-naming.js';
import {
CrossModelAstType,
DiagramEdge,
SystemDiagram,
isDiagramEdge,
isDiagramNode,
isEntity,
Expand All @@ -24,7 +25,8 @@ export function registerValidationChecks(services: CrossModelServices): void {

const checks: ValidationChecks<CrossModelAstType> = {
AstNode: validator.checkUniqueId,
DiagramEdge: validator.checkDiagramEdge
DiagramEdge: validator.checkDiagramEdge,
SystemDiagram: validator.checkUniqueIdWithinDiagram
};
registry.register(checks, validator);
}
Expand Down Expand Up @@ -61,6 +63,24 @@ export class CrossModelValidator {
);
}

checkUniqueIdWithinDiagram(diagram: SystemDiagram, accept: ValidationAcceptor): void {
const knownIds: string[] = [];
for (const node of diagram.nodes) {
if (node.id && knownIds.includes(node.id)) {
accept('error', 'Must provide a unique id.', { node, property: ID_PROPERTY });
} else if (node.id) {
knownIds.push(node.id);
}
}
for (const edge of diagram.edges) {
if (edge.id && knownIds.includes(edge.id)) {
accept('error', 'Must provide a unique id.', { node: edge, property: ID_PROPERTY });
} else if (edge.id) {
knownIds.push(edge.id);
}
}
}

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' });
Expand Down
14 changes: 0 additions & 14 deletions extensions/crossmodel-lang/src/language-server/util/name-util.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@
********************************************************************************/
import { describe, expect, test } from '@jest/globals';
import { EmptyFileSystem } from 'langium';

import { createCrossModelServices } from '../../../src/language-server/cross-model-module.js';
import { CrossModelRoot } from '../../../src/language-server/generated/ast.js';
import { findNextId } from '../../../src/language-server/util/name-util.js';
import { parseDocument } from '../test-utils/utils.js';
import { createCrossModelServices } from '../../src/language-server/cross-model-module.js';
import { CrossModelRoot, DiagramNode } from '../../src/language-server/generated/ast.js';
import { parseDocument } from './test-utils/utils.js';

const services = createCrossModelServices({ ...EmptyFileSystem });
const cmServices = services.CrossModel;
Expand All @@ -32,27 +30,27 @@ describe('NameUtil', () => {
test('should return given name if unique', async () => {
const document = await parseDocument<CrossModelRoot>(cmServices, ex1);

expect(findNextId(document.parseResult.value.diagram!, 'nodeA')).toBe('nodeA');
expect(cmServices.references.IdProvider.findNextId(DiagramNode, 'nodeA', document.parseResult.value.diagram!)).toBe('nodeA');
});

test('should return unique name if given is taken', async () => {
const document = await parseDocument<CrossModelRoot>(cmServices, ex2);

const result = findNextId(document.parseResult.value.diagram!, 'nodeA');
const result = cmServices.references.IdProvider.findNextId(DiagramNode, 'nodeA', document.parseResult.value.diagram!);

expect(result).toBe('nodeA1');
});

test('should properly count up if name is taken', async () => {
const document = await parseDocument<CrossModelRoot>(cmServices, ex3);

expect(findNextId(document.parseResult.value.diagram!, 'nodeA')).toBe('nodeA2');
expect(cmServices.references.IdProvider.findNextId(DiagramNode, 'nodeA', document.parseResult.value.diagram!)).toBe('nodeA2');
});

test('should find lowest count if multiple are taken', async () => {
const document = await parseDocument<CrossModelRoot>(cmServices, ex4);

expect(findNextId(document.parseResult.value.diagram!, 'nodeA')).toBe('nodeA3');
expect(cmServices.references.IdProvider.findNextId(DiagramNode, 'nodeA', document.parseResult.value.diagram!)).toBe('nodeA3');
});
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"scripts": {
"build": "lerna run build",
"clean": "lerna run clean && rimraf node_modules",
"format": "yarn prettier-eslint --write '**/*.{ts,tsx,js,cjs,mjs}' '**/package.json'",
"format": "yarn prettier-eslint --write '**/*.{ts,tsx,js,cjs,mjs,css}' '**/package.json'",
"postinstall": "theia check:theia-version",
"lint": "lerna run lint",
"prepare": "lerna run prepare",
Expand Down
8 changes: 4 additions & 4 deletions packages/core/style/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
/* Hide number input arrow buttons as they look bad on dark themes */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
-webkit-appearance: none;
margin: 0;
}

input[type='number'] {
appearance: textfield;
-moz-appearance: textfield;
appearance: textfield;
-moz-appearance: textfield;
}
35 changes: 33 additions & 2 deletions packages/glsp-client/src/browser/crossmodel-client-contribution.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
/********************************************************************************
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/
import { GLSPClient } from '@eclipse-glsp/protocol';
import { BaseGLSPClientContribution } from '@eclipse-glsp/theia-integration';
import { Action, ActionMessage, ActionMessageHandler, ConnectionProvider, GLSPClient, JsonrpcGLSPClient } from '@eclipse-glsp/protocol';
import { BaseGLSPClientContribution, TheiaJsonrpcGLSPClient } from '@eclipse-glsp/theia-integration';
import { Emitter } from '@theia/core';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { inject, injectable } from '@theia/core/shared/inversify';
import { Disposable, MessageConnection } from '@theia/core/shared/vscode-languageserver-protocol';
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
import '../../style/diagram.css';
import { CrossModelDiagramLanguage } from '../common/crossmodel-diagram-language';
Expand Down Expand Up @@ -51,4 +53,33 @@ export class CrossModelClientContribution extends BaseGLSPClientContribution {
await this.waitForBackendConnected();
return super.start(glspClient);
}

protected override async createGLSPClient(connectionProvider: ConnectionProvider): Promise<GLSPClient> {
return new FixedTheiaJsonrpcGLSPClient({
id: this.id,
connectionProvider,
messageService: this.messageService
});
}
}

export class FixedTheiaJsonrpcGLSPClient extends TheiaJsonrpcGLSPClient {
protected actionMessageEmitter = new Emitter<ActionMessage<Action>>();
protected onActionMessageEvent = this.actionMessageEmitter.event;

override onActionMessage(handler: ActionMessageHandler, clientId?: string | undefined): Disposable {
return this.onActionMessageEvent(msg => {
if (!clientId || msg.clientId === clientId) {
handler(msg);
}
});
}

protected override async doCreateConnection(): Promise<MessageConnection> {
const connection = await super.doCreateConnection();
connection.onNotification(JsonrpcGLSPClient.ActionMessageNotification, msg => {
this.actionMessageEmitter.fire(msg);
});
return connection;
}
}
5 changes: 0 additions & 5 deletions packages/glsp-client/src/browser/views.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,6 @@ export class EntityNodeView extends RectangularNodeView {

const classNode: any = (
<g>
<defs>
<filter id='dropShadow'>
<feDropShadow dx='0.5' dy='0.5' stdDeviation='0.4' />
</filter>
</defs>
<rect x={0} y={0} rx={6} width={Math.max(0, node.bounds.width)} height={Math.max(0, node.bounds.height)} />

{/* The renderChildren function will render SVG objects for the children of the node object. */}
Expand Down
32 changes: 16 additions & 16 deletions packages/glsp-client/style/diagram.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,49 @@
********************************************************************************/

:root {
--sprotty-background: var(--theia-layout-color3);
--sprotty-edge: var(--theia-editor-foreground);
--sprotty-border: var(--theia-editor-foreground);
--sprotty-background: var(--theia-layout-color3);
--sprotty-edge: var(--theia-editor-foreground);
--sprotty-border: var(--theia-editor-foreground);
}

/* Standard sprotty */
.sprotty {
height: 100%;
height: 100%;
}

.sprotty-graph .sprotty-node {
fill: inherit;
fill: inherit;
}

.sprotty-graph {
font-size: 15pt;
height: 100%;
background: var(--sprotty-background);
font-size: 15pt;
height: 100%;
background: var(--sprotty-background);
}

/* Nodes */
.diagram-node {
fill: #fffcdf;
stroke: black;
filter: url(#dropShadow);
fill: #fffcdf;
stroke: black;
filter: drop-shadow(1px 1px 1px rgba(0, 0, 0, 0.8));
}

.entity-header {
font-weight: bold;
font-weight: bold;
}

.diagram-node > .sprotty-node:not(.selected) {
stroke-width: 1px;
stroke-width: 1px;
}

.header-container-diagram-node {
stroke: 1;
stroke: 1;
}

.sprotty-node.mouseover:not(.selected) {
stroke-width: 3px;
stroke-width: 3px;
}

.sprotty-node.selected {
stroke-width: 5px;
stroke-width: 5px;
}
2 changes: 1 addition & 1 deletion packages/property-view/style/property-view.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#property-view {
overflow-y: auto;
overflow-y: auto;
}
Loading
Loading