Skip to content

Commit d99b56c

Browse files
committed
Refactor logic for updating lexical/syntactic tokens
- Fix typo - Refactor update tokens logic Remove the replaceXTokens methods from DocumentManager and implement updates as changes on DocumentTokens directly. - Add after-edit callback and use it to update tokens
1 parent 8f1001a commit d99b56c

File tree

3 files changed

+108
-123
lines changed

3 files changed

+108
-123
lines changed

Sources/SourceKitLSP/CapabilityRegistry.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ public final class CapabilityRegistry {
142142
}
143143
}
144144

145-
/// Checks whether a registration for semantic tokens for the given languages exists.
145+
/// Checks whether a registration for semantic tokens for the given language exists.
146146
public func hasSemanticTokensRegistration(for language: Language) -> Bool {
147147
registration(for: [language], in: semanticTokens) != nil
148148
}

Sources/SourceKitLSP/DocumentManager.swift

Lines changed: 37 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ public struct DocumentTokens {
3737
action(&syntactic)
3838
action(&semantic)
3939
}
40+
41+
// Remove all lexical tokens that overlap with `range`.
42+
public mutating func replaceLexicalTokens(in range: Range<Position>, with newTokens: [SyntaxHighlightingToken]) {
43+
lexical.removeAll { $0.range.overlaps(range) }
44+
lexical += newTokens
45+
}
4046
}
4147

4248
public struct DocumentSnapshot {
@@ -140,19 +146,27 @@ public final class DocumentManager {
140146

141147
/// Applies the given edits to the document.
142148
///
143-
/// - parameter editCallback: Optional closure to call for each edit.
149+
/// - parameter beforeCallback: Optional closure to call before each edit.
150+
/// - parameter afterCallback: Optional closure to call after each edit.
144151
/// - parameter before: The document contents *before* the edit is applied.
152+
/// - parameter after: The document contents *after* the edit is applied.
145153
/// - returns: The contents of the file after all the edits are applied.
146154
/// - throws: Error.missingDocument if the document is not open.
147155
@discardableResult
148-
public func edit(_ uri: DocumentURI, newVersion: Int, edits: [TextDocumentContentChangeEvent], editCallback: ((_ before: DocumentSnapshot, TextDocumentContentChangeEvent) -> Void)? = nil) throws -> DocumentSnapshot {
156+
public func edit(
157+
_ uri: DocumentURI,
158+
newVersion: Int,
159+
edits: [TextDocumentContentChangeEvent],
160+
beforeCallback: ((_ before: DocumentSnapshot, TextDocumentContentChangeEvent) -> Void)? = nil,
161+
afterCallback: ((_ after: DocumentSnapshot) -> DocumentTokens?)? = nil
162+
) throws -> DocumentSnapshot {
149163
return try queue.sync {
150164
guard let document = documents[uri] else {
151165
throw Error.missingDocument(uri)
152166
}
153167

154168
for edit in edits {
155-
if let f = editCallback {
169+
if let f = beforeCallback {
156170
f(document.latestSnapshot, edit)
157171
}
158172

@@ -199,75 +213,28 @@ public final class DocumentManager {
199213
document.latestTokens = DocumentTokens()
200214
}
201215

216+
if let f = afterCallback, let tokens = f(document.latestSnapshot) {
217+
document.latestTokens = tokens
218+
}
202219
}
203220

204221
document.latestVersion = newVersion
205222
return document.latestSnapshot
206223
}
207224
}
208225

209-
/// Replaces the semantic tokens for a document.
226+
/// Updates the tokens in a document.
210227
///
211228
/// - parameter uri: The URI of the document to be updated
212-
/// - parameter tokens: The tokens to be used
229+
/// - parameter tokens: The new tokens for the document
213230
@discardableResult
214-
public func replaceSemanticTokens(
215-
_ uri: DocumentURI,
216-
tokens: [SyntaxHighlightingToken]
217-
) throws -> DocumentSnapshot {
231+
public func updateTokens(_ uri: DocumentURI, tokens: DocumentTokens) throws -> DocumentSnapshot {
218232
return try queue.sync {
219233
guard let document = documents[uri] else {
220234
throw Error.missingDocument(uri)
221235
}
222236

223-
document.latestTokens.semantic = tokens
224-
return document.latestSnapshot
225-
}
226-
}
227-
228-
/// Replaces the syntactic tokens for a document.
229-
///
230-
/// - parameter uri: The URI of the document to be updated
231-
/// - parameter tokens: The tokens to be used
232-
@discardableResult
233-
public func replaceSyntacticTokens(
234-
_ uri: DocumentURI,
235-
tokens: [SyntaxHighlightingToken]
236-
) throws -> DocumentSnapshot {
237-
return try queue.sync {
238-
guard let document = documents[uri] else {
239-
throw Error.missingDocument(uri)
240-
}
241-
242-
document.latestTokens.syntactic = tokens
243-
return document.latestSnapshot
244-
}
245-
}
246-
247-
/// Replaces the given lexical tokens in a document
248-
/// within the given range.
249-
///
250-
/// - parameter uri: The URI of the document to be updated
251-
/// - parameter range: The range to replace tokens in (nil means the entire document)
252-
/// - parameter newTokens: The tokens to be added
253-
@discardableResult
254-
public func replaceLexicalTokens(
255-
_ uri: DocumentURI,
256-
in range: Range<Position>? = nil,
257-
with newTokens: [SyntaxHighlightingToken]
258-
) throws -> DocumentSnapshot {
259-
return try queue.sync {
260-
guard let document = documents[uri] else {
261-
throw Error.missingDocument(uri)
262-
}
263-
264-
// Remove all tokens that overlap with `range`
265-
// (or the entire document if `range` is `nil`)
266-
document.latestTokens.lexical.removeAll { token in
267-
range.map { token.range.overlaps($0) } ?? true
268-
}
269-
270-
document.latestTokens.lexical += newTokens
237+
document.latestTokens = tokens
271238

272239
return document.latestSnapshot
273240
}
@@ -303,11 +270,21 @@ extension DocumentManager {
303270
}
304271
}
305272

306-
/// Convenience wrapper for `edit(_:newVersion:edits:editCallback:)` that logs on failure.
273+
/// Convenience wrapper for `edit(_:newVersion:edits:beforeCallback:)` that logs on failure.
307274
@discardableResult
308-
func edit(_ note: DidChangeTextDocumentNotification, editCallback: ((_ before: DocumentSnapshot, TextDocumentContentChangeEvent) -> Void)? = nil) -> DocumentSnapshot? {
275+
func edit(
276+
_ note: DidChangeTextDocumentNotification,
277+
beforeCallback: ((_ before: DocumentSnapshot, TextDocumentContentChangeEvent) -> Void)? = nil,
278+
afterCallback: ((_ after: DocumentSnapshot) -> DocumentTokens?)? = nil
279+
) -> DocumentSnapshot? {
309280
return orLog("failed to edit document", level: .error) {
310-
try edit(note.textDocument.uri, newVersion: note.textDocument.version ?? -1, edits: note.contentChanges, editCallback: editCallback)
281+
try edit(
282+
note.textDocument.uri,
283+
newVersion: note.textDocument.version ?? -1,
284+
edits: note.contentChanges,
285+
beforeCallback: beforeCallback,
286+
afterCallback: afterCallback
287+
)
311288
}
312289
}
313290
}

Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift

Lines changed: 70 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -161,77 +161,100 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {
161161
}
162162
}
163163

164-
/// Update lexical and syntactic tokens for the given `snapshot`.
165-
/// Should only be called on `queue`.
164+
/// Updates the lexical and syntactic tokens for the given `snapshot`.
165+
/// Must be called on `self.queue`.
166166
private func updateLexicalAndSyntacticTokens(
167167
response: SKDResponseDictionary,
168168
for snapshot: DocumentSnapshot
169169
) {
170170
dispatchPrecondition(condition: .onQueue(queue))
171171

172+
let uri = snapshot.document.uri
173+
let docTokens = updatedLexicalAndSyntacticTokens(response: response, for: snapshot)
174+
175+
do {
176+
try documentManager.updateTokens(uri, tokens: docTokens)
177+
} catch {
178+
log("Updating lexical and syntactic tokens failed: \(error)", level: .warning)
179+
}
180+
}
181+
182+
/// Returns the updated lexical and syntactic tokens for the given `snapshot`.
183+
private func updatedLexicalAndSyntacticTokens(
184+
response: SKDResponseDictionary,
185+
for snapshot: DocumentSnapshot
186+
) -> DocumentTokens {
187+
var docTokens = snapshot.tokens
188+
172189
guard let offset: Int = response[keys.offset],
173190
let length: Int = response[keys.length],
174191
let start: Position = snapshot.positionOf(utf8Offset: offset),
175192
let end: Position = snapshot.positionOf(utf8Offset: offset + length) else {
176193
log("updateLexicalAndSyntacticTokens failed, no range found", level: .error)
177-
return
194+
return docTokens
178195
}
179196

180-
let uri = snapshot.document.uri
181197
let range = start..<end
182198

183199
// If the range is empty we don't have to (and shouldn't) update anything.
184200
// This is important, since the substructure may be empty, causing us to
185201
// unnecessarily remove all syntactic tokens.
186202
guard !range.isEmpty else {
187-
return
203+
return docTokens
188204
}
189205

190206
if let syntaxMap: SKDResponseArray = response[keys.syntaxmap] {
191207
let tokenParser = SyntaxHighlightingTokenParser(sourcekitd: sourcekitd)
192208
let tokens = tokenParser.parseTokens(syntaxMap, in: snapshot)
193209

194-
do {
195-
try documentManager.replaceLexicalTokens(uri, in: range, with: tokens)
196-
} catch {
197-
log("updating lexical tokens for \(uri) failed: \(error)", level: .warning)
198-
}
210+
docTokens.replaceLexicalTokens(in: range, with: tokens)
199211
}
200212

201213
if let substructure: SKDResponseArray = response[keys.substructure] {
202214
let tokenParser = SyntaxHighlightingTokenParser(sourcekitd: sourcekitd, useName: true)
203215
let tokens = tokenParser.parseTokens(substructure, in: snapshot)
204-
do {
205-
try documentManager.replaceSyntacticTokens(uri, tokens: tokens)
206-
} catch {
207-
log("updating syntactic tokens for \(uri) failed: \(error)", level: .warning)
208-
}
216+
217+
docTokens.syntactic = tokens
209218
}
219+
220+
return docTokens
210221
}
211222

212-
/// Update semantic tokens for the given `snapshot`.
213-
/// Should only be called on `queue`.
223+
/// Updates the semantic tokens for the given `snapshot`.
224+
/// Must be called on `self.queue`.
214225
private func updateSemanticTokens(
215226
response: SKDResponseDictionary,
216227
for snapshot: DocumentSnapshot
217228
) {
218229
dispatchPrecondition(condition: .onQueue(queue))
219230

220231
let uri = snapshot.document.uri
221-
guard let skTokens: SKDResponseArray = response[keys.annotations] else {
222-
return
223-
}
224-
225-
let tokenParser = SyntaxHighlightingTokenParser(sourcekitd: sourcekitd)
226-
let tokens = tokenParser.parseTokens(skTokens, in: snapshot)
232+
let docTokens = updatedSemanticTokens(response: response, for: snapshot)
227233

228234
do {
229-
try documentManager.replaceSemanticTokens(uri, tokens: tokens)
235+
try documentManager.updateTokens(uri, tokens: docTokens)
230236
} catch {
231-
log("updating semantic tokens for \(uri) failed: \(error)", level: .warning)
237+
log("Updating semantic tokens failed: \(error)", level: .warning)
232238
}
233239
}
234240

241+
/// Returns the updated semantic tokens for the given `snapshot`.
242+
private func updatedSemanticTokens(
243+
response: SKDResponseDictionary,
244+
for snapshot: DocumentSnapshot
245+
) -> DocumentTokens {
246+
var docTokens = snapshot.tokens
247+
248+
if let skTokens: SKDResponseArray = response[keys.annotations] {
249+
let tokenParser = SyntaxHighlightingTokenParser(sourcekitd: sourcekitd)
250+
let tokens = tokenParser.parseTokens(skTokens, in: snapshot)
251+
252+
docTokens.semantic = tokens
253+
}
254+
255+
return docTokens
256+
}
257+
235258
/// Inform the client about changes to the syntax highlighting tokens.
236259
private func requestTokensRefresh() {
237260
if clientCapabilities.workspace?.semanticTokens?.refreshSupport ?? false {
@@ -491,50 +514,35 @@ extension SwiftLanguageServer {
491514

492515
self.queue.async {
493516
let uri = note.textDocument.uri
494-
let newVersion = note.textDocument.version ?? -1
495517
var lastResponse: SKDResponseDictionary? = nil
496518

497-
// We iterate manually over the `contentChanges` here instead of passing
498-
// the notification directly to the convenience method `edit(_:editCallback:)`
499-
// since we need to update the tokens in lockstep with the edits in the
500-
// document.
501-
502-
do {
503-
for edit in note.contentChanges {
504-
try self.documentManager.edit(uri, newVersion: newVersion, edits: [edit]) { (before: DocumentSnapshot, edit: TextDocumentContentChangeEvent) in
505-
let req = SKDRequestDictionary(sourcekitd: self.sourcekitd)
506-
req[keys.request] = self.requests.editor_replacetext
507-
req[keys.name] = note.textDocument.uri.pseudoPath
508-
509-
if let range = edit.range {
510-
guard let offset = before.utf8Offset(of: range.lowerBound), let end = before.utf8Offset(of: range.upperBound) else {
511-
fatalError("invalid edit \(range)")
512-
}
519+
self.documentManager.edit(note) { (before: DocumentSnapshot, edit: TextDocumentContentChangeEvent) in
520+
let req = SKDRequestDictionary(sourcekitd: self.sourcekitd)
521+
req[keys.request] = self.requests.editor_replacetext
522+
req[keys.name] = note.textDocument.uri.pseudoPath
513523

514-
req[keys.offset] = offset
515-
req[keys.length] = end - offset
516-
517-
} else {
518-
// Full text
519-
req[keys.offset] = 0
520-
req[keys.length] = before.text.utf8.count
521-
}
522-
523-
req[keys.sourcetext] = edit.text
524-
525-
lastResponse = try? self.sourcekitd.sendSync(req)
524+
if let range = edit.range {
525+
guard let offset = before.utf8Offset(of: range.lowerBound), let end = before.utf8Offset(of: range.upperBound) else {
526+
fatalError("invalid edit \(range)")
526527
}
527528

528-
// SourceKit seems to respond with an empty substructure after sending an
529-
// empty range for an edit, causing all syntactic tokens to get removed
530-
// therefore we only update them if the range is non-empty.
529+
req[keys.offset] = offset
530+
req[keys.length] = end - offset
531531

532-
if let dict = lastResponse, let snapshot = self.documentManager.latestSnapshot(uri) {
533-
self.updateLexicalAndSyntacticTokens(response: dict, for: snapshot)
534-
}
532+
} else {
533+
// Full text
534+
req[keys.offset] = 0
535+
req[keys.length] = before.text.utf8.count
536+
}
537+
538+
req[keys.sourcetext] = edit.text
539+
lastResponse = try? self.sourcekitd.sendSync(req)
540+
} afterCallback: { (after: DocumentSnapshot) in
541+
if let dict = lastResponse {
542+
return self.updatedLexicalAndSyntacticTokens(response: dict, for: after)
543+
} else {
544+
return nil
535545
}
536-
} catch {
537-
log("failed to edit document: \(error)", level: .error)
538546
}
539547

540548
if let dict = lastResponse, let snapshot = self.documentManager.latestSnapshot(uri) {

0 commit comments

Comments
 (0)