diff --git a/packages/safe-ds-lang/src/language/lsp/safe-ds-call-hierarchy-provider.ts b/packages/safe-ds-lang/src/language/lsp/safe-ds-call-hierarchy-provider.ts index 97d08f8f4..1238d6bbe 100644 --- a/packages/safe-ds-lang/src/language/lsp/safe-ds-call-hierarchy-provider.ts +++ b/packages/safe-ds-lang/src/language/lsp/safe-ds-call-hierarchy-provider.ts @@ -114,13 +114,13 @@ export class SafeDsCallHierarchyProvider extends AbstractCallHierarchyProvider { return undefined; } - const targetNode = findLeafNodeAtOffset(rootNode.$cstNode, it.segment.offset); - if (!targetNode) { + const targetCstNode = findLeafNodeAtOffset(rootNode.$cstNode, it.segment.offset); + if (!targetCstNode) { /* c8 ignore next 2 */ return undefined; } - const containingDeclaration = getContainerOfType(targetNode.astNode, isSdsDeclaration); + const containingDeclaration = getContainerOfType(targetCstNode.astNode, isSdsDeclaration); if (isSdsParameter(containingDeclaration)) { // For parameters, we return their containing callable instead return getContainerOfType(containingDeclaration.$container, isSdsDeclaration); diff --git a/packages/safe-ds-lang/src/language/lsp/safe-ds-language-server.ts b/packages/safe-ds-lang/src/language/lsp/safe-ds-language-server.ts new file mode 100644 index 000000000..2ac8bc6a6 --- /dev/null +++ b/packages/safe-ds-lang/src/language/lsp/safe-ds-language-server.ts @@ -0,0 +1,193 @@ +/* + * This file can be removed, once Langium supports the TypeHierarchyProvider directly. + */ + +/* c8 ignore start */ +import type { LangiumServices, LangiumSharedServices } from 'langium'; +import { + addCallHierarchyHandler, + addCodeActionHandler, + addCodeLensHandler, + addCompletionHandler, + addConfigurationChangeHandler, + addDiagnosticsHandler, + addDocumentHighlightsHandler, + addDocumentLinkHandler, + addDocumentsHandler, + addDocumentSymbolHandler, + addExecuteCommandHandler, + addFindReferencesHandler, + addFoldingRangeHandler, + addFormattingHandler, + addGoToDeclarationHandler, + addGotoDefinitionHandler, + addGoToImplementationHandler, + addGoToTypeDefinitionHandler, + addHoverHandler, + addInlayHintHandler, + addRenameHandler, + addSemanticTokenHandler, + addSignatureHelpHandler, + addWorkspaceSymbolHandler, + createServerRequestHandler, + DefaultLanguageServer, + isOperationCancelled, + URI, +} from 'langium'; +import { + type CancellationToken, + type Connection, + type HandlerResult, + type InitializeParams, + type InitializeResult, + LSPErrorCodes, + ResponseError, + type ServerRequestHandler, + type TypeHierarchySubtypesParams, + type TypeHierarchySupertypesParams, +} from 'vscode-languageserver'; +import { TypeHierarchyProvider } from './safe-ds-type-hierarchy-provider.js'; + +interface LangiumAddedServices { + lsp: { + TypeHierarchyProvider?: TypeHierarchyProvider; + }; +} + +export class SafeDsLanguageServer extends DefaultLanguageServer { + protected override hasService( + callback: (language: LangiumServices & LangiumAddedServices) => object | undefined, + ): boolean { + return this.services.ServiceRegistry.all.some((language) => callback(language) !== undefined); + } + + protected override buildInitializeResult(params: InitializeParams): InitializeResult { + const hasTypeHierarchyProvider = this.hasService((e) => e.lsp.TypeHierarchyProvider); + const otherCapabilities = super.buildInitializeResult(params).capabilities; + + return { + capabilities: { + ...otherCapabilities, + typeHierarchyProvider: hasTypeHierarchyProvider ? {} : undefined, + }, + }; + } +} + +export const startLanguageServer = (services: LangiumSharedServices): void => { + const connection = services.lsp.Connection; + if (!connection) { + throw new Error('Starting a language server requires the languageServer.Connection service to be set.'); + } + + addDocumentsHandler(connection, services); + addDiagnosticsHandler(connection, services); + addCompletionHandler(connection, services); + addFindReferencesHandler(connection, services); + addDocumentSymbolHandler(connection, services); + addGotoDefinitionHandler(connection, services); + addGoToTypeDefinitionHandler(connection, services); + addGoToImplementationHandler(connection, services); + addDocumentHighlightsHandler(connection, services); + addFoldingRangeHandler(connection, services); + addFormattingHandler(connection, services); + addCodeActionHandler(connection, services); + addRenameHandler(connection, services); + addHoverHandler(connection, services); + addInlayHintHandler(connection, services); + addSemanticTokenHandler(connection, services); + addExecuteCommandHandler(connection, services); + addSignatureHelpHandler(connection, services); + addCallHierarchyHandler(connection, services); + addTypeHierarchyHandler(connection, services); + addCodeLensHandler(connection, services); + addDocumentLinkHandler(connection, services); + addConfigurationChangeHandler(connection, services); + addGoToDeclarationHandler(connection, services); + addWorkspaceSymbolHandler(connection, services); + + connection.onInitialize((params) => { + return services.lsp.LanguageServer.initialize(params); + }); + connection.onInitialized((params) => { + return services.lsp.LanguageServer.initialized(params); + }); + + // Make the text document manager listen on the connection for open, change and close text document events. + const documents = services.workspace.TextDocuments; + documents.listen(connection); + + // Start listening for incoming messages from the client. + connection.listen(); +}; + +export const addTypeHierarchyHandler = function (connection: Connection, sharedServices: LangiumSharedServices): void { + connection.languages.typeHierarchy.onPrepare( + createServerRequestHandler((services, document, params, cancelToken) => { + const typeHierarchyProvider = (services).lsp.TypeHierarchyProvider; + if (typeHierarchyProvider) { + return typeHierarchyProvider.prepareTypeHierarchy(document, params, cancelToken) ?? null; + } + return null; + }, sharedServices), + ); + + connection.languages.typeHierarchy.onSupertypes( + createTypeHierarchyRequestHandler((services, params, cancelToken) => { + const typeHierarchyProvider = (services).lsp.TypeHierarchyProvider; + if (typeHierarchyProvider) { + return typeHierarchyProvider.supertypes(params, cancelToken) ?? null; + } + return null; + }, sharedServices), + ); + + connection.languages.typeHierarchy.onSubtypes( + createTypeHierarchyRequestHandler((services, params, cancelToken) => { + const typeHierarchyProvider = (services).lsp.TypeHierarchyProvider; + if (typeHierarchyProvider) { + return typeHierarchyProvider.subtypes(params, cancelToken) ?? null; + } + return null; + }, sharedServices), + ); +}; + +export const createTypeHierarchyRequestHandler = function < + P extends TypeHierarchySupertypesParams | TypeHierarchySubtypesParams, + R, + PR, + E = void, +>( + serviceCall: (services: LangiumServices, params: P, cancelToken: CancellationToken) => HandlerResult, + sharedServices: LangiumSharedServices, +): ServerRequestHandler { + const serviceRegistry = sharedServices.ServiceRegistry; + return async (params: P, cancelToken: CancellationToken) => { + const uri = URI.parse(params.item.uri); + const language = serviceRegistry.getServices(uri); + if (!language) { + const message = `Could not find service instance for uri: '${uri.toString()}'`; + // eslint-disable-next-line no-console + console.error(message); + throw new Error(message); + } + try { + // eslint-disable-next-line @typescript-eslint/return-await + return await serviceCall(language, params, cancelToken); + } catch (err) { + return responseError(err); + } + }; +}; + +const responseError = function (err: unknown): ResponseError { + if (isOperationCancelled(err)) { + return new ResponseError(LSPErrorCodes.RequestCancelled, 'The request has been cancelled.'); + } + if (err instanceof ResponseError) { + return err; + } + throw err; +}; +/* c8 ignore stop */ diff --git a/packages/safe-ds-lang/src/language/lsp/safe-ds-type-hierarchy-provider.ts b/packages/safe-ds-lang/src/language/lsp/safe-ds-type-hierarchy-provider.ts new file mode 100644 index 000000000..e66441292 --- /dev/null +++ b/packages/safe-ds-lang/src/language/lsp/safe-ds-type-hierarchy-provider.ts @@ -0,0 +1,279 @@ +import { + type AstNode, + findDeclarationNodeAtOffset, + findLeafNodeAtOffset, + getContainerOfType, + getDocument, + type GrammarConfig, + type LangiumDocument, + type LangiumDocuments, + type LangiumServices, + type NameProvider, + NodeKindProvider, + type ReferenceDescription, + type References, + stream, + type Stream, + URI, +} from 'langium'; +import { + type CancellationToken, + SymbolKind, + type TypeHierarchyItem, + type TypeHierarchyPrepareParams, + type TypeHierarchySubtypesParams, + type TypeHierarchySupertypesParams, +} from 'vscode-languageserver'; +import { + isSdsClass, + isSdsEnum, + isSdsEnumVariant, + isSdsParentTypeList, + SdsClass, + SdsEnum, + SdsEnumVariant, +} from '../generated/ast.js'; +import type { SafeDsServices } from '../safe-ds-module.js'; +import { SafeDsClassHierarchy } from '../typing/safe-ds-class-hierarchy.js'; +import { SafeDsNodeInfoProvider } from './safe-ds-node-info-provider.js'; +import { getEnumVariants } from '../helpers/nodeProperties.js'; + +/* c8 ignore start */ +export interface TypeHierarchyProvider { + prepareTypeHierarchy( + document: LangiumDocument, + params: TypeHierarchyPrepareParams, + cancelToken?: CancellationToken, + ): TypeHierarchyItem[] | undefined; + + supertypes(params: TypeHierarchySupertypesParams, cancelToken?: CancellationToken): TypeHierarchyItem[] | undefined; + + subtypes(params: TypeHierarchySubtypesParams, cancelToken?: CancellationToken): TypeHierarchyItem[] | undefined; +} + +export abstract class AbstractTypeHierarchyProvider implements TypeHierarchyProvider { + protected readonly grammarConfig: GrammarConfig; + protected readonly nameProvider: NameProvider; + protected readonly documents: LangiumDocuments; + protected readonly references: References; + + protected constructor(services: LangiumServices) { + this.grammarConfig = services.parser.GrammarConfig; + this.nameProvider = services.references.NameProvider; + this.documents = services.shared.workspace.LangiumDocuments; + this.references = services.references.References; + } + + prepareTypeHierarchy( + document: LangiumDocument, + params: TypeHierarchyPrepareParams, + _cancelToken?: CancellationToken, + ): TypeHierarchyItem[] | undefined { + const rootNode = document.parseResult.value; + const targetNode = findDeclarationNodeAtOffset( + rootNode.$cstNode, + document.textDocument.offsetAt(params.position), + this.grammarConfig.nameRegexp, + ); + if (!targetNode) { + return undefined; + } + + const declarationNode = this.references.findDeclarationNode(targetNode); + if (!declarationNode) { + return undefined; + } + + return this.getTypeHierarchyItems(declarationNode.astNode, document); + } + + protected getTypeHierarchyItems(targetNode: AstNode, document: LangiumDocument): TypeHierarchyItem[] | undefined { + const nameNode = this.nameProvider.getNameNode(targetNode); + const name = this.nameProvider.getName(targetNode); + if (!nameNode || !targetNode.$cstNode || name === undefined) { + return undefined; + } + + return [ + { + kind: SymbolKind.Method, + name, + range: targetNode.$cstNode.range, + selectionRange: nameNode.range, + uri: document.uri.toString(), + ...this.getTypeHierarchyItem(targetNode), + }, + ]; + } + + protected getTypeHierarchyItem(_targetNode: AstNode): Partial | undefined { + return undefined; + } + + supertypes( + params: TypeHierarchySupertypesParams, + _cancelToken?: CancellationToken, + ): TypeHierarchyItem[] | undefined { + const document = this.documents.getOrCreateDocument(URI.parse(params.item.uri)); + const rootNode = document.parseResult.value; + const targetNode = findDeclarationNodeAtOffset( + rootNode.$cstNode, + document.textDocument.offsetAt(params.item.range.start), + this.grammarConfig.nameRegexp, + ); + if (!targetNode) { + return undefined; + } + return this.getSupertypes(targetNode.astNode); + } + + /** + * Override this method to collect the supertypes for your language. + */ + protected abstract getSupertypes(node: AstNode): TypeHierarchyItem[] | undefined; + + subtypes(params: TypeHierarchySubtypesParams, _cancelToken?: CancellationToken): TypeHierarchyItem[] | undefined { + const document = this.documents.getOrCreateDocument(URI.parse(params.item.uri)); + const rootNode = document.parseResult.value; + const targetNode = findDeclarationNodeAtOffset( + rootNode.$cstNode, + document.textDocument.offsetAt(params.item.range.start), + this.grammarConfig.nameRegexp, + ); + if (!targetNode) { + return undefined; + } + + const references = this.references.findReferences(targetNode.astNode, { + includeDeclaration: false, + }); + return this.getSubtypes(targetNode.astNode, references); + } + + /** + * Override this method to collect the subtypes for your language. + */ + protected abstract getSubtypes( + node: AstNode, + references: Stream, + ): TypeHierarchyItem[] | undefined; +} +/* c8 ignore stop */ + +export class SafeDsTypeHierarchyProvider extends AbstractTypeHierarchyProvider { + private readonly classHierarchy: SafeDsClassHierarchy; + private readonly nodeKindProvider: NodeKindProvider; + private readonly nodeInfoProvider: SafeDsNodeInfoProvider; + + constructor(services: SafeDsServices) { + super(services); + this.classHierarchy = services.types.ClassHierarchy; + this.nodeKindProvider = services.shared.lsp.NodeKindProvider; + this.nodeInfoProvider = services.lsp.NodeInfoProvider; + } + + protected override getTypeHierarchyItem(targetNode: AstNode): Partial | undefined { + { + return { + kind: this.nodeKindProvider.getSymbolKind(targetNode), + tags: this.nodeInfoProvider.getTags(targetNode), + detail: this.nodeInfoProvider.getDetails(targetNode), + }; + } + } + + protected override getSupertypes(node: AstNode): TypeHierarchyItem[] | undefined { + if (isSdsClass(node)) { + return this.getSupertypesOfClass(node); + } else if (isSdsEnumVariant(node)) { + return this.getSupertypesOfEnumVariant(node); + } else { + return undefined; + } + } + + private getSupertypesOfClass(node: SdsClass): TypeHierarchyItem[] | undefined { + const parentClass = this.classHierarchy.streamSuperclasses(node).head(); + if (!parentClass) { + /* c8 ignore next 2 */ + return undefined; + } + + return this.getTypeHierarchyItems(parentClass, getDocument(parentClass)); + } + + private getSupertypesOfEnumVariant(node: SdsEnumVariant): TypeHierarchyItem[] | undefined { + const containingEnum = getContainerOfType(node, isSdsEnum); + if (!containingEnum) { + /* c8 ignore next 2 */ + return undefined; + } + + return this.getTypeHierarchyItems(containingEnum, getDocument(containingEnum)); + } + + protected override getSubtypes( + node: AstNode, + references: Stream, + ): TypeHierarchyItem[] | undefined { + let items: TypeHierarchyItem[]; + + if (isSdsClass(node)) { + items = this.getSubtypesOfClass(references); + } else if (isSdsEnum(node)) { + items = this.getSubtypesOfEnum(node); + } else { + return undefined; + } + + if (items.length === 0) { + return undefined; + } + + return items; + } + + private getSubtypesOfClass(references: Stream): TypeHierarchyItem[] { + return references + .flatMap((it) => { + const document = this.documents.getOrCreateDocument(it.sourceUri); + const rootNode = document.parseResult.value; + if (!rootNode.$cstNode) { + /* c8 ignore next 2 */ + return undefined; + } + + const targetCstNode = findLeafNodeAtOffset(rootNode.$cstNode, it.segment.offset); + if (!targetCstNode) { + /* c8 ignore next 2 */ + return undefined; + } + + // Only consider the first parent type + const targetNode = targetCstNode.astNode; + if (!isSdsParentTypeList(targetNode.$container) || targetNode.$containerIndex !== 0) { + return undefined; + } + + const containingClass = getContainerOfType(targetNode, isSdsClass); + if (!containingClass) { + /* c8 ignore next 2 */ + return undefined; + } + + return this.getTypeHierarchyItems(containingClass, document); + }) + .filter((it) => it !== undefined) + .toArray() as TypeHierarchyItem[]; + } + + private getSubtypesOfEnum(node: SdsEnum): TypeHierarchyItem[] { + const variants = getEnumVariants(node); + const document = getDocument(node); + + return stream(variants) + .flatMap((it) => this.getTypeHierarchyItems(it, document)) + .filter((it) => it !== undefined) + .toArray() as TypeHierarchyItem[]; + } +} diff --git a/packages/safe-ds-lang/src/language/main.ts b/packages/safe-ds-lang/src/language/main.ts index 79ab43598..c170d1c6a 100644 --- a/packages/safe-ds-lang/src/language/main.ts +++ b/packages/safe-ds-lang/src/language/main.ts @@ -1,6 +1,6 @@ -import { startLanguageServer as doStartLanguageServer } from 'langium'; import { NodeFileSystem } from 'langium/node'; import { createConnection, ProposedFeatures } from 'vscode-languageserver/node.js'; +import { startLanguageServer as doStartLanguageServer } from './lsp/safe-ds-language-server.js'; import { createSafeDsServices } from './safe-ds-module.js'; /* c8 ignore start */ diff --git a/packages/safe-ds-lang/src/language/safe-ds-module.ts b/packages/safe-ds-lang/src/language/safe-ds-module.ts index b531db32c..0692ad01b 100644 --- a/packages/safe-ds-lang/src/language/safe-ds-module.ts +++ b/packages/safe-ds-lang/src/language/safe-ds-module.ts @@ -23,10 +23,12 @@ import { SafeDsCallHierarchyProvider } from './lsp/safe-ds-call-hierarchy-provid import { SafeDsDocumentSymbolProvider } from './lsp/safe-ds-document-symbol-provider.js'; import { SafeDsFormatter } from './lsp/safe-ds-formatter.js'; import { SafeDsInlayHintProvider } from './lsp/safe-ds-inlay-hint-provider.js'; +import { SafeDsLanguageServer } from './lsp/safe-ds-language-server.js'; import { SafeDsNodeInfoProvider } from './lsp/safe-ds-node-info-provider.js'; import { SafeDsNodeKindProvider } from './lsp/safe-ds-node-kind-provider.js'; import { SafeDsSemanticTokenProvider } from './lsp/safe-ds-semantic-token-provider.js'; import { SafeDsSignatureHelpProvider } from './lsp/safe-ds-signature-help-provider.js'; +import { SafeDsTypeHierarchyProvider } from './lsp/safe-ds-type-hierarchy-provider.js'; import { SafeDsPartialEvaluator } from './partialEvaluation/safe-ds-partial-evaluator.js'; import { SafeDsScopeComputation } from './scoping/safe-ds-scope-computation.js'; import { SafeDsScopeProvider } from './scoping/safe-ds-scope-provider.js'; @@ -62,6 +64,7 @@ export type SafeDsAddedServices = { }; lsp: { NodeInfoProvider: SafeDsNodeInfoProvider; + TypeHierarchyProvider: SafeDsTypeHierarchyProvider; }; types: { ClassHierarchy: SafeDsClassHierarchy; @@ -115,6 +118,7 @@ export const SafeDsModule: Module new SafeDsNodeInfoProvider(services), SemanticTokenProvider: (services) => new SafeDsSemanticTokenProvider(services), SignatureHelp: (services) => new SafeDsSignatureHelpProvider(services), + TypeHierarchyProvider: (services) => new SafeDsTypeHierarchyProvider(services), }, parser: { ValueConverter: () => new SafeDsValueConverter(), @@ -138,6 +142,7 @@ export type SafeDsSharedServices = LangiumSharedServices; export const SafeDsSharedModule: Module> = { lsp: { + LanguageServer: (services) => new SafeDsLanguageServer(services), NodeKindProvider: () => new SafeDsNodeKindProvider(), }, workspace: { @@ -187,7 +192,6 @@ export const createSafeDsServices = function (context: DefaultSharedModuleContex * @param context Optional module context with the LSP connection. * @return An object wrapping the shared services and the language-specific services. */ -/* c8 ignore start */ export const createSafeDsServicesWithBuiltins = async function (context: DefaultSharedModuleContext): Promise<{ shared: LangiumSharedServices; SafeDs: SafeDsServices; @@ -196,4 +200,3 @@ export const createSafeDsServicesWithBuiltins = async function (context: Default await shared.workspace.WorkspaceManager.initializeWorkspace([]); return { shared, SafeDs }; }; -/* c8 ignore stop */ diff --git a/packages/safe-ds-lang/tests/language/lsp/safe-ds-type-hierarchy-provider.test.ts b/packages/safe-ds-lang/tests/language/lsp/safe-ds-type-hierarchy-provider.test.ts new file mode 100644 index 000000000..138e931c4 --- /dev/null +++ b/packages/safe-ds-lang/tests/language/lsp/safe-ds-type-hierarchy-provider.test.ts @@ -0,0 +1,219 @@ +import { NodeFileSystem } from 'langium/node'; +import { clearDocuments, parseHelper } from 'langium/test'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { type TypeHierarchyItem } from 'vscode-languageserver'; +import { createSafeDsServices } from '../../../src/language/index.js'; +import { findTestRanges } from '../../helpers/testRanges.js'; + +const services = createSafeDsServices(NodeFileSystem).SafeDs; +const typeHierarchyProvider = services.lsp.TypeHierarchyProvider!; +const workspaceManager = services.shared.workspace.WorkspaceManager; +const parse = parseHelper(services); + +describe('SafeDsTypeHierarchyProvider', async () => { + beforeEach(async () => { + // Load the builtin library + await workspaceManager.initializeWorkspace([]); + }); + + afterEach(async () => { + await clearDocuments(services); + }); + + describe('supertypes', () => { + const testCases: TypeHierarchyProviderTest[] = [ + { + testName: 'class without parent types', + code: `class »«C`, + expectedItems: [{ name: 'Any' }], + }, + { + testName: 'class with single parent type', + code: ` + class »«C sub D + class D + `, + expectedItems: [{ name: 'D' }], + }, + { + testName: 'class with multiple parent types', + code: ` + class »«C sub D, E + class D + class E + `, + expectedItems: [{ name: 'D' }], + }, + { + testName: 'enum', + code: ` + enum »«E + `, + expectedItems: undefined, + }, + { + testName: 'enum variant', + code: ` + enum E { + »«V + } + `, + expectedItems: [{ name: 'E' }], + }, + ]; + + it.each(testCases)('should list all supertypes ($testName)', async ({ code, expectedItems }) => { + const result = await getActualSimpleSupertypes(code); + expect(result).toStrictEqual(expectedItems); + }); + }); + + describe('subtypes', () => { + const testCases: TypeHierarchyProviderTest[] = [ + { + testName: 'class without subclasses', + code: `class »«C`, + expectedItems: undefined, + }, + { + testName: 'class without subclasses but with references', + code: ` + class »«C + + class D { + attr c: C + } + `, + expectedItems: undefined, + }, + { + testName: 'class with subclasses but not used as first parent type', + code: ` + class »«C + class D + class E sub D, C + `, + expectedItems: undefined, + }, + { + testName: 'class with subclasses and used as first parent type', + code: ` + class »«C + class D + class E sub C, D + `, + expectedItems: [{ name: 'E' }], + }, + { + testName: 'enum without variant', + code: `enum »«E`, + expectedItems: undefined, + }, + { + testName: 'enum with variants', + code: ` + enum »«E { + V1 + V2 + } + `, + expectedItems: [{ name: 'V1' }, { name: 'V2' }], + }, + { + testName: 'enum variant', + code: ` + enum E { + »«V + } + `, + expectedItems: undefined, + }, + ]; + + it.each(testCases)('should list all subtypes ($testName)', async ({ code, expectedItems }) => { + const result = await getActualSimpleSubtypes(code); + expect(result).toStrictEqual(expectedItems); + }); + }); +}); + +const getActualSimpleSupertypes = async (code: string): Promise => { + return typeHierarchyProvider + .supertypes({ + item: await getUniqueTypeHierarchyItem(code), + }) + ?.map((type) => ({ + name: type.name, + })); +}; + +const getActualSimpleSubtypes = async (code: string): Promise => { + return typeHierarchyProvider + .subtypes({ + item: await getUniqueTypeHierarchyItem(code), + }) + ?.map((type) => ({ + name: type.name, + })); +}; + +const getUniqueTypeHierarchyItem = async (code: string): Promise => { + const document = await parse(code); + + const testRangesResult = findTestRanges(code, document.uri); + if (testRangesResult.isErr) { + throw new Error(testRangesResult.error.message); + } else if (testRangesResult.value.length !== 1) { + throw new Error(`Expected exactly one test range, but got ${testRangesResult.value.length}.`); + } + const testRangeStart = testRangesResult.value[0]!.start; + + const items = + typeHierarchyProvider.prepareTypeHierarchy(document, { + textDocument: { + uri: document.textDocument.uri, + }, + position: { + line: testRangeStart.line, + // Since the test range cannot be placed inside the identifier, we place it in front of the identifier. + // Then we need to move the cursor one character to the right to be inside the identifier. + character: testRangeStart.character + 1, + }, + }) ?? []; + + if (items.length !== 1) { + throw new Error(`Expected exactly one call hierarchy item, but got ${items.length}.`); + } + + return items[0]!; +}; + +/** + * A test case for {@link SafeDsTypeHierarchyProvider.supertypes} and {@link SafeDsTypeHierarchyProvider.subtypes}. + */ +interface TypeHierarchyProviderTest { + /** + * A short description of the test case. + */ + testName: string; + + /** + * The code to parse. + */ + code: string; + + /** + * The expected type hierarchy items. + */ + expectedItems: SimpleTypeHierarchyItem[] | undefined; +} + +/** + * A simplified variant of {@link TypeHierarchyItem}. + */ +interface SimpleTypeHierarchyItem { + /** + * The name of the declaration. + */ + name: string; +}