Skip to content

Commit e6cf723

Browse files
authored
Merge pull request #975 from ahoppen/ahoppen/normalize-identifier-start
Support cursor info-based requests when cursor is placed at the end of an identifier
2 parents 9aee36b + f5e26e4 commit e6cf723

File tree

4 files changed

+125
-9
lines changed

4 files changed

+125
-9
lines changed

Sources/SourceKitLSP/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ target_sources(SourceKitLSP PRIVATE
2929
Swift/SourceKitD+ResponseError.swift
3030
Swift/SwiftCommand.swift
3131
Swift/SwiftLanguageServer.swift
32+
Swift/SymbolInfo.swift
3233
Swift/SyntaxHighlightingToken.swift
3334
Swift/SyntaxHighlightingTokenParser.swift
3435
Swift/SyntaxTreeManager.swift

Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -540,15 +540,6 @@ extension SwiftLanguageServer {
540540
return HoverResponse(contents: .markupContent(MarkupContent(kind: .markdown, value: result)), range: nil)
541541
}
542542

543-
public func symbolInfo(_ req: SymbolInfoRequest) async throws -> [SymbolDetails] {
544-
let uri = req.textDocument.uri
545-
let position = req.position
546-
guard let cursorInfo = try await cursorInfo(uri, position..<position) else {
547-
return []
548-
}
549-
return [cursorInfo.symbolInfo]
550-
}
551-
552543
public func documentColor(_ req: DocumentColorRequest) async throws -> [ColorInformation] {
553544
let snapshot = try self.documentManager.latestSnapshot(req.textDocument.uri)
554545

@@ -1223,6 +1214,13 @@ extension DocumentSnapshot {
12231214
return lowerBound..<upperBound
12241215
}
12251216

1217+
func position(of position: Position) -> AbsolutePosition? {
1218+
guard let offset = utf8Offset(of: position) else {
1219+
return nil
1220+
}
1221+
return AbsolutePosition(utf8Offset: offset)
1222+
}
1223+
12261224
func indexOf(utf8Offset: Int) -> String.Index? {
12271225
return text.utf8.index(text.startIndex, offsetBy: utf8Offset, limitedBy: text.endIndex)
12281226
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import LanguageServerProtocol
14+
import SwiftSyntax
15+
16+
fileprivate class StartOfIdentifierFinder: SyntaxAnyVisitor {
17+
let requestedPosition: AbsolutePosition
18+
var resolvedPosition: AbsolutePosition?
19+
20+
init(position: AbsolutePosition) {
21+
self.requestedPosition = position
22+
super.init(viewMode: .sourceAccurate)
23+
}
24+
25+
override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
26+
if (node.position...node.endPosition).contains(requestedPosition) {
27+
return .visitChildren
28+
} else {
29+
return .skipChildren
30+
}
31+
}
32+
33+
override func visit(_ token: TokenSyntax) -> SyntaxVisitorContinueKind {
34+
if token.tokenKind.isPunctuation || token.tokenKind == .endOfFile {
35+
return .skipChildren
36+
}
37+
if (token.positionAfterSkippingLeadingTrivia...token.endPositionBeforeTrailingTrivia).contains(requestedPosition) {
38+
self.resolvedPosition = token.positionAfterSkippingLeadingTrivia
39+
}
40+
return .skipChildren
41+
}
42+
}
43+
44+
extension SwiftLanguageServer {
45+
/// VS Code considers the position after an identifier as part of an identifier. Ie. if you have `let foo| = 1`, then
46+
/// it considers the cursor to be positioned at the identifier. This scenario is hit, when selecting an identifier by
47+
/// double-clicking it and then eg. performing jump-to-definition. In that case VS Code will send the position after
48+
/// the identifier.
49+
/// `sourcekitd`, on the other hand, does not consider the position after the identifier as part of the identifier.
50+
/// To bridge the gap here, normalize any positions inside, or directly after, an identifier to the identifier's
51+
/// start.
52+
private func adjustPositionToStartOfIdentifier(
53+
_ position: Position,
54+
in snapshot: DocumentSnapshot
55+
) async -> Position {
56+
let tree = await self.syntaxTreeManager.syntaxTree(for: snapshot)
57+
guard let swiftSyntaxPosition = snapshot.position(of: position) else {
58+
return position
59+
}
60+
let visitor = StartOfIdentifierFinder(position: swiftSyntaxPosition)
61+
visitor.walk(tree)
62+
if let resolvedPosition = visitor.resolvedPosition {
63+
return snapshot.position(of: resolvedPosition) ?? position
64+
} else {
65+
return position
66+
}
67+
}
68+
69+
public func symbolInfo(_ req: SymbolInfoRequest) async throws -> [SymbolDetails] {
70+
let uri = req.textDocument.uri
71+
let snapshot = try documentManager.latestSnapshot(uri)
72+
let position = await self.adjustPositionToStartOfIdentifier(req.position, in: snapshot)
73+
guard let cursorInfo = try await cursorInfo(uri, position..<position) else {
74+
return []
75+
}
76+
return [cursorInfo.symbolInfo]
77+
}
78+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import LanguageServerProtocol
14+
import SKTestSupport
15+
import XCTest
16+
17+
class DefinitionTests: XCTestCase {
18+
func testJumpToDefinitionAtEndOfIdentifier() async throws {
19+
let testClient = try await TestSourceKitLSPClient()
20+
let uri = DocumentURI.for(.swift)
21+
22+
let positions = testClient.openDocument(
23+
"""
24+
let 1️⃣foo = 1
25+
_ = foo2️⃣
26+
""",
27+
uri: uri
28+
)
29+
30+
let response = try await testClient.send(
31+
DefinitionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["2️⃣"])
32+
)
33+
guard case .locations(let locations) = response else {
34+
XCTFail("Expected locations response")
35+
return
36+
}
37+
XCTAssertEqual(locations, [Location(uri: uri, range: Range(positions["1️⃣"]))])
38+
}
39+
}

0 commit comments

Comments
 (0)