Skip to content

Commit f5e26e4

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 f5e26e4

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

@@ -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: 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)