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

Replace name/name_val with id/name in grammar for better semantics #42

Merged
merged 2 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ node_modules
dist
out
lib
examples
examples
generated
3 changes: 2 additions & 1 deletion configs/base.jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module.exports = {
}
],
['github-actions', { silent: false }],
'summary'
'summary',
'default'
]
};
2 changes: 1 addition & 1 deletion extensions/crossmodel-lang/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"symlink:electron": "symlink-dir . ../../applications/electron-app/plugins/crossmodel-lang",
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 jest --passWithNoTests",
"vscode:prepublish": "yarn lint",
"watch": "yarn watch:webpack",
"watch": "yarn watch:esbuild",
"watch:esbuild": "node esbuild.mjs --watch",
"watch:tsc": "tsc -b tsconfig.json --watch",
"watch:webpack": "webpack --mode development --watch"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ 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 { findAvailableNodeName } from '../../language-server/util/name-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 +34,7 @@ export class CrossModelAddEntityOperationHandler extends JsonOperationHandler {
const node: DiagramNode = {
$type: DiagramNode,
$container: container,
name: findAvailableNodeName(container, entityDescription.name + 'Node'),
id: findNextId(container, entityDescription.name + 'Node'),
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,18 +32,18 @@ export class CrossModelCreateEdgeOperationHandler extends JsonCreateEdgeOperatio
const edge: DiagramEdge = {
$type: DiagramEdge,
$container: this.modelState.diagramRoot,
name: relationship.name,
id: relationship.id,
relationship: {
ref: relationship,
$refText: this.modelState.nameProvider.getName(relationship) || relationship.name || ''
$refText: this.modelState.idProvider.getExternalId(relationship) || relationship.id || ''
},
sourceNode: {
ref: sourceNode,
$refText: this.modelState.nameProvider.getLocalName(sourceNode) || sourceNode.name || ''
$refText: this.modelState.idProvider.getNodeId(sourceNode) || sourceNode.id || ''
},
targetNode: {
ref: targetNode,
$refText: this.modelState.nameProvider.getLocalName(targetNode) || targetNode.name || ''
$refText: this.modelState.idProvider.getNodeId(targetNode) || targetNode.id || ''
}
};
this.modelState.diagramRoot.edges.push(edge);
Expand All @@ -55,22 +55,22 @@ export class CrossModelCreateEdgeOperationHandler extends JsonCreateEdgeOperatio
* 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.entity?.ref?.name || sourceNode.entity?.$refText;
const target = targetNode.entity?.ref?.name || targetNode.entity?.$refText;
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 name = UriUtils.basename(uri).split('.')[0];
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,
name,
id,
type: '1:1',
parent: { $refText: sourceNode.entity?.$refText || '' },
child: { $refText: targetNode.entity?.$refText || '' }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ 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 { findAvailableNodeName } from '../../language-server/util/name-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 Down Expand Up @@ -37,9 +37,9 @@ export class CrossModelDropEntityOperationHandler extends JsonOperationHandler {
const node: DiagramNode = {
$type: DiagramNode,
$container: container,
name: findAvailableNodeName(container, root.entity.name + 'Node'),
id: findNextId(container, root.entity.id + 'Node'),
entity: {
$refText: this.modelState.nameProvider.getFullyQualifiedName(root.entity) || root.entity.name || '',
$refText: this.modelState.idProvider.getExternalId(root.entity) || root.entity.id || '',
ref: root.entity
},
x: (x += 10),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class GEntityNodeBuilder extends GNodeBuilder {
.addCssClass('entity-header-compartment')
.add(
GLabel.builder()
.text(entityRef?.name_val || 'unresolved')
.text(entityRef?.name || 'unresolved')
.id(`${this.proxy.id}_label`)
.addCssClass('entity-header-label')
.build()
Expand All @@ -54,23 +54,23 @@ export class GEntityNodeBuilder extends GNodeBuilder {
// Add the attributes of the entity.
for (const attribute of entityRef.attributes) {
const attributeCompartment = GCompartment.builder()
.id(`${this.proxy.id}_${attribute.name}_attribute`)
.id(`${this.proxy.id}_${attribute.id}_attribute`)
.addCssClass('attribute-compartment')
.layout('hbox')
.addLayoutOption('paddingBottom', 3)
.addLayoutOption('paddingTop', 3);

attributeCompartment.add(
GLabel.builder()
.id(`${this.proxy.id}_${attribute.name}_attribute_name`)
.text(attribute.name_val || '')
.id(`${this.proxy.id}_${attribute.id}_attribute_name`)
.text(attribute.name || '')
.addCssClass('attribute')
.build()
);
attributeCompartment.add(GLabel.builder().text(' : ').id(`${this.proxy.id}_${attribute.name}_attribute_del`).build());
attributeCompartment.add(GLabel.builder().text(' : ').id(`${this.proxy.id}_${attribute.id}_attribute_del`).build());
attributeCompartment.add(
GLabel.builder()
.id(`${this.proxy.id}_${attribute.name}_attribute_type`)
.id(`${this.proxy.id}_${attribute.id}_attribute_type`)
.text(attribute.datatype?.toString() || '')
.addCssClass('datatype')
.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ export class CrossModelGModelFactory implements GModelFactory {
protected createDiagramEdge(edge: DiagramEdge): GEdge {
const id = this.modelState.index.createId(edge) ?? 'unknown';

const parentDiagramNode = edge.sourceNode?.ref?.name || edge.sourceNode?.$refText;
const childDiagramNode = edge.targetNode?.ref?.name || edge.targetNode?.$refText;
const sourceId = edge.sourceNode?.ref?.id || edge.sourceNode?.$refText;
const targetId = edge.targetNode?.ref?.id || edge.targetNode?.$refText;

return GEdge.builder()
.id(id)
.addCssClasses('diagram-edge', 'relationship')
.sourceId(parentDiagramNode || '')
.targetId(childDiagramNode || '')
.sourceId(sourceId || '')
.targetId(targetId || '')
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class CrossModelIndex extends GModelIndex {
protected idToSemanticNode = new Map<string, AstNode>();

createId(node?: AstNode): string | undefined {
return this.services.language.references.QualifiedNameProvider.getLocalName(node);
return this.services.language.references.IdProvider.getNodeId(node);
}

indexSemanticRoot(root: CrossModelRoot): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { DefaultModelState, JsonModelState, ModelState, hasFunctionProp } from '
import { inject, injectable } from 'inversify';
import { URI } from 'vscode-uri';
import { CrossModelLSPServices } from '../../integration.js';
import { QualifiedNameProvider } from '../../language-server/cross-model-naming.js';
import { IdProvider } from '../../language-server/cross-model-naming.js';
import { CrossModelRoot, SystemDiagram } from '../../language-server/generated/ast.js';
import { ModelService } from '../../model-server/model-service.js';
import { Serializer } from '../../model-server/serializer.js';
Expand Down Expand Up @@ -59,8 +59,8 @@ export class CrossModelState extends DefaultModelState implements JsonModelState
return this.services.language.serializer.Serializer;
}

get nameProvider(): QualifiedNameProvider {
return this.services.language.references.QualifiedNameProvider;
get idProvider(): IdProvider {
return this.services.language.references.IdProvider;
}

get sourceModel(): CrossModelSourceModel {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { CrossModelDocumentBuilder } from './cross-model-document-builder.js';
import { CrossModelModelFormatter } from './cross-model-formatter.js';
import { CrossModelLangiumDocuments } from './cross-model-langium-documents.js';
import { CrossModelLanguageServer } from './cross-model-language-server.js';
import { QualifiedNameProvider } from './cross-model-naming.js';
import { DefaultIdProvider } from './cross-model-naming.js';
import { CrossModelPackageManager } from './cross-model-package-manager.js';
import { CrossModelScopeProvider } from './cross-model-scope-provider.js';
import { CrossModelScopeComputation } from './cross-model-scope.js';
Expand Down Expand Up @@ -123,7 +123,7 @@ export interface CrossModelModuleContext {
*/
export interface CrossModelAddedServices {
references: {
QualifiedNameProvider: QualifiedNameProvider;
IdProvider: DefaultIdProvider;
};
validation: {
CrossModelValidator: CrossModelValidator;
Expand Down Expand Up @@ -153,17 +153,18 @@ export function createCrossModelModule(
references: {
ScopeComputation: services => new CrossModelScopeComputation(services),
ScopeProvider: services => new CrossModelScopeProvider(services),
QualifiedNameProvider: services => new QualifiedNameProvider(services)
IdProvider: services => new DefaultIdProvider(services),
NameProvider: services => services.references.IdProvider
},
validation: {
CrossModelValidator: () => new CrossModelValidator()
CrossModelValidator: services => new CrossModelValidator(services)
},
lsp: {
CompletionProvider: services => new CrossModelCompletionProvider(services),
Formatter: () => new CrossModelModelFormatter()
},
serializer: {
Serializer: services => new CrossModelSerializer(services)
Serializer: () => new CrossModelSerializer()
},
parser: {
TokenBuilder: () => new CrossModelTokenBuilder(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,38 @@
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/

import { AstNode, CstNode, findNodeForProperty, getDocument, isNamed, NameProvider } from 'langium';
import { AstNode, CstNode, findNodeForProperty, NameProvider } 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';

export const ID_PROPERTY = 'id';

export type IdentifiedAstNode = AstNode & {
[ID_PROPERTY]: string;
};

export function hasId(node?: AstNode): node is IdentifiedAstNode {
return !!node && ID_PROPERTY in node && typeof node[ID_PROPERTY] === 'string';
}

export function getId(node?: AstNode): string | undefined {
return hasId(node) ? node[ID_PROPERTY] : undefined;
}

export interface IdProvider extends NameProvider {
getNodeId(node?: AstNode): string | undefined;
getLocalId(node?: AstNode): string | undefined;
getExternalId(node?: AstNode): string | undefined;
}

/**
* A name provider that returns the fully qualified name of a node by default but also exposes methods to get other names:
harmen-xb marked this conversation as resolved.
Show resolved Hide resolved
* - The local name is just the name of the node itself if it has a name.
* - The qualified name / document-local name is the name of the node itself plus all it's named parents within the document
* - The fully qualified is the package name plus the document-local name.
*/
export class QualifiedNameProvider implements NameProvider {
export class DefaultIdProvider implements NameProvider, IdProvider {
constructor(
protected services: CrossModelServices,
protected packageManager = services.shared.workspace.PackageManager
Expand All @@ -24,8 +45,8 @@ export class QualifiedNameProvider implements NameProvider {
* @param node node
* @returns direct, local name of the node if available
*/
getLocalName(node?: AstNode): string | undefined {
return node && isNamed(node) ? node.name : undefined;
getNodeId(node?: AstNode): string | undefined {
return getId(node);
}

/**
Expand All @@ -35,17 +56,23 @@ export class QualifiedNameProvider implements NameProvider {
* @param node node
* @returns qualified, document-local name
*/
getQualifiedName(node?: AstNode): string | undefined {
getLocalId(node?: AstNode): string | undefined {
if (!node) {
return undefined;
}
let name = this.getLocalName(node);
let id = this.getNodeId(node);
if (!id) {
return undefined;
}
let parent = node.$container;
while (parent && isNamed(parent)) {
name = concat(parent.name, name);
while (parent) {
const parentId = this.getNodeId(parent);
if (parentId) {
id = parentId + '.' + id;
}
parent = parent.$container;
}
return name;
return id;
}

/**
Expand All @@ -55,24 +82,25 @@ export class QualifiedNameProvider implements NameProvider {
* @param packageName package name
* @returns fully qualified, package-local name
*/
getFullyQualifiedName(
node: AstNode,
packageName = this.packageManager.getPackageInfoByDocument(getDocument(node))?.referenceName ?? UNKNOWN_PROJECT_REFERENCE
): string | undefined {
const packageLocalName = this.getQualifiedName(node);
return packageName + '/' + packageLocalName;
getExternalId(node?: AstNode, packageName = this.getPackageName(node)): string | undefined {
const localId = this.getLocalId(node);
if (!localId) {
return undefined;
}
return packageName + '/' + localId;
}

getPackageName(node?: AstNode): string {
return !node
? UNKNOWN_PROJECT_REFERENCE
: this.packageManager.getPackageInfoByDocument(findDocument(node))?.referenceName ?? UNKNOWN_PROJECT_REFERENCE;
}

getName(node?: AstNode): string | undefined {
return node ? this.getFullyQualifiedName(node) : undefined;
return node ? this.getExternalId(node) : undefined;
}

getNameNode(node: AstNode): CstNode | undefined {
return findNodeForProperty(node.$cstNode, 'name');
return findNodeForProperty(node.$cstNode, ID_PROPERTY);
}
}

function concat(...parts: (string | undefined)[]): string | undefined {
const name = parts.filter(part => !!part && part.length > 0).join('.');
return name.length === 0 ? undefined : name;
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,10 @@ export class CrossModelPackageManager {
return this.getPackageInfoByDocument(doc)?.id || UNKNOWN_PROJECT_ID;
}

getPackageInfoByDocument(doc: LangiumDocument): PackageInfo | undefined {
getPackageInfoByDocument(doc?: LangiumDocument): PackageInfo | undefined {
if (!doc) {
return undefined;
}
// during document parsing we store the package URI in the document
const packageUri = (doc as any)['packageUri'] as URI | undefined;
return this.getPackageInfoByURI(packageUri ?? doc.uri);
Expand Down
Loading
Loading