Skip to content

Commit f9c4d86

Browse files
authored
Merge pull request #1462 from ahoppen/jump-to-protocol-requirement
When performing jump-to-definition on a method implementing a protocol requirement, jump to the requirement
2 parents e1f80e4 + 202d723 commit f9c4d86

File tree

6 files changed

+164
-11
lines changed

6 files changed

+164
-11
lines changed

Sources/SourceKitLSP/Clang/ClangLanguageService.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,10 @@ actor ClangLanguageService: LanguageService, MessageHandler {
338338
return try await clangd.send(request)
339339
}
340340

341+
public func canonicalDeclarationPosition(of position: Position, in uri: DocumentURI) async -> Position? {
342+
return nil
343+
}
344+
341345
func _crash() {
342346
// Since `clangd` doesn't have a method to crash it, kill it.
343347
#if os(Windows)

Sources/SourceKitLSP/LanguageService.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ public protocol LanguageService: AnyObject, Sendable {
132132

133133
// MARK: - Build System Integration
134134

135-
/// Sent when the `BuildSystem` has resolved build settings, such as for the intial build settings
135+
/// Sent when the `BuildSystem` has resolved build settings, such as for the initial build settings
136136
/// or when the settings have changed (e.g. modified build system files). This may be sent before
137137
/// the respective `DocumentURI` has been opened.
138138
func documentUpdatedBuildSettings(_ uri: DocumentURI) async
@@ -226,6 +226,16 @@ public protocol LanguageService: AnyObject, Sendable {
226226
/// A return value of `nil` indicates that this language service does not support syntactic test discovery.
227227
func syntacticDocumentTests(for uri: DocumentURI, in workspace: Workspace) async throws -> [AnnotatedTestItem]?
228228

229+
/// A position that is canonical for all positions within a declaration. For example, if we have the following
230+
/// declaration, then all `|` markers should return the same canonical position.
231+
/// ```
232+
/// func |fo|o(|ba|r: Int)
233+
/// ```
234+
/// The actual position returned by the method does not matter. All that's relevant is the canonicalization.
235+
///
236+
/// Returns `nil` if no canonical position could be determined.
237+
func canonicalDeclarationPosition(of position: Position, in uri: DocumentURI) async -> Position?
238+
229239
/// Crash the language server. Should be used for crash recovery testing only.
230240
func _crash() async
231241
}

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1757,23 +1757,87 @@ extension SourceKitLSPServer {
17571757
position: req.position
17581758
)
17591759
)
1760-
let locations = try await symbols.asyncMap { (symbol) -> [Location] in
1761-
if symbol.bestLocalDeclaration != nil && !(symbol.isDynamic ?? true)
1762-
&& symbol.usr?.hasPrefix("s:") ?? false /* Swift symbols have USRs starting with s: */
1760+
1761+
let canonicalOriginatorLocation = await languageService.canonicalDeclarationPosition(
1762+
of: req.position,
1763+
in: req.textDocument.uri
1764+
)
1765+
1766+
// Returns `true` if `location` points to the same declaration that the definition request was initiated from.
1767+
@Sendable func isAtCanonicalOriginatorLocation(_ location: Location) async -> Bool {
1768+
guard location.uri == req.textDocument.uri, let canonicalOriginatorLocation else {
1769+
return false
1770+
}
1771+
return await languageService.canonicalDeclarationPosition(of: location.range.lowerBound, in: location.uri)
1772+
== canonicalOriginatorLocation
1773+
}
1774+
1775+
var locations = try await symbols.asyncMap { (symbol) -> [Location] in
1776+
var locations: [Location]
1777+
if let bestLocalDeclaration = symbol.bestLocalDeclaration,
1778+
!(symbol.isDynamic ?? true),
1779+
symbol.usr?.hasPrefix("s:") ?? false /* Swift symbols have USRs starting with s: */
17631780
{
17641781
// If we have a known non-dynamic symbol within Swift, we don't need to do an index lookup.
17651782
// For non-Swift symbols, we need to perform an index lookup because the best local declaration will point to
1766-
// a header file but jump-to-definition should prefer the implementation (there's the declaration request to jump
1767-
// to the function's declaration).
1768-
return [symbol.bestLocalDeclaration].compactMap { $0 }
1783+
// a header file but jump-to-definition should prefer the implementation (there's the declaration request to
1784+
// jump to the function's declaration).
1785+
locations = [bestLocalDeclaration]
1786+
} else {
1787+
locations = try await self.definitionLocations(
1788+
for: symbol,
1789+
in: req.textDocument.uri,
1790+
languageService: languageService
1791+
)
17691792
}
1770-
return try await self.definitionLocations(for: symbol, in: req.textDocument.uri, languageService: languageService)
1793+
1794+
// If the symbol's location is is where we initiated rename from, also show the declarations that the symbol
1795+
// overrides.
1796+
if let location = locations.only,
1797+
let usr = symbol.usr,
1798+
let index = workspace.index(checkedFor: .deletedFiles),
1799+
await isAtCanonicalOriginatorLocation(location)
1800+
{
1801+
let baseUSRs = index.occurrences(ofUSR: usr, roles: .overrideOf).flatMap {
1802+
$0.relations.filter { $0.roles.contains(.overrideOf) }.map(\.symbol.usr)
1803+
}
1804+
locations += baseUSRs.compactMap {
1805+
guard let baseDeclOccurrence = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: $0) else {
1806+
return nil
1807+
}
1808+
return indexToLSPLocation(baseDeclOccurrence.location)
1809+
}
1810+
}
1811+
1812+
return locations
17711813
}.flatMap { $0 }
1814+
17721815
// Remove any duplicate locations. We might end up with duplicate locations when performing a definition request
17731816
// on eg. `MyStruct()` when no explicit initializer is declared. In this case we get two symbol infos, one for the
17741817
// declaration of the `MyStruct` type and one for the initializer, which is implicit and thus has the location of
17751818
// the `MyStruct` declaration itself.
1776-
return locations.unique
1819+
locations = locations.unique
1820+
1821+
// Try removing any results that would point back to the location we are currently at. This ensures that eg. in the
1822+
// following case we only show line 2 when performing jump-to-definition on `TestImpl.doThing`.
1823+
//
1824+
// ```
1825+
// protocol TestProtocol {
1826+
// func doThing()
1827+
// }
1828+
// struct TestImpl: TestProtocol {
1829+
// func doThing() { }
1830+
// }
1831+
// ```
1832+
//
1833+
// If this would result in no locations, don't apply the filter. This way, performing jump-to-definition in the
1834+
// middle of a function's base name takes us to the base name start, indicating that jump-to-definition was able to
1835+
// resolve the location and didn't fail.
1836+
let nonOriginatorLocations = await locations.asyncFilter { await !isAtCanonicalOriginatorLocation($0) }
1837+
if !nonOriginatorLocations.isEmpty {
1838+
locations = nonOriginatorLocations
1839+
}
1840+
return locations
17771841
}
17781842

17791843
func definition(

Sources/SourceKitLSP/Swift/CodeActions/SyntaxRefactoringCodeActionProvider.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,13 @@ extension RemoveSeparatorsFromIntegerLiteral: SyntaxRefactoringCodeActionProvide
121121
}
122122
}
123123

124-
extension Syntax {
124+
extension SyntaxProtocol {
125125
/// Finds the innermost parent of the given type while not walking outside of nodes that satisfy `stoppingIf`.
126126
func findParentOfSelf<ParentType: SyntaxProtocol>(
127127
ofType: ParentType.Type,
128128
stoppingIf: (Syntax) -> Bool
129129
) -> ParentType? {
130-
var node: Syntax? = self
130+
var node: Syntax? = Syntax(self)
131131
while let unwrappedNode = node, !stoppingIf(unwrappedNode) {
132132
if let expectedType = unwrappedNode.as(ParentType.self) {
133133
return expectedType

Sources/SourceKitLSP/Swift/SwiftLanguageService.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,21 @@ extension SwiftLanguageService {
318318
await self.sourcekitd.removeNotificationHandler(self)
319319
}
320320

321+
public func canonicalDeclarationPosition(of position: Position, in uri: DocumentURI) async -> Position? {
322+
guard let snapshot = try? documentManager.latestSnapshot(uri) else {
323+
return nil
324+
}
325+
let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot)
326+
let decl = syntaxTree.token(at: snapshot.absolutePosition(of: position))?.findParentOfSelf(
327+
ofType: DeclSyntax.self,
328+
stoppingIf: { $0.is(CodeBlockSyntax.self) || $0.is(MemberBlockSyntax.self) }
329+
)
330+
guard let decl else {
331+
return nil
332+
}
333+
return snapshot.position(of: decl.positionAfterSkippingLeadingTrivia)
334+
}
335+
321336
/// Tell sourcekitd to crash itself. For testing purposes only.
322337
public func _crash() async {
323338
let req = sourcekitd.dictionary([

Tests/SourceKitLSPTests/DefinitionTests.swift

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
@_spi(Testing) import LSPLogging
1314
import LanguageServerProtocol
1415
import SKTestSupport
1516
import XCTest
@@ -541,4 +542,63 @@ class DefinitionTests: XCTestCase {
541542
])
542543
)
543544
}
545+
546+
func testJumpToDefinitionOnProtocolImplementationJumpsToRequirement() async throws {
547+
let project = try await IndexedSingleSwiftFileTestProject(
548+
"""
549+
protocol TestProtocol {
550+
func 1️⃣doThing()
551+
}
552+
553+
struct TestImpl: TestProtocol {
554+
func 2️⃣do3️⃣Thing() { }
555+
}
556+
"""
557+
)
558+
559+
let definitionFromBaseName = try await project.testClient.send(
560+
DefinitionRequest(textDocument: TextDocumentIdentifier(project.fileURI), position: project.positions["2️⃣"])
561+
)
562+
XCTAssertEqual(
563+
definitionFromBaseName,
564+
.locations([Location(uri: project.fileURI, range: Range(project.positions["1️⃣"]))])
565+
)
566+
567+
let definitionFromInsideBaseName = try await project.testClient.send(
568+
DefinitionRequest(textDocument: TextDocumentIdentifier(project.fileURI), position: project.positions["3️⃣"])
569+
)
570+
XCTAssertEqual(
571+
definitionFromInsideBaseName,
572+
.locations([Location(uri: project.fileURI, range: Range(project.positions["1️⃣"]))])
573+
)
574+
}
575+
576+
func testJumpToDefinitionOnProtocolImplementationShowsAllFulfilledRequirements() async throws {
577+
let project = try await IndexedSingleSwiftFileTestProject(
578+
"""
579+
protocol TestProtocol {
580+
func 1️⃣doThing()
581+
}
582+
583+
protocol OtherProtocol {
584+
func 2️⃣doThing()
585+
}
586+
587+
struct TestImpl: TestProtocol, OtherProtocol {
588+
func 3️⃣doThing() { }
589+
}
590+
"""
591+
)
592+
593+
let result = try await project.testClient.send(
594+
DefinitionRequest(textDocument: TextDocumentIdentifier(project.fileURI), position: project.positions["3️⃣"])
595+
)
596+
XCTAssertEqual(
597+
result,
598+
.locations([
599+
Location(uri: project.fileURI, range: Range(project.positions["1️⃣"])),
600+
Location(uri: project.fileURI, range: Range(project.positions["2️⃣"])),
601+
])
602+
)
603+
}
544604
}

0 commit comments

Comments
 (0)