Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Sources/SourceKitLSP/LanguageService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -438,6 +439,10 @@ package extension LanguageService {
throw ResponseError.requestNotImplemented(DeclarationRequest.self)
}

func typeDefinition(_ request: TypeDefinitionRequest) async throws -> LocationsOrLocationLinksResponse? {
throw ResponseError.requestNotImplemented(TypeDefinitionRequest.self)
}

Comment on lines +442 to +445
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also forward the request to clangd in the ClangLanguageService? Not sure if clangd supports the request right now but if it does in the future, we’ll pick it up that way.

func documentSymbolHighlight(_ req: DocumentHighlightRequest) async throws -> [DocumentHighlight]? {
throw ResponseError.requestNotImplemented(DocumentHighlightRequest.self)
}
Expand Down
11 changes: 11 additions & 0 deletions Sources/SourceKitLSP/SourceKitLSPServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,8 @@ extension SourceKitLSPServer: QueueBasedMessageHandler {
await self.handleRequest(for: request, requestHandler: self.declaration)
case let request as RequestAndReply<DefinitionRequest>:
await self.handleRequest(for: request, requestHandler: self.definition)
case let request as RequestAndReply<TypeDefinitionRequest>:
await self.handleRequest(for: request, requestHandler: self.typeDefinition)
Comment on lines +793 to +794
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This list is ordered alphabetically? Could insert this at t?

case let request as RequestAndReply<DoccDocumentationRequest>:
await self.handleRequest(for: request, requestHandler: self.doccDocumentation)
case let request as RequestAndReply<DocumentColorRequest>:
Expand Down Expand Up @@ -1146,6 +1148,7 @@ extension SourceKitLSPServer {
completionProvider: completionOptions,
signatureHelpProvider: signatureHelpOptions,
definitionProvider: .bool(true),
typeDefinitionProvider: .bool(true),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also advertise the typeDefinitionProvider here?

implementationProvider: .bool(true),
referencesProvider: .bool(true),
documentHighlightProvider: .bool(true),
Expand Down Expand Up @@ -2003,6 +2006,14 @@ extension SourceKitLSPServer {
return try await languageService.declaration(req)
}

func typeDefinition(
_ req: TypeDefinitionRequest,
workspace: Workspace,
languageService: any LanguageService
) async throws -> LocationsOrLocationLinksResponse? {
return try await languageService.typeDefinition(req)
}

/// Return the locations for jump to definition from the given `SymbolDetails`.
private func definitionLocations(
for symbol: SymbolDetails,
Expand Down
54 changes: 54 additions & 0 deletions Sources/SwiftLanguageService/TypeDefinition.swift
Comment thread
loveucifer marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//===----------------------------------------------------------------------===//
//
// 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.
///
/// Given a source location, finds the type of the symbol at that position
/// and returns the location of that type's definition.
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)

guard let typeUsr: String = dict[keys.typeUsr] else {
return nil
}

// Try cursorInfo with USR lookup first - it knows the current in-memory state
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s no second here, so I think the comment no longer applies

if let typeInfo = try await cursorInfoFromTypeUSR(typeUsr, in: snapshot),
let location = typeInfo.symbolInfo.bestLocalDeclaration
{
return .locations([location])
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to my comments in the inlay type hints PR, this doesn’t handle jumping to generated interfaces. Instead of doing another ad-hoc fix like you did in the inlay hints PR, I’d really like to see this unified with the logic in indexBasedJumpToDefinition. Could you look into how we can best re-use code between both that function, inlay hints resolve and the type definition request?


return nil
}
}
258 changes: 258 additions & 0 deletions Tests/SourceKitLSPTests/TypeDefinitionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
//===----------------------------------------------------------------------===//
//
// 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 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<T> {
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️⃣"]))
}
Comment thread
ahoppen marked this conversation as resolved.

func testTypeDefinitionFunctionReturnType() async throws {
let testClient = try await TestSourceKitLSPClient()
let uri = DocumentURI(for: .swift)

let positions = testClient.openDocument(
"""
struct 1️⃣MyType {}
func 2️⃣create() -> 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 testTypeDefinitionClassType() async throws {
let testClient = try await TestSourceKitLSPClient()
let uri = DocumentURI(for: .swift)

let positions = testClient.openDocument(
"""
class 1️⃣MyClass {}
let 2️⃣instance = MyClass()
""",
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 testTypeDefinitionEnumType() async throws {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Classes and enums are just the same as a struct in the type system, so these two tests don’t really provide much value and I think we can delete them.

let testClient = try await TestSourceKitLSPClient()
let uri = DocumentURI(for: .swift)

let positions = testClient.openDocument(
"""
enum 1️⃣Status { case active, inactive }
let 2️⃣current = Status.active
""",
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️⃣"]))
}
}