Skip to content

Commit 015e486

Browse files
authored
Merge pull request #1073 from ahoppen/ahoppen/adjust-completion-pos-using-swiftsyntax
Find completion start position using swift-syntax instead of ASCII-based implementation
2 parents 7514ad9 + 9dd5f1d commit 015e486

File tree

5 files changed

+102
-88
lines changed

5 files changed

+102
-88
lines changed

Sources/SourceKitLSP/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ add_library(SourceKitLSP STATIC
1717
target_sources(SourceKitLSP PRIVATE
1818
Clang/ClangLanguageServer.swift)
1919
target_sources(SourceKitLSP PRIVATE
20+
Swift/AdjustPositionToStartOfIdentifier.swift
2021
Swift/CodeCompletion.swift
2122
Swift/CodeCompletionSession.swift
2223
Swift/CommentXML.swift
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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+
}
29+
return .skipChildren
30+
}
31+
32+
override func visit(_ token: TokenSyntax) -> SyntaxVisitorContinueKind {
33+
if token.tokenKind.isPunctuation || token.tokenKind == .endOfFile {
34+
return .skipChildren
35+
}
36+
if (token.positionAfterSkippingLeadingTrivia...token.endPositionBeforeTrailingTrivia).contains(requestedPosition) {
37+
self.resolvedPosition = token.positionAfterSkippingLeadingTrivia
38+
}
39+
return .skipChildren
40+
}
41+
}
42+
43+
extension SwiftLanguageServer {
44+
/// VS Code considers the position after an identifier as part of an identifier. Ie. if you have `let foo| = 1`, then
45+
/// it considers the cursor to be positioned at the identifier. This scenario is hit, when selecting an identifier by
46+
/// double-clicking it and then eg. performing jump-to-definition. In that case VS Code will send the position after
47+
/// the identifier.
48+
/// `sourcekitd`, on the other hand, does not consider the position after the identifier as part of the identifier.
49+
/// To bridge the gap here, normalize any positions inside, or directly after, an identifier to the identifier's
50+
/// start.
51+
func adjustPositionToStartOfIdentifier(
52+
_ position: Position,
53+
in snapshot: DocumentSnapshot
54+
) async -> Position {
55+
let tree = await self.syntaxTreeManager.syntaxTree(for: snapshot)
56+
guard let swiftSyntaxPosition = snapshot.position(of: position) else {
57+
return position
58+
}
59+
let visitor = StartOfIdentifierFinder(position: swiftSyntaxPosition)
60+
visitor.walk(tree)
61+
if let resolvedPosition = visitor.resolvedPosition {
62+
return snapshot.position(of: resolvedPosition) ?? position
63+
}
64+
return position
65+
}
66+
67+
}

Sources/SourceKitLSP/Swift/CodeCompletion.swift

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,7 @@ extension SwiftLanguageServer {
2020
public func completion(_ req: CompletionRequest) async throws -> CompletionList {
2121
let snapshot = try documentManager.latestSnapshot(req.textDocument.uri)
2222

23-
guard let completionPos = adjustCompletionLocation(req.position, in: snapshot) else {
24-
logger.error("invalid completion position \(req.position, privacy: .public)")
25-
return CompletionList(isIncomplete: true, items: [])
26-
}
23+
let completionPos = await adjustPositionToStartOfIdentifier(req.position, in: snapshot)
2724

2825
guard let offset = snapshot.utf8Offset(of: completionPos) else {
2926
logger.error(
@@ -58,35 +55,4 @@ extension SwiftLanguageServer {
5855
filterText: filterText
5956
)
6057
}
61-
62-
/// Adjust completion position to the start of identifier characters.
63-
private func adjustCompletionLocation(_ pos: Position, in snapshot: DocumentSnapshot) -> Position? {
64-
guard pos.line < snapshot.lineTable.count else {
65-
// Line out of range.
66-
return nil
67-
}
68-
let lineSlice = snapshot.lineTable[pos.line]
69-
let startIndex = lineSlice.startIndex
70-
71-
let identifierChars = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "_"))
72-
73-
guard var loc = lineSlice.utf16.index(startIndex, offsetBy: pos.utf16index, limitedBy: lineSlice.endIndex) else {
74-
// Column out of range.
75-
return nil
76-
}
77-
while loc != startIndex {
78-
let prev = lineSlice.index(before: loc)
79-
if !identifierChars.contains(lineSlice.unicodeScalars[prev]) {
80-
break
81-
}
82-
loc = prev
83-
}
84-
85-
// ###aabccccccdddddd
86-
// ^ ^- loc ^-requestedLoc
87-
// `- startIndex
88-
89-
let adjustedOffset = lineSlice.utf16.distance(from: startIndex, to: loc)
90-
return Position(line: pos.line, utf16index: adjustedOffset)
91-
}
9258
}

Sources/SourceKitLSP/Swift/SymbolInfo.swift

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -11,61 +11,8 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
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-
}
4314

4415
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-
6916
public func symbolInfo(_ req: SymbolInfoRequest) async throws -> [SymbolDetails] {
7017
let uri = req.textDocument.uri
7118
let snapshot = try documentManager.latestSnapshot(uri)

Tests/SourceKitLSPTests/SwiftCompletionTests.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -945,6 +945,39 @@ final class SwiftCompletionTests: XCTestCase {
945945
XCTAssert(secondCompletionResults.items.isEmpty)
946946
}
947947

948+
func testNonAsciiCompletionFilter() async throws {
949+
let testClient = try await TestSourceKitLSPClient()
950+
let uri = DocumentURI.for(.swift)
951+
let positions = testClient.openDocument(
952+
"""
953+
struct Foo {
954+
func 👨‍👩‍👦👨‍👩‍👦() {}
955+
func test() {
956+
self.1️⃣👨‍👩‍👦2️⃣
957+
}
958+
}
959+
""",
960+
uri: uri
961+
)
962+
let completions = try await testClient.send(
963+
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["2️⃣"])
964+
)
965+
XCTAssertEqual(
966+
completions.items,
967+
[
968+
CompletionItem(
969+
label: "👨‍👩‍👦👨‍👩‍👦()",
970+
kind: .method,
971+
detail: "Void",
972+
deprecated: false,
973+
filterText: "👨‍👩‍👦👨‍👩‍👦()",
974+
insertText: "👨‍👩‍👦👨‍👩‍👦()",
975+
insertTextFormat: .plain,
976+
textEdit: .textEdit(TextEdit(range: positions["1️⃣"]..<positions["2️⃣"], newText: "👨‍👩‍👦👨‍👩‍👦()"))
977+
)
978+
]
979+
)
980+
}
948981
}
949982

950983
private func countFs(_ response: CompletionList) -> Int {

0 commit comments

Comments
 (0)