From 942f8024622b9b9bf4f5432d4d2bd38490e882aa Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Sat, 16 Sep 2023 17:49:40 +0200 Subject: [PATCH] Support multi-target references --- .../src/language-server/generated/grammar.ts | 3 +- .../domain-model-rename-refactoring.ts | 28 +++-- .../src/language-server/domain-model.langium | 2 +- .../src/language-server/generated/ast.ts | 4 +- .../src/language-server/generated/grammar.ts | 4 +- .../src/language-server/generated/grammar.ts | 24 ++-- .../src/language-server/generated/grammar.ts | 12 +- .../src/generator/ast-generator.ts | 14 ++- .../src/generator/grammar-serializer.ts | 6 +- .../langium/src/grammar/generated/grammar.ts | 71 ++++++++--- .../src/grammar/langium-grammar.langium | 4 +- .../grammar/references/grammar-references.ts | 15 ++- .../type-collector/declared-types.ts | 3 +- .../type-collector/inferred-types.ts | 9 +- .../type-system/type-collector/plain-types.ts | 4 +- .../type-system/type-collector/types.ts | 5 +- .../src/grammar/validation/types-validator.ts | 2 +- .../langium/src/languages/generated/ast.ts | 4 + .../src/lsp/call-hierarchy-provider.ts | 10 +- .../src/lsp/completion/completion-provider.ts | 18 ++- .../langium/src/lsp/definition-provider.ts | 29 ++--- .../src/lsp/document-highlight-provider.ts | 15 +-- packages/langium/src/lsp/hover-provider.ts | 34 ++++-- .../src/lsp/implementation-provider.ts | 12 +- .../langium/src/lsp/references-provider.ts | 6 +- packages/langium/src/lsp/rename-provider.ts | 25 ++-- .../src/lsp/type-hierarchy-provider.ts | 10 +- packages/langium/src/lsp/type-provider.ts | 12 +- packages/langium/src/parser/langium-parser.ts | 20 ++-- packages/langium/src/references/linker.ts | 102 +++++++++++++--- packages/langium/src/references/references.ts | 112 +++++++++++------- packages/langium/src/references/scope.ts | 39 +++++- .../langium/src/serializer/json-serializer.ts | 79 +++++++++--- packages/langium/src/syntax-tree.ts | 44 ++++++- packages/langium/src/utils/ast-utils.ts | 40 +++---- .../src/validation/document-validator.ts | 12 +- .../langium/src/workspace/ast-descriptions.ts | 49 ++++---- packages/langium/src/workspace/documents.ts | 4 +- .../ast-reflection-interpreter.test.ts | 3 +- .../grammar/type-system/types-util.test.ts | 3 +- .../test/references/multi-reference.test.ts | 102 ++++++++++++++++ 41 files changed, 712 insertions(+), 282 deletions(-) create mode 100644 packages/langium/test/references/multi-reference.test.ts diff --git a/examples/arithmetics/src/language-server/generated/grammar.ts b/examples/arithmetics/src/language-server/generated/grammar.ts index c9de8398e..80a76afda 100644 --- a/examples/arithmetics/src/language-server/generated/grammar.ts +++ b/examples/arithmetics/src/language-server/generated/grammar.ts @@ -593,7 +593,8 @@ export const ArithmeticsGrammar = (): Grammar => loadedArithmeticsGrammar ?? (lo "type": { "$ref": "#/types@0" }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } }, { diff --git a/examples/domainmodel/src/language-server/domain-model-rename-refactoring.ts b/examples/domainmodel/src/language-server/domain-model-rename-refactoring.ts index 1b647b978..775bb47ae 100644 --- a/examples/domainmodel/src/language-server/domain-model-rename-refactoring.ts +++ b/examples/domainmodel/src/language-server/domain-model-rename-refactoring.ts @@ -31,22 +31,24 @@ export class DomainModelRenameProvider extends DefaultRenameProvider { const offset = document.textDocument.offsetAt(params.position); const leafNode = CstUtils.findDeclarationNodeAtOffset(rootNode, offset, this.grammarConfig.nameRegexp); if (!leafNode) return undefined; - const targetNode = this.references.findDeclaration(leafNode); - if (!targetNode) return undefined; - if (isNamed(targetNode)) targetNode.name = params.newName; - const location = this.getNodeLocation(targetNode); - if (location) { - const change = TextEdit.replace(location.range, params.newName); - const uri = location.uri; - if (uri) { - if (changes[uri]) { - changes[uri].push(change); - } else { - changes[uri] = [change]; + const targetNodes = this.references.findDeclarations(leafNode); + if (!targetNodes.length) return undefined; + for (const node of targetNodes) { + if (isNamed(node)) node.name = params.newName; + const location = this.getNodeLocation(node); + if (location) { + const change = TextEdit.replace(location.range, params.newName); + const uri = location.uri; + if (uri) { + if (changes[uri]) { + changes[uri].push(change); + } else { + changes[uri] = [change]; + } } } } - + const targetNode = targetNodes[0]; for (const node of AstUtils.streamAst(targetNode)) { const qn = this.buildQualifiedName(node); if (qn) { diff --git a/examples/domainmodel/src/language-server/domain-model.langium b/examples/domainmodel/src/language-server/domain-model.langium index b1cc17e3a..28e2e3ac2 100644 --- a/examples/domainmodel/src/language-server/domain-model.langium +++ b/examples/domainmodel/src/language-server/domain-model.langium @@ -18,7 +18,7 @@ DataType: 'datatype' name=ID; Entity: - 'entity' name=ID ('extends' superType=[Entity:QualifiedName])? '{' + 'entity' name=ID ('extends' superType=[*Entity:QualifiedName])? '{' (features+=Feature)* '}'; diff --git a/examples/domainmodel/src/language-server/generated/ast.ts b/examples/domainmodel/src/language-server/generated/ast.ts index 8bfa033c0..42b4d4690 100644 --- a/examples/domainmodel/src/language-server/generated/ast.ts +++ b/examples/domainmodel/src/language-server/generated/ast.ts @@ -4,7 +4,7 @@ ******************************************************************************/ /* eslint-disable */ -import type { AstNode, Reference, ReferenceInfo, TypeMetaData } from 'langium'; +import type { AstNode, Reference, MultiReference, ReferenceInfo, TypeMetaData } from 'langium'; import { AbstractAstReflection } from 'langium'; export const DomainModelTerminals = { @@ -64,7 +64,7 @@ export interface Entity extends AstNode { readonly $type: 'Entity'; features: Array; name: string; - superType?: Reference; + superType?: MultiReference; } export const Entity = 'Entity'; diff --git a/examples/domainmodel/src/language-server/generated/grammar.ts b/examples/domainmodel/src/language-server/generated/grammar.ts index 6aaf222cd..4675f0ccc 100644 --- a/examples/domainmodel/src/language-server/generated/grammar.ts +++ b/examples/domainmodel/src/language-server/generated/grammar.ts @@ -211,6 +211,7 @@ export const DomainModelGrammar = (): Grammar => loadedDomainModelGrammar ?? (lo "operator": "=", "terminal": { "$type": "CrossReference", + "isMulti": true, "type": { "$ref": "#/rules@5" }, @@ -305,7 +306,8 @@ export const DomainModelGrammar = (): Grammar => loadedDomainModelGrammar ?? (lo }, "arguments": [] }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } } ] diff --git a/examples/requirements/src/language-server/generated/grammar.ts b/examples/requirements/src/language-server/generated/grammar.ts index 401d4fbb8..1d7b92df1 100644 --- a/examples/requirements/src/language-server/generated/grammar.ts +++ b/examples/requirements/src/language-server/generated/grammar.ts @@ -168,7 +168,8 @@ export const RequirementsGrammar = (): Grammar => loadedRequirementsGrammar ?? ( "type": { "$ref": "#/rules@1" }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } }, { @@ -187,7 +188,8 @@ export const RequirementsGrammar = (): Grammar => loadedRequirementsGrammar ?? ( "type": { "$ref": "#/rules@1" }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } } ], @@ -428,7 +430,8 @@ export const TestsGrammar = (): Grammar => loadedTestsGrammar ?? (loadedTestsGra }, "arguments": [] }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } }, { @@ -454,7 +457,8 @@ export const TestsGrammar = (): Grammar => loadedTestsGrammar ?? (loadedTestsGra }, "arguments": [] }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } } ], @@ -480,7 +484,8 @@ export const TestsGrammar = (): Grammar => loadedTestsGrammar ?? (loadedTestsGra "type": { "$ref": "#/rules@3" }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } }, { @@ -499,7 +504,8 @@ export const TestsGrammar = (): Grammar => loadedTestsGrammar ?? (loadedTestsGra "type": { "$ref": "#/rules@3" }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } } ], @@ -672,7 +678,8 @@ export const TestsGrammar = (): Grammar => loadedTestsGrammar ?? (loadedTestsGra "type": { "$ref": "#/rules@3" }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } }, { @@ -691,7 +698,8 @@ export const TestsGrammar = (): Grammar => loadedTestsGrammar ?? (loadedTestsGra "type": { "$ref": "#/rules@3" }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } } ], diff --git a/examples/statemachine/src/language-server/generated/grammar.ts b/examples/statemachine/src/language-server/generated/grammar.ts index c0b748685..42b3ad240 100644 --- a/examples/statemachine/src/language-server/generated/grammar.ts +++ b/examples/statemachine/src/language-server/generated/grammar.ts @@ -94,7 +94,8 @@ export const StatemachineGrammar = (): Grammar => loadedStatemachineGrammar ?? ( "type": { "$ref": "#/rules@3" }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } }, { @@ -204,7 +205,8 @@ export const StatemachineGrammar = (): Grammar => loadedStatemachineGrammar ?? ( "type": { "$ref": "#/rules@2" }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false }, "cardinality": "+" }, @@ -256,7 +258,8 @@ export const StatemachineGrammar = (): Grammar => loadedStatemachineGrammar ?? ( "type": { "$ref": "#/rules@1" }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } }, { @@ -272,7 +275,8 @@ export const StatemachineGrammar = (): Grammar => loadedStatemachineGrammar ?? ( "type": { "$ref": "#/rules@3" }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } } ] diff --git a/packages/langium-cli/src/generator/ast-generator.ts b/packages/langium-cli/src/generator/ast-generator.ts index dc906cf1c..d308f0cb8 100644 --- a/packages/langium-cli/src/generator/ast-generator.ts +++ b/packages/langium-cli/src/generator/ast-generator.ts @@ -3,6 +3,7 @@ * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. ******************************************************************************/ + import type { Grammar, LangiumCoreServices } from 'langium'; import { type Generated, expandToNode, joinToNode, toString } from 'langium/generate'; import type { AstTypes, Property, PropertyDefaultValue } from 'langium/grammar'; @@ -14,14 +15,14 @@ import { collectTerminalRegexps } from './langium-util.js'; export function generateAst(services: LangiumCoreServices, grammars: Grammar[], config: LangiumConfig): string { const astTypes = collectAst(grammars, services.shared.workspace.LangiumDocuments); - const crossRef = grammars.some(grammar => hasCrossReferences(grammar)); + const crossRef = getCrossReferenceTypes(grammars); const importFrom = config.langiumInternal ? `../../syntax-tree${config.importExtension}` : 'langium'; /* eslint-disable @typescript-eslint/indent */ const fileNode = expandToNode` ${generatedHeader} /* eslint-disable */ - import type { AstNode${crossRef ? ', Reference' : ''}, ReferenceInfo, TypeMetaData } from '${importFrom}'; + import type { AstNode${crossRef.single ? ', Reference' : ''}${crossRef.multi ? ', MultiReference' : ''}, ReferenceInfo, TypeMetaData } from '${importFrom}'; import { AbstractAstReflection } from '${importFrom}'; ${generateTerminalConstants(grammars, config)} @@ -37,8 +38,13 @@ export function generateAst(services: LangiumCoreServices, grammars: Grammar[], /* eslint-enable @typescript-eslint/indent */ } -function hasCrossReferences(grammar: Grammar): boolean { - return Boolean(AstUtils.streamAllContents(grammar).find(GrammarAST.isCrossReference)); +function getCrossReferenceTypes(grammars: Grammar[]): { single: boolean, multi: boolean } { + const allCrossReferences = grammars.flatMap(grammar => AstUtils.streamAllContents(grammar).filter(GrammarAST.isCrossReference).toArray()); + const multiCrossReferences = allCrossReferences.filter(e => e.isMulti); + return { + single: multiCrossReferences.length < allCrossReferences.length, + multi: multiCrossReferences.length > 0 + }; } function generateAstReflection(config: LangiumConfig, astTypes: AstTypes): Generated { diff --git a/packages/langium-cli/src/generator/grammar-serializer.ts b/packages/langium-cli/src/generator/grammar-serializer.ts index 03aacec08..5907ed433 100644 --- a/packages/langium-cli/src/generator/grammar-serializer.ts +++ b/packages/langium-cli/src/generator/grammar-serializer.ts @@ -4,7 +4,7 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import type { Grammar, LangiumCoreServices, Reference } from 'langium'; +import type { Grammar, LangiumCoreServices } from 'langium'; import { expandToNode, joinToNode, normalizeEOL, toString } from 'langium/generate'; import type { URI } from 'vscode-uri'; import type { LangiumConfig } from '../package-types.js'; @@ -30,9 +30,9 @@ export function serializeGrammar(services: LangiumCoreServices, grammars: Gramma grammar => { const production = config.mode === 'production'; const delimiter = production ? "'" : '`'; - const uriConverter = (uri: URI, ref: Reference) => { + const uriConverter = (uri: URI) => { // We expect the grammar to be self-contained after the transformations we've done before - throw new Error(`Unexpected reference to symbol '${ref.$refText}' in document: ${uri.toString()}`); + throw new Error(`Unexpected reference to element in document: ${uri.toString()}`); }; const serializedGrammar = services.serializer.JsonSerializer.serialize(grammar, { space: production ? undefined : 2, diff --git a/packages/langium/src/grammar/generated/grammar.ts b/packages/langium/src/grammar/generated/grammar.ts index 72060e563..93cf23a03 100644 --- a/packages/langium/src/grammar/generated/grammar.ts +++ b/packages/langium/src/grammar/generated/grammar.ts @@ -66,7 +66,8 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar }, "arguments": [] }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } }, { @@ -92,7 +93,8 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar }, "arguments": [] }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } } ], @@ -136,7 +138,8 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar }, "arguments": [] }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } }, { @@ -162,7 +165,8 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar }, "arguments": [] }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } } ], @@ -289,7 +293,8 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar }, "arguments": [] }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } }, { @@ -315,7 +320,8 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar }, "arguments": [] }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } } ], @@ -788,6 +794,16 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar }, "arguments": [] } + }, + { + "$type": "Assignment", + "feature": "isMulti", + "operator": "?=", + "terminal": { + "$type": "Keyword", + "value": "*" + }, + "cardinality": "?" } ] } @@ -859,7 +875,8 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar }, "arguments": [] }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } }, { @@ -1127,7 +1144,8 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar }, "arguments": [] }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } }, { @@ -1205,7 +1223,8 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar }, "arguments": [] }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } }, { @@ -1231,7 +1250,8 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar }, "arguments": [] }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } } ], @@ -1834,7 +1854,8 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar }, "arguments": [] }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } }, { @@ -2055,7 +2076,8 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar }, "arguments": [] }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } }, { @@ -2140,7 +2162,8 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar }, "arguments": [] }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } }, { @@ -2437,7 +2460,8 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar }, "arguments": [] }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } }, "definesHiddenTokens": false, @@ -2530,7 +2554,8 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar }, "arguments": [] }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } }, { @@ -2841,6 +2866,16 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "$type": "Keyword", "value": "[" }, + { + "$type": "Assignment", + "feature": "isMulti", + "operator": "?=", + "terminal": { + "$type": "Keyword", + "value": "*" + }, + "cardinality": "?" + }, { "$type": "Assignment", "feature": "type", @@ -2850,7 +2885,8 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "type": { "$ref": "#/types@0" }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } }, { @@ -3496,7 +3532,8 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar }, "arguments": [] }, - "deprecatedSyntax": false + "deprecatedSyntax": false, + "isMulti": false } } ] diff --git a/packages/langium/src/grammar/langium-grammar.langium b/packages/langium/src/grammar/langium-grammar.langium index c6f362297..59095e263 100644 --- a/packages/langium/src/grammar/langium-grammar.langium +++ b/packages/langium/src/grammar/langium-grammar.langium @@ -47,7 +47,7 @@ ArrayType infers TypeDefinition: ReferenceType infers TypeDefinition: SimpleType | - {infer ReferenceType} '@' referenceType=SimpleType; + {infer ReferenceType} '@' referenceType=SimpleType (isMulti?='*')?; SimpleType infers TypeDefinition: '(' TypeDefinition ')' | @@ -166,7 +166,7 @@ AssignableAlternatives infers AbstractElement: AssignableTerminal ({infer Alternatives.elements+=current} ('|' elements+=AssignableTerminal)+)?; CrossReference infers AbstractElement: - {infer CrossReference} '[' type=[AbstractType] ((deprecatedSyntax?='|' | ':') terminal=CrossReferenceableTerminal )? ']'; + {infer CrossReference} '[' isMulti?='*'? type=[AbstractType] ((deprecatedSyntax?='|' | ':') terminal=CrossReferenceableTerminal )? ']'; CrossReferenceableTerminal infers AbstractElement: Keyword | RuleCall; diff --git a/packages/langium/src/grammar/references/grammar-references.ts b/packages/langium/src/grammar/references/grammar-references.ts index a96697d1e..d75ef5724 100644 --- a/packages/langium/src/grammar/references/grammar-references.ts +++ b/packages/langium/src/grammar/references/grammar-references.ts @@ -29,18 +29,20 @@ export class LangiumGrammarReferences extends DefaultReferences { this.documents = services.shared.workspace.LangiumDocuments; } - override findDeclaration(sourceCstNode: CstNode): AstNode | undefined { + override findDeclarations(sourceCstNode: CstNode): AstNode[] { const nodeElem = sourceCstNode.astNode; const assignment = findAssignment(sourceCstNode); if (assignment && assignment.feature === 'feature') { // Only search for a special declaration if the cst node is the feature property of the action/assignment if (isAssignment(nodeElem)) { - return this.findAssignmentDeclaration(nodeElem); + const decl = this.findAssignmentDeclaration(nodeElem); + return decl ? [decl] : []; } else if (isAction(nodeElem)) { - return this.findActionDeclaration(nodeElem); + const decl = this.findActionDeclaration(nodeElem); + return decl ? [decl] : []; } } - return super.findDeclaration(sourceCstNode); + return super.findDeclarations(sourceCstNode); } override findReferences(targetNode: AstNode, options: FindReferencesOptions): Stream { @@ -56,10 +58,7 @@ export class LangiumGrammarReferences extends DefaultReferences { const interfaceNode = getContainerOfType(targetNode, isInterface); if (interfaceNode) { if (includeDeclaration) { - const ref = this.getReferenceToSelf(targetNode); - if (ref) { - refs.push(ref); - } + refs.push(...this.getSelfReferences(targetNode)); } const interfaces = collectChildrenTypes(interfaceNode, this, this.documents, this.nodeLocator); const targetRules: Array = []; diff --git a/packages/langium/src/grammar/type-system/type-collector/declared-types.ts b/packages/langium/src/grammar/type-system/type-collector/declared-types.ts index a0368a331..b46147b11 100644 --- a/packages/langium/src/grammar/type-system/type-collector/declared-types.ts +++ b/packages/langium/src/grammar/type-system/type-collector/declared-types.ts @@ -78,7 +78,8 @@ export function typeDefinitionToPropertyType(type: TypeDefinition): PlainPropert }; } else if (isReferenceType(type)) { return { - referenceType: typeDefinitionToPropertyType(type.referenceType) + referenceType: typeDefinitionToPropertyType(type.referenceType), + mode: type.isMulti ? 'multi' : 'single' }; } else if (isUnionType(type)) { return { diff --git a/packages/langium/src/grammar/type-system/type-collector/inferred-types.ts b/packages/langium/src/grammar/type-system/type-collector/inferred-types.ts index 58a45ae0a..503a37e98 100644 --- a/packages/langium/src/grammar/type-system/type-collector/inferred-types.ts +++ b/packages/langium/src/grammar/type-system/type-collector/inferred-types.ts @@ -32,7 +32,7 @@ type TypeAlternative = { type TypeCollection = { types: Set - reference: boolean + reference: 'single' | 'multi' | false } interface TypeCollectionContext { @@ -476,7 +476,7 @@ function findTypes(terminal: AbstractElement, types: TypeCollection): void { if (refTypeName) { types.types.add(refTypeName); } - types.reference = true; + types.reference = terminal.isMulti ? 'multi' : 'single'; } } @@ -684,14 +684,15 @@ function extractUnions(interfaces: PlainInterface[], unions: PlainUnion[], decla return astTypes; } -function toPropertyType(array: boolean, reference: boolean, types: string[]): PlainPropertyType { +function toPropertyType(array: boolean, reference: false | 'single' | 'multi', types: string[]): PlainPropertyType { if (array) { return { elementType: toPropertyType(false, reference, types) }; } else if (reference) { return { - referenceType: toPropertyType(false, false, types) + referenceType: toPropertyType(false, false, types), + mode: reference }; } else if (types.length === 1) { const type = types[0]; diff --git a/packages/langium/src/grammar/type-system/type-collector/plain-types.ts b/packages/langium/src/grammar/type-system/type-collector/plain-types.ts index 9406cac50..3921c59aa 100644 --- a/packages/langium/src/grammar/type-system/type-collector/plain-types.ts +++ b/packages/langium/src/grammar/type-system/type-collector/plain-types.ts @@ -62,6 +62,7 @@ export type PlainPropertyType = export interface PlainReferenceType { referenceType: PlainPropertyType; + mode: 'single' | 'multi' } export function isPlainReferenceType(propertyType: PlainPropertyType): propertyType is PlainReferenceType { @@ -176,7 +177,8 @@ function plainToPropertyType(type: PlainPropertyType, union: UnionType | undefin }; } else if (isPlainReferenceType(type)) { return { - referenceType: plainToPropertyType(type.referenceType, undefined, interfaces, unions) + referenceType: plainToPropertyType(type.referenceType, undefined, interfaces, unions), + mode: type.mode }; } else if (isPlainPropertyUnion(type)) { return { diff --git a/packages/langium/src/grammar/type-system/type-collector/types.ts b/packages/langium/src/grammar/type-system/type-collector/types.ts index a52bc8256..cf6fdeddc 100644 --- a/packages/langium/src/grammar/type-system/type-collector/types.ts +++ b/packages/langium/src/grammar/type-system/type-collector/types.ts @@ -29,6 +29,7 @@ export type PropertyType = export interface ReferenceType { referenceType: PropertyType + mode: 'single' | 'multi' | false } export function isReferenceType(propertyType: PropertyType): propertyType is ReferenceType { @@ -343,7 +344,7 @@ function isInterfaceAssignable(from: InterfaceType, to: InterfaceType, visited: function propertyTypeToKeyString(type: PropertyType): string { if (isReferenceType(type)) { - return `@(${propertyTypeToKeyString(type.referenceType)})}`; + return `@(${propertyTypeToKeyString(type.referenceType)})${type.mode === 'multi' ? '*' : ''}`; } else if (isArrayType(type)) { return type.elementType ? `(${propertyTypeToKeyString(type.elementType)})[]` : 'unknown[]'; } else if (isPropertyUnion(type)) { @@ -368,7 +369,7 @@ export function propertyTypeToString(type?: PropertyType, mode: 'AstType' | 'Dec } if (isReferenceType(type)) { const refType = propertyTypeToString(type.referenceType, mode); - return mode === 'AstType' ? `Reference<${refType}>` : `@${typeParenthesis(type.referenceType, refType)}`; + return mode === 'AstType' ? `${type.mode === 'multi' ? 'Multi' : ''}Reference<${refType}>` : `@${typeParenthesis(type.referenceType, refType)}${type.mode === 'multi' ? '*' : ''}`; } else if (isArrayType(type)) { const arrayType = propertyTypeToString(type.elementType, mode); return mode === 'AstType' ? `Array<${arrayType}>` : `${type.elementType ? typeParenthesis(type.elementType, arrayType) : 'unknown'}[]`; diff --git a/packages/langium/src/grammar/validation/types-validator.ts b/packages/langium/src/grammar/validation/types-validator.ts index 14f3327ab..fb13b1c9d 100644 --- a/packages/langium/src/grammar/validation/types-validator.ts +++ b/packages/langium/src/grammar/validation/types-validator.ts @@ -378,7 +378,7 @@ function validatePropertiesConsistency( // a corresponding declared type const matchingProp = (type: PropertyType): PropertyType => { if (isPropertyUnion(type)) return { types: type.types.map(t => matchingProp(t)) }; - if (isReferenceType(type)) return { referenceType: matchingProp(type.referenceType) }; + if (isReferenceType(type)) return { referenceType: matchingProp(type.referenceType), mode: type.mode }; if (isArrayType(type)) return { elementType: type.elementType && matchingProp(type.elementType) }; if (isValueType(type)) { const resource = resources.typeToValidationInfo.get(type.value.name); diff --git a/packages/langium/src/languages/generated/ast.ts b/packages/langium/src/languages/generated/ast.ts index e96439b5e..90532debb 100644 --- a/packages/langium/src/languages/generated/ast.ts +++ b/packages/langium/src/languages/generated/ast.ts @@ -287,6 +287,7 @@ export function isParserRule(item: unknown): item is ParserRule { export interface ReferenceType extends AstNode { readonly $container: ArrayType | ReferenceType | Type | TypeAttribute | UnionType; readonly $type: 'ReferenceType'; + isMulti: boolean; referenceType: TypeDefinition; } @@ -443,6 +444,7 @@ export function isCharacterRange(item: unknown): item is CharacterRange { export interface CrossReference extends AbstractElement { readonly $type: 'CrossReference'; deprecatedSyntax: boolean; + isMulti: boolean; terminal?: AbstractElement; type: Reference; } @@ -889,6 +891,7 @@ export class LangiumGrammarAstReflection extends AbstractAstReflection { return { name: 'ReferenceType', properties: [ + { name: 'isMulti', defaultValue: false }, { name: 'referenceType' } ] }; @@ -1011,6 +1014,7 @@ export class LangiumGrammarAstReflection extends AbstractAstReflection { properties: [ { name: 'cardinality' }, { name: 'deprecatedSyntax', defaultValue: false }, + { name: 'isMulti', defaultValue: false }, { name: 'lookahead' }, { name: 'terminal' }, { name: 'type' } diff --git a/packages/langium/src/lsp/call-hierarchy-provider.ts b/packages/langium/src/lsp/call-hierarchy-provider.ts index 3c509ee43..42290bb18 100644 --- a/packages/langium/src/lsp/call-hierarchy-provider.ts +++ b/packages/langium/src/lsp/call-hierarchy-provider.ts @@ -54,12 +54,16 @@ export abstract class AbstractCallHierarchyProvider implements CallHierarchyProv return undefined; } - const declarationNode = this.references.findDeclarationNode(targetNode); - if (!declarationNode) { + const declarationNodes = this.references.findDeclarationNodes(targetNode); + if (!declarationNodes) { return undefined; } - return this.getCallHierarchyItems(declarationNode.astNode, document); + const items: CallHierarchyItem[] = []; + for (const declarationNode of declarationNodes) { + items.push(...(this.getCallHierarchyItems(declarationNode.astNode, document) ?? [])); + } + return items; } protected getCallHierarchyItems(targetNode: AstNode, document: LangiumDocument): CallHierarchyItem[] | undefined { diff --git a/packages/langium/src/lsp/completion/completion-provider.ts b/packages/langium/src/lsp/completion/completion-provider.ts index 73d8a53a3..c2e309389 100644 --- a/packages/langium/src/lsp/completion/completion-provider.ts +++ b/packages/langium/src/lsp/completion/completion-provider.ts @@ -9,7 +9,7 @@ import type { LangiumCompletionParser } from '../../parser/langium-parser.js'; import type { NameProvider } from '../../references/name-provider.js'; import type { ScopeProvider } from '../../references/scope-provider.js'; import type { LangiumServices } from '../lsp-services.js'; -import type { AstNode, AstNodeDescription, AstReflection, CstNode, ReferenceInfo } from '../../syntax-tree.js'; +import type { AstNode, AstNodeDescription, AstReflection, CstNode, MultiReference, Reference, ReferenceInfo } from '../../syntax-tree.js'; import type { CancellationToken } from '../../utils/cancellation.js'; import type { MaybePromise } from '../../utils/promise-utils.js'; import type { LangiumDocument, TextDocument } from '../../workspace/documents.js'; @@ -420,10 +420,20 @@ export class DefaultCompletionProvider implements CompletionProvider { }; assignMandatoryProperties(this.astReflection, node); } + let reference: Reference | MultiReference; + if (next.feature.isMulti) { + reference = { + $refText: '', + items: [] + }; + } else { + reference = { + $refText: '', + ref: undefined + }; + } const refInfo: ReferenceInfo = { - reference: { - $refText: '' - }, + reference, container: node, property: assignment.feature }; diff --git a/packages/langium/src/lsp/definition-provider.ts b/packages/langium/src/lsp/definition-provider.ts index a9edab761..aedf2f4e7 100644 --- a/packages/langium/src/lsp/definition-provider.ts +++ b/packages/langium/src/lsp/definition-provider.ts @@ -61,26 +61,27 @@ export class DefaultDefinitionProvider implements DefinitionProvider { } protected collectLocationLinks(sourceCstNode: CstNode, _params: DefinitionParams): MaybePromise { - const goToLink = this.findLink(sourceCstNode); - if (goToLink) { - return [LocationLink.create( - goToLink.targetDocument.textDocument.uri, - (goToLink.target.astNode.$cstNode ?? goToLink.target).range, - goToLink.target.range, - goToLink.source.range - )]; + const goToLinks = this.findLinks(sourceCstNode); + if (goToLinks.length > 0) { + return goToLinks.map(link => LocationLink.create( + link.targetDocument.textDocument.uri, + (link.target.astNode.$cstNode ?? link.target).range, + link.target.range, + link.source.range + )); } return undefined; } - protected findLink(source: CstNode): GoToLink | undefined { - const target = this.references.findDeclarationNode(source); - if (target?.astNode) { + protected findLinks(source: CstNode): GoToLink[] { + const targets = this.references.findDeclarationNodes(source); + const links: GoToLink[] = []; + for (const target of targets) { const targetDocument = getDocument(target.astNode); - if (target && targetDocument) { - return { source, target, targetDocument }; + if (targets && targetDocument) { + links.push({ source, target, targetDocument }); } } - return undefined; + return links; } } diff --git a/packages/langium/src/lsp/document-highlight-provider.ts b/packages/langium/src/lsp/document-highlight-provider.ts index 13859cd33..b7b5278e3 100644 --- a/packages/langium/src/lsp/document-highlight-provider.ts +++ b/packages/langium/src/lsp/document-highlight-provider.ts @@ -51,14 +51,15 @@ export class DefaultDocumentHighlightProvider implements DocumentHighlightProvid if (!selectedNode) { return undefined; } - const targetAstNode = this.references.findDeclaration(selectedNode); - if (targetAstNode) { - const includeDeclaration = UriUtils.equals(getDocument(targetAstNode).uri, document.uri); - const options: FindReferencesOptions = { documentUri: document.uri, includeDeclaration }; - const references = this.references.findReferences(targetAstNode, options); - return references.map(ref => this.createDocumentHighlight(ref)).toArray(); + const targetAstNode = this.references.findDeclarations(selectedNode); + const highlights: DocumentHighlight[] = []; + for (const target of targetAstNode) { + const includeDeclaration = UriUtils.equals(getDocument(target).uri, document.uri); + const options: FindReferencesOptions = { documentUri: document.uri, includeDeclaration: includeDeclaration }; + const references = this.references.findReferences(target, options); + highlights.push(...references.map(ref => this.createDocumentHighlight(ref)).toArray()); } - return undefined; + return highlights; } /** diff --git a/packages/langium/src/lsp/hover-provider.ts b/packages/langium/src/lsp/hover-provider.ts index e557d5efb..e43c7164e 100644 --- a/packages/langium/src/lsp/hover-provider.ts +++ b/packages/langium/src/lsp/hover-provider.ts @@ -32,28 +32,43 @@ export abstract class AstNodeHoverProvider implements HoverProvider { protected readonly references: References; protected readonly grammarConfig: GrammarConfig; + protected readonly languageId: string; constructor(services: LangiumServices) { this.references = services.references.References; this.grammarConfig = services.parser.GrammarConfig; + this.languageId = services.LanguageMetaData.languageId; } - getHoverContent(document: LangiumDocument, params: HoverParams): MaybePromise { + async getHoverContent(document: LangiumDocument, params: HoverParams): Promise { const rootNode = document.parseResult?.value?.$cstNode; if (rootNode) { const offset = document.textDocument.offsetAt(params.position); const cstNode = findDeclarationNodeAtOffset(rootNode, offset, this.grammarConfig.nameRegexp); if (cstNode && cstNode.offset + cstNode.length > offset) { - const targetNode = this.references.findDeclaration(cstNode); - if (targetNode) { - return this.getAstNodeHoverContent(targetNode); + const contents: string[] = []; + const targetNodes = this.references.findDeclarations(cstNode); + for (const targetNode of targetNodes) { + const content = await this.getAstNodeHoverContent(targetNode); + if (typeof content === 'string') { + contents.push(content); + } + } + if (contents.length > 0) { + return { + contents: { + kind: 'markdown', + language: this.languageId, + value: contents.join(' ') + } + }; } } } return undefined; } - protected abstract getAstNodeHoverContent(node: AstNode): MaybePromise; + protected abstract getAstNodeHoverContent(node: AstNode): MaybePromise; } @@ -66,16 +81,11 @@ export class MultilineCommentHoverProvider extends AstNodeHoverProvider { this.documentationProvider = services.documentation.DocumentationProvider; } - protected getAstNodeHoverContent(node: AstNode): MaybePromise { + protected getAstNodeHoverContent(node: AstNode): MaybePromise { const content = this.documentationProvider.getDocumentation(node); if (content) { - return { - contents: { - kind: 'markdown', - value: content - } - }; + return content; } return undefined; } diff --git a/packages/langium/src/lsp/implementation-provider.ts b/packages/langium/src/lsp/implementation-provider.ts index 442bffdcd..b3871feda 100644 --- a/packages/langium/src/lsp/implementation-provider.ts +++ b/packages/langium/src/lsp/implementation-provider.ts @@ -33,14 +33,18 @@ export abstract class AbstractGoToImplementationProvider implements Implementati this.grammarConfig = services.parser.GrammarConfig; } - getImplementation(document: LangiumDocument, params: ImplementationParams, cancelToken = CancellationToken.None): MaybePromise { + async getImplementation(document: LangiumDocument, params: ImplementationParams, cancelToken = CancellationToken.None): Promise { const rootNode = document.parseResult.value; if (rootNode.$cstNode) { const sourceCstNode = findDeclarationNodeAtOffset(rootNode.$cstNode, document.textDocument.offsetAt(params.position), this.grammarConfig.nameRegexp); if (sourceCstNode) { - const nodeDeclaration = this.references.findDeclaration(sourceCstNode); - if (nodeDeclaration) { - return this.collectGoToImplementationLocationLinks(nodeDeclaration, cancelToken); + const nodeDeclarations = this.references.findDeclarations(sourceCstNode); + const links: LocationLink[] = []; + for (const node of nodeDeclarations) { + const location = await this.collectGoToImplementationLocationLinks(node, cancelToken); + if (location) { + links.push(...location); + } } } } diff --git a/packages/langium/src/lsp/references-provider.ts b/packages/langium/src/lsp/references-provider.ts index 0e4ca3eef..c5688ae47 100644 --- a/packages/langium/src/lsp/references-provider.ts +++ b/packages/langium/src/lsp/references-provider.ts @@ -56,10 +56,10 @@ export class DefaultReferencesProvider implements ReferencesProvider { protected getReferences(selectedNode: LeafCstNode, params: ReferenceParams, _document: LangiumDocument): Location[] { const locations: Location[] = []; - const targetAstNode = this.references.findDeclaration(selectedNode); - if (targetAstNode) { + const targetAstNode = this.references.findDeclarations(selectedNode); + for (const target of targetAstNode) { const options = { includeDeclaration: params.context.includeDeclaration }; - this.references.findReferences(targetAstNode, options).forEach(reference => { + this.references.findReferences(target, options).forEach(reference => { locations.push(Location.create(reference.sourceUri.toString(), reference.segment.range)); }); } diff --git a/packages/langium/src/lsp/rename-provider.ts b/packages/langium/src/lsp/rename-provider.ts index 1bb9b3f4e..27bf54c57 100644 --- a/packages/langium/src/lsp/rename-provider.ts +++ b/packages/langium/src/lsp/rename-provider.ts @@ -53,15 +53,24 @@ export class DefaultRenameProvider implements RenameProvider { async rename(document: LangiumDocument, params: RenameParams): Promise { const changes: Record = {}; const rootNode = document.parseResult.value.$cstNode; - if (!rootNode) return undefined; + if (!rootNode) { + return undefined; + } const offset = document.textDocument.offsetAt(params.position); const leafNode = findDeclarationNodeAtOffset(rootNode, offset, this.grammarConfig.nameRegexp); - if (!leafNode) return undefined; - const targetNode = this.references.findDeclaration(leafNode); - if (!targetNode) return undefined; + if (!leafNode) { + return undefined; + } + const targetNodes = this.references.findDeclarations(leafNode); + if (targetNodes.length === 0) { + return undefined; + } + // We only need to find the references to a single target node + // All other nodes should be found via `findReferences` if done correctly + const targetNode = targetNodes[0]; const options = { onlyLocal: false, includeDeclaration: true }; const references = this.references.findReferences(targetNode, options); - references.forEach(ref => { + for (const ref of references) { const change = TextEdit.replace(ref.segment.range, params.newName); const uri = ref.sourceUri.toString(); if (changes[uri]) { @@ -69,7 +78,7 @@ export class DefaultRenameProvider implements RenameProvider { } else { changes[uri] = [change]; } - }); + } return { changes }; } @@ -80,12 +89,12 @@ export class DefaultRenameProvider implements RenameProvider { protected renameNodeRange(doc: LangiumDocument, position: Position): Range | undefined { const rootNode = doc.parseResult.value.$cstNode; const offset = doc.textDocument.offsetAt(position); - if (rootNode && offset) { + if (rootNode) { const leafNode = findDeclarationNodeAtOffset(rootNode, offset, this.grammarConfig.nameRegexp); if (!leafNode) { return undefined; } - const isCrossRef = this.references.findDeclaration(leafNode); + const isCrossRef = this.references.findDeclarations(leafNode).length > 0; // return range if selected CstNode is the name node or it is a crosslink which points to a declaration if (isCrossRef || this.isNameNode(leafNode)) { return leafNode.range; diff --git a/packages/langium/src/lsp/type-hierarchy-provider.ts b/packages/langium/src/lsp/type-hierarchy-provider.ts index a9eec8308..b8d00cba7 100644 --- a/packages/langium/src/lsp/type-hierarchy-provider.ts +++ b/packages/langium/src/lsp/type-hierarchy-provider.ts @@ -58,12 +58,12 @@ export abstract class AbstractTypeHierarchyProvider implements TypeHierarchyProv return undefined; } - const declarationNode = this.references.findDeclarationNode(targetNode); - if (!declarationNode) { - return undefined; + const declarationNodes = this.references.findDeclarationNodes(targetNode); + const items: TypeHierarchyItem[] = []; + for (const declarationNode of declarationNodes) { + items.push(...(this.getTypeHierarchyItems(declarationNode.astNode, document) ?? [])); } - - return this.getTypeHierarchyItems(declarationNode.astNode, document); + return items; } protected getTypeHierarchyItems(targetNode: AstNode, document: LangiumDocument): TypeHierarchyItem[] | undefined { diff --git a/packages/langium/src/lsp/type-provider.ts b/packages/langium/src/lsp/type-provider.ts index 19b72a42d..f52f5fe9e 100644 --- a/packages/langium/src/lsp/type-provider.ts +++ b/packages/langium/src/lsp/type-provider.ts @@ -31,14 +31,18 @@ export abstract class AbstractTypeDefinitionProvider implements TypeDefinitionPr this.references = services.references.References; } - getTypeDefinition(document: LangiumDocument, params: TypeDefinitionParams, cancelToken = CancellationToken.None): MaybePromise { + async getTypeDefinition(document: LangiumDocument, params: TypeDefinitionParams, cancelToken = CancellationToken.None): Promise { const rootNode = document.parseResult.value; if (rootNode.$cstNode) { const sourceCstNode = findDeclarationNodeAtOffset(rootNode.$cstNode, document.textDocument.offsetAt(params.position)); if (sourceCstNode) { - const nodeDeclaration = this.references.findDeclaration(sourceCstNode); - if (nodeDeclaration) { - return this.collectGoToTypeLocationLinks(nodeDeclaration, cancelToken); + const nodeDeclarations = this.references.findDeclarations(sourceCstNode); + const links: LocationLink[] = []; + for (const node of nodeDeclarations) { + const location = await this.collectGoToTypeLocationLinks(node, cancelToken); + if (location) { + links.push(...location); + } } } } diff --git a/packages/langium/src/parser/langium-parser.ts b/packages/langium/src/parser/langium-parser.ts index a073a26bd..d192985d8 100644 --- a/packages/langium/src/parser/langium-parser.ts +++ b/packages/langium/src/parser/langium-parser.ts @@ -48,7 +48,7 @@ type RuleImpl = (args: Args) => any; interface AssignmentElement { assignment?: Assignment - isCrossRef: boolean + crossRef?: 'single' | 'multi' } export interface BaseParser { @@ -208,11 +208,11 @@ export class LangiumParser extends AbstractLangiumParser { const token = this.wrapper.wrapConsume(idx, tokenType); if (!this.isRecording() && this.isValidToken(token)) { const leafNode = this.nodeBuilder.buildLeafNode(token, feature); - const { assignment, isCrossRef } = this.getAssignment(feature); + const { assignment, crossRef } = this.getAssignment(feature); const current = this.current; if (assignment) { const convertedValue = isKeyword(feature) ? token.image : this.converter.convert(token.image, leafNode); - this.assign(assignment.operator, assignment.feature, convertedValue, leafNode, isCrossRef); + this.assign(assignment.operator, assignment.feature, convertedValue, leafNode, crossRef); } else if (isDataTypeNode(current)) { let text = token.image; if (!isKeyword(feature)) { @@ -245,9 +245,9 @@ export class LangiumParser extends AbstractLangiumParser { } private performSubruleAssignment(result: any, feature: AbstractElement, cstNode: CompositeCstNode): void { - const { assignment, isCrossRef } = this.getAssignment(feature); + const { assignment, crossRef } = this.getAssignment(feature); if (assignment) { - this.assign(assignment.operator, assignment.feature, result, cstNode, isCrossRef); + this.assign(assignment.operator, assignment.feature, result, cstNode, crossRef); } else if (!assignment) { // If we call a subrule without an assignment we either: // 1. append the result of the subrule (data type rule) @@ -285,7 +285,7 @@ export class LangiumParser extends AbstractLangiumParser { this.stack.pop(); this.stack.push(newItem); if (action.feature && action.operator) { - this.assign(action.operator, action.feature, last, last.$cstNode, false); + this.assign(action.operator, action.feature, last, last.$cstNode); } } } @@ -313,17 +313,19 @@ export class LangiumParser extends AbstractLangiumParser { const assignment = getContainerOfType(feature, isAssignment); this.assignmentMap.set(feature, { assignment: assignment, - isCrossRef: assignment ? isCrossReference(assignment.terminal) : false + crossRef: assignment && isCrossReference(assignment.terminal) ? (assignment.terminal.isMulti ? 'multi' : 'single') : undefined }); } return this.assignmentMap.get(feature)!; } - private assign(operator: string, feature: string, value: unknown, cstNode: CstNode, isCrossRef: boolean): void { + private assign(operator: string, feature: string, value: unknown, cstNode: CstNode, crossRef?: 'multi' | 'single'): void { const obj = this.current; let item: unknown; - if (isCrossRef && typeof value === 'string') { + if (crossRef === 'single' && typeof value === 'string') { item = this.linker.buildReference(obj, feature, cstNode, value); + } else if (crossRef === 'multi' && typeof value === 'string') { + item = this.linker.buildMultiReference(obj, feature, cstNode, value); } else { item = value; } diff --git a/packages/langium/src/references/linker.ts b/packages/langium/src/references/linker.ts index 8e0cb803f..b408563d2 100644 --- a/packages/langium/src/references/linker.ts +++ b/packages/langium/src/references/linker.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import type { LangiumCoreServices } from '../services.js'; -import type { AstNode, AstNodeDescription, AstReflection, CstNode, LinkingError, Reference, ReferenceInfo } from '../syntax-tree.js'; +import type { AstNode, AstNodeDescription, AstReflection, CstNode, LinkingError, MultiReference, MultiReferenceItem, Reference, ReferenceInfo } from '../syntax-tree.js'; import type { AstNodeLocator } from '../workspace/ast-node-locator.js'; import type { LangiumDocument, LangiumDocuments } from '../workspace/documents.js'; import type { ScopeProvider } from './scope-provider.js'; @@ -46,6 +46,15 @@ export interface Linker { */ getCandidate(refInfo: ReferenceInfo): AstNodeDescription | LinkingError; + /** + * Determines a candidate AST node description for linking the given reference. + * + * @param node The AST node containing the reference. + * @param refId The reference identifier used to build a scope. + * @param reference The actual reference to resolve. + */ + getCandidates(refInfo: ReferenceInfo): AstNodeDescription[] | LinkingError; + /** * Creates a cross reference node being aware of its containing AstNode, the corresponding CstNode, * the cross reference text denoting the target AstNode being already extracted of the document text, @@ -65,13 +74,20 @@ export interface Linker { */ buildReference(node: AstNode, property: string, refNode: CstNode | undefined, refText: string): Reference; + buildMultiReference(node: AstNode, property: string, refNode: CstNode | undefined, refText: string): MultiReference; + } -interface DefaultReference extends Reference { - _ref?: AstNode | LinkingError; +export interface DefaultReference extends Reference { + _ref: AstNode | LinkingError | undefined; _nodeDescription?: AstNodeDescription; } +export interface DefaultMultiReference extends MultiReference { + _items: MultiReferenceItem[]; + _linkingError?: LinkingError; +} + export class DefaultLinker implements Linker { protected readonly reflection: AstReflection; protected readonly scopeProvider: ScopeProvider; @@ -93,27 +109,46 @@ export class DefaultLinker implements Linker { } protected doLink(refInfo: ReferenceInfo, document: LangiumDocument): void { - const ref = refInfo.reference as DefaultReference; + const ref = refInfo.reference as DefaultReference | DefaultMultiReference; // The reference may already have been resolved lazily by accessing its `ref` property. - if (ref._ref === undefined) { + if ('_ref' in ref && ref._ref === undefined) { try { const description = this.getCandidate(refInfo); if (isLinkingError(description)) { ref._ref = description; } else { ref._nodeDescription = description; - if (this.langiumDocuments().hasDocument(description.documentUri)) { - // The target document is already loaded + const linkedNode = this.loadAstNode(description); + ref._ref = linkedNode ?? this.createLinkingError(refInfo, description); + } + } catch (err) { + ref._ref = { + info: refInfo, + message: `An error occurred while resolving reference to '${ref.$refText}': ${err}` + }; + } + } else if ('_items' in ref && ref._items.length === 0 && !ref._linkingError) { + try { + const descriptions = this.getCandidates(refInfo); + if (isLinkingError(descriptions)) { + ref._linkingError = descriptions; + } else { + const items: MultiReferenceItem[] = []; + for (const description of descriptions) { const linkedNode = this.loadAstNode(description); - ref._ref = linkedNode ?? this.createLinkingError(refInfo, description); + if (linkedNode) { + items.push({ ref: linkedNode, $nodeDescription: description }); + } } + ref._items = items; } } catch (err) { - ref._ref = { - ...refInfo, + ref._linkingError = { + info: refInfo, message: `An error occurred while resolving reference to '${ref.$refText}': ${err}` }; } + } // Add the reference to the document's array of references document.references.push(ref); @@ -121,8 +156,13 @@ export class DefaultLinker implements Linker { unlink(document: LangiumDocument): void { for (const ref of document.references) { - delete (ref as DefaultReference)._ref; - delete (ref as DefaultReference)._nodeDescription; + if ('_ref' in ref) { + (ref as DefaultReference)._ref = undefined; + delete (ref as DefaultReference)._nodeDescription; + } else if ('_items' in ref) { + (ref as DefaultMultiReference)._items = []; + delete (ref as DefaultMultiReference)._linkingError; + } } document.references = []; } @@ -133,6 +173,12 @@ export class DefaultLinker implements Linker { return description ?? this.createLinkingError(refInfo); } + getCandidates(refInfo: ReferenceInfo): AstNodeDescription[] | LinkingError { + const scope = this.scopeProvider.getScope(refInfo); + const descriptions = scope.getElements(refInfo.reference.$refText).distinct(desc => `${desc.documentUri}#${desc.path}`).toArray(); + return descriptions.length > 0 ? descriptions : this.createLinkingError(refInfo); + } + buildReference(node: AstNode, property: string, refNode: CstNode | undefined, refText: string): Reference { // See behavior description in doc of Linker, update that on changes in here. // eslint-disable-next-line @typescript-eslint/no-this-alias @@ -140,6 +186,7 @@ export class DefaultLinker implements Linker { const reference: DefaultReference = { $refNode: refNode, $refText: refText, + _ref: undefined, get ref() { if (isAstNode(this._ref)) { @@ -172,6 +219,33 @@ export class DefaultLinker implements Linker { return reference; } + buildMultiReference(node: AstNode, property: string, refNode: CstNode | undefined, refText: string): MultiReference { + // See behavior description in doc of Linker, update that on changes in here. + // eslint-disable-next-line @typescript-eslint/no-this-alias + const linker = this; + const reference: DefaultMultiReference = { + $refNode: refNode, + $refText: refText, + _items: [], + + get items() { + return this._items; + }, + get error() { + if (this._linkingError) { + return this._linkingError; + } + const refs = this.items; + if (refs.length > 0) { + return undefined; + } else { + return (this._linkingError = linker.createLinkingError({ reference, container: node, property })); + } + } + }; + return reference; + } + protected getLinkedNode(refInfo: ReferenceInfo): { node?: AstNode, descr?: AstNodeDescription, error?: LinkingError } { try { const description = this.getCandidate(refInfo); @@ -192,7 +266,7 @@ export class DefaultLinker implements Linker { } catch (err) { return { error: { - ...refInfo, + info: refInfo, message: `An error occurred while resolving reference to '${refInfo.reference.$refText}': ${err}` } }; @@ -219,7 +293,7 @@ export class DefaultLinker implements Linker { } const referenceType = this.reflection.getReferenceType(refInfo); return { - ...refInfo, + info: refInfo, message: `Could not resolve reference to ${referenceType} named '${refInfo.reference.$refText}'.`, targetDescription }; diff --git a/packages/langium/src/references/references.ts b/packages/langium/src/references/references.ts index 34c9089eb..13bebfa16 100644 --- a/packages/langium/src/references/references.ts +++ b/packages/langium/src/references/references.ts @@ -13,11 +13,12 @@ import type { IndexManager } from '../workspace/index-manager.js'; import type { NameProvider } from './name-provider.js'; import type { URI } from '../utils/uri-utils.js'; import { findAssignment } from '../utils/grammar-utils.js'; -import { isReference } from '../syntax-tree.js'; -import { getDocument } from '../utils/ast-utils.js'; +import { isMultiReference, isReference } from '../syntax-tree.js'; +import { getDocument, getReferenceNodes, streamAst, streamContents } from '../utils/ast-utils.js'; import { isChildNode, toDocumentSegment } from '../utils/cst-utils.js'; import { stream } from '../utils/stream.js'; import { UriUtils } from '../utils/uri-utils.js'; +import { isCrossReference } from '../languages/generated/ast.js'; /** * Language-specific service for finding references and declaration of a given `CstNode`. @@ -25,20 +26,20 @@ import { UriUtils } from '../utils/uri-utils.js'; export interface References { /** - * If the CstNode is a reference node the target CstNode will be returned. - * If the CstNode is a significant node of the CstNode this CstNode will be returned. + * If the CstNode is a reference node the target AstNodes will be returned. + * If the CstNode is a significant node of the CstNode this AstNode will be returned. * * @param sourceCstNode CstNode that points to a AstNode */ - findDeclaration(sourceCstNode: CstNode): AstNode | undefined; + findDeclarations(sourceCstNode: CstNode): AstNode[]; /** - * If the CstNode is a reference node the target CstNode will be returned. + * If the CstNode is a reference node the target CstNodes will be returned. * If the CstNode is a significant node of the CstNode this CstNode will be returned. * * @param sourceCstNode CstNode that points to a AstNode */ - findDeclarationNode(sourceCstNode: CstNode): CstNode | undefined; + findDeclarationNodes(sourceCstNode: CstNode): CstNode[]; /** * Finds all references to the target node as references (local references) or reference descriptions. @@ -49,10 +50,6 @@ export interface References { } export interface FindReferencesOptions { - /** - * @deprecated Since v1.2.0. Please use `documentUri` instead. - */ - onlyLocal?: boolean; /** * When set, the `findReferences` method will only return references/declarations from the specified document. */ @@ -67,28 +64,30 @@ export class DefaultReferences implements References { protected readonly nameProvider: NameProvider; protected readonly index: IndexManager; protected readonly nodeLocator: AstNodeLocator; + protected hasMultiReference: boolean; constructor(services: LangiumCoreServices) { this.nameProvider = services.references.NameProvider; this.index = services.shared.workspace.IndexManager; this.nodeLocator = services.workspace.AstNodeLocator; + this.hasMultiReference = streamAst(services.Grammar).some(node => isCrossReference(node) && node.isMulti); } - findDeclaration(sourceCstNode: CstNode): AstNode | undefined { + findDeclarations(sourceCstNode: CstNode): AstNode[] { if (sourceCstNode) { const assignment = findAssignment(sourceCstNode); const nodeElem = sourceCstNode.astNode; if (assignment && nodeElem) { const reference = (nodeElem as GenericAstNode)[assignment.feature]; - if (isReference(reference)) { - return reference.ref; + if (isReference(reference) || isMultiReference(reference)) { + return getReferenceNodes(reference); } else if (Array.isArray(reference)) { for (const ref of reference) { - if (isReference(ref) && ref.$refNode + if ((isReference(ref) || isMultiReference(ref)) && ref.$refNode && ref.$refNode.offset <= sourceCstNode.offset && ref.$refNode.end >= sourceCstNode.end) { - return ref.ref; + return getReferenceNodes(ref); } } } @@ -97,29 +96,54 @@ export class DefaultReferences implements References { const nameNode = this.nameProvider.getNameNode(nodeElem); // Only return the targeted node in case the targeted cst node is the name node or part of it if (nameNode && (nameNode === sourceCstNode || isChildNode(sourceCstNode, nameNode))) { - return nodeElem; + return this.getSelfNodes(nodeElem); } } } - return undefined; + return []; } - findDeclarationNode(sourceCstNode: CstNode): CstNode | undefined { - const astNode = this.findDeclaration(sourceCstNode); - if (astNode?.$cstNode) { - const targetNode = this.nameProvider.getNameNode(astNode); - return targetNode ?? astNode.$cstNode; + /** + * In case your grammar features multi references (i.e. references that can target multiple elements at once), + * you can override this method to return all possible sibling elements for the specified node. Make sure to return the node itself as well. + * + * By default, only direct siblings (i.e. those within the same container of the specified node) with the same name as the specified node are returned. + * This is also reflected in the builtin implementations of all `Scope` interface. + * If your language behaves differently, you might need to adjust the `StreamScope` and `MapScope` behavior accordingly. + */ + protected getSelfNodes(node: AstNode): AstNode[] { + if (!this.hasMultiReference) { + return [node]; + } else { + const name = this.nameProvider.getName(node); + const container = node.$container; + // We need the name to find the siblings + // If the name is not available, we just return the specified node + // Similarly, we cannot find siblings in case the container is not available + if (!name || !container) { + return [node]; + } + const siblings = streamContents(container).filter(n => this.nameProvider.getName(n) === name); + return siblings.toArray(); } - return undefined; + } + + findDeclarationNodes(sourceCstNode: CstNode): CstNode[] { + const astNodes = this.findDeclarations(sourceCstNode); + const cstNodes: CstNode[] = []; + for (const astNode of astNodes) { + const cstNode = this.nameProvider.getNameNode(astNode) ?? astNode.$cstNode; + if (cstNode) { + cstNodes.push(cstNode); + } + } + return cstNodes; } findReferences(targetNode: AstNode, options: FindReferencesOptions): Stream { const refs: ReferenceDescription[] = []; if (options.includeDeclaration) { - const ref = this.getReferenceToSelf(targetNode); - if (ref) { - refs.push(ref); - } + refs.push(...this.getSelfReferences(targetNode)); } let indexReferences = this.index.findAllReferences(targetNode, this.nodeLocator.getAstNodePath(targetNode)); if (options.documentUri) { @@ -129,20 +153,24 @@ export class DefaultReferences implements References { return stream(refs); } - protected getReferenceToSelf(targetNode: AstNode): ReferenceDescription | undefined { - const nameNode = this.nameProvider.getNameNode(targetNode); - if (nameNode) { - const doc = getDocument(targetNode); - const path = this.nodeLocator.getAstNodePath(targetNode); - return { - sourceUri: doc.uri, - sourcePath: path, - targetUri: doc.uri, - targetPath: path, - segment: toDocumentSegment(nameNode), - local: true - }; + protected getSelfReferences(targetNode: AstNode): ReferenceDescription[] { + const selfNodes = this.getSelfNodes(targetNode); + const references: ReferenceDescription[] = []; + for (const selfNode of selfNodes) { + const nameNode = this.nameProvider.getNameNode(selfNode); + if (nameNode) { + const doc = getDocument(selfNode); + const path = this.nodeLocator.getAstNodePath(selfNode); + references.push({ + sourceUri: doc.uri, + sourcePath: path, + targetUri: doc.uri, + targetPath: path, + segment: toDocumentSegment(nameNode), + local: true + }); + } } - return undefined; + return references; } } diff --git a/packages/langium/src/references/scope.ts b/packages/langium/src/references/scope.ts index 103181bd3..307f5bb81 100644 --- a/packages/langium/src/references/scope.ts +++ b/packages/langium/src/references/scope.ts @@ -5,6 +5,7 @@ ******************************************************************************/ import type { AstNodeDescription } from '../syntax-tree.js'; +import { MultiMap } from '../utils/collections.js'; import type { Stream } from '../utils/stream.js'; import { EMPTY_STREAM, stream } from '../utils/stream.js'; @@ -22,6 +23,8 @@ export interface Scope { */ getElement(name: string): AstNodeDescription | undefined; + getElements(name: string): Stream; + /** * Create a stream of all elements in the scope. This is used to compute completion proposals to be * shown in the editor. @@ -59,8 +62,9 @@ export class StreamScope implements Scope { } getElement(name: string): AstNodeDescription | undefined { + const lowerCaseName = this.caseInsensitive ? name.toLowerCase() : name; const local = this.caseInsensitive - ? this.elements.find(e => e.name.toLowerCase() === name.toLowerCase()) + ? this.elements.find(e => e.name.toLowerCase() === lowerCaseName) : this.elements.find(e => e.name === name); if (local) { return local; @@ -70,28 +74,40 @@ export class StreamScope implements Scope { } return undefined; } + + getElements(name: string): Stream { + const lowerCaseName = this.caseInsensitive ? name.toLowerCase() : name; + const local = this.caseInsensitive + ? this.elements.filter(e => e.name.toLowerCase() === lowerCaseName) + : this.elements.filter(e => e.name === name); + if (!local.isEmpty()) { + return local; + } else { + return this.outerScope ? this.outerScope.getElements(name) : EMPTY_STREAM; + } + } } export class MapScope implements Scope { - readonly elements: Map; + readonly elements: MultiMap; readonly outerScope?: Scope; readonly caseInsensitive: boolean; constructor(elements: Iterable, outerScope?: Scope, options?: ScopeOptions) { - this.elements = new Map(); + this.elements = new MultiMap(); this.caseInsensitive = options?.caseInsensitive ?? false; for (const element of elements) { const name = this.caseInsensitive ? element.name.toLowerCase() : element.name; - this.elements.set(name, element); + this.elements.add(name, element); } this.outerScope = outerScope; } getElement(name: string): AstNodeDescription | undefined { const localName = this.caseInsensitive ? name.toLowerCase() : name; - const local = this.elements.get(localName); + const local = this.elements.get(localName)[0]; if (local) { return local; } @@ -101,6 +117,16 @@ export class MapScope implements Scope { return undefined; } + getElements(name: string): Stream { + const localName = this.caseInsensitive ? name.toLowerCase() : name; + const local = this.elements.get(localName); + if (local.length > 0) { + return stream(local); + } else { + return this.outerScope ? this.outerScope.getElements(name) : EMPTY_STREAM; + } + } + getAllElements(): Stream { let elementStream = stream(this.elements.values()); if (this.outerScope) { @@ -115,6 +141,9 @@ export const EMPTY_SCOPE: Scope = { getElement(): undefined { return undefined; }, + getElements(): Stream { + return EMPTY_STREAM; + }, getAllElements(): Stream { return EMPTY_STREAM; } diff --git a/packages/langium/src/serializer/json-serializer.ts b/packages/langium/src/serializer/json-serializer.ts index 00373bc95..4e6e3f381 100644 --- a/packages/langium/src/serializer/json-serializer.ts +++ b/packages/langium/src/serializer/json-serializer.ts @@ -8,8 +8,8 @@ import { URI } from 'vscode-uri'; import type { CommentProvider } from '../documentation/comment-provider.js'; import type { NameProvider } from '../references/name-provider.js'; import type { LangiumCoreServices } from '../services.js'; -import type { AstNode, CstNode, GenericAstNode, Mutable, Reference } from '../syntax-tree.js'; -import { isAstNode, isReference } from '../syntax-tree.js'; +import type { AstNode, CstNode, GenericAstNode, MultiReference, MultiReferenceItem, Mutable, Reference } from '../syntax-tree.js'; +import { isAstNode, isMultiReference, isReference } from '../syntax-tree.js'; import { getDocument } from '../utils/ast-utils.js'; import { findNodesForProperty } from '../utils/grammar-utils.js'; import type { AstNodeLocator } from '../workspace/ast-node-locator.js'; @@ -29,7 +29,7 @@ export interface JsonSerializeOptions { /** The replacer parameter for `JSON.stringify`; the default replacer given as parameter should be used to apply basic replacements. */ replacer?: (key: string, value: unknown, defaultReplacer: (key: string, value: unknown) => unknown) => unknown /** Used to convert and serialize URIs when the target of a cross-reference is in a different document. */ - uriConverter?: (uri: URI, reference: Reference) => string + uriConverter?: (uri: URI, node: AstNode) => string } export interface JsonDeserializeOptions { @@ -96,6 +96,8 @@ export interface JsonSerializer { interface IntermediateReference { /** URI pointing to the target element. This is either `#${path}` if the target is in the same document, or `${documentURI}#${path}` otherwise. */ $ref?: string + /** URI pointing to the target elements. This is the multi reference equivalent for {@link $ref}. */ + $refs?: string[] /** The actual text used to look up the reference target in the surrounding scope. */ $refText?: string /** If any problem occurred while resolving the reference, it is described by this property. */ @@ -156,7 +158,7 @@ export class DefaultJsonSerializer implements JsonSerializer { let targetUri = ''; if (this.currentDocument && this.currentDocument !== targetDocument) { if (uriConverter) { - targetUri = uriConverter(targetDocument.uri, value); + targetUri = uriConverter(targetDocument.uri, refValue); } else { targetUri = targetDocument.uri.toString(); } @@ -172,6 +174,27 @@ export class DefaultJsonSerializer implements JsonSerializer { $refText } satisfies IntermediateReference; } + } else if (isMultiReference(value)) { + const $refText = refText ? value.$refText : undefined; + const $refs: string[] = []; + for (const item of value.items) { + const refValue = item.ref; + const targetDocument = getDocument(item.ref); + let targetUri = ''; + if (this.currentDocument && this.currentDocument !== targetDocument) { + if (uriConverter) { + targetUri = uriConverter(targetDocument.uri, refValue); + } else { + targetUri = targetDocument.uri.toString(); + } + } + const targetPath = this.astNodeLocator.getAstNodePath(refValue); + $refs.push(`${targetUri}#${targetPath}`); + } + return { + $refs, + $refText + } satisfies IntermediateReference; } else if (isAstNode(value)) { let astNode: AstNodeWithTextRegion | undefined = undefined; if (textRegions) { @@ -245,32 +268,56 @@ export class DefaultJsonSerializer implements JsonSerializer { mutable.$containerIndex = containerIndex; } - protected reviveReference(container: AstNode, property: string, root: AstNode, reference: IntermediateReference, options: JsonDeserializeOptions): Reference | undefined { + protected reviveReference(container: AstNode, property: string, root: AstNode, reference: IntermediateReference, options: JsonDeserializeOptions): Reference | MultiReference | undefined { let refText = reference.$refText; let error = reference.$error; + let ref: Mutable | Mutable | undefined; if (reference.$ref) { - const ref = this.getRefNode(root, reference.$ref, options.uriConverter); - if (isAstNode(ref)) { + const refNode = this.getRefNode(root, reference.$ref, options.uriConverter); + if (isAstNode(refNode)) { if (!refText) { - refText = this.nameProvider.getName(ref); + refText = this.nameProvider.getName(refNode); } return { $refText: refText ?? '', - ref + ref: refNode }; } else { - error = ref; + error = refNode; + } + } else if (reference.$refs) { + const refs: MultiReferenceItem[] = []; + for (const refUri of reference.$refs) { + const refNode = this.getRefNode(root, refUri, options.uriConverter); + if (isAstNode(refNode)) { + refs.push({ ref: refNode }); + } + } + if (refs.length === 0) { + ref = { + $refText: refText ?? '', + items: refs + }; + error ??= 'Could not resolve multi-reference'; + } else { + return { + $refText: refText ?? '', + items: refs + }; } } if (error) { - const ref: Mutable = { - $refText: refText ?? '' + ref ??= { + $refText: refText ?? '', + ref: undefined }; ref.error = { - container, - property, - message: error, - reference: ref + info: { + container, + property, + reference: ref + }, + message: error }; return ref; } else { diff --git a/packages/langium/src/syntax-tree.ts b/packages/langium/src/syntax-tree.ts index a318cf57a..c1527dc1f 100644 --- a/packages/langium/src/syntax-tree.ts +++ b/packages/langium/src/syntax-tree.ts @@ -51,7 +51,7 @@ export interface Reference { * resolution by the `Linker` in case it has not been done yet. If the reference cannot be resolved, * the value is `undefined`. */ - readonly ref?: T; + readonly ref: T | undefined; /** If any problem occurred while resolving the reference, it is described by this property. */ readonly error?: LinkingError; @@ -63,8 +63,40 @@ export interface Reference { readonly $nodeDescription?: AstNodeDescription; } +export interface MultiReference { + /** The CST node from which the reference was parsed */ + readonly $refNode?: CstNode; + /** The actual text used to look up in the surrounding scope */ + readonly $refText: string; + /** + * The resolved references. Accessing this property may trigger cross-reference + * resolution by the `Linker` in case it has not been done yet. + * If no references can be found, the array is empty (but not `undefined`) + * and the `error` property is set. + */ + readonly items: Array>; + /** If any problem occurred while resolving the reference, it is described by this property. */ + readonly error?: LinkingError; +} + +/** + * Represents a single resolved reference of a {@link MultiReference} instance. + */ +export interface MultiReferenceItem { + /** The node description for the AstNode returned by `ref` */ + readonly $nodeDescription?: AstNodeDescription; + /** + * The target AST node of this reference. + */ + readonly ref: T; +} + export function isReference(obj: unknown): obj is Reference { - return typeof obj === 'object' && obj !== null && typeof (obj as Reference).$refText === 'string'; + return typeof obj === 'object' && obj !== null && typeof (obj as Reference).$refText === 'string' && 'ref' in obj; +} + +export function isMultiReference(obj: unknown): obj is MultiReference { + return typeof obj === 'object' && obj !== null && typeof (obj as Reference).$refText === 'string' && 'items' in obj; } export type ResolvedReference = Reference & { @@ -107,7 +139,7 @@ export function isAstNodeDescription(obj: unknown): obj is AstNodeDescription { * unresolved references. */ export interface ReferenceInfo { - reference: Reference + reference: Reference | MultiReference container: AstNode property: string index?: number @@ -116,15 +148,15 @@ export interface ReferenceInfo { /** * Used to collect information when the `Linker` service fails to resolve a cross-reference. */ -export interface LinkingError extends ReferenceInfo { +export interface LinkingError { message: string; + info: ReferenceInfo; targetDescription?: AstNodeDescription; } export function isLinkingError(obj: unknown): obj is LinkingError { return typeof obj === 'object' && obj !== null - && isAstNode((obj as LinkingError).container) - && isReference((obj as LinkingError).reference) + && typeof (obj as LinkingError).info === 'object' && typeof (obj as LinkingError).message === 'string'; } diff --git a/packages/langium/src/utils/ast-utils.ts b/packages/langium/src/utils/ast-utils.ts index 92030e945..5069fb682 100644 --- a/packages/langium/src/utils/ast-utils.ts +++ b/packages/langium/src/utils/ast-utils.ts @@ -5,11 +5,11 @@ ******************************************************************************/ import type { Range } from 'vscode-languageserver-types'; -import type { AstNode, AstReflection, CstNode, GenericAstNode, Mutable, PropertyType, Reference, ReferenceInfo } from '../syntax-tree.js'; +import type { AstNode, AstReflection, CstNode, GenericAstNode, MultiReference, Mutable, PropertyType, Reference, ReferenceInfo } from '../syntax-tree.js'; import type { Stream, TreeStream } from './stream.js'; import type { LangiumDocument } from '../workspace/documents.js'; -import { isAstNode, isReference } from '../syntax-tree.js'; -import { DONE_RESULT, stream, StreamImpl, TreeStreamImpl } from './stream.js'; +import { isAstNode, isMultiReference, isReference } from '../syntax-tree.js'; +import { DONE_RESULT, StreamImpl, TreeStreamImpl } from './stream.js'; import { inRange } from './cst-utils.js'; /** @@ -91,6 +91,18 @@ export function findRootNode(node: AstNode): AstNode { return node; } +/** + * Returns all AST nodes that are referenced by the given reference or multi-reference. + */ +export function getReferenceNodes(reference: Reference | MultiReference): AstNode[] { + if (isReference(reference)) { + return reference.ref ? [reference.ref] : []; + } else if (isMultiReference(reference)) { + return reference.items.map(item => item.ref); + } + return []; +} + export interface AstStreamOptions { /** * Optional target range that the nodes in the stream need to intersect @@ -190,14 +202,14 @@ export function streamReferences(node: AstNode): Stream { const property = state.keys[state.keyIndex]; if (!property.startsWith('$')) { const value = (node as GenericAstNode)[property]; - if (isReference(value)) { + if (isReference(value) || isMultiReference(value)) { state.keyIndex++; return { done: false, value: { reference: value, container: node, property } }; } else if (Array.isArray(value)) { while (state.arrayIndex < value.length) { const index = state.arrayIndex++; const element = value[index]; - if (isReference(element)) { + if (isReference(element) || isMultiReference(value)) { return { done: false, value: { reference: element, container: node, property, index } }; } } @@ -210,24 +222,6 @@ export function streamReferences(node: AstNode): Stream { }); } -/** - * Returns a Stream of references to the target node from the AstNode tree - * - * @param targetNode AstNode we are looking for - * @param lookup AstNode where we search for references. If not provided, the root node of the document is used as the default value - */ -export function findLocalReferences(targetNode: AstNode, lookup = getDocument(targetNode).parseResult.value): Stream { - const refs: Reference[] = []; - streamAst(lookup).forEach(node => { - streamReferences(node).forEach(refInfo => { - if (refInfo.reference.ref === targetNode) { - refs.push(refInfo.reference); - } - }); - }); - return stream(refs); -} - /** * Assigns all mandatory AST properties to the specified node. * diff --git a/packages/langium/src/validation/document-validator.ts b/packages/langium/src/validation/document-validator.ts index e5e1d56d5..c7fbf839c 100644 --- a/packages/langium/src/validation/document-validator.ts +++ b/packages/langium/src/validation/document-validator.ts @@ -160,14 +160,14 @@ export class DefaultDocumentValidator implements DocumentValidator { const linkingError = reference.error; if (linkingError) { const info: DiagnosticInfo = { - node: linkingError.container, - property: linkingError.property, - index: linkingError.index, + node: linkingError.info.container, + property: linkingError.info.property, + index: linkingError.info.index, data: { code: DocumentValidator.LinkingError, - containerType: linkingError.container.$type, - property: linkingError.property, - refText: linkingError.reference.$refText + containerType: linkingError.info.container.$type, + property: linkingError.info.property, + refText: linkingError.info.reference.$refText } satisfies LinkingErrorData }; diagnostics.push(this.toDiagnostic('error', linkingError.message, info)); diff --git a/packages/langium/src/workspace/ast-descriptions.ts b/packages/langium/src/workspace/ast-descriptions.ts index 277238d11..1bce2b8c3 100644 --- a/packages/langium/src/workspace/ast-descriptions.ts +++ b/packages/langium/src/workspace/ast-descriptions.ts @@ -11,7 +11,7 @@ import type { AstNode, AstNodeDescription, ReferenceInfo } from '../syntax-tree. import type { AstNodeLocator } from './ast-node-locator.js'; import type { DocumentSegment, LangiumDocument } from './documents.js'; import { CancellationToken } from '../utils/cancellation.js'; -import { isLinkingError } from '../syntax-tree.js'; +import { isMultiReference, isReference } from '../syntax-tree.js'; import { getDocument, streamAst, streamReferences } from '../utils/ast-utils.js'; import { toDocumentSegment } from '../utils/cst-utils.js'; import { interruptAndCheck } from '../utils/promise-utils.js'; @@ -117,32 +117,41 @@ export class DefaultReferenceDescriptionProvider implements ReferenceDescription const rootNode = document.parseResult.value; for (const astNode of streamAst(rootNode)) { await interruptAndCheck(cancelToken); - streamReferences(astNode).filter(refInfo => !isLinkingError(refInfo)).forEach(refInfo => { - // TODO: Consider logging a warning or throw an exception when DocumentState is < than Linked - const description = this.createDescription(refInfo); - if (description) { - descr.push(description); + streamReferences(astNode).forEach(refInfo => { + if (!refInfo.reference.error) { + descr.push(...this.createInfoDescriptions(refInfo)); } }); } return descr; } - protected createDescription(refInfo: ReferenceInfo): ReferenceDescription | undefined { - const targetNodeDescr = refInfo.reference.$nodeDescription; - const refCstNode = refInfo.reference.$refNode; - if (!targetNodeDescr || !refCstNode) { - return undefined; + protected createInfoDescriptions(refInfo: ReferenceInfo): ReferenceDescription[] { + const reference = refInfo.reference; + if (reference.error || !reference.$refNode) { + return []; } - const docUri = getDocument(refInfo.container).uri; - return { - sourceUri: docUri, - sourcePath: this.nodeLocator.getAstNodePath(refInfo.container), - targetUri: targetNodeDescr.documentUri, - targetPath: targetNodeDescr.path, - segment: toDocumentSegment(refCstNode), - local: UriUtils.equals(targetNodeDescr.documentUri, docUri) - }; + let items: AstNodeDescription[] = []; + if (isReference(reference) && reference.$nodeDescription) { + items = [reference.$nodeDescription]; + } else if (isMultiReference(reference)) { + items = reference.items.map(e => e.$nodeDescription).filter((e): e is AstNodeDescription => !!e); + } + const sourceUri = getDocument(refInfo.container).uri; + const sourcePath = this.nodeLocator.getAstNodePath(refInfo.container); + const descriptions: ReferenceDescription[] = []; + const segment = toDocumentSegment(reference.$refNode); + for (const item of items) { + descriptions.push({ + sourceUri, + sourcePath, + targetUri: item.documentUri, + targetPath: item.path, + segment, + local: UriUtils.equals(item.documentUri, sourceUri) + }); + } + return descriptions; } } diff --git a/packages/langium/src/workspace/documents.ts b/packages/langium/src/workspace/documents.ts index 51b79d7d4..e93038d2e 100644 --- a/packages/langium/src/workspace/documents.ts +++ b/packages/langium/src/workspace/documents.ts @@ -18,7 +18,7 @@ import type { FileSystemProvider } from './file-system-provider.js'; import type { ParseResult, ParserOptions } from '../parser/langium-parser.js'; import type { ServiceRegistry } from '../service-registry.js'; import type { LangiumSharedCoreServices } from '../services.js'; -import type { AstNode, AstNodeDescription, Mutable, Reference } from '../syntax-tree.js'; +import type { AstNode, AstNodeDescription, MultiReference, Mutable, Reference } from '../syntax-tree.js'; import type { MultiMap } from '../utils/collections.js'; import type { Stream } from '../utils/stream.js'; import { TextDocument } from './documents.js'; @@ -42,7 +42,7 @@ export interface LangiumDocument { /** Result of the scope precomputation phase */ precomputedScopes?: PrecomputedScopes; /** An array of all cross-references found in the AST while linking */ - references: Reference[]; + references: Array; /** Result of the validation phase */ diagnostics?: Diagnostic[] } diff --git a/packages/langium/test/grammar/ast-reflection-interpreter.test.ts b/packages/langium/test/grammar/ast-reflection-interpreter.test.ts index c74944fff..736cbb225 100644 --- a/packages/langium/test/grammar/ast-reflection-interpreter.test.ts +++ b/packages/langium/test/grammar/ast-reflection-interpreter.test.ts @@ -30,7 +30,8 @@ describe('AST reflection interpreter', () => { type: { referenceType: { value: superType - } + }, + mode: 'single' } }); diff --git a/packages/langium/test/grammar/type-system/types-util.test.ts b/packages/langium/test/grammar/type-system/types-util.test.ts index a48a59483..d0a964ae9 100644 --- a/packages/langium/test/grammar/type-system/types-util.test.ts +++ b/packages/langium/test/grammar/type-system/types-util.test.ts @@ -19,7 +19,8 @@ describe('isAstType', () => { expect(isAstType({ referenceType: { value: new InterfaceType('Test', true, false) - } + }, + mode: 'single' })).toBeFalsy(); }); diff --git a/packages/langium/test/references/multi-reference.test.ts b/packages/langium/test/references/multi-reference.test.ts new file mode 100644 index 000000000..2d1aa2dc4 --- /dev/null +++ b/packages/langium/test/references/multi-reference.test.ts @@ -0,0 +1,102 @@ +/****************************************************************************** + * Copyright 2024 TypeFox GmbH + * This program and the accompanying materials are made available under the + * terms of the MIT License, which is available in the project root. + ******************************************************************************/ + +import { describe, test, expect } from 'vitest'; +import { createServicesForGrammar } from 'langium/grammar'; +import { parseHelper } from 'langium/test'; +import { expandToString } from 'langium/generate'; +import type { MultiReference } from 'langium'; + +const languageService = await createServicesForGrammar({ + grammar: ` + grammar test + entry Model: (persons+=Person | greetings+=Greeting)*; + Person: 'person' name=ID; + Greeting: 'hello' person=[*Person:ID]; + terminal ID: /[\\w]+/; + hidden terminal WS :/\\s+/; + ` +}); +const parse = parseHelper(languageService); + +describe('Multi Reference', () => { + + const text = expandToString` + person Alice + person Bob + person Alice + + hello Alice + `.trim(); + + test('Can reference multiple elements', async () => { + const document = await parse(text); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const model = document.parseResult.value as any; + expect(model.persons).toHaveLength(3); + expect(model.greetings).toHaveLength(1); + const greeting = model.greetings[0]; + const ref = greeting.person as MultiReference; + expect(ref).toBeDefined(); + expect(ref.error).toBeUndefined(); + expect(ref.items).toHaveLength(2); + expect(ref.items[0].ref).toHaveProperty('name', 'Alice'); + expect(ref.items[0].ref.$cstNode?.range.start.line).toBe(0); + expect(ref.items[1].ref).toHaveProperty('name', 'Alice'); + expect(ref.items[1].ref.$cstNode?.range.start.line).toBe(2); + }); + + test('Can find multiple declarations for the reference', async () => { + const references = languageService.references.References; + const document = await parse(text); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const model = document.parseResult.value as any; + const aliceRef = model.greetings[0].person; + expect(aliceRef).toHaveProperty('$refText', 'Alice'); + const declarations = references.findDeclarations(aliceRef.$refNode); + expect(declarations).toHaveLength(2); + for (let i = 0; i < declarations.length; i++) { + expect(declarations[i]).toHaveProperty('name', 'Alice'); + expect(declarations[i].$cstNode?.range.start.line).toBe(i * 2); + } + }); + + test('Can find sibling declarations for own declaration', async () => { + const references = languageService.references.References; + const nameProvider = languageService.references.NameProvider; + const document = await parse(text); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const model = document.parseResult.value as any; + const alice1 = model.persons[0]; + expect(alice1).toHaveProperty('name', 'Alice'); + const alice1Name = nameProvider.getNameNode(alice1); + expect(alice1Name).toBeDefined(); + const declarations = references.findDeclarations(alice1Name!); + expect(declarations).toHaveLength(2); + for (let i = 0; i < declarations.length; i++) { + expect(declarations[i]).toHaveProperty('name', 'Alice'); + expect(declarations[i].$cstNode?.range.start.line).toBe(i * 2); + } + }); + + test('Can find all references and declarations for a multi reference element', async () => { + const referencesService = languageService.references.References; + const document = await parse(text); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const model = document.parseResult.value as any; + const alice2 = model.persons[2]; + expect(alice2).toHaveProperty('name', 'Alice'); + const references = referencesService.findReferences(alice2, { + includeDeclaration: true + }).toArray().sort((a, b) => a.segment.offset - b.segment.offset); + expect(references).toHaveLength(3); + for (let i = 0; i < references.length; i++) { + expect(references[i].segment.range.start.line).toBe(i * 2); + expect(text.substring(references[i].segment.offset, references[i].segment.end)).toBe('Alice'); + } + }); + +});