Skip to content

Commit 65cfba1

Browse files
committed
Refactor DocumentTokens to Use the New Parser
Re-core the "lexical" token stream on top of SwiftSyntax and call into the parser to (re-)generate these "lexical tokens" when the document changes. Leave the semantic tokens alone since they take some non-zero amount of name lookup to fully resolve references to fully replicate.
1 parent 93948c9 commit 65cfba1

File tree

5 files changed

+99
-56
lines changed

5 files changed

+99
-56
lines changed

Package.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ let package = Package(
4949
"SourceKitD",
5050
"SKSwiftPMWorkspace",
5151
.product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"),
52+
.product(name: "SwiftSyntax", package: "swift-syntax"),
53+
.product(name: "SwiftParser", package: "swift-syntax"),
5254
],
5355
exclude: ["CMakeLists.txt"]),
5456

Sources/SourceKitLSP/DocumentManager.swift

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -153,17 +153,14 @@ public final class DocumentManager {
153153
// Remove all tokens in the updated range and shift later ones.
154154
let rangeAdjuster = RangeAdjuster(edit: edit)!
155155

156-
document.latestTokens.withMutableTokensOfEachKind { tokens in
157-
tokens = Array(tokens.lazy
158-
.compactMap {
159-
var token = $0
160-
if let adjustedRange = rangeAdjuster.adjust(token.range) {
161-
token.range = adjustedRange
162-
return token
163-
} else {
164-
return nil
165-
}
166-
})
156+
document.latestTokens.semantic = document.latestTokens.semantic.compactMap {
157+
var token = $0
158+
if let adjustedRange = rangeAdjuster.adjust(token.range) {
159+
token.range = adjustedRange
160+
return token
161+
} else {
162+
return nil
163+
}
167164
}
168165
} else {
169166
// Full text replacement.

Sources/SourceKitLSP/DocumentTokens.swift

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,92 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import LanguageServerProtocol
14+
import SwiftSyntax
1415

1516
/// Syntax highlighting tokens for a particular document.
1617
public struct DocumentTokens {
17-
/// Lexical tokens, e.g. keywords, raw identifiers, ...
18-
public var lexical: [SyntaxHighlightingToken] = []
18+
/// The syntax tree representing the entire document.
19+
public var syntaxTree: SourceFileSyntax?
1920
/// Semantic tokens, e.g. variable references, type references, ...
2021
public var semantic: [SyntaxHighlightingToken] = []
22+
}
2123

22-
private var merged: [SyntaxHighlightingToken] {
23-
lexical.mergingTokens(with: semantic)
24-
}
25-
public var mergedAndSorted: [SyntaxHighlightingToken] {
26-
merged.sorted { $0.start < $1.start }
24+
extension DocumentSnapshot {
25+
private func highlightingToken(from syntax: SyntaxClassifiedRange) -> [SyntaxHighlightingToken] {
26+
guard let (kind, modifiers) = syntax.kind.parseKindAndModifiers else {
27+
return []
28+
}
29+
30+
guard
31+
let start: Position = self.positionOf(utf8Offset: syntax.offset),
32+
let end: Position = self.positionOf(utf8Offset: syntax.endOffset)
33+
else {
34+
return []
35+
}
36+
37+
let multiLineRange = start..<end
38+
let ranges = multiLineRange.splitToSingleLineRanges(in: self)
39+
40+
return ranges.map {
41+
SyntaxHighlightingToken(
42+
range: $0,
43+
kind: kind,
44+
modifiers: modifiers
45+
)
46+
}
2747
}
2848

29-
/// Modifies the syntax highlighting tokens of each kind
30-
/// (lexical and semantic) according to `action`.
31-
public mutating func withMutableTokensOfEachKind(_ action: (inout [SyntaxHighlightingToken]) -> Void) {
32-
action(&lexical)
33-
action(&semantic)
49+
/// Computes an array of syntax highlighting tokens from the syntax tree that
50+
/// have been merged with any semantic tokens from SourceKit. If the provided
51+
/// range is non-empty, this function restricts its output to only those
52+
/// tokens whose ranges overlap it. If no range is provided, tokens for the
53+
/// entire document are returned.
54+
///
55+
/// - Parameter range: The range of tokens to restrict this function to, if any.
56+
/// - Returns: An array of syntax highlighting tokens.
57+
public func mergedAndSortedTokens(in range: Range<Position>? = nil) -> [SyntaxHighlightingToken] {
58+
guard let tree = self.tokens.syntaxTree else {
59+
return self.tokens.semantic
60+
}
61+
let range = range.flatMap({ self.utf8OffsetRange(of: $0) }).map({ ByteSourceRange(offset: $0.startIndex, length: $0.count) })
62+
?? ByteSourceRange(offset: 0, length: tree.byteSize)
63+
return tree
64+
.classifications(in: range)
65+
.flatMap({ self.highlightingToken(from: $0) })
66+
.mergingTokens(with: self.tokens.semantic)
67+
.sorted { $0.start < $1.start }
3468
}
69+
}
3570

36-
// Replace all lexical tokens in `range`.
37-
public mutating func replaceLexical(in range: Range<Position>, with newTokens: [SyntaxHighlightingToken]) {
38-
lexical.removeAll { $0.range.overlaps(range) }
39-
lexical += newTokens
71+
extension SyntaxClassification {
72+
fileprivate var parseKindAndModifiers: (SyntaxHighlightingToken.Kind, SyntaxHighlightingToken.Modifiers)? {
73+
switch self {
74+
case .none:
75+
return nil
76+
case .editorPlaceholder:
77+
return nil
78+
case .stringInterpolationAnchor:
79+
return nil
80+
case .keyword:
81+
return (.keyword, [])
82+
case .identifier, .typeIdentifier, .dollarIdentifier:
83+
return (.identifier, [])
84+
case .operatorIdentifier:
85+
return (.operator, [])
86+
case .integerLiteral, .floatingLiteral:
87+
return (.number, [])
88+
case .stringLiteral:
89+
return (.string, [])
90+
case .poundDirectiveKeyword:
91+
return (.macro, [])
92+
case .buildConfigId, .objectLiteral:
93+
return (.macro, [])
94+
case .attribute:
95+
return (.modifier, [])
96+
case .lineComment, .blockComment:
97+
return (.comment, [])
98+
case .docLineComment, .docBlockComment:
99+
return (.comment, .documentation)
100+
}
40101
}
41102
}

Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift

Lines changed: 11 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import LSPLogging
1717
import SKCore
1818
import SKSupport
1919
import SourceKitD
20+
import SwiftSyntax
21+
import SwiftParser
2022

2123
#if os(Windows)
2224
import WinSDK
@@ -168,13 +170,12 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {
168170
/// Updates the lexical tokens for the given `snapshot`.
169171
/// Must be called on `self.queue`.
170172
private func updateLexicalTokens(
171-
response: SKDResponseDictionary,
172173
for snapshot: DocumentSnapshot
173174
) {
174175
dispatchPrecondition(condition: .onQueue(queue))
175176

176177
let uri = snapshot.document.uri
177-
let docTokens = updatedLexicalTokens(response: response, for: snapshot)
178+
let docTokens = updateSyntaxTree(for: snapshot)
178179

179180
do {
180181
try documentManager.updateTokens(uri, tokens: docTokens)
@@ -184,31 +185,13 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {
184185
}
185186

186187
/// Returns the updated lexical tokens for the given `snapshot`.
187-
private func updatedLexicalTokens(
188-
response: SKDResponseDictionary,
188+
private func updateSyntaxTree(
189189
for snapshot: DocumentSnapshot
190190
) -> DocumentTokens {
191191
logExecutionTime(level: .debug) {
192192
var docTokens = snapshot.tokens
193193

194-
guard let offset: Int = response[keys.offset],
195-
let length: Int = response[keys.length],
196-
let start: Position = snapshot.positionOf(utf8Offset: offset),
197-
let end: Position = snapshot.positionOf(utf8Offset: offset + length) else {
198-
// This e.g. happens in the case of empty edits
199-
log("did not update lexical/syntactic tokens, no range found", level: .debug)
200-
return docTokens
201-
}
202-
203-
let range = start..<end
204-
205-
if let syntaxMap: SKDResponseArray = response[keys.syntaxmap] {
206-
let tokenParser = SyntaxHighlightingTokenParser(sourcekitd: sourcekitd)
207-
var tokens: [SyntaxHighlightingToken] = []
208-
tokenParser.parseTokens(syntaxMap, in: snapshot, into: &tokens)
209-
210-
docTokens.replaceLexical(in: range, with: tokens)
211-
}
194+
docTokens.syntaxTree = Parser.parse(source: snapshot.text)
212195

213196
return docTokens
214197
}
@@ -438,7 +421,7 @@ extension SwiftLanguageServer {
438421
}
439422
self.publishDiagnostics(
440423
response: dict, for: snapshot, compileCommand: compileCmd)
441-
self.updateLexicalTokens(response: dict, for: snapshot)
424+
self.updateLexicalTokens(for: snapshot)
442425
}
443426

444427
public func documentUpdatedBuildSettings(_ uri: DocumentURI, change: FileBuildSettingsChange) {
@@ -503,7 +486,7 @@ extension SwiftLanguageServer {
503486
return
504487
}
505488
self.publishDiagnostics(response: dict, for: snapshot, compileCommand: compileCommand)
506-
self.updateLexicalTokens(response: dict, for: snapshot)
489+
self.updateLexicalTokens(for: snapshot)
507490
}
508491
}
509492

@@ -557,8 +540,8 @@ extension SwiftLanguageServer {
557540

558541
self.adjustDiagnosticRanges(of: note.textDocument.uri, for: edit)
559542
} updateDocumentTokens: { (after: DocumentSnapshot) in
560-
if let dict = lastResponse {
561-
return self.updatedLexicalTokens(response: dict, for: after)
543+
if lastResponse != nil {
544+
return self.updateSyntaxTree(for: after)
562545
} else {
563546
return DocumentTokens()
564547
}
@@ -876,7 +859,7 @@ extension SwiftLanguageServer {
876859
return
877860
}
878861

879-
let tokens = snapshot.tokens.mergedAndSorted
862+
let tokens = snapshot.mergedAndSortedTokens()
880863
let encodedTokens = tokens.lspEncoded
881864

882865
req.reply(DocumentSemanticTokensResponse(data: encodedTokens))
@@ -899,7 +882,7 @@ extension SwiftLanguageServer {
899882
return
900883
}
901884

902-
let tokens = snapshot.tokens.mergedAndSorted.filter { $0.range.overlaps(range) }
885+
let tokens = snapshot.mergedAndSortedTokens(in: range)
903886
let encodedTokens = tokens.lspEncoded
904887

905888
req.reply(DocumentSemanticTokensResponse(data: encodedTokens))

Sources/SourceKitLSP/Swift/SyntaxHighlightingTokenParser.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ struct SyntaxHighlightingTokenParser {
187187

188188
extension Range where Bound == Position {
189189
/// Splits a potentially multi-line range to multiple single-line ranges.
190-
fileprivate func splitToSingleLineRanges(in snapshot: DocumentSnapshot) -> [Self] {
190+
func splitToSingleLineRanges(in snapshot: DocumentSnapshot) -> [Self] {
191191
if isEmpty {
192192
return []
193193
}

0 commit comments

Comments
 (0)