Skip to content

Commit 8f1001a

Browse files
committed
Use ranges to represent tokens and improve overlap checks
- Simplify overlap checks - Add makeToken helper in SemanticTokensTests - Represent SyntaxHighlightingTokens using ranges internally - Fix lengths for function signatures with special characters e.g. emojis - Fix handling of the empty range-case in updateLexicalAndSyntacticTokens
1 parent 46da752 commit 8f1001a

File tree

5 files changed

+237
-198
lines changed

5 files changed

+237
-198
lines changed

Sources/SKTestSupport/Array+SyntaxHighlightingToken.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ extension Array where Element == SyntaxHighlightingToken {
4242

4343
append(SyntaxHighlightingToken(
4444
start: current,
45-
length: length,
45+
utf16length: length,
4646
kind: kind,
4747
modifiers: modifiers
4848
))

Sources/SourceKitLSP/DocumentManager.swift

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,8 @@ public final class DocumentManager {
177177
document.latestTokens.withMutableTokensOfEachKind { tokens in
178178
tokens = Array(tokens.lazy
179179
.filter {
180-
// Only keep tokens that don't overlap or bound with the edit range
181-
$0.start >= range.upperBound
182-
|| range.lowerBound >= $0.sameLineEnd
183-
|| range.isEmpty
180+
// Only keep tokens that don't overlap with the edit range
181+
!$0.range.overlaps(range)
184182
}
185183
.map {
186184
// Shift tokens after the edit range
@@ -263,13 +261,10 @@ public final class DocumentManager {
263261
throw Error.missingDocument(uri)
264262
}
265263

266-
// Remove all tokens in `range` (or the entire document if `range` is `nil`)
264+
// Remove all tokens that overlap with `range`
265+
// (or the entire document if `range` is `nil`)
267266
document.latestTokens.lexical.removeAll { token in
268-
range.map {
269-
token.start <= $0.upperBound
270-
&& $0.lowerBound <= token.sameLineEnd
271-
&& !$0.isEmpty
272-
} ?? true
267+
range.map { token.range.overlaps($0) } ?? true
273268
}
274269

275270
document.latestTokens.lexical += newTokens

Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -169,21 +169,27 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {
169169
) {
170170
dispatchPrecondition(condition: .onQueue(queue))
171171

172+
guard let offset: Int = response[keys.offset],
173+
let length: Int = response[keys.length],
174+
let start: Position = snapshot.positionOf(utf8Offset: offset),
175+
let end: Position = snapshot.positionOf(utf8Offset: offset + length) else {
176+
log("updateLexicalAndSyntacticTokens failed, no range found", level: .error)
177+
return
178+
}
179+
172180
let uri = snapshot.document.uri
181+
let range = start..<end
182+
183+
// If the range is empty we don't have to (and shouldn't) update anything.
184+
// This is important, since the substructure may be empty, causing us to
185+
// unnecessarily remove all syntactic tokens.
186+
guard !range.isEmpty else {
187+
return
188+
}
173189

174190
if let syntaxMap: SKDResponseArray = response[keys.syntaxmap] {
175191
let tokenParser = SyntaxHighlightingTokenParser(sourcekitd: sourcekitd)
176192
let tokens = tokenParser.parseTokens(syntaxMap, in: snapshot)
177-
let range: Range<Position>?
178-
179-
if let offset: Int = response[keys.offset],
180-
let length: Int = response[keys.length],
181-
let start: Position = snapshot.positionOf(utf8Offset: offset),
182-
let end: Position = snapshot.positionOf(utf8Offset: offset + length) {
183-
range = start..<end
184-
} else {
185-
range = nil
186-
}
187193

188194
do {
189195
try documentManager.replaceLexicalTokens(uri, in: range, with: tokens)
@@ -523,7 +529,7 @@ extension SwiftLanguageServer {
523529
// empty range for an edit, causing all syntactic tokens to get removed
524530
// therefore we only update them if the range is non-empty.
525531

526-
if !(edit.range?.isEmpty ?? false), let dict = lastResponse, let snapshot = self.documentManager.latestSnapshot(uri) {
532+
if let dict = lastResponse, let snapshot = self.documentManager.latestSnapshot(uri) {
527533
self.updateLexicalAndSyntacticTokens(response: dict, for: snapshot)
528534
}
529535
}
@@ -843,7 +849,7 @@ extension SwiftLanguageServer {
843849
return
844850
}
845851

846-
let tokens = snapshot.tokens.mergedAndSorted.filter { $0.sameLineRange.overlaps(range) }
852+
let tokens = snapshot.tokens.mergedAndSorted.filter { $0.range.overlaps(range) }
847853
let encodedTokens = tokens.lspEncoded
848854

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

Sources/SourceKitLSP/Swift/SyntaxHighlightingToken.swift

Lines changed: 75 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -16,56 +16,48 @@ import LSPLogging
1616

1717
/// A ranged token in the document used for syntax highlighting.
1818
public struct SyntaxHighlightingToken: Hashable {
19-
public var start: Position
20-
public var length: Int
19+
/// The range of the token in the document. Must be on a single line.
20+
public var range: Range<Position> {
21+
didSet {
22+
assert(range.lowerBound.line == range.upperBound.line)
23+
}
24+
}
25+
/// The token type.
2126
public var kind: Kind
27+
/// Additional metadata about the token.
2228
public var modifiers: Modifiers
2329

24-
/// The end of a token. Note that this requires the token to be
25-
/// on a single line, which is the case for all tokens emitted
26-
/// by parseTokens, however.
27-
public var sameLineEnd: Position {
28-
Position(line: start.line, utf16index: start.utf16index + length)
30+
/// The (inclusive) start position of the token.
31+
/// Setting it shifts the token and preserves the length.
32+
public var start: Position {
33+
get { range.lowerBound }
34+
set {
35+
let length = utf16length
36+
range = newValue..<Position(line: newValue.line, utf16index: newValue.utf16index + length)
37+
}
2938
}
30-
public var sameLineRange: Range<Position> {
31-
start..<sameLineEnd
39+
/// The (exclusive) end position of the token.
40+
public var end: Position { range.upperBound }
41+
/// The length of the token in UTF-16 code units.
42+
public var utf16length: Int {
43+
get { end.utf16index - start.utf16index }
44+
set {
45+
assert(newValue >= 0)
46+
range = start..<Position(line: start.line, utf16index: start.utf16index + newValue)
47+
}
3248
}
3349

34-
public init(
35-
start: Position,
36-
length: Int,
37-
kind: Kind,
38-
modifiers: Modifiers = []
39-
) {
40-
self.start = start
41-
self.length = length
50+
public init(range: Range<Position>, kind: Kind, modifiers: Modifiers = []) {
51+
assert(range.lowerBound.line == range.upperBound.line)
52+
53+
self.range = range
4254
self.kind = kind
4355
self.modifiers = modifiers
4456
}
4557

46-
/// Splits a potentially multi-line token to multiple single-line tokens.
47-
public func splitToSingleLineTokens(in snapshot: DocumentSnapshot) -> [Self] {
48-
guard let startIndex = snapshot.index(of: start) else {
49-
fatalError("Token \(self) begins outside of the document")
50-
}
51-
52-
let endIndex = snapshot.text.index(startIndex, offsetBy: length)
53-
let text = snapshot.text[startIndex..<endIndex]
54-
let lines = text.split(separator: "\n")
55-
56-
return lines
57-
.enumerated()
58-
.map { (i, content) in
59-
Self(
60-
start: Position(
61-
line: start.line + i,
62-
utf16index: i == 0 ? start.utf16index : 0
63-
),
64-
length: content.count,
65-
kind: kind,
66-
modifiers: modifiers
67-
)
68-
}
58+
public init(start: Position, utf16length: Int, kind: Kind, modifiers: Modifiers = []) {
59+
let range = start..<Position(line: start.line, utf16index: start.utf16index + utf16length)
60+
self.init(range: range, kind: kind, modifiers: modifiers)
6961
}
7062

7163
/// The token type. Represented using an int to make the conversion to
@@ -223,7 +215,7 @@ extension Array where Element == SyntaxHighlightingToken {
223215
rawTokens += [
224216
UInt32(lineDelta),
225217
UInt32(charDelta),
226-
UInt32(token.length),
218+
UInt32(token.utf16length),
227219
token.kind.rawValue,
228220
token.modifiers.rawValue
229221
]
@@ -236,8 +228,35 @@ extension Array where Element == SyntaxHighlightingToken {
236228
/// preferring the given array's tokens if duplicate ranges are
237229
/// found.
238230
public func mergingTokens(with other: [SyntaxHighlightingToken]) -> [SyntaxHighlightingToken] {
239-
let otherRanges = Set(other.map(\.sameLineRange))
240-
return filter { !otherRanges.contains($0.sameLineRange) } + other
231+
let otherRanges = Set(other.map(\.range))
232+
return filter { !otherRanges.contains($0.range) } + other
233+
}
234+
}
235+
236+
extension Range where Bound == Position {
237+
/// Splits a potentially multi-line range to multiple single-line ranges.
238+
fileprivate func splitToSingleLineRanges(in snapshot: DocumentSnapshot) -> [Self] {
239+
guard let startIndex = snapshot.index(of: lowerBound),
240+
let endIndex = snapshot.index(of: upperBound) else {
241+
fatalError("Range \(self) reaches outside of the document")
242+
}
243+
244+
let text = snapshot.text[startIndex..<endIndex]
245+
let lines = text.split(separator: "\n")
246+
247+
return lines
248+
.enumerated()
249+
.map { (i, content) in
250+
let start = Position(
251+
line: lowerBound.line + i,
252+
utf16index: i == 0 ? lowerBound.utf16index : 0
253+
)
254+
let end = Position(
255+
line: start.line,
256+
utf16index: start.utf16index + content.utf16.count
257+
)
258+
return start..<end
259+
}
241260
}
242261
}
243262

@@ -271,7 +290,7 @@ struct SyntaxHighlightingTokenParser {
271290
if useName && [.function, .method, .enumMember].contains(kind) && modifiers.contains(.declaration),
272291
let name: String = response[keys.name],
273292
name.contains("("),
274-
let funcNameLength: Int = name.split(separator: "(").first?.utf16.count {
293+
let funcNameLength: Int = name.split(separator: "(").first?.utf8.count {
275294
length = funcNameLength
276295
}
277296

@@ -282,14 +301,18 @@ struct SyntaxHighlightingTokenParser {
282301
length += 2
283302
}
284303

285-
let multiLineToken = SyntaxHighlightingToken(
286-
start: start,
287-
length: length,
288-
kind: kind,
289-
modifiers: modifiers
290-
)
291-
292-
tokens += multiLineToken.splitToSingleLineTokens(in: snapshot)
304+
if let end: Position = snapshot.positionOf(utf8Offset: offset + length) {
305+
let multiLineRange = start..<end
306+
let ranges = multiLineRange.splitToSingleLineRanges(in: snapshot)
307+
308+
tokens += ranges.map {
309+
SyntaxHighlightingToken(
310+
range: $0,
311+
kind: kind,
312+
modifiers: modifiers
313+
)
314+
}
315+
}
293316
}
294317

295318
if let substructure: SKDResponseArray = response[keys.substructure] {

0 commit comments

Comments
 (0)