-
Notifications
You must be signed in to change notification settings - Fork 373
Implement textDocument/typeDefinition request #2445
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
d8b8d71
94ad50d
2a360c0
b7e76ff
82a7726
0e52fed
cf56bf2
5ee63b7
cccdde2
d73aba2
6028898
e786a14
f1f46f5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This list is ordered alphabetically? Could insert this at |
||
| case let request as RequestAndReply<DoccDocumentationRequest>: | ||
| await self.handleRequest(for: request, requestHandler: self.doccDocumentation) | ||
| case let request as RequestAndReply<DocumentColorRequest>: | ||
|
|
@@ -1146,6 +1148,7 @@ extension SourceKitLSPServer { | |
| completionProvider: completionOptions, | ||
| signatureHelpProvider: signatureHelpOptions, | ||
| definitionProvider: .bool(true), | ||
| typeDefinitionProvider: .bool(true), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you also advertise the |
||
| implementationProvider: .bool(true), | ||
| referencesProvider: .bool(true), | ||
| documentHighlightProvider: .bool(true), | ||
|
|
@@ -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, | ||
|
|
||
|
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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There’s no |
||
| if let typeInfo = try await cursorInfoFromTypeUSR(typeUsr, in: snapshot), | ||
| let location = typeInfo.symbolInfo.bestLocalDeclaration | ||
| { | ||
| return .locations([location]) | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
|
||
| return nil | ||
| } | ||
| } | ||
| 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️⃣"])) | ||
| } | ||
|
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 { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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️⃣"])) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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.