diff --git a/Sources/ClangLanguageService/ClangLanguageService.swift b/Sources/ClangLanguageService/ClangLanguageService.swift index b0246149c..cb035a32e 100644 --- a/Sources/ClangLanguageService/ClangLanguageService.swift +++ b/Sources/ClangLanguageService/ClangLanguageService.swift @@ -490,6 +490,10 @@ extension ClangLanguageService { return await workspace.buildServerManager.locationsOrLocationLinksAdjustedForCopiedFiles(result) } + package func typeDefinition(_ req: TypeDefinitionRequest) async throws -> LocationsOrLocationLinksResponse? { + return try await forwardRequestToClangd(req) + } + package func completion(_ req: CompletionRequest) async throws -> CompletionList { return try await forwardRequestToClangd(req) } diff --git a/Sources/SourceKitLSP/CMakeLists.txt b/Sources/SourceKitLSP/CMakeLists.txt index f4c25ecc3..ecc446eb7 100644 --- a/Sources/SourceKitLSP/CMakeLists.txt +++ b/Sources/SourceKitLSP/CMakeLists.txt @@ -4,6 +4,7 @@ add_library(SourceKitLSP STATIC DocumentManager.swift DocumentSnapshot+FromFileContents.swift DocumentSnapshot+PositionConversions.swift + DefinitionLocations.swift GeneratedInterfaceDocumentURLData.swift Hooks.swift IndexProgressManager.swift diff --git a/Sources/SourceKitLSP/DefinitionLocations.swift b/Sources/SourceKitLSP/DefinitionLocations.swift new file mode 100644 index 000000000..18f3bd1ac --- /dev/null +++ b/Sources/SourceKitLSP/DefinitionLocations.swift @@ -0,0 +1,160 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +package import IndexStoreDB +@_spi(SourceKitLSP) package import LanguageServerProtocol +@_spi(SourceKitLSP) import SKLogging +package import SemanticIndex + +/// Converts a location from the symbol index to an LSP location. +/// +/// - Parameter location: The symbol index location +/// - Returns: The LSP location +package func indexToLSPLocation(_ location: SymbolLocation) -> Location? { + guard !location.path.isEmpty else { return nil } + return Location( + uri: location.documentUri, + range: Range( + Position( + // 1-based -> 0-based + // Note that we still use max(0, ...) as a fallback if the location is zero. + line: max(0, location.line - 1), + // Technically we would need to convert the UTF-8 column to a UTF-16 column. This would require reading the + // file. In practice they almost always coincide, so we accept the incorrectness here to avoid the file read. + utf16index: max(0, location.utf8Column - 1) + ) + ) + ) +} + +/// The result of looking up definition locations for a symbol. +package struct DefinitionLocationsResult { + /// The locations of the symbol's definition. + package let locations: [Location] + /// The occurrences from the index lookup, if any. Can be used by callers to avoid duplicate index lookups. + package let indexOccurrences: [SymbolOccurrence] + + package init(locations: [Location], indexOccurrences: [SymbolOccurrence] = []) { + self.locations = locations + self.indexOccurrences = indexOccurrences + } +} + +/// Return the locations for jump to definition from the given `SymbolDetails`. +package func definitionLocations( + for symbol: SymbolDetails, + originatorUri: DocumentURI, + index: CheckedIndex?, + languageService: any LanguageService +) async throws -> DefinitionLocationsResult { + // If this symbol is a module then generate a textual interface + if symbol.kind == .module { + // For module symbols, prefer using systemModule information if available + let moduleName: String + let groupName: String? + + if let systemModule = symbol.systemModule { + moduleName = systemModule.moduleName + groupName = systemModule.groupName + } else if let name = symbol.name { + moduleName = name + groupName = nil + } else { + return DefinitionLocationsResult(locations: []) + } + + let location = try await definitionInInterface( + moduleName: moduleName, + groupName: groupName, + symbolUSR: nil, + originatorUri: originatorUri, + languageService: languageService + ) + return DefinitionLocationsResult(locations: [location]) + } + + // System symbols use generated interface + if symbol.isSystem ?? false, let systemModule = symbol.systemModule { + let location = try await definitionInInterface( + moduleName: systemModule.moduleName, + groupName: systemModule.groupName, + symbolUSR: symbol.usr, + originatorUri: originatorUri, + languageService: languageService + ) + return DefinitionLocationsResult(locations: [location]) + } + + guard let index else { + if let bestLocalDeclaration = symbol.bestLocalDeclaration { + return DefinitionLocationsResult(locations: [bestLocalDeclaration]) + } + return DefinitionLocationsResult(locations: []) + } + + guard let usr = symbol.usr else { return DefinitionLocationsResult(locations: []) } + logger.info("Performing indexed jump-to-definition with USR \(usr)") + + let occurrences = index.definitionOrDeclarationOccurrences(ofUSR: usr) + + if occurrences.isEmpty { + if let bestLocalDeclaration = symbol.bestLocalDeclaration { + return DefinitionLocationsResult(locations: [bestLocalDeclaration]) + } + // Fallback: The symbol was not found in the index. This often happens with + // third-party binary frameworks or libraries where indexing data is missing. + // If module info is available, fallback to generating the textual interface. + if let systemModule = symbol.systemModule { + let location = try await definitionInInterface( + moduleName: systemModule.moduleName, + groupName: systemModule.groupName, + symbolUSR: symbol.usr, + originatorUri: originatorUri, + languageService: languageService + ) + return DefinitionLocationsResult(locations: [location]) + } + } + + return DefinitionLocationsResult( + locations: occurrences.compactMap { indexToLSPLocation($0.location) }.sorted(), + indexOccurrences: occurrences + ) +} + +/// Generate the generated interface for the given module, write it to disk and return the location to which to jump +/// to get to the definition of `symbolUSR`. +/// +/// `originatorUri` is the URI of the file from which the definition request is performed. It is used to determine the +/// compiler arguments to generate the generated interface. +package func definitionInInterface( + moduleName: String, + groupName: String?, + symbolUSR: String?, + originatorUri: DocumentURI, + languageService: any LanguageService +) async throws -> Location { + let documentForBuildSettings = originatorUri.buildSettingsFile + + guard + let interfaceDetails = try await languageService.openGeneratedInterface( + document: documentForBuildSettings, + moduleName: moduleName, + groupName: groupName, + symbolUSR: symbolUSR + ) + else { + throw ResponseError.unknown("Could not generate Swift Interface for \(moduleName)") + } + let position = interfaceDetails.position ?? Position(line: 0, utf16index: 0) + return Location(uri: interfaceDetails.uri, range: Range(position)) +} diff --git a/Sources/SourceKitLSP/LanguageService.swift b/Sources/SourceKitLSP/LanguageService.swift index 058d2c4e2..ffc108992 100644 --- a/Sources/SourceKitLSP/LanguageService.swift +++ b/Sources/SourceKitLSP/LanguageService.swift @@ -242,6 +242,7 @@ package protocol LanguageService: AnyObject, Sendable { func definition(_ request: DefinitionRequest) async throws -> LocationsOrLocationLinksResponse? func declaration(_ request: DeclarationRequest) async throws -> LocationsOrLocationLinksResponse? + func typeDefinition(_ request: TypeDefinitionRequest) async throws -> LocationsOrLocationLinksResponse? func documentSymbolHighlight(_ req: DocumentHighlightRequest) async throws -> [DocumentHighlight]? func foldingRange(_ req: FoldingRangeRequest) async throws -> [FoldingRange]? func documentSymbol(_ req: DocumentSymbolRequest) async throws -> DocumentSymbolResponse? @@ -438,6 +439,10 @@ package extension LanguageService { throw ResponseError.requestNotImplemented(DeclarationRequest.self) } + func typeDefinition(_ request: TypeDefinitionRequest) async throws -> LocationsOrLocationLinksResponse? { + throw ResponseError.requestNotImplemented(TypeDefinitionRequest.self) + } + func documentSymbolHighlight(_ req: DocumentHighlightRequest) async throws -> [DocumentHighlight]? { throw ResponseError.requestNotImplemented(DocumentHighlightRequest.self) } diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index 13e763f4f..a051cdc77 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -790,6 +790,8 @@ extension SourceKitLSPServer: QueueBasedMessageHandler { await self.handleRequest(for: request, requestHandler: self.declaration) case let request as RequestAndReply: await self.handleRequest(for: request, requestHandler: self.definition) + case let request as RequestAndReply: + await self.handleRequest(for: request, requestHandler: self.typeDefinition) case let request as RequestAndReply: await self.handleRequest(for: request, requestHandler: self.doccDocumentation) case let request as RequestAndReply: @@ -1146,6 +1148,7 @@ extension SourceKitLSPServer { completionProvider: completionOptions, signatureHelpProvider: signatureHelpOptions, definitionProvider: .bool(true), + typeDefinitionProvider: .bool(true), implementationProvider: .bool(true), referencesProvider: .bool(true), documentHighlightProvider: .bool(true), @@ -1974,27 +1977,6 @@ extension SourceKitLSPServer { return try await languageService.documentDiagnostic(req) } - /// Converts a location from the symbol index to an LSP location. - /// - /// - Parameter location: The symbol index location - /// - Returns: The LSP location - private nonisolated func indexToLSPLocation(_ location: SymbolLocation) -> Location? { - guard !location.path.isEmpty else { return nil } - return Location( - uri: location.documentUri, - range: Range( - Position( - // 1-based -> 0-based - // Note that we still use max(0, ...) as a fallback if the location is zero. - line: max(0, location.line - 1), - // Technically we would need to convert the UTF-8 column to a UTF-16 column. This would require reading the - // file. In practice they almost always coincide, so we accept the incorrectness here to avoid the file read. - utf16index: max(0, location.utf8Column - 1) - ) - ) - ) - } - func declaration( _ req: DeclarationRequest, workspace: Workspace, @@ -2003,110 +1985,12 @@ extension SourceKitLSPServer { return try await languageService.declaration(req) } - /// Return the locations for jump to definition from the given `SymbolDetails`. - private func definitionLocations( - for symbol: SymbolDetails, - in uri: DocumentURI, + func typeDefinition( + _ req: TypeDefinitionRequest, + workspace: Workspace, languageService: any LanguageService - ) async throws -> [Location] { - // If this symbol is a module then generate a textual interface - if symbol.kind == .module { - // For module symbols, prefer using systemModule information if available - let moduleName: String - let groupName: String? - - if let systemModule = symbol.systemModule { - moduleName = systemModule.moduleName - groupName = systemModule.groupName - } else if let name = symbol.name { - moduleName = name - groupName = nil - } else { - return [] - } - - let interfaceLocation = try await self.definitionInInterface( - moduleName: moduleName, - groupName: groupName, - symbolUSR: nil, - originatorUri: uri, - languageService: languageService - ) - return [interfaceLocation] - } - - if symbol.isSystem ?? false, let systemModule = symbol.systemModule { - let location = try await self.definitionInInterface( - moduleName: systemModule.moduleName, - groupName: systemModule.groupName, - symbolUSR: symbol.usr, - originatorUri: uri, - languageService: languageService - ) - return [location] - } - - guard let index = await self.workspaceForDocument(uri: uri)?.index(checkedFor: .deletedFiles) else { - if let bestLocalDeclaration = symbol.bestLocalDeclaration { - return [bestLocalDeclaration] - } else { - return [] - } - } - guard let usr = symbol.usr else { return [] } - logger.info("Performing indexed jump-to-definition with USR \(usr)") - var occurrences = index.definitionOrDeclarationOccurrences(ofUSR: usr) - if symbol.isDynamic ?? true { - lazy var transitiveReceiverUsrs: [String]? = { - if let receiverUsrs = symbol.receiverUsrs { - return transitiveSubtypeClosure( - ofUsrs: receiverUsrs, - index: index - ) - } else { - return nil - } - }() - occurrences += occurrences.flatMap { - let overriddenUsrs = index.occurrences(relatedToUSR: $0.symbol.usr, roles: .overrideOf).map(\.symbol.usr) - let overriddenSymbolDefinitions = overriddenUsrs.compactMap { - index.primaryDefinitionOrDeclarationOccurrence(ofUSR: $0) - } - // Only contain overrides that are children of one of the receiver types or their subtypes or extensions. - return overriddenSymbolDefinitions.filter { override in - override.relations.contains(where: { - guard $0.roles.contains(.childOf) else { - return false - } - if let transitiveReceiverUsrs, !transitiveReceiverUsrs.contains($0.symbol.usr) { - return false - } - return true - }) - } - } - } - - if occurrences.isEmpty { - if let bestLocalDeclaration = symbol.bestLocalDeclaration { - return [bestLocalDeclaration] - } - // Fallback: The symbol was not found in the index. This often happens with - // third-party binary frameworks or libraries where indexing data is missing. - // If module info is available, fallback to generating the textual interface. - if let systemModule = symbol.systemModule { - let location = try await self.definitionInInterface( - moduleName: systemModule.moduleName, - groupName: systemModule.groupName, - symbolUSR: symbol.usr, - originatorUri: uri, - languageService: languageService - ) - return [location] - } - } - - return occurrences.compactMap { indexToLSPLocation($0.location) }.sorted() + ) async throws -> LocationsOrLocationLinksResponse? { + return try await languageService.typeDefinition(req) } /// Returns the result of a `DefinitionRequest` by running a `SymbolInfoRequest`, inspecting @@ -2152,11 +2036,48 @@ extension SourceKitLSPServer { // jump to the function's declaration). locations = [bestLocalDeclaration] } else { - locations = try await self.definitionLocations( + let index = await workspace.index(checkedFor: .deletedFiles) + let definitionResult = try await definitionLocations( for: symbol, - in: req.textDocument.uri, + originatorUri: req.textDocument.uri, + index: index, languageService: languageService ) + locations = definitionResult.locations + // For dynamic symbols, also include overridden definitions + if let index, symbol.isDynamic ?? true, !definitionResult.indexOccurrences.isEmpty { + lazy var transitiveReceiverUsrs: [String]? = { + if let receiverUsrs = symbol.receiverUsrs { + return transitiveSubtypeClosure( + ofUsrs: receiverUsrs, + index: index + ) + } else { + return nil + } + }() + // Use the occurrences already retrieved by definitionLocations to avoid duplicate index lookup + let overriddenLocations = definitionResult.indexOccurrences.flatMap { occurrence -> [Location] in + let overriddenUsrs = index.occurrences(relatedToUSR: occurrence.symbol.usr, roles: .overrideOf) + .map(\.symbol.usr) + let overriddenSymbolDefinitions = overriddenUsrs.compactMap { + index.primaryDefinitionOrDeclarationOccurrence(ofUSR: $0) + } + // Only contain overrides that are children of one of the receiver types or their subtypes or extensions. + return overriddenSymbolDefinitions.filter { override in + override.relations.contains(where: { + guard $0.roles.contains(.childOf) else { + return false + } + if let transitiveReceiverUsrs, !transitiveReceiverUsrs.contains($0.symbol.usr) { + return false + } + return true + }) + }.compactMap { indexToLSPLocation($0.location) } + } + locations += overriddenLocations + } } // If the symbol's location is is where we initiated rename from, also show the declarations that the symbol @@ -2230,36 +2151,6 @@ extension SourceKitLSPServer { return .locations(remappedLocations) } - /// Generate the generated interface for the given module, write it to disk and return the location to which to jump - /// to get to the definition of `symbolUSR`. - /// - /// `originatorUri` is the URI of the file from which the definition request is performed. It is used to determine the - /// compiler arguments to generate the generated interface. - func definitionInInterface( - moduleName: String, - groupName: String?, - symbolUSR: String?, - originatorUri: DocumentURI, - languageService: any LanguageService - ) async throws -> Location { - // Let openGeneratedInterface handle all the logic, including checking if we're already in the right interface - let documentForBuildSettings = originatorUri.buildSettingsFile - - guard - let interfaceDetails = try await languageService.openGeneratedInterface( - document: documentForBuildSettings, - moduleName: moduleName, - groupName: groupName, - symbolUSR: symbolUSR - ) - else { - throw ResponseError.unknown("Could not generate Swift Interface for \(moduleName)") - } - let position = interfaceDetails.position ?? Position(line: 0, utf16index: 0) - let loc = Location(uri: interfaceDetails.uri, range: Range(position)) - return loc - } - func implementation( _ req: ImplementationRequest, workspace: Workspace, @@ -2434,7 +2325,7 @@ extension SourceKitLSPServer { // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed func indexToLSPLocation2(_ location: SymbolLocation) -> Location? { - return self.indexToLSPLocation(location) + return indexToLSPLocation(location) } // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed @@ -2478,7 +2369,7 @@ extension SourceKitLSPServer { // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed func indexToLSPLocation2(_ location: SymbolLocation) -> Location? { - return self.indexToLSPLocation(location) + return indexToLSPLocation(location) } // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed @@ -2600,7 +2491,7 @@ extension SourceKitLSPServer { // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed func indexToLSPLocation2(_ location: SymbolLocation) -> Location? { - return self.indexToLSPLocation(location) + return indexToLSPLocation(location) } // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed diff --git a/Sources/SwiftLanguageService/CMakeLists.txt b/Sources/SwiftLanguageService/CMakeLists.txt index f49644670..ae022ef8b 100644 --- a/Sources/SwiftLanguageService/CMakeLists.txt +++ b/Sources/SwiftLanguageService/CMakeLists.txt @@ -52,6 +52,7 @@ add_library(SwiftLanguageService STATIC SyntaxHighlightingTokens.swift SyntaxTreeManager.swift TestDiscovery.swift + TypeDefinition.swift VariableTypeInfo.swift ) set_target_properties(SwiftLanguageService PROPERTIES diff --git a/Sources/SwiftLanguageService/InlayHintResolve.swift b/Sources/SwiftLanguageService/InlayHintResolve.swift index 7da9747e9..b0c924d9e 100644 --- a/Sources/SwiftLanguageService/InlayHintResolve.swift +++ b/Sources/SwiftLanguageService/InlayHintResolve.swift @@ -16,6 +16,7 @@ import IndexStoreDB import SemanticIndex import SourceKitD import SourceKitLSP +import SwiftExtensions extension SwiftLanguageService { /// Resolves an inlay hint by looking up the type definition location. @@ -87,27 +88,14 @@ extension SwiftLanguageService { return nil } - // For local types, return the local declaration - if let location = typeInfo.symbolInfo.bestLocalDeclaration { - return location - } - - // For SDK types, fall back to generated interface - if typeInfo.symbolInfo.isSystem ?? false, - let systemModule = typeInfo.symbolInfo.systemModule - { - let interfaceDetails = try await self.openGeneratedInterface( - document: snapshot.uri, - moduleName: systemModule.moduleName, - groupName: systemModule.groupName, - symbolUSR: typeInfo.symbolInfo.usr - ) - if let details = interfaceDetails { - let position = details.position ?? Position(line: 0, utf16index: 0) - return Location(uri: details.uri, range: Range(position)) - } - } + let index = await sourceKitLSPServer?.workspaceForDocument(uri: snapshot.uri)?.index(checkedFor: .deletedFiles) + let locations = try await SourceKitLSP.definitionLocations( + for: typeInfo.symbolInfo, + originatorUri: snapshot.uri, + index: index, + languageService: self + ).locations - return nil + return locations.only } } diff --git a/Sources/SwiftLanguageService/SwiftLanguageService.swift b/Sources/SwiftLanguageService/SwiftLanguageService.swift index 78b5b133d..7bd0d5409 100644 --- a/Sources/SwiftLanguageService/SwiftLanguageService.swift +++ b/Sources/SwiftLanguageService/SwiftLanguageService.swift @@ -391,6 +391,7 @@ extension SwiftLanguageService { retriggerCharacters: [",", ":"] ), definitionProvider: nil, + typeDefinitionProvider: .bool(true), implementationProvider: .bool(true), referencesProvider: nil, documentHighlightProvider: .bool(true), diff --git a/Sources/SwiftLanguageService/TypeDefinition.swift b/Sources/SwiftLanguageService/TypeDefinition.swift new file mode 100644 index 000000000..2640e33e7 --- /dev/null +++ b/Sources/SwiftLanguageService/TypeDefinition.swift @@ -0,0 +1,77 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import IndexStoreDB +@_spi(SourceKitLSP) package import LanguageServerProtocol +import SemanticIndex +import SourceKitD +import SourceKitLSP + +extension SwiftLanguageService { + /// Handles the textDocument/typeDefinition request. + package func typeDefinition(_ request: TypeDefinitionRequest) async throws -> LocationsOrLocationLinksResponse? { + let uri = request.textDocument.uri + let position = request.position + + let snapshot = try await self.latestSnapshot(for: uri) + let compileCommand = await self.compileCommand(for: uri, fallbackAfterTimeout: false) + + let skreq = sourcekitd.dictionary([ + keys.cancelOnSubsequentRequest: 0, + keys.offset: snapshot.utf8Offset(of: position), + keys.sourceFile: snapshot.uri.sourcekitdSourceFile, + keys.primaryFile: snapshot.uri.primaryFile?.pseudoPath, + keys.compilerArgs: compileCommand?.compilerArgs as [any SKDRequestValue]?, + ]) + + let dict = try await send(sourcekitdRequest: \.cursorInfo, skreq, snapshot: snapshot) + let documentManager = try self.documentManager + + // If the cursor is on a type symbol itself, use its USR directly. + // Otherwise get the type of the symbol at this position. + let symbol: SymbolDetails + if let cursorInfo = CursorInfo(dict, snapshot: snapshot, documentManager: documentManager, sourcekitd: sourcekitd) { + switch cursorInfo.symbolInfo.kind { + case .class, .struct, .enum, .interface, .typeParameter: + symbol = cursorInfo.symbolInfo + default: + guard let typeUsr: String = dict[keys.typeUsr], + let typeSymbol = try await cursorInfoFromTypeUSR(typeUsr, in: snapshot)?.symbolInfo + else { + return nil + } + symbol = typeSymbol + } + } else { + guard let typeUsr: String = dict[keys.typeUsr], + let typeSymbol = try await cursorInfoFromTypeUSR(typeUsr, in: snapshot)?.symbolInfo + else { + return nil + } + symbol = typeSymbol + } + + let index = await sourceKitLSPServer?.workspaceForDocument(uri: uri)?.index(checkedFor: .deletedFiles) + let locations = try await SourceKitLSP.definitionLocations( + for: symbol, + originatorUri: uri, + index: index, + languageService: self + ).locations + + if locations.isEmpty { + return nil + } + + return .locations(locations) + } +} diff --git a/Tests/SourceKitLSPTests/TypeDefinitionTests.swift b/Tests/SourceKitLSPTests/TypeDefinitionTests.swift new file mode 100644 index 000000000..c248b7da9 --- /dev/null +++ b/Tests/SourceKitLSPTests/TypeDefinitionTests.swift @@ -0,0 +1,206 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +@_spi(SourceKitLSP) import LanguageServerProtocol +import SKTestSupport +import SwiftExtensions +import XCTest + +final class TypeDefinitionTests: SourceKitLSPTestCase { + func testTypeDefinitionLocalType() async throws { + let testClient = try await TestSourceKitLSPClient() + let uri = DocumentURI(for: .swift) + + let positions = testClient.openDocument( + """ + struct 1️⃣MyType {} + let 2️⃣x = MyType() + """, + uri: uri + ) + + let response = try await testClient.send( + TypeDefinitionRequest( + textDocument: TextDocumentIdentifier(uri), + position: positions["2️⃣"] + ) + ) + + guard case .locations(let locations) = response, let location = locations.first else { + XCTFail("Expected location response") + return + } + + XCTAssertEqual(location.uri, uri) + XCTAssertEqual(location.range, Range(positions["1️⃣"])) + } + + func testTypeDefinitionCrossModule() async throws { + let project = try await SwiftPMTestProject( + files: [ + "LibA/MyType.swift": """ + public struct 1️⃣MyType { + public init() {} + } + """, + "LibB/UseType.swift": """ + import LibA + let 2️⃣x = MyType() + """, + ], + manifest: """ + let package = Package( + name: "MyLibrary", + targets: [ + .target(name: "LibA"), + .target(name: "LibB", dependencies: ["LibA"]), + ] + ) + """, + enableBackgroundIndexing: true + ) + + let (uri, positions) = try project.openDocument("UseType.swift") + + let response = try await project.testClient.send( + TypeDefinitionRequest( + textDocument: TextDocumentIdentifier(uri), + position: positions["2️⃣"] + ) + ) + + guard case .locations(let locations) = response, let location = locations.first else { + XCTFail("Expected location response") + return + } + + XCTAssertEqual(location.uri, try project.uri(for: "MyType.swift")) + XCTAssertEqual(location.range, try Range(project.position(of: "1️⃣", in: "MyType.swift"))) + } + + func testTypeDefinitionGenericType() async throws { + let testClient = try await TestSourceKitLSPClient() + let uri = DocumentURI(for: .swift) + + let positions = testClient.openDocument( + """ + struct 1️⃣Container { + var value: T + } + let 2️⃣x = Container(value: 42) + """, + uri: uri + ) + + let response = try await testClient.send( + TypeDefinitionRequest( + textDocument: TextDocumentIdentifier(uri), + position: positions["2️⃣"] + ) + ) + + guard case .locations(let locations) = response, let location = locations.first else { + XCTFail("Expected location response") + return + } + + XCTAssertEqual(location.uri, uri) + XCTAssertEqual(location.range, Range(positions["1️⃣"])) + } + + func testTypeDefinitionOnTypeAnnotation() async throws { + let testClient = try await TestSourceKitLSPClient() + let uri = DocumentURI(for: .swift) + + let positions = testClient.openDocument( + """ + struct 1️⃣MyType {} + let x: 2️⃣MyType = MyType() + """, + uri: uri + ) + + let response = try await testClient.send( + TypeDefinitionRequest( + textDocument: TextDocumentIdentifier(uri), + position: positions["2️⃣"] + ) + ) + + guard case .locations(let locations) = response, let location = locations.first else { + XCTFail("Expected location response") + return + } + + XCTAssertEqual(location.uri, uri) + XCTAssertEqual(location.range, Range(positions["1️⃣"])) + } + + func testTypeDefinitionFunctionParameter() async throws { + let testClient = try await TestSourceKitLSPClient() + let uri = DocumentURI(for: .swift) + + let positions = testClient.openDocument( + """ + struct 1️⃣MyType {} + func process(_ 2️⃣value: MyType) {} + """, + uri: uri + ) + + let response = try await testClient.send( + TypeDefinitionRequest( + textDocument: TextDocumentIdentifier(uri), + position: positions["2️⃣"] + ) + ) + + guard case .locations(let locations) = response, let location = locations.first else { + XCTFail("Expected location response") + return + } + + XCTAssertEqual(location.uri, uri) + XCTAssertEqual(location.range, Range(positions["1️⃣"])) + } + + func testTypeDefinitionGeneratedInterface() async throws { + let testClient = try await TestSourceKitLSPClient() + let uri = DocumentURI(for: .swift) + + let positions = testClient.openDocument( + """ + let 1️⃣x = "hello" + """, + uri: uri + ) + + let response = try await testClient.send( + TypeDefinitionRequest( + textDocument: TextDocumentIdentifier(uri), + position: positions["1️⃣"] + ) + ) + + guard case .locations(let locations) = response, let location = locations.only else { + XCTFail("Expected single location response") + return + } + + // Should jump to String in the generated Swift interface + XCTAssertTrue( + location.uri.pseudoPath.hasSuffix(".swiftinterface"), + "Expected swiftinterface file, got: \(location.uri.pseudoPath)" + ) + assertContains(location.uri.pseudoPath, "String") + } +}