Skip to content

Commit 17f6568

Browse files
committed
Implement lexical and semantic highlighting for Swift
This is an implementation of LSP's semantic tokens for Swift. Both lexical and semantic tokens are provided by using the syntaxmap and the semantic annotations provided as part of SourceKit's open responses and document update notifications. While lexical tokens are parsed and stored in the DocumentManager synchronously, semantic tokens are provided asynchronously. If an edit occurs, tokens are automatically shifted by the DocumentManager. This is especially relevant for lexical tokens, which are updated in deltas. In addition, unit tests are added that assert that both lexical and semantic tokens are provided and shifted correctly upon edits.
1 parent 9f7bfc0 commit 17f6568

15 files changed

+1624
-33
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ SourceKit-LSP is still in early development, so you may run into rough edges wit
4848
| Local Refactoring || |
4949
| Formatting || |
5050
| Folding || |
51-
| Syntax Highlighting | | Not currently part of LSP. |
51+
| Syntax Highlighting | | Both syntactic and semantic tokens |
5252
| Document Symbols || |
5353

5454

Sources/LSPLogging/Logging.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,23 @@ public func orLog<R>(
6262
}
6363
}
6464

65+
/// Logs the time that the given block takes to execute in milliseconds.
66+
public func logExecutionTime<R>(
67+
_ prefix: String = #function,
68+
level: LogLevel = .default,
69+
logger: Logger = Logger.shared,
70+
_ block: () throws -> R
71+
) rethrows -> R {
72+
let start = Date()
73+
let result = try block()
74+
let deltaMs = -start.timeIntervalSinceNow * 1000
75+
logger.log(
76+
"\(prefix)\(prefix.isEmpty ? "" : " ")took \(String(format: "%.2f", deltaMs)) ms to execute",
77+
level: level
78+
)
79+
return result
80+
}
81+
6582
public protocol LogHandler: AnyObject {
6683
func handle(_ message: String, level: LogLevel)
6784
}

Sources/LanguageServerProtocol/Requests/DocumentSymbolRequest.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public struct DocumentSymbol: Hashable, Codable {
9191
public var selectionRange: Range<Position>
9292

9393
/// Children of this symbol, e.g. properties of a class.
94-
public var children: [DocumentSymbol]?
94+
public var children: [DocumentSymbol]
9595

9696
public init(
9797
name: String,
@@ -100,7 +100,7 @@ public struct DocumentSymbol: Hashable, Codable {
100100
deprecated: Bool? = nil,
101101
range: Range<Position>,
102102
selectionRange: Range<Position>,
103-
children: [DocumentSymbol]? = nil)
103+
children: [DocumentSymbol] = [])
104104
{
105105
self.name = name
106106
self.detail = detail

Sources/LanguageServerProtocol/Requests/WorkspaceSemanticTokensRefreshRequest.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,6 @@
1616
public struct WorkspaceSemanticTokensRefreshRequest: RequestType, Hashable {
1717
public static let method: String = "workspace/semanticTokens/refresh"
1818
public typealias Response = VoidResponse
19+
20+
public init() {}
1921
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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 SourceKitLSP
14+
import LanguageServerProtocol
15+
16+
extension Array where Element == SyntaxHighlightingToken {
17+
/// Decodes the LSP representation of syntax highlighting tokens
18+
public init(lspEncodedTokens rawTokens: [UInt32]) {
19+
self.init()
20+
assert(rawTokens.count.isMultiple(of: 5))
21+
reserveCapacity(rawTokens.count / 5)
22+
23+
var current = Position(line: 0, utf16index: 0)
24+
25+
for i in stride(from: 0, to: rawTokens.count, by: 5) {
26+
let lineDelta = Int(rawTokens[i])
27+
let charDelta = Int(rawTokens[i + 1])
28+
let length = Int(rawTokens[i + 2])
29+
let rawKind = rawTokens[i + 3]
30+
let rawModifiers = rawTokens[i + 4]
31+
32+
current.line += lineDelta
33+
34+
if lineDelta == 0 {
35+
current.utf16index += charDelta
36+
} else {
37+
current.utf16index = charDelta
38+
}
39+
40+
guard let kind = SyntaxHighlightingToken.Kind(rawValue: rawKind) else { continue }
41+
let modifiers = SyntaxHighlightingToken.Modifiers(rawValue: rawModifiers)
42+
43+
append(SyntaxHighlightingToken(
44+
start: current,
45+
utf16length: length,
46+
kind: kind,
47+
modifiers: modifiers
48+
))
49+
}
50+
}
51+
}

Sources/SourceKitD/sourcekitd_uids.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public struct sourcekitd_keys {
1616
public let actionname: sourcekitd_uid_t
1717
public let actionuid: sourcekitd_uid_t
1818
public let annotated_decl: sourcekitd_uid_t
19+
public let annotations: sourcekitd_uid_t
1920
public let associated_usrs: sourcekitd_uid_t
2021
public let bodylength: sourcekitd_uid_t
2122
public let bodyoffset: sourcekitd_uid_t
@@ -40,6 +41,7 @@ public struct sourcekitd_keys {
4041
public let filepath: sourcekitd_uid_t
4142
public let fixits: sourcekitd_uid_t
4243
public let id: sourcekitd_uid_t
44+
public let is_system: sourcekitd_uid_t
4345
public let kind: sourcekitd_uid_t
4446
public let length: sourcekitd_uid_t
4547
public let line: sourcekitd_uid_t
@@ -61,6 +63,7 @@ public struct sourcekitd_keys {
6163
public let substructure: sourcekitd_uid_t
6264
public let syntactic_only: sourcekitd_uid_t
6365
public let syntaxmap: sourcekitd_uid_t
66+
public let enablesyntaxmap: sourcekitd_uid_t
6467
public let text: sourcekitd_uid_t
6568
public let typename: sourcekitd_uid_t
6669
public let usr: sourcekitd_uid_t
@@ -86,6 +89,7 @@ public struct sourcekitd_keys {
8689
actionname = api.uid_get_from_cstr("key.actionname")!
8790
actionuid = api.uid_get_from_cstr("key.actionuid")!
8891
annotated_decl = api.uid_get_from_cstr("key.annotated_decl")!
92+
annotations = api.uid_get_from_cstr("key.annotations")!
8993
associated_usrs = api.uid_get_from_cstr("key.associated_usrs")!
9094
bodylength = api.uid_get_from_cstr("key.bodylength")!
9195
bodyoffset = api.uid_get_from_cstr("key.bodyoffset")!
@@ -110,6 +114,7 @@ public struct sourcekitd_keys {
110114
filepath = api.uid_get_from_cstr("key.filepath")!
111115
fixits = api.uid_get_from_cstr("key.fixits")!
112116
id = api.uid_get_from_cstr("key.id")!
117+
is_system = api.uid_get_from_cstr("key.is_system")!
113118
kind = api.uid_get_from_cstr("key.kind")!
114119
length = api.uid_get_from_cstr("key.length")!
115120
line = api.uid_get_from_cstr("key.line")!
@@ -131,6 +136,7 @@ public struct sourcekitd_keys {
131136
substructure = api.uid_get_from_cstr("key.substructure")!
132137
syntactic_only = api.uid_get_from_cstr("key.syntactic_only")!
133138
syntaxmap = api.uid_get_from_cstr("key.syntaxmap")!
139+
enablesyntaxmap = api.uid_get_from_cstr("key.enablesyntaxmap")!
134140
text = api.uid_get_from_cstr("key.text")!
135141
typename = api.uid_get_from_cstr("key.typename")!
136142
usr = api.uid_get_from_cstr("key.usr")!
@@ -272,12 +278,20 @@ public struct sourcekitd_values {
272278
public let decl_generic_type_param: sourcekitd_uid_t
273279
public let ref_generic_type_param: sourcekitd_uid_t
274280
public let ref_module: sourcekitd_uid_t
281+
public let syntaxtype_attribute_builtin: sourcekitd_uid_t
275282
public let syntaxtype_comment: sourcekitd_uid_t
276283
public let syntaxtype_comment_marker: sourcekitd_uid_t
277284
public let syntaxtype_comment_url: sourcekitd_uid_t
278285
public let syntaxtype_doccomment: sourcekitd_uid_t
279286
public let syntaxtype_doccomment_field: sourcekitd_uid_t
287+
public let syntaxtype_keyword: sourcekitd_uid_t
288+
public let syntaxtype_number: sourcekitd_uid_t
289+
public let syntaxtype_string: sourcekitd_uid_t
290+
public let syntaxtype_string_interpolation_anchor: sourcekitd_uid_t
291+
public let syntaxtype_type_identifier: sourcekitd_uid_t
292+
public let syntaxtype_identifier: sourcekitd_uid_t
280293
public let expr_object_literal: sourcekitd_uid_t
294+
public let expr_call: sourcekitd_uid_t
281295

282296
public let kind_keyword: sourcekitd_uid_t
283297

@@ -367,12 +381,20 @@ public struct sourcekitd_values {
367381
decl_generic_type_param = api.uid_get_from_cstr("source.lang.swift.decl.generic_type_param")!
368382
ref_generic_type_param = api.uid_get_from_cstr("source.lang.swift.ref.generic_type_param")!
369383
ref_module = api.uid_get_from_cstr("source.lang.swift.ref.module")!
384+
syntaxtype_attribute_builtin = api.uid_get_from_cstr("source.lang.swift.syntaxtype.attribute.builtin")!
370385
syntaxtype_comment = api.uid_get_from_cstr("source.lang.swift.syntaxtype.comment")!
371386
syntaxtype_comment_marker = api.uid_get_from_cstr("source.lang.swift.syntaxtype.comment.mark")!
372387
syntaxtype_comment_url = api.uid_get_from_cstr("source.lang.swift.syntaxtype.comment.url")!
373388
syntaxtype_doccomment = api.uid_get_from_cstr("source.lang.swift.syntaxtype.doccomment")!
374389
syntaxtype_doccomment_field = api.uid_get_from_cstr("source.lang.swift.syntaxtype.doccomment.field")!
390+
syntaxtype_keyword = api.uid_get_from_cstr("source.lang.swift.syntaxtype.keyword")!
391+
syntaxtype_number = api.uid_get_from_cstr("source.lang.swift.syntaxtype.number")!
392+
syntaxtype_string = api.uid_get_from_cstr("source.lang.swift.syntaxtype.string")!
393+
syntaxtype_string_interpolation_anchor = api.uid_get_from_cstr("source.lang.swift.syntaxtype.string_interpolation_anchor")!
394+
syntaxtype_type_identifier = api.uid_get_from_cstr("source.lang.swift.syntaxtype.typeidentifier")!
395+
syntaxtype_identifier = api.uid_get_from_cstr("source.lang.swift.syntaxtype.identifier")!
375396
expr_object_literal = api.uid_get_from_cstr("source.lang.swift.expr.object_literal")!
397+
expr_call = api.uid_get_from_cstr("source.lang.swift.expr.call")!
376398

377399
kind_keyword = api.uid_get_from_cstr("source.lang.swift.keyword")!
378400
}

Sources/SourceKitLSP/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ endif()
88
add_library(SourceKitLSP
99
CapabilityRegistry.swift
1010
DocumentManager.swift
11+
DocumentTokens.swift
1112
IndexStoreDB+MainFilesProvider.swift
1213
SourceKitIndexDelegate.swift
1314
SourceKitLSPCommandMetadata.swift
@@ -30,6 +31,8 @@ target_sources(SourceKitLSP PRIVATE
3031
Swift/SourceKitD+ResponseError.swift
3132
Swift/SwiftCommand.swift
3233
Swift/SwiftLanguageServer.swift
34+
Swift/SyntaxHighlightingToken.swift
35+
Swift/SyntaxHighlightingTokenParser.swift
3336
Swift/VariableTypeInfo.swift)
3437
set_target_properties(SourceKitLSP PROPERTIES
3538
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})

Sources/SourceKitLSP/DocumentManager.swift

Lines changed: 95 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,23 @@ public struct DocumentSnapshot {
1919
public var document: Document
2020
public var version: Int
2121
public var lineTable: LineTable
22-
public var text: String { return lineTable.content }
22+
/// Syntax highlighting tokens for the document. Note that
23+
/// `uri` + `latestVersion` only uniquely identifies a snapshot's content,
24+
/// the tokens are updated independently and only used internally.
25+
public var tokens: DocumentTokens
2326

24-
public init(document: Document, version: Int, lineTable: LineTable) {
27+
public var text: String { lineTable.content }
28+
29+
public init(
30+
document: Document,
31+
version: Int,
32+
lineTable: LineTable,
33+
tokens: DocumentTokens
34+
) {
2535
self.document = document
2636
self.version = version
2737
self.lineTable = lineTable
38+
self.tokens = tokens
2839
}
2940

3041
func index(of pos: Position) -> String.Index? {
@@ -37,17 +48,24 @@ public final class Document {
3748
public let language: Language
3849
var latestVersion: Int
3950
var latestLineTable: LineTable
51+
var latestTokens: DocumentTokens
4052

4153
init(uri: DocumentURI, language: Language, version: Int, text: String) {
4254
self.uri = uri
4355
self.language = language
4456
self.latestVersion = version
4557
self.latestLineTable = LineTable(text)
58+
self.latestTokens = DocumentTokens()
4659
}
4760

4861
/// **Not thread safe!** Use `DocumentManager.latestSnapshot` instead.
4962
fileprivate var latestSnapshot: DocumentSnapshot {
50-
return DocumentSnapshot(document: self, version: latestVersion, lineTable: latestLineTable)
63+
DocumentSnapshot(
64+
document: self,
65+
version: latestVersion,
66+
lineTable: latestLineTable,
67+
tokens: latestTokens
68+
)
5169
}
5270
}
5371

@@ -98,43 +116,99 @@ public final class DocumentManager {
98116

99117
/// Applies the given edits to the document.
100118
///
101-
/// - parameter editCallback: Optional closure to call for each edit.
119+
/// - parameter willEditDocument: Optional closure to call before each edit.
120+
/// - parameter updateDocumentTokens: Optional closure to call after each edit.
102121
/// - parameter before: The document contents *before* the edit is applied.
122+
/// - parameter after: The document contents *after* the edit is applied.
103123
/// - returns: The contents of the file after all the edits are applied.
104124
/// - throws: Error.missingDocument if the document is not open.
105125
@discardableResult
106-
public func edit(_ uri: DocumentURI, newVersion: Int, edits: [TextDocumentContentChangeEvent], editCallback: ((_ before: DocumentSnapshot, TextDocumentContentChangeEvent) -> Void)? = nil) throws -> DocumentSnapshot {
126+
public func edit(
127+
_ uri: DocumentURI,
128+
newVersion: Int,
129+
edits: [TextDocumentContentChangeEvent],
130+
willEditDocument: ((_ before: DocumentSnapshot, TextDocumentContentChangeEvent) -> Void)? = nil,
131+
updateDocumentTokens: ((_ after: DocumentSnapshot) -> DocumentTokens)? = nil
132+
) throws -> DocumentSnapshot {
107133
return try queue.sync {
108134
guard let document = documents[uri] else {
109135
throw Error.missingDocument(uri)
110136
}
111137

112138
for edit in edits {
113-
if let f = editCallback {
139+
if let f = willEditDocument {
114140
f(document.latestSnapshot, edit)
115141
}
116142

117143
if let range = edit.range {
118-
119144
document.latestLineTable.replace(
120145
fromLine: range.lowerBound.line,
121146
utf16Offset: range.lowerBound.utf16index,
122147
toLine: range.upperBound.line,
123148
utf16Offset: range.upperBound.utf16index,
124149
with: edit.text)
150+
151+
// Remove all tokens in the updated range and shift later ones.
125152

153+
let replacedLineCount = 1 + range.upperBound.line - range.lowerBound.line
154+
let newLines = edit.text.split(separator: "\n", omittingEmptySubsequences: false)
155+
let upperUtf16IndexAfterEdit = (
156+
newLines.count == 1 ? range.lowerBound.utf16index : 0
157+
) + newLines.last!.utf16.count
158+
let lastLineCharDelta = upperUtf16IndexAfterEdit - range.upperBound.utf16index
159+
let lineDelta = newLines.count - replacedLineCount // may be negative
160+
161+
document.latestTokens.withMutableTokensOfEachKind { tokens in
162+
tokens = Array(tokens.lazy
163+
.filter {
164+
// Only keep tokens that don't overlap with the edit range
165+
!$0.range.overlaps(range)
166+
}
167+
.map {
168+
// Shift tokens after the edit range
169+
var token = $0
170+
if token.start.line == range.upperBound.line
171+
&& token.start.utf16index >= range.upperBound.utf16index {
172+
token.move(lineDelta: lineDelta, utf16indexDelta: lastLineCharDelta)
173+
} else if token.start.line > range.upperBound.line {
174+
token.move(lineDelta: lineDelta)
175+
}
176+
return token
177+
})
178+
}
126179
} else {
127180
// Full text replacement.
128181
document.latestLineTable = LineTable(edit.text)
182+
document.latestTokens = DocumentTokens()
129183
}
130184

185+
if let f = updateDocumentTokens {
186+
document.latestTokens = f(document.latestSnapshot)
187+
}
131188
}
132189

133190
document.latestVersion = newVersion
134191
return document.latestSnapshot
135192
}
136193
}
137194

195+
/// Updates the tokens in a document.
196+
///
197+
/// - parameter uri: The URI of the document to be updated
198+
/// - parameter tokens: The new tokens for the document
199+
@discardableResult
200+
public func updateTokens(_ uri: DocumentURI, tokens: DocumentTokens) throws -> DocumentSnapshot {
201+
return try queue.sync {
202+
guard let document = documents[uri] else {
203+
throw Error.missingDocument(uri)
204+
}
205+
206+
document.latestTokens = tokens
207+
208+
return document.latestSnapshot
209+
}
210+
}
211+
138212
public func latestSnapshot(_ uri: DocumentURI) -> DocumentSnapshot? {
139213
return queue.sync {
140214
guard let document = documents[uri] else {
@@ -165,11 +239,22 @@ extension DocumentManager {
165239
}
166240
}
167241

168-
/// Convenience wrapper for `edit(_:newVersion:edits:editCallback:)` that logs on failure.
242+
/// Convenience wrapper for `edit(_:newVersion:edits:willEditDocument:updateDocumentTokens:)`
243+
/// that logs on failure.
169244
@discardableResult
170-
func edit(_ note: DidChangeTextDocumentNotification, editCallback: ((_ before: DocumentSnapshot, TextDocumentContentChangeEvent) -> Void)? = nil) -> DocumentSnapshot? {
245+
func edit(
246+
_ note: DidChangeTextDocumentNotification,
247+
willEditDocument: ((_ before: DocumentSnapshot, TextDocumentContentChangeEvent) -> Void)? = nil,
248+
updateDocumentTokens: ((_ after: DocumentSnapshot) -> DocumentTokens)? = nil
249+
) -> DocumentSnapshot? {
171250
return orLog("failed to edit document", level: .error) {
172-
try edit(note.textDocument.uri, newVersion: note.textDocument.version ?? -1, edits: note.contentChanges, editCallback: editCallback)
251+
try edit(
252+
note.textDocument.uri,
253+
newVersion: note.textDocument.version ?? -1,
254+
edits: note.contentChanges,
255+
willEditDocument: willEditDocument,
256+
updateDocumentTokens: updateDocumentTokens
257+
)
173258
}
174259
}
175260
}

0 commit comments

Comments
 (0)