diff --git a/configs/jest.config.js b/configs/jest.config.js new file mode 100644 index 0000000..257ddde --- /dev/null +++ b/configs/jest.config.js @@ -0,0 +1,5 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node' +}; diff --git a/extensions/crossmodel-lang/jest.config.js b/extensions/crossmodel-lang/jest.config.js new file mode 100644 index 0000000..18d7e3c --- /dev/null +++ b/extensions/crossmodel-lang/jest.config.js @@ -0,0 +1,5 @@ +const baseConfig = require('../../configs/jest.config'); + +module.exports = { + ...baseConfig +}; diff --git a/extensions/crossmodel-lang/package.json b/extensions/crossmodel-lang/package.json index 38e7e1a..7d4e688 100644 --- a/extensions/crossmodel-lang/package.json +++ b/extensions/crossmodel-lang/package.json @@ -41,6 +41,7 @@ "symlink": "yarn symlink:browser && yarn symlink:electron", "symlink:browser": "symlink-dir . ../../applications/browser-app/plugins/crossmodel-lang", "symlink:electron": "symlink-dir . ../../applications/electron-app/plugins/crossmodel-lang", + "test": "jest", "vscode:prepublish": "yarn lint", "watch": "yarn watch:webpack", "watch:tsc": "tsc -b tsconfig.json --watch", diff --git a/extensions/crossmodel-lang/src/language-server/cross-model.langium b/extensions/crossmodel-lang/src/language-server/cross-model.langium index 46fbd29..32d782b 100644 --- a/extensions/crossmodel-lang/src/language-server/cross-model.langium +++ b/extensions/crossmodel-lang/src/language-server/cross-model.langium @@ -18,6 +18,7 @@ Entity: EntityFields infers Entity: ( 'id' ':' name=STRING | + 'name' ':' name_val=STRING | 'description' ':' description=STRING | 'attributes' ':' EntityAttributes ) @@ -80,7 +81,8 @@ SystemDiagramFields infers SystemDiagram: 'nodes' ':' SystemDiagramNodes | 'edges' ':' SystemDiagramEdge | 'id' ':' name=STRING | - 'description' ':' description=STRING + 'description' ':' description=STRING | + 'name' ':' name_val=STRING ) ; diff --git a/extensions/crossmodel-lang/src/language-server/generated/ast.ts b/extensions/crossmodel-lang/src/language-server/generated/ast.ts index 82beaf3..64f8e69 100644 --- a/extensions/crossmodel-lang/src/language-server/generated/ast.ts +++ b/extensions/crossmodel-lang/src/language-server/generated/ast.ts @@ -61,6 +61,7 @@ export interface Entity extends AstNode { attributes: Array description?: string name?: string + name_val?: string } export const Entity = 'Entity'; @@ -107,6 +108,7 @@ export interface SystemDiagram extends AstNode { description?: string edges: Array name?: string + name_val?: string nodes: Array } diff --git a/extensions/crossmodel-lang/src/language-server/generated/grammar.ts b/extensions/crossmodel-lang/src/language-server/generated/grammar.ts index 7ef401f..4a670fb 100644 --- a/extensions/crossmodel-lang/src/language-server/generated/grammar.ts +++ b/extensions/crossmodel-lang/src/language-server/generated/grammar.ts @@ -149,6 +149,31 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load } ] }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "name" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name_val", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, { "$type": "Group", "elements": [ @@ -811,6 +836,31 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load } } ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "name" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name_val", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] } ] }, diff --git a/extensions/crossmodel-lang/test/language-server/cross-model-lang-diagram.test.ts b/extensions/crossmodel-lang/test/language-server/cross-model-lang-diagram.test.ts new file mode 100644 index 0000000..ade1f67 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/cross-model-lang-diagram.test.ts @@ -0,0 +1,133 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +import { describe, expect, test } from '@jest/globals'; +import { EmptyFileSystem, isReference } from 'langium'; + +import { parseDocument } from './utils/utils'; +import { diagram1, diagram2, diagram3, diagram4, diagram5, diagram6 } from './utils/test-documents/diagram/index'; + +import { CrossModelRoot } from '../../src/language-server/generated/ast'; +import { createCrossModelServices } from '../../src/language-server/cross-model-module'; + +const services = createCrossModelServices({ ...EmptyFileSystem }).CrossModel; + +describe('CrossModel language Diagram', () => { + describe('Diagram without nodes and edges', () => { + test('Simple file for diagram', async () => { + const document = diagram1; + const parsedDocument = await parseDocument(services, document); + const model = parsedDocument.parseResult.value as CrossModelRoot; + + expect(model).toHaveProperty('diagram'); + expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); + expect(parsedDocument.parseResult.parserErrors.length).toBe(0); + + expect(model.diagram?.name).toBe('Systemdiagram1'); + }); + + test('Diagram with indentation error', async () => { + const document = diagram4; + const parsedDocument = await parseDocument(services, document); + const model = parsedDocument.parseResult.value as CrossModelRoot; + + expect(model).toHaveProperty('diagram'); + expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); + expect(parsedDocument.parseResult.parserErrors.length).toBe(1); + }); + }); + + describe('Diagram with nodes', () => { + test('Simple file for diagram and nodes', async () => { + const document = diagram2; + const parsedDocument = await parseDocument(services, document); + const model = parsedDocument.parseResult.value as CrossModelRoot; + const node1 = model.diagram?.nodes[0]; + + expect(model).toHaveProperty('diagram'); + expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); + expect(parsedDocument.parseResult.parserErrors.length).toBe(0); + + expect(model.diagram?.nodes.length).toBe(1); + + expect(node1?.name).toBe('CustomerNode'); + expect(isReference(node1?.for)).toBe(true); + expect(node1?.for?.$refText).toBe('Customer'); + expect(node1?.x).toBe(100); + }); + }); + + describe('Diagram with edges', () => { + test('Simple file for diagram and edges', async () => { + const document = diagram3; + const parsedDocument = await parseDocument(services, document); + const model = parsedDocument.parseResult.value as CrossModelRoot; + const edge1 = model.diagram?.edges[0]; + + expect(model).toHaveProperty('diagram'); + expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); + expect(parsedDocument.parseResult.parserErrors.length).toBe(0); + + expect(model.diagram?.edges.length).toBe(1); + + expect(edge1?.name).toBe('OrderCustomerEdge'); + expect(isReference(edge1?.for)).toBe(true); + expect(edge1?.for?.$refText).toBe('Order_Customer'); + }); + }); + + describe('Diagram with nodes and edges', () => { + test('Simple file for diagram and edges', async () => { + const document = diagram5; + const parsedDocument = await parseDocument(services, document); + const model = parsedDocument.parseResult.value as CrossModelRoot; + const node1 = model.diagram?.nodes[0]; + const edge1 = model.diagram?.edges[0]; + + expect(model).toHaveProperty('diagram'); + + expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); + expect(parsedDocument.parseResult.parserErrors.length).toBe(0); + + expect(model.diagram?.name_val).toBe('System diagram 1'); + expect(model.diagram?.description).toBe('This is a basic diagram with nodes and edges'); + expect(model.diagram?.nodes.length).toBe(1); + expect(model.diagram?.edges.length).toBe(1); + + expect(node1?.name).toBe('CustomerNode'); + expect(isReference(node1?.for)).toBe(true); + expect(node1?.for?.$refText).toBe('Customer'); + expect(node1?.x).toBe(100); + + expect(edge1?.name).toBe('OrderCustomerEdge'); + expect(isReference(edge1?.for)).toBe(true); + expect(edge1?.for?.$refText).toBe('Order_Customer'); + }); + + test('Simple file for diagram and edges, but descirption and name coming last', async () => { + const document = diagram6; + const parsedDocument = await parseDocument(services, document); + const model = parsedDocument.parseResult.value as CrossModelRoot; + const node1 = model.diagram?.nodes[0]; + const edge1 = model.diagram?.edges[0]; + + expect(model).toHaveProperty('diagram'); + expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); + expect(parsedDocument.parseResult.parserErrors.length).toBe(0); + + expect(model.diagram?.name_val).toBe('System diagram 1'); + expect(model.diagram?.description).toBe('This is a basic diagram with nodes and edges'); + expect(model.diagram?.nodes.length).toBe(1); + expect(model.diagram?.edges.length).toBe(1); + + expect(node1?.name).toBe('CustomerNode'); + expect(isReference(node1?.for)).toBe(true); + expect(node1?.for?.$refText).toBe('Customer'); + expect(node1?.x).toBe(100); + + expect(edge1?.name).toBe('OrderCustomerEdge'); + expect(isReference(edge1?.for)).toBe(true); + expect(edge1?.for?.$refText).toBe('Order_Customer'); + }); + }); +}); diff --git a/extensions/crossmodel-lang/test/language-server/cross-model-lang-entity.test.ts b/extensions/crossmodel-lang/test/language-server/cross-model-lang-entity.test.ts new file mode 100644 index 0000000..137d30e --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/cross-model-lang-entity.test.ts @@ -0,0 +1,79 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ + +import { describe, expect, test } from '@jest/globals'; +import { EmptyFileSystem } from 'langium'; + +import { parseDocument } from './utils/utils'; +import { entity1, entity2, entity3, entity4 } from './utils/test-documents/entity/index'; + +import { CrossModelRoot } from '../../src/language-server/generated/ast'; +import { createCrossModelServices } from '../../src/language-server/cross-model-module'; + +const services = createCrossModelServices({ ...EmptyFileSystem }).CrossModel; + +describe('CrossModel language Entity', () => { + describe('Without attributes', () => { + test('Simple file for entity', async () => { + const document = entity1; + const parsedDocument = await parseDocument(services, document); + const model = parsedDocument.parseResult.value as CrossModelRoot; + + expect(model).toHaveProperty('entity'); + expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); + expect(parsedDocument.parseResult.parserErrors.length).toBe(0); + + expect(model.entity?.name).toBe('Customer'); + expect(model.entity?.name_val).toBe('Customer'); + expect(model.entity?.description).toBe('A customer with whom a transaction has been made.'); + }); + }); + + describe('With attributes', () => { + test('entity with attributes', async () => { + const document = entity2; + const parsedDocument = await parseDocument(services, document); + const model = parsedDocument.parseResult.value as CrossModelRoot; + + expect(model).toHaveProperty('entity'); + + expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); + expect(parsedDocument.parseResult.parserErrors.length).toBe(0); + + expect(model.entity?.attributes.length).toBe(6); + expect(model.entity?.attributes[0].name).toBe('Id'); + expect(model.entity?.attributes[0].name_val).toBe('Id'); + expect(model.entity?.attributes[0].datatype).toBe('int'); + }); + + test('entity with attributes coming before the description and name', async () => { + const document = entity4; + const parsedDocument = await parseDocument(services, document); + const model = parsedDocument.parseResult.value as CrossModelRoot; + + expect(model).toHaveProperty('entity'); + expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); + expect(parsedDocument.parseResult.parserErrors.length).toBe(0); + + expect(model.entity?.name).toBe('Customer'); + expect(model.entity?.name_val).toBe('Customer'); + expect(model.entity?.description).toBe('A customer with whom a transaction has been made.'); + + expect(model.entity?.attributes.length).toBe(6); + expect(model.entity?.attributes[0].name).toBe('Id'); + expect(model.entity?.attributes[0].name_val).toBe('Id'); + expect(model.entity?.attributes[0].datatype).toBe('int'); + }); + + test('entity with indentation error', async () => { + const document = entity3; + const parsedDocument = await parseDocument(services, document); + const model = parsedDocument.parseResult.value as CrossModelRoot; + + expect(model).toHaveProperty('entity'); + expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); + expect(parsedDocument.parseResult.parserErrors.length).toBe(1); + }); + }); +}); diff --git a/extensions/crossmodel-lang/test/language-server/cross-model-lang-relationship.test.ts b/extensions/crossmodel-lang/test/language-server/cross-model-lang-relationship.test.ts new file mode 100644 index 0000000..54c8838 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/cross-model-lang-relationship.test.ts @@ -0,0 +1,46 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ + +import { describe, expect, test } from '@jest/globals'; +import { EmptyFileSystem, isReference } from 'langium'; + +import { parseDocument } from './utils/utils'; +import { relationship1, relationship2 } from './utils/test-documents/relationship/index'; + +import { CrossModelRoot } from '../../src/language-server/generated/ast'; +import { createCrossModelServices } from '../../src/language-server/cross-model-module'; + +const services = createCrossModelServices({ ...EmptyFileSystem }).CrossModel; + +describe('CrossModel language Relationship', () => { + test('Simple file for relationship', async () => { + const document = relationship1; + const parsedDocument = await parseDocument(services, document); + const model = parsedDocument.parseResult.value as CrossModelRoot; + + expect(model).toHaveProperty('relationship'); + expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); + expect(parsedDocument.parseResult.parserErrors.length).toBe(0); + + expect(model.relationship?.name).toBe('Order_Customer'); + expect(model.relationship?.name_val).toBe('Customer Order relationship'); + expect(model.relationship?.type).toBe('1:1'); + expect(model.relationship?.description).toBe('A relationship between a customer and an order.'); + + expect(isReference(model.relationship?.parent)).toBe(true); + expect(isReference(model.relationship?.child)).toBe(true); + expect(model.relationship?.parent?.$refText).toBe('Customer'); + expect(model.relationship?.child?.$refText).toBe('Order'); + }); + + test('relationship with indentation error', async () => { + const document = relationship2; + const parsedDocument = await parseDocument(services, document); + const model = parsedDocument.parseResult.value as CrossModelRoot; + + expect(model).toHaveProperty('relationship'); + expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); + expect(parsedDocument.parseResult.parserErrors.length).toBe(1); + }); +}); diff --git a/extensions/crossmodel-lang/test/language-server/lexer/cross-model-indent-stack.test.ts b/extensions/crossmodel-lang/test/language-server/lexer/cross-model-indent-stack.test.ts new file mode 100644 index 0000000..0a2d1fe --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/lexer/cross-model-indent-stack.test.ts @@ -0,0 +1,129 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ + +import { describe, expect, test, beforeEach } from '@jest/globals'; +import { indentStack } from '../../../src/language-server/lexer/cross-model-indent-stack'; + +describe('IndentStack', () => { + beforeEach(() => { + indentStack.reset(); + }); + + describe('get', () => { + test('should return a copy of the current stack', () => { + indentStack.push(2); + indentStack.push(4); + + const stack = indentStack.get(); + + expect(stack).toEqual([0, 2, 4]); + }); + + test('should return an [0] if the stack is empty', () => { + const stack = indentStack.get(); + + expect(stack).toEqual([0]); + }); + + test('should not modify the original stack when modifying the returned array', () => { + indentStack.push(2); + indentStack.push(4); + + const stack = indentStack.get(); + stack.pop(); // Modify the returned array + + expect(stack).toEqual([0, 2]); // The returned array is modified + expect(indentStack.get()).toEqual([0, 2, 4]); // The original stack remains unchanged + }); + }); + + describe('push', () => { + test('should push the given value onto the stack', () => { + indentStack.push(2); + expect(indentStack.get()).toEqual([0, 2]); + }); + + test('should push multiple values onto the stack', () => { + indentStack.push(2); + indentStack.push(4); + indentStack.push(6); + expect(indentStack.get()).toEqual([0, 2, 4, 6]); + }); + }); + + describe('reset', () => { + test('should reset the stack to contain only the initial indentation level', () => { + indentStack.push(2); + indentStack.push(4); + indentStack.reset(); + expect(indentStack.get()).toEqual([0]); + }); + }); + + describe('pop', () => { + test('should remove and return the topmost indentation level from the stack', () => { + indentStack.push(2); + + const poppedValue = indentStack.pop(); + + expect(poppedValue).toBe(2); + expect(indentStack.get()).toEqual([0]); + }); + + test('should return undefined if the stack is empty', () => { + // Pop 0 after reset + const poppedValue1 = indentStack.pop(); + const poppedValue2 = indentStack.pop(); + + expect(poppedValue1).toBe(0); + expect(poppedValue2).toBeUndefined(); + }); + }); + + describe('length', () => { + test('should return 1 when only the initial indentation level is present', () => { + expect(indentStack.length()).toBe(1); + }); + + test('should return the length of indentation levels in the stack', () => { + indentStack.push(2); + indentStack.push(4); + expect(indentStack.length()).toBe(3); + }); + }); + + describe('getLast', () => { + test('should return the last indentation level from the stack', () => { + indentStack.push(2); + indentStack.push(4); + + const lastValue = indentStack.getLast(); + + expect(lastValue).toBe(4); + }); + + test('should throw an IndentStackError if the stack is empty', () => { + indentStack.pop(); + + expect(() => indentStack.getLast()).toThrow(); + }); + }); + + describe('findLastIndex', () => { + test('should return the index of the last occurrence of the given value in the stack', () => { + indentStack.push(2); + indentStack.push(4); + indentStack.push(2); + + const lastIndex = indentStack.findLastIndex(2); + + expect(lastIndex).toBe(3); + }); + + test('should return -1 if the given value is not found in the stack', () => { + const lastIndex = indentStack.findLastIndex(2); + expect(lastIndex).toBe(-1); + }); + }); +}); diff --git a/extensions/crossmodel-lang/test/language-server/lexer/cross-model-indentation-tokens.test.ts b/extensions/crossmodel-lang/test/language-server/lexer/cross-model-indentation-tokens.test.ts new file mode 100644 index 0000000..d431b4b --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/lexer/cross-model-indentation-tokens.test.ts @@ -0,0 +1,306 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +import { describe, expect, test, beforeEach, beforeAll } from '@jest/globals'; +import { Lexer, TokenType, createToken, tokenMatcher } from 'chevrotain'; + +import { SPACES, NEWLINE, INDENT, DEDENT } from '../../../src/language-server/lexer/cross-model-indentation-tokens'; +import { indentStack } from '../../../src/language-server/lexer/cross-model-indent-stack'; + +describe('matchIndentBase', () => { + let TESTTOKEN: TokenType; + let LINETOKEN: TokenType; + let testLexer: Lexer; + + beforeAll(() => { + TESTTOKEN = createToken({ + name: 'TESTTOKEN', + pattern: /TESTTOKEN/ + }); + + LINETOKEN = createToken({ + name: 'LINETOKEN', + pattern: /-/ + }); + + testLexer = new Lexer([NEWLINE, DEDENT, INDENT, LINETOKEN, TESTTOKEN, SPACES]); + }); + + beforeEach(() => { + indentStack.reset(); + }); + + describe('SPACES token', () => { + test('should not produce a token for spaces between words', () => { + const input = 'TESTTOKEN TESTTOKEN'; + + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(2); + lexResult.tokens.map(token => { + expect(tokenMatcher(token, TESTTOKEN)).toBe(true); + }); + }); + + test('should not produce a token for spaces at the end of a line', () => { + const input = 'TESTTOKEN '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(1); + lexResult.tokens.map(token => { + expect(tokenMatcher(token, TESTTOKEN)).toBe(true); + }); + }); + }); + + describe('NEWLINE token', () => { + test('should match a newline character', () => { + const input = '\n'; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(0); + expect(lexResult.groups).toHaveProperty('nl'); + expect(lexResult.groups.nl).toHaveLength(1); + expect(tokenMatcher(lexResult.groups.nl[0], NEWLINE)).toBe(true); + }); + + test('should match a newline character with carriage return', () => { + const input = '\r\n'; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(0); + expect(lexResult.groups).toHaveProperty('nl'); + expect(lexResult.groups.nl).toHaveLength(1); + expect(tokenMatcher(lexResult.groups.nl[0], NEWLINE)).toBe(true); + }); + + test('should match a newline in the middle of a line', () => { + const input = 'TESTTOKEN\nTESTTOKEN'; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(2); + expect(tokenMatcher(lexResult.tokens[0], TESTTOKEN)).toBe(true); + expect(tokenMatcher(lexResult.tokens[1], TESTTOKEN)).toBe(true); + + expect(lexResult.groups).toHaveProperty('nl'); + expect(lexResult.groups.nl).toHaveLength(1); + expect(tokenMatcher(lexResult.groups.nl[0], NEWLINE)).toBe(true); + }); + + test('should match a newline preceded by spaces', () => { + const input = ' \n'; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.groups).toHaveProperty('nl'); + expect(lexResult.groups.nl).toHaveLength(1); + expect(tokenMatcher(lexResult.groups.nl[0], NEWLINE)).toBe(true); + expect(lexResult.groups.nl[0].image).toBe('\n'); + }); + + test('should match a newline followed by spaces', () => { + const input = '\n '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.groups).toHaveProperty('nl'); + expect(lexResult.groups.nl).toHaveLength(1); + expect(tokenMatcher(lexResult.groups.nl[0], NEWLINE)).toBe(true); + expect(lexResult.groups.nl[0].image).toBe('\n'); + }); + }); + + describe('INDENT token', () => { + test('should match indentation at the start of a line', () => { + const input = ' '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(1); + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(lexResult.tokens[0].image).toBe(input); + expect(indentStack.getLast()).toBe(4); + }); + + test('should match indentation at the start of a line, when there are other token after it', () => { + const input = ' TESTTOKEN'; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(2); + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[1], TESTTOKEN)).toBe(true); + expect(lexResult.tokens[0].image).toBe(' '); + // Check what the current indentation is + expect(indentStack.getLast()).toBe(4); + }); + + test('should match indentation at the start of a line', () => { + const input = 'TESTTOKEN\n '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(2); + expect(tokenMatcher(lexResult.tokens[0], TESTTOKEN)).toBe(true); + expect(tokenMatcher(lexResult.tokens[1], INDENT)).toBe(true); + expect(lexResult.tokens[1].image).toBe(' '); + // Check what the current indentation is + expect(indentStack.getLast()).toBe(4); + }); + + test('should match indentation when only new lines preceding', () => { + const input = '\n\n\n\n '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(1); + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(lexResult.tokens[0].image).toBe(' '); + expect(indentStack.getLast()).toBe(4); + expect(lexResult.groups.nl).toHaveLength(4); + }); + + test('Should only match follow up indentation', () => { + const input = ' \n '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(2); + expect(lexResult.groups.nl).toHaveLength(1); + + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(lexResult.tokens[0].image).toBe(' '); + expect(tokenMatcher(lexResult.tokens[1], INDENT)).toBe(true); + expect(lexResult.tokens[1].image).toBe(' '); + expect(indentStack.getLast()).toBe(6); + }); + + // Should not match + test('should not match indentation after another token', () => { + const input = 'TESTTOKEN '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(1); + expect(tokenMatcher(lexResult.tokens[0], TESTTOKEN)).toBe(true); + expect(indentStack.getLast()).toBe(0); + }); + + test('Should not match sucessive same indentation level', () => { + const input = ' \n '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(1); + expect(lexResult.groups.nl).toHaveLength(1); + + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(lexResult.tokens[0].image).toBe(' '); + expect(indentStack.getLast()).toBe(4); + }); + + test('Should not match lower indentation', () => { + const input = ' \n \n '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(3); + expect(lexResult.groups.nl).toHaveLength(2); + + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(lexResult.tokens[0].image).toBe(' '); + expect(tokenMatcher(lexResult.tokens[1], INDENT)).toBe(true); + expect(lexResult.tokens[1].image).toBe(' '); + expect(tokenMatcher(lexResult.tokens[2], DEDENT)).toBe(true); + expect(indentStack.getLast()).toBe(2); + }); + }); + + describe('INDENT token and lists(-)', () => { + test('should match a single level of indentation at the start of a line with -', () => { + const input = ' - '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(2); + expect(lexResult.tokens[0].image).toBe(' '); + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[1], LINETOKEN)).toBe(true); + expect(indentStack.getLast()).toBe(6); + }); + + test('should match a indentation after indentation with -', () => { + const input = ' - \n '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(3); + expect(lexResult.tokens[0].image).toBe(' '); + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + + expect(tokenMatcher(lexResult.tokens[1], LINETOKEN)).toBe(true); + + expect(lexResult.tokens[2].image).toBe(' '); + expect(tokenMatcher(lexResult.tokens[2], INDENT)).toBe(true); + expect(indentStack.getLast()).toBe(8); + }); + + test('should match a dedentation after indentation with -', () => { + const input = ' \n - \n '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(4); + expect(lexResult.tokens[0].image).toBe(' '); + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + + expect(tokenMatcher(lexResult.tokens[1], INDENT)).toBe(true); + expect(lexResult.tokens[1].image).toBe(' '); + + expect(tokenMatcher(lexResult.tokens[2], LINETOKEN)).toBe(true); + + expect(tokenMatcher(lexResult.tokens[3], DEDENT)).toBe(true); + expect(indentStack.getLast()).toBe(2); + }); + + test('should not match second indentation with same level', () => { + const input = ' - \n '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(2); + expect(lexResult.tokens[0].image).toBe(' '); + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[1], LINETOKEN)).toBe(true); + expect(indentStack.getLast()).toBe(6); + }); + }); + + describe('DEDENT token', () => { + test('should match a dedentation', () => { + const input = ' \n \n '; + + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(3); + expect(lexResult.tokens[0].image).toBe(' '); + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + + expect(tokenMatcher(lexResult.tokens[1], INDENT)).toBe(true); + expect(lexResult.tokens[1].image).toBe(' '); + + expect(tokenMatcher(lexResult.tokens[2], DEDENT)).toBe(true); + expect(indentStack.getLast()).toBe(2); + }); + + test('should match a dedentation after dedentation', () => { + const input = ' \n \n \n \n '; + + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(5); + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[1], INDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[2], INDENT)).toBe(true); + + expect(tokenMatcher(lexResult.tokens[3], DEDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[4], DEDENT)).toBe(true); + expect(indentStack.getLast()).toBe(2); + }); + + test('should not match a dedentation whens on the same level', () => { + const input = ' \n '; + const lexResult = testLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(1); + expect(lexResult.tokens[0].image).toBe(' '); + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + }); + }); +}); diff --git a/extensions/crossmodel-lang/test/language-server/lexer/cross-model-lexer.test.ts b/extensions/crossmodel-lang/test/language-server/lexer/cross-model-lexer.test.ts new file mode 100644 index 0000000..46338d9 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/lexer/cross-model-lexer.test.ts @@ -0,0 +1,95 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ + +import { describe, expect, test, beforeAll } from '@jest/globals'; +import { EmptyFileSystem } from 'langium'; +import { tokenMatcher } from 'chevrotain'; + +import { CrossModelLexer } from '../../../src/language-server/lexer/cross-model-lexer'; +import { DEDENT, INDENT } from '../../../src/language-server/lexer/cross-model-indentation-tokens'; +import { createCrossModelServices } from '../../../src/language-server/cross-model-module'; + +const services = createCrossModelServices({ ...EmptyFileSystem }).CrossModel; + +describe('CrossModelLexer', () => { + let crossModelLexer: CrossModelLexer; + + beforeAll(() => { + crossModelLexer = new CrossModelLexer(services); + }); + + describe('Simple keywords', () => { + test('should tokenize a simple word', () => { + const input = 'entity'; + + const lexResult = crossModelLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(1); + lexResult.tokens.map(token => { + expect(token.image).toBe('entity'); + }); + }); + + test('should tokenize a couple of simple words', () => { + const input = 'entity entity entity'; + + const lexResult = crossModelLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(3); + lexResult.tokens.map(token => { + expect(token.image).toBe('entity'); + }); + }); + }); + + describe('Indentation', () => { + test('Simple indentation, should give indent and dedent token', () => { + const input = ' '; + + const lexResult = crossModelLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(2); + + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[1], DEDENT)).toBe(true); + }); + + test('single indentation but stay on same level, should give 1 indent and 1 dedent token', () => { + const input = ' \n '; + + const lexResult = crossModelLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(2); + + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[1], DEDENT)).toBe(true); + }); + + test('double indentation, should give indent and dedent token', () => { + const input = ' \n '; + + const lexResult = crossModelLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(4); + + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[1], INDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[2], DEDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[3], DEDENT)).toBe(true); + }); + + test('double indentation, but dedent in text', () => { + const input = ' \n \n '; + + const lexResult = crossModelLexer.tokenize(input); + + expect(lexResult.tokens).toHaveLength(4); + + expect(tokenMatcher(lexResult.tokens[0], INDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[1], INDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[2], DEDENT)).toBe(true); + expect(tokenMatcher(lexResult.tokens[3], DEDENT)).toBe(true); + }); + }); +}); diff --git a/extensions/crossmodel-lang/test/language-server/lexer/cross-model-token-generator.test.ts b/extensions/crossmodel-lang/test/language-server/lexer/cross-model-token-generator.test.ts new file mode 100644 index 0000000..77ccdea --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/lexer/cross-model-token-generator.test.ts @@ -0,0 +1,58 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ + +import { describe, expect, test, beforeAll } from '@jest/globals'; + +import { Grammar } from 'langium'; +import { ExampleGrammarWithIndent } from '../utils/example-grammar'; +import { TokenType } from 'chevrotain'; + +import { ExampleGrammarWithNoIndent } from '../utils/example-grammar-no-indent'; +import { DEDENT, INDENT, NEWLINE, SPACES } from '../../../src/language-server/lexer/cross-model-indentation-tokens'; +import { CrossModelTokenBuilder } from '../../../src/language-server/lexer/cross-model-token-generator'; + +describe('CrossModelTokenBuilder', () => { + let tokenBuilder: CrossModelTokenBuilder; + let exampleGrammerWithIndentation: Grammar; + let exampleGrammerWithNoIndentation: Grammar; + + beforeAll(() => { + tokenBuilder = new CrossModelTokenBuilder(); + exampleGrammerWithIndentation = ExampleGrammarWithIndent(); + exampleGrammerWithNoIndentation = ExampleGrammarWithNoIndent(); + }); + + describe('buildTokens', () => { + test('Should give NEWLINE token in first spot', () => { + const tokens = tokenBuilder.buildTokens(exampleGrammerWithIndentation) as TokenType[]; + + expect(tokens[0]).toBe(NEWLINE); + }); + + test('Should give DEDENT token in second spot', () => { + const tokens = tokenBuilder.buildTokens(exampleGrammerWithIndentation) as TokenType[]; + + expect(tokens[1]).toBe(DEDENT); + }); + + test('Should give INDENT token in third spot', () => { + const tokens = tokenBuilder.buildTokens(exampleGrammerWithIndentation) as TokenType[]; + + expect(tokens[2]).toBe(INDENT); + }); + + test('Should give SPACE token in last spot', () => { + const tokens = tokenBuilder.buildTokens(exampleGrammerWithIndentation) as TokenType[]; + + const spaceToken = tokens.pop(); + expect(spaceToken).toBe(SPACES); + }); + + test('Should throw error when missing indentation in grammar', () => { + expect(() => { + tokenBuilder.buildTokens(exampleGrammerWithNoIndentation) as TokenType[]; + }).toThrow(); + }); + }); +}); diff --git a/extensions/crossmodel-lang/test/language-server/utils/example-grammar-no-indent.ts b/extensions/crossmodel-lang/test/language-server/utils/example-grammar-no-indent.ts new file mode 100644 index 0000000..ff372c5 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/example-grammar-no-indent.ts @@ -0,0 +1,1343 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ + +import { loadGrammarFromJson, Grammar } from 'langium'; + +let loadedExampleGrammarWithIndent: Grammar | undefined; +export const ExampleGrammarWithNoIndent = (): Grammar => + loadedExampleGrammarWithIndent ?? + (loadedExampleGrammarWithIndent = loadGrammarFromJson(`{ + "$type": "Grammar", + "isDeclared": true, + "name": "CrossModel", + "rules": [ + { + "$type": "ParserRule", + "name": "CrossModelRoot", + "entry": true, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Assignment", + "feature": "entity", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@1" + }, + "arguments": [] + } + }, + { + "$type": "Assignment", + "feature": "relationship", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@6" + }, + "arguments": [] + } + }, + { + "$type": "Assignment", + "feature": "diagram", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@9" + }, + "arguments": [] + } + } + ], + "cardinality": "?" + }, + "definesHiddenTokens": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "Entity", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "entity" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@2" + }, + "arguments": [], + "cardinality": "*" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "EntityFields", + "inferredType": { + "$type": "InferredType", + "name": "Entity" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "name" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name_val", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "description" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "description", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "attributes" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@3" + }, + "arguments": [] + } + ] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "EntityAttributes", + "inferredType": { + "$type": "InferredType", + "name": "EntityFields" + }, + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + }, + { + "$type": "Assignment", + "feature": "attributes", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@4" + }, + "arguments": [] + }, + "cardinality": "*" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "EntityAttribute", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "-" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@5" + }, + "arguments": [], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "EntityAttributeFields", + "inferredType": { + "$type": "InferredType", + "name": "EntityAttribute" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "name" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name_val", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "datatype" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "datatype", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "description" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "description", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "Relationship", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "relationship" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@7" + }, + "arguments": [], + "cardinality": "*" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "RelationshipFields", + "inferredType": { + "$type": "InferredType", + "name": "Relationship" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "name" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name_val", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "description" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "description", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "parent" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "parent", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@1" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@17" + }, + "arguments": [] + }, + "deprecatedSyntax": false + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "child" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "child", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@1" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@17" + }, + "arguments": [] + }, + "deprecatedSyntax": false + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "type" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "type", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@8" + }, + "arguments": [] + } + } + ] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "RelationshipType", + "dataType": "string", + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Keyword", + "value": "1:1" + }, + { + "$type": "Keyword", + "value": "1:n" + }, + { + "$type": "Keyword", + "value": "n:1" + }, + { + "$type": "Keyword", + "value": "n:m" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "SystemDiagram", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "diagram" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@10" + }, + "arguments": [], + "cardinality": "*" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "SystemDiagramFields", + "inferredType": { + "$type": "InferredType", + "name": "SystemDiagram" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "nodes" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@11" + }, + "arguments": [] + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "edges" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@14" + }, + "arguments": [] + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "SystemDiagramNodes", + "inferredType": { + "$type": "InferredType", + "name": "SystemDiagramFields" + }, + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + }, + { + "$type": "Assignment", + "feature": "nodes", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@12" + }, + "arguments": [] + }, + "cardinality": "*" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "DiagramNode", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "-" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@13" + }, + "arguments": [], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "DiagramNodeFields", + "inferredType": { + "$type": "InferredType", + "name": "DiagramNode" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "for" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "for", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@1" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@17" + }, + "arguments": [] + }, + "deprecatedSyntax": false + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "x" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "x", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@19" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "y" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "y", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@19" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "width" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "width", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@19" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "height" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "height", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@19" + }, + "arguments": [] + } + } + ] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "SystemDiagramEdge", + "inferredType": { + "$type": "InferredType", + "name": "SystemDiagramFields" + }, + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + }, + { + "$type": "Assignment", + "feature": "edges", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@15" + }, + "arguments": [] + }, + "cardinality": "*" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "DiagramEdge", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "-" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@16" + }, + "arguments": [], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "DiagramEdgeFields", + "inferredType": { + "$type": "InferredType", + "name": "DiagramEdge" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "for" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "for", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@6" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@17" + }, + "arguments": [] + }, + "deprecatedSyntax": false + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "QualifiedName", + "dataType": "string", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "." + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + ], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "TerminalRule", + "name": "STRING", + "definition": { + "$type": "RegexToken", + "regex": "\\"[^\\"]*\\"|'[^']*'" + }, + "fragment": false, + "hidden": false + }, + { + "$type": "TerminalRule", + "name": "NUMBER", + "type": { + "$type": "ReturnType", + "name": "number" + }, + "definition": { + "$type": "RegexToken", + "regex": "(-)?[0-9]+(\\\\.[0-9]*)?" + }, + "fragment": false, + "hidden": false + }, + { + "$type": "TerminalRule", + "hidden": true, + "name": "SL_COMMENT", + "definition": { + "$type": "RegexToken", + "regex": "#[^\\\\n\\\\r]*" + }, + "fragment": false + }, + { + "$type": "TerminalRule", + "hidden": true, + "name": "NEWLINE", + "definition": { + "$type": "CharacterRange", + "left": { + "$type": "Keyword", + "value": "this_string_does_not_matter_newline#$%^&*((" + } + }, + "fragment": false + }, + { + "$type": "TerminalRule", + "name": "DEDENT", + "definition": { + "$type": "CharacterRange", + "left": { + "$type": "Keyword", + "value": "this_string_does_not_matter_dedent#$%^&*((" + } + }, + "fragment": false, + "hidden": false + }, + { + "$type": "TerminalRule", + "hidden": true, + "name": "SPACES", + "definition": { + "$type": "CharacterRange", + "left": { + "$type": "Keyword", + "value": "this_string_does_not_matter_spaces#$%^&*((" + } + }, + "fragment": false + } + ], + "definesHiddenTokens": false, + "hiddenTokens": [], + "imports": [], + "interfaces": [], + "types": [], + "usedGrammars": [] +}`)); diff --git a/extensions/crossmodel-lang/test/language-server/utils/example-grammar.ts b/extensions/crossmodel-lang/test/language-server/utils/example-grammar.ts new file mode 100644 index 0000000..ab5f75a --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/example-grammar.ts @@ -0,0 +1,1356 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ + +import { loadGrammarFromJson, Grammar } from 'langium'; + +let loadedExampleGrammarWithIndent: Grammar | undefined; +export const ExampleGrammarWithIndent = (): Grammar => + loadedExampleGrammarWithIndent ?? + (loadedExampleGrammarWithIndent = loadGrammarFromJson(`{ + "$type": "Grammar", + "isDeclared": true, + "name": "CrossModel", + "rules": [ + { + "$type": "ParserRule", + "name": "CrossModelRoot", + "entry": true, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Assignment", + "feature": "entity", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@1" + }, + "arguments": [] + } + }, + { + "$type": "Assignment", + "feature": "relationship", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@6" + }, + "arguments": [] + } + }, + { + "$type": "Assignment", + "feature": "diagram", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@9" + }, + "arguments": [] + } + } + ], + "cardinality": "?" + }, + "definesHiddenTokens": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "Entity", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "entity" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@2" + }, + "arguments": [], + "cardinality": "*" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "EntityFields", + "inferredType": { + "$type": "InferredType", + "name": "Entity" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "name" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name_val", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "description" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "description", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "attributes" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@3" + }, + "arguments": [] + } + ] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "EntityAttributes", + "inferredType": { + "$type": "InferredType", + "name": "EntityFields" + }, + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + }, + { + "$type": "Assignment", + "feature": "attributes", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@4" + }, + "arguments": [] + }, + "cardinality": "*" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "EntityAttribute", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "-" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@5" + }, + "arguments": [], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "EntityAttributeFields", + "inferredType": { + "$type": "InferredType", + "name": "EntityAttribute" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "name" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name_val", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "datatype" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "datatype", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "description" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "description", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "Relationship", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "relationship" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@7" + }, + "arguments": [], + "cardinality": "*" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "RelationshipFields", + "inferredType": { + "$type": "InferredType", + "name": "Relationship" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "name" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name_val", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "description" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "description", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "parent" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "parent", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@1" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@17" + }, + "arguments": [] + }, + "deprecatedSyntax": false + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "child" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "child", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@1" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@17" + }, + "arguments": [] + }, + "deprecatedSyntax": false + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "type" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "type", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@8" + }, + "arguments": [] + } + } + ] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "RelationshipType", + "dataType": "string", + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Keyword", + "value": "1:1" + }, + { + "$type": "Keyword", + "value": "1:n" + }, + { + "$type": "Keyword", + "value": "n:1" + }, + { + "$type": "Keyword", + "value": "n:m" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "SystemDiagram", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "diagram" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@10" + }, + "arguments": [], + "cardinality": "*" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "SystemDiagramFields", + "inferredType": { + "$type": "InferredType", + "name": "SystemDiagram" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "nodes" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@11" + }, + "arguments": [] + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "edges" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@14" + }, + "arguments": [] + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "SystemDiagramNodes", + "inferredType": { + "$type": "InferredType", + "name": "SystemDiagramFields" + }, + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + }, + { + "$type": "Assignment", + "feature": "nodes", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@12" + }, + "arguments": [] + }, + "cardinality": "*" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "DiagramNode", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "-" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@13" + }, + "arguments": [], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "DiagramNodeFields", + "inferredType": { + "$type": "InferredType", + "name": "DiagramNode" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "for" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "for", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@1" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@17" + }, + "arguments": [] + }, + "deprecatedSyntax": false + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "x" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "x", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@19" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "y" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "y", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@19" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "width" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "width", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@19" + }, + "arguments": [] + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "height" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "height", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@19" + }, + "arguments": [] + } + } + ] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "SystemDiagramEdge", + "inferredType": { + "$type": "InferredType", + "name": "SystemDiagramFields" + }, + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + }, + { + "$type": "Assignment", + "feature": "edges", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@15" + }, + "arguments": [] + }, + "cardinality": "*" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "DiagramEdge", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "-" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@16" + }, + "arguments": [], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "DiagramEdgeFields", + "inferredType": { + "$type": "InferredType", + "name": "DiagramEdge" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "for" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "for", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@6" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@17" + }, + "arguments": [] + }, + "deprecatedSyntax": false + } + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ] + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "QualifiedName", + "dataType": "string", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "." + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + ], + "cardinality": "*" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "TerminalRule", + "name": "STRING", + "definition": { + "$type": "RegexToken", + "regex": "\\"[^\\"]*\\"|'[^']*'" + }, + "fragment": false, + "hidden": false + }, + { + "$type": "TerminalRule", + "name": "NUMBER", + "type": { + "$type": "ReturnType", + "name": "number" + }, + "definition": { + "$type": "RegexToken", + "regex": "(-)?[0-9]+(\\\\.[0-9]*)?" + }, + "fragment": false, + "hidden": false + }, + { + "$type": "TerminalRule", + "hidden": true, + "name": "SL_COMMENT", + "definition": { + "$type": "RegexToken", + "regex": "#[^\\\\n\\\\r]*" + }, + "fragment": false + }, + { + "$type": "TerminalRule", + "hidden": true, + "name": "NEWLINE", + "definition": { + "$type": "CharacterRange", + "left": { + "$type": "Keyword", + "value": "this_string_does_not_matter_newline#$%^&*((" + } + }, + "fragment": false + }, + { + "$type": "TerminalRule", + "name": "DEDENT", + "definition": { + "$type": "CharacterRange", + "left": { + "$type": "Keyword", + "value": "this_string_does_not_matter_dedent#$%^&*((" + } + }, + "fragment": false, + "hidden": false + }, + { + "$type": "TerminalRule", + "name": "INDENT", + "definition": { + "$type": "CharacterRange", + "left": { + "$type": "Keyword", + "value": "this_string_does_not_matter_indent#$%^&*((" + } + }, + "fragment": false, + "hidden": false + }, + { + "$type": "TerminalRule", + "hidden": true, + "name": "SPACES", + "definition": { + "$type": "CharacterRange", + "left": { + "$type": "Keyword", + "value": "this_string_does_not_matter_spaces#$%^&*((" + } + }, + "fragment": false + } + ], + "definesHiddenTokens": false, + "hiddenTokens": [], + "imports": [], + "interfaces": [], + "types": [], + "usedGrammars": [] +}`)); diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram1.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram1.ts new file mode 100644 index 0000000..493fbe1 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram1.ts @@ -0,0 +1,5 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export const diagram1 = `diagram: + id: "Systemdiagram1"`; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram2.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram2.ts new file mode 100644 index 0000000..8c6b41f --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram2.ts @@ -0,0 +1,12 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export const diagram2 = `diagram: + id: "Systemdiagram1" + nodes: + - id: 'CustomerNode' + for: 'Customer' + x: 100 + y: 100 + height: 100 + width: 100`; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram3.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram3.ts new file mode 100644 index 0000000..8e20401 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram3.ts @@ -0,0 +1,8 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export const diagram3 = `diagram: + id: "Systemdiagram1" + edges: + - id: 'OrderCustomerEdge' + for: 'Order_Customer'`; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram4.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram4.ts new file mode 100644 index 0000000..bb4fbde --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram4.ts @@ -0,0 +1,5 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export const diagram4 = `diagram: +id: "Systemdiagram1"`; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram5.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram5.ts new file mode 100644 index 0000000..4155ae3 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram5.ts @@ -0,0 +1,17 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export const diagram5 = `diagram: + id: "Systemdiagram1" + name: "System diagram 1" + description: "This is a basic diagram with nodes and edges" + edges: + - id: 'OrderCustomerEdge' + for: 'Order_Customer' + nodes: + - id: 'CustomerNode' + for: 'Customer' + x: 100 + y: 100 + height: 100 + width: 100`; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram6.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram6.ts new file mode 100644 index 0000000..d8cab3b --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/diagram6.ts @@ -0,0 +1,17 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export const diagram6 = `diagram: + id: "Systemdiagram1" + edges: + - id: 'OrderCustomerEdge' + for: 'Order_Customer' + nodes: + - id: 'CustomerNode' + for: 'Customer' + x: 100 + y: 100 + height: 100 + width: 100 + name: "System diagram 1" + description: "This is a basic diagram with nodes and edges"`; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/index.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/index.ts new file mode 100644 index 0000000..8cc7d55 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/diagram/index.ts @@ -0,0 +1,9 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export * from './diagram1'; +export * from './diagram2'; +export * from './diagram3'; +export * from './diagram4'; +export * from './diagram5'; +export * from './diagram6'; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity1.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity1.ts new file mode 100644 index 0000000..2673bf7 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity1.ts @@ -0,0 +1,7 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export const entity1 = `entity: + id: 'Customer' + name: 'Customer' + description: 'A customer with whom a transaction has been made.'`; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity2.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity2.ts new file mode 100644 index 0000000..4c471bc --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity2.ts @@ -0,0 +1,26 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export const entity2 = `entity: + id: 'Customer' + name: 'Customer' + description: 'A customer with whom a transaction has been made.' + attributes: + - id: 'Id' + name: 'Id' + datatype: 'int' + - id: 'FirstName' + name: 'FirstName' + datatype: 'varchar' + - id: 'LastName' + name: 'LastName' + datatype: 'varchar' + - id: 'City' + name: 'City' + datatype: 'varchar' + - id: 'Country' + name: 'Country' + datatype: 'varchar' + - id: 'Phone' + name: 'Phone' + datatype: 'varchar'`; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity3.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity3.ts new file mode 100644 index 0000000..7dd9716 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity3.ts @@ -0,0 +1,26 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export const entity3 = `entity: + id: 'Customer' + name: 'Customer' + description: 'A customer with whom a transaction has been made.' +attributes: + - id: 'Id' + name: 'Id' + datatype: 'int' + - id: 'FirstName' + name: 'FirstName' + datatype: 'varchar' + - id: 'LastName' + name: 'LastName' + datatype: 'varchar' + - id: 'City' + name: 'City' + datatype: 'varchar' + - id: 'Country' + name: 'Country' + datatype: 'varchar' + - id: 'Phone' + name: 'Phone' + datatype: 'varchar'`; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity4.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity4.ts new file mode 100644 index 0000000..30a2b35 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/entity4.ts @@ -0,0 +1,26 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export const entity4 = `entity: + id: 'Customer' + attributes: + - id: 'Id' + name: 'Id' + datatype: 'int' + - id: 'FirstName' + name: 'FirstName' + datatype: 'varchar' + - id: 'LastName' + name: 'LastName' + datatype: 'varchar' + - id: 'City' + name: 'City' + datatype: 'varchar' + - id: 'Country' + name: 'Country' + datatype: 'varchar' + - id: 'Phone' + name: 'Phone' + datatype: 'varchar' + name: 'Customer' + description: 'A customer with whom a transaction has been made.'`; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/index.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/index.ts new file mode 100644 index 0000000..c96f8df --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/entity/index.ts @@ -0,0 +1,7 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export * from './entity1'; +export * from './entity2'; +export * from './entity3'; +export * from './entity4'; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/index.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/index.ts new file mode 100644 index 0000000..9eee07d --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/index.ts @@ -0,0 +1,5 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export * from './relationship1'; +export * from './relationship2'; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/relationship1.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/relationship1.ts new file mode 100644 index 0000000..569bfb9 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/relationship1.ts @@ -0,0 +1,10 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export const relationship1 = `relationship: + id: 'Order_Customer' + parent: 'Customer' + child: 'Order' + type: 1:1 + name: "Customer Order relationship" + description: "A relationship between a customer and an order."`; diff --git a/extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/relationship2.ts b/extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/relationship2.ts new file mode 100644 index 0000000..df39b1c --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/test-documents/relationship/relationship2.ts @@ -0,0 +1,8 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +export const relationship2 = `relationship: + id: 'Order_Customer' +parent: 'Customer' + child: 'Order' + type: 1:1`; diff --git a/extensions/crossmodel-lang/test/language-server/utils/utils.ts b/extensions/crossmodel-lang/test/language-server/utils/utils.ts new file mode 100644 index 0000000..bcf2ea3 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/utils/utils.ts @@ -0,0 +1,26 @@ +/******************************************************************************** + * Copyright (c) 2023 CrossBreeze. + ********************************************************************************/ +import { AstNode, LangiumDocument, LangiumServices } from 'langium'; +import { URI } from 'vscode-uri'; + +export async function parseDocument(services: LangiumServices, input: string): Promise> { + const document = await parseHelper(services)(input); + if (!document.parseResult) { + throw new Error('Could not parse document'); + } + return document; +} + +export function parseHelper(services: LangiumServices): (input: string) => Promise> { + const metaData = services.LanguageMetaData; + const documentBuilder = services.shared.workspace.DocumentBuilder; + return async input => { + const randomNumber = Math.floor(Math.random() * 10000000) + 1000000; + const uri = URI.parse(`file:///${randomNumber}${metaData.fileExtensions[0]}`); + const document = services.shared.workspace.LangiumDocumentFactory.fromString(input, uri); + services.shared.workspace.LangiumDocuments.addDocument(document); + await documentBuilder.build([document]); + return document; + }; +} diff --git a/package.json b/package.json index 9ac73d1..172fa70 100644 --- a/package.json +++ b/package.json @@ -8,17 +8,17 @@ "extensions/*" ], "scripts": { + "clean": "lerna run clean && rimraf node_modules", "postinstall": "theia check:theia-version", "lint": "lerna run lint", "prepare": "lerna run prepare", "start:browser": "yarn theia:browser start", "start:electron": "yarn theia:electron start", "start:verdaccio": "yarn verdaccio --config verdaccio-config.yaml", - "test": "vitest --config configs/vitest.config.ts", + "test": "lerna run test && vitest --config configs/vitest.config.ts", "theia:browser": "yarn --cwd applications/browser-app", "theia:electron": "yarn --cwd applications/electron-app", - "watch": "lerna run --parallel watch", - "clean": "lerna run clean && rimraf node_modules" + "watch": "lerna run --parallel watch" }, "devDependencies": { "@testing-library/react": "^11.2.7", diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index 02b3dcd..f90a841 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -4,5 +4,5 @@ "noEmit": true }, "exclude": ["**/node_modules", "**/.eslintrc.js"], - "include": ["packages/*/src", "applications/*/src", "applications/*/scripts", "extensions/*/src"] + "include": ["packages/*/src", "applications/*/src", "applications/*/scripts", "extensions/*/src", "extensions/*/test"] }