Skip to content

Commit

Permalink
Replace name/name_val with id/name in grammar for better semantics (#42)
Browse files Browse the repository at this point in the history
- Use 'id' consistently instead of 'name' even for descriptions
- Ignore generated files with prettier
- Ensure we give warning if ID field is missing
  • Loading branch information
martin-fleck-at authored Nov 30, 2023
1 parent 4fdee98 commit 76c7430
Show file tree
Hide file tree
Showing 34 changed files with 410 additions and 369 deletions.
4 changes: 3 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# keep in sync with .prettierignore if useful
node_modules
dist
out
lib
examples
examples
generated
7 changes: 7 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# keep in sync with .eslintignore if useful
node_modules
dist
out
lib
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:
* - 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.
* A name provider that returns the fully qualified ID of a node by default but also exposes methods to get other names:
* - The Node ID is just the id of the node itself if it has an id.
* - The Local ID is the Node ID itself plus the Node ID of all it's parents within the same document.
* - The External ID is the Local ID prefixed with the package 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

0 comments on commit 76c7430

Please sign in to comment.