Skip to content

Commit d164203

Browse files
committed
Keep track of semantic tokens in a SemanticTokensManager
Storing the semantic tokens inside `Document` was an anti-pattern because the semantic tokens only applied to Swift and were also being updated while the document contents themselves stayed constant. Instead, we should store the semantic tokens in a separate `SemanticTokensManager` that only exists in the `SwiftLanguageServer` and has the sole responsibility of tracking semantic tokens.
1 parent 30df945 commit d164203

File tree

6 files changed

+117
-92
lines changed

6 files changed

+117
-92
lines changed

Sources/SourceKitLSP/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
add_library(SourceKitLSP STATIC
33
CapabilityRegistry.swift
44
DocumentManager.swift
5-
DocumentTokens.swift
65
IndexStoreDB+MainFilesProvider.swift
76
RangeAdjuster.swift
87
Sequence+AsyncMap.swift
@@ -26,6 +25,7 @@ target_sources(SourceKitLSP PRIVATE
2625
Swift/SemanticRefactorCommand.swift
2726
Swift/SemanticRefactoring.swift
2827
Swift/SemanticTokens.swift
28+
Swift/SemanticTokensManager.swift
2929
Swift/SourceKitD+ResponseError.swift
3030
Swift/SwiftCommand.swift
3131
Swift/SwiftLanguageServer.swift

Sources/SourceKitLSP/DocumentManager.swift

Lines changed: 2 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,6 @@ public struct DocumentSnapshot: Identifiable {
4141
public let id: ID
4242
public let language: Language
4343
public let lineTable: LineTable
44-
/// Syntax highlighting tokens for the document. Note that
45-
/// `uri` + `latestVersion` only uniquely identifies a snapshot's content,
46-
/// the tokens are updated independently and only used internally.
47-
public let tokens: DocumentTokens
4844

4945
public var uri: DocumentURI { id.uri }
5046
public var version: Int { id.version }
@@ -54,13 +50,11 @@ public struct DocumentSnapshot: Identifiable {
5450
uri: DocumentURI,
5551
language: Language,
5652
version: Int,
57-
lineTable: LineTable,
58-
tokens: DocumentTokens
53+
lineTable: LineTable
5954
) {
6055
self.id = ID(uri: uri, version: version)
6156
self.language = language
6257
self.lineTable = lineTable
63-
self.tokens = tokens
6458
}
6559

6660
func index(of pos: Position) -> String.Index? {
@@ -73,14 +67,12 @@ public final class Document {
7367
public let language: Language
7468
var latestVersion: Int
7569
var latestLineTable: LineTable
76-
var latestTokens: DocumentTokens
7770

7871
init(uri: DocumentURI, language: Language, version: Int, text: String) {
7972
self.uri = uri
8073
self.language = language
8174
self.latestVersion = version
8275
self.latestLineTable = LineTable(text)
83-
self.latestTokens = DocumentTokens()
8476
}
8577

8678
/// **Not thread safe!** Use `DocumentManager.latestSnapshot` instead.
@@ -89,8 +81,7 @@ public final class Document {
8981
uri: self.uri,
9082
language: self.language,
9183
version: latestVersion,
92-
lineTable: latestLineTable,
93-
tokens: latestTokens
84+
lineTable: latestLineTable
9485
)
9586
}
9687
}
@@ -178,23 +169,9 @@ public final class DocumentManager {
178169
toLine: range.upperBound.line,
179170
utf16Offset: range.upperBound.utf16index,
180171
with: edit.text)
181-
182-
// Remove all tokens in the updated range and shift later ones.
183-
let rangeAdjuster = RangeAdjuster(edit: edit)!
184-
185-
document.latestTokens.semantic = document.latestTokens.semantic.compactMap {
186-
var token = $0
187-
if let adjustedRange = rangeAdjuster.adjust(token.range) {
188-
token.range = adjustedRange
189-
return token
190-
} else {
191-
return nil
192-
}
193-
}
194172
} else {
195173
// Full text replacement.
196174
document.latestLineTable = LineTable(edit.text)
197-
document.latestTokens = DocumentTokens()
198175
}
199176
}
200177

@@ -206,23 +183,6 @@ public final class DocumentManager {
206183
}
207184
}
208185

209-
/// Updates the tokens in a document.
210-
///
211-
/// - parameter uri: The URI of the document to be updated
212-
/// - parameter tokens: The new tokens for the document
213-
@discardableResult
214-
public func updateTokens(_ uri: DocumentURI, tokens: DocumentTokens) throws -> DocumentSnapshot {
215-
return try queue.sync {
216-
guard let document = documents[uri] else {
217-
throw Error.missingDocument(uri)
218-
}
219-
220-
document.latestTokens = tokens
221-
222-
return document.latestSnapshot
223-
}
224-
}
225-
226186
public func latestSnapshot(_ uri: DocumentURI) -> DocumentSnapshot? {
227187
return queue.sync {
228188
guard let document = documents[uri] else {

Sources/SourceKitLSP/DocumentTokens.swift

Lines changed: 0 additions & 22 deletions
This file was deleted.

Sources/SourceKitLSP/Swift/SemanticTokens.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,13 @@ extension SwiftLanguageServer {
3030
in range: Range<Position>? = nil
3131
) async -> [SyntaxHighlightingToken] {
3232
let tree = await syntaxTreeManager.syntaxTree(for: snapshot)
33+
let semanticTokens = await semanticTokensManager.semanticTokens(for: snapshot.id)
3334
let range = range.flatMap({ $0.byteSourceRange(in: snapshot) })
3435
?? ByteSourceRange(offset: 0, length: tree.totalLength.utf8Length)
3536
return tree
3637
.classifications(in: range)
3738
.flatMap({ $0.highlightingTokens(in: snapshot) })
38-
.mergingTokens(with: snapshot.tokens.semantic)
39+
.mergingTokens(with: semanticTokens ?? [])
3940
.sorted { $0.start < $1.start }
4041
}
4142

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2021 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+
15+
/// Keeps track of the semantic tokens that sourcekitd has sent us for given
16+
/// document snapshots.
17+
actor SemanticTokensManager {
18+
private var semanticTokens: [DocumentSnapshot.ID: [SyntaxHighlightingToken]] = [:]
19+
20+
/// The semantic tokens for the given snapshot or `nil` if no semantic tokens
21+
/// have been computed yet.
22+
func semanticTokens(for snapshotID: DocumentSnapshot.ID) -> [SyntaxHighlightingToken]? {
23+
return semanticTokens[snapshotID]
24+
}
25+
26+
/// Set the semantic tokens that sourcekitd has sent us for the given document
27+
/// snapshot.
28+
///
29+
/// This discards any semantic tokens for any older versions of this document.
30+
func setSemanticTokens(for snapshotID: DocumentSnapshot.ID, semanticTokens tokens: [SyntaxHighlightingToken]) {
31+
semanticTokens[snapshotID] = tokens
32+
// Delete semantic tokens for older versions of this document.
33+
for key in semanticTokens.keys {
34+
if key < snapshotID {
35+
semanticTokens[key] = nil
36+
}
37+
}
38+
}
39+
40+
/// If we have semantic tokens for `preEditSnapshotID`, shift the tokens
41+
/// according to `edits` and store these shifted results for `postEditSnapshot`.
42+
///
43+
/// This allows us to maintain semantic tokens after an edit for all the
44+
/// non-edited regions.
45+
///
46+
/// - Note: The semantic tokens stored from this edit might not be correct if
47+
/// the edits affect semantic highlighting for tokens out of the edit region.
48+
/// These will be updated when sourcekitd sends us new semantic tokens,
49+
/// which are stored in `SemanticTokensManager` by calling `setSemanticTokens`.
50+
func registerEdit(
51+
preEditSnapshot preEditSnapshotID: DocumentSnapshot.ID,
52+
postEditSnapshot postEditSnapshotID: DocumentSnapshot.ID,
53+
edits: [TextDocumentContentChangeEvent]
54+
) {
55+
guard var semanticTokens = semanticTokens(for: preEditSnapshotID) else {
56+
return
57+
}
58+
for edit in edits {
59+
// Remove all tokens in the updated range and shift later ones.
60+
guard let rangeAdjuster = RangeAdjuster(edit: edit) else {
61+
// We have a full document edit and can't update semantic tokens
62+
return
63+
}
64+
65+
semanticTokens = semanticTokens.compactMap {
66+
var token = $0
67+
if let adjustedRange = rangeAdjuster.adjust(token.range) {
68+
token.range = adjustedRange
69+
return token
70+
} else {
71+
return nil
72+
}
73+
}
74+
}
75+
setSemanticTokens(for: postEditSnapshotID, semanticTokens: semanticTokens)
76+
}
77+
78+
/// Discard any semantic tokens for documents with the given URI.
79+
///
80+
/// This should be called when a document is being closed and the semantic
81+
/// tokens are thus no longer needed.
82+
func discaredSemanticTokens(for document: DocumentURI) {
83+
// Delete semantic tokens for older versions of this document.
84+
for key in semanticTokens.keys {
85+
if key.uri == document {
86+
semanticTokens[key] = nil
87+
}
88+
}
89+
}
90+
}

Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ public actor SwiftLanguageServer: ToolchainLanguageServer {
126126
var currentCompletionSession: CodeCompletionSession? = nil
127127

128128
let syntaxTreeManager = SyntaxTreeManager()
129+
let semanticTokensManager = SemanticTokensManager()
129130

130131
nonisolated var keys: sourcekitd_keys { return sourcekitd.keys }
131132
nonisolated var requests: sourcekitd_requests { return sourcekitd.requests }
@@ -198,37 +199,21 @@ public actor SwiftLanguageServer: ToolchainLanguageServer {
198199
self.stateChangeHandlers.append(handler)
199200
}
200201

201-
/// Updates the semantic tokens for the given `snapshot`.
202-
private func updateSemanticTokens(
203-
response: SKDResponseDictionary,
204-
for snapshot: DocumentSnapshot
205-
) {
206-
let docTokens = updatedSemanticTokens(response: response, for: snapshot)
207-
208-
do {
209-
try documentManager.updateTokens(snapshot.uri, tokens: docTokens)
210-
} catch {
211-
log("Updating semantic tokens failed: \(error)", level: .warning)
212-
}
213-
}
214-
215-
/// Returns the updated semantic tokens for the given `snapshot`.
216-
private func updatedSemanticTokens(
217-
response: SKDResponseDictionary,
202+
/// Returns the semantic tokens in `response` for the given `snapshot`.
203+
private func semanticTokens(
204+
of response: SKDResponseDictionary,
218205
for snapshot: DocumentSnapshot
219-
) -> DocumentTokens {
206+
) -> [SyntaxHighlightingToken]? {
220207
logExecutionTime(level: .debug) {
221-
var docTokens = snapshot.tokens
222-
223208
if let skTokens: SKDResponseArray = response[keys.annotations] {
224209
let tokenParser = SyntaxHighlightingTokenParser(sourcekitd: sourcekitd)
225210
var tokens: [SyntaxHighlightingToken] = []
226211
tokenParser.parseTokens(skTokens, in: snapshot, into: &tokens)
227212

228-
docTokens.semantic = tokens
213+
return tokens
229214
}
230215

231-
return docTokens
216+
return nil
232217
}
233218
}
234219

@@ -344,9 +329,9 @@ public actor SwiftLanguageServer: ToolchainLanguageServer {
344329

345330
if let dict = try? self.sourcekitd.sendSync(req) {
346331
let isSemaStage = dict[keys.diagnostic_stage] as sourcekitd_uid_t? == sourcekitd.values.diag_stage_sema
347-
if isSemaStage {
332+
if isSemaStage, let semanticTokens = semanticTokens(of: dict, for: snapshot) {
348333
// Only update semantic tokens if the 0,0 replacetext request returned semantic information.
349-
updateSemanticTokens(response: dict, for: snapshot)
334+
await semanticTokensManager.setSemanticTokens(for: snapshot.id, semanticTokens: semanticTokens)
350335
}
351336
if enablePublishDiagnostics {
352337
await publishDiagnostics(response: dict, for: snapshot, compileCommand: compileCommand)
@@ -494,7 +479,7 @@ extension SwiftLanguageServer {
494479
await self.publishDiagnostics(response: dict, for: snapshot, compileCommand: compileCommand)
495480
}
496481

497-
public func closeDocument(_ note: DidCloseTextDocumentNotification) {
482+
public func closeDocument(_ note: DidCloseTextDocumentNotification) async {
498483
let keys = self.keys
499484

500485
self.documentManager.close(note)
@@ -509,6 +494,8 @@ extension SwiftLanguageServer {
509494
self.currentDiagnostics[uri] = nil
510495

511496
_ = try? self.sourcekitd.sendSync(req)
497+
498+
await semanticTokensManager.discaredSemanticTokens(for: note.textDocument.uri)
512499
}
513500

514501
public func changeDocument(_ note: DidChangeTextDocumentNotification) async {
@@ -552,7 +539,16 @@ extension SwiftLanguageServer {
552539
guard let (preEditSnapshot, postEditSnapshot) = editResult else {
553540
return
554541
}
555-
await syntaxTreeManager.registerEdit(preEditSnapshot: preEditSnapshot, postEditSnapshot: postEditSnapshot, edits: ConcurrentEdits(fromSequential: edits))
542+
await syntaxTreeManager.registerEdit(
543+
preEditSnapshot: preEditSnapshot,
544+
postEditSnapshot: postEditSnapshot,
545+
edits: ConcurrentEdits(fromSequential: edits)
546+
)
547+
await semanticTokensManager.registerEdit(
548+
preEditSnapshot: preEditSnapshot.id,
549+
postEditSnapshot: postEditSnapshot.id,
550+
edits: note.contentChanges
551+
)
556552

557553
if let dict = lastResponse {
558554
let compileCommand = await self.buildSettings(for: note.textDocument.uri)

0 commit comments

Comments
 (0)