Skip to content

Commit 42c2b12

Browse files
committed
Support cursor info-based requests when cursor is placed at the end of an identifier
VS Code considers the position after an identifier as part of an identifier. Ie. if you have `let foo| = 1`, then it considers the cursor to be positioned at the identifier. This scenario is hit, when selecting an identifier by double-clicking it and then eg. performing jump-to-definition. In that case VS Code will send the position after the identifier. `sourcekitd`, on the other hand, does not consider the position after the identifier as part of the identifier. To bridge the gap here, normalize any positions inside, or directly after, an identifier to the identifier's start. Fixes #820 rdar://115557453
1 parent 8af0bb5 commit 42c2b12

File tree

5 files changed

+141
-16
lines changed

5 files changed

+141
-16
lines changed

Sources/SourceKitLSP/Swift/DocumentSymbols.swift

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -223,13 +223,6 @@ fileprivate extension FunctionDeclSyntax {
223223
}
224224
}
225225

226-
fileprivate extension SyntaxProtocol {
227-
/// The position range of this node without its leading and trailing trivia.
228-
var rangeWithoutTrivia: Range<AbsolutePosition> {
229-
return positionAfterSkippingLeadingTrivia..<endPositionBeforeTrailingTrivia
230-
}
231-
}
232-
233226
fileprivate extension TokenKind {
234227
var isOperator: Bool {
235228
switch self {

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

@@ -1222,6 +1213,13 @@ extension DocumentSnapshot {
12221213
return lowerBound..<upperBound
12231214
}
12241215

1216+
func position(of position: Position) -> AbsolutePosition? {
1217+
guard let offset = utf8Offset(of: position) else {
1218+
return nil
1219+
}
1220+
return AbsolutePosition(utf8Offset: offset)
1221+
}
1222+
12251223
func indexOf(utf8Offset: Int) -> String.Index? {
12261224
return text.utf8.index(text.startIndex, offsetBy: utf8Offset, limitedBy: text.endIndex)
12271225
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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.rangeWithoutTrivia.contains(requestedPosition) {
27+
return .skipChildren
28+
} else {
29+
return .visitChildren
30+
}
31+
}
32+
33+
override func visit(_ token: TokenSyntax) -> SyntaxVisitorContinueKind {
34+
if token.rangeWithoutTrivia.contains(requestedPosition) {
35+
self.resolvedPosition = token.positionAfterSkippingLeadingTrivia
36+
}
37+
return .skipChildren
38+
}
39+
}
40+
41+
extension SwiftLanguageServer {
42+
/// VS Code considers the position after an identifier as part of an identifier. Ie. if you have `let foo| = 1`, then
43+
/// it considers the cursor to be positioned at the identifier. This scenario is hit, when selecting an identifier by
44+
/// double-clicking it and then eg. performing jump-to-definition. In that case VS Code will send the position after
45+
/// the identifier.
46+
/// `sourcekitd`, on the other hand, does not consider the position after the identifier as part of the identifier.
47+
/// To bridge the gap here, normalize any positions inside, or directly after, an identifier to the identifier's
48+
/// start.
49+
private func adjustPositionToStartOfIdentifier(
50+
_ position: Position,
51+
in snapshot: DocumentSnapshot
52+
) async -> Position {
53+
let tree = await self.syntaxTreeManager.syntaxTree(for: snapshot)
54+
guard let swiftSyntaxPosition = snapshot.position(of: position) else {
55+
return position
56+
}
57+
let visitor = StartOfIdentifierFinder(position: swiftSyntaxPosition)
58+
visitor.walk(tree)
59+
if let resolvedPosition = visitor.resolvedPosition {
60+
return snapshot.position(of: resolvedPosition) ?? position
61+
} else {
62+
return position
63+
}
64+
}
65+
66+
public func symbolInfo(_ req: SymbolInfoRequest) async throws -> [SymbolDetails] {
67+
let uri = req.textDocument.uri
68+
let snapshot = try documentManager.latestSnapshot(uri)
69+
let position = await self.adjustPositionToStartOfIdentifier(req.position, in: snapshot)
70+
guard let cursorInfo = try await cursorInfo(uri, position..<position) else {
71+
return []
72+
}
73+
return [cursorInfo.symbolInfo]
74+
}
75+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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 SwiftSyntax
14+
15+
extension SyntaxProtocol {
16+
/// The position range of this node without its leading and trailing trivia.
17+
var rangeWithoutTrivia: Range<AbsolutePosition> {
18+
return positionAfterSkippingLeadingTrivia..<endPositionBeforeTrailingTrivia
19+
}
20+
}
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)