Skip to content

Commit 56d1775

Browse files
committed
Request and merge semantic tokens
This hasn’t been well tested yet. Contributes to #50
1 parent 4e4e519 commit 56d1775

File tree

2 files changed

+79
-3
lines changed

2 files changed

+79
-3
lines changed

Sources/CodeEditorView/CodeStorageDelegate.swift

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,8 +256,8 @@ extension CodeStorageDelegate {
256256

257257
extension CodeStorageDelegate {
258258

259-
/// Tokenise the substring of the given text storage that contains the specified lines and store token tokens as part
260-
/// of the line information.
259+
/// Tokenise the substring of the given text storage that contains the specified lines and store tokens as part of the
260+
/// line information.
261261
///
262262
/// - Parameters:
263263
/// - originalRange: The character range that contains all characters that have changed.
@@ -435,11 +435,87 @@ extension CodeStorageDelegate {
435435
currentLine += 1
436436
}
437437

438+
requestSemanticTokens(for: lines, in: textStorage)
439+
438440
if visualDebugging {
439441
textStorage.addAttribute(.backgroundColor, value: visualDebuggingTrailingColour, range: highlightingRange)
440442
textStorage.addAttribute(.backgroundColor, value: visualDebuggingLinesColour, range: range)
441443
}
442444
}
445+
446+
/// Query semantic tokens for the given lines from the language service (if available) and merge them into the token
447+
/// information for those lines (maintained in the line map),
448+
///
449+
/// - Parameters:
450+
/// lines: The lines for which semantic token information is requested.
451+
/// textStorage: The text storage whose contents is being tokenised.
452+
///
453+
func requestSemanticTokens(for lines: Range<Int>, in textStorage: NSTextStorage) {
454+
guard let firstLine = lines.first else { return }
455+
456+
Task {
457+
do {
458+
if let semanticTokens = try await languageService?.tokens(for: lines) {
459+
460+
guard lines.count == semanticTokens.count else {
461+
logger.error("Language service returned an array of incorrect length; expected \(lines.count), but got \(semanticTokens.count)")
462+
return
463+
}
464+
465+
// We need to avoid concurrent write access to the line map.
466+
await MainActor.run {
467+
468+
// Merge the semantic tokens into the syntactic tokens per line
469+
for i in 0..<lines.count {
470+
merge(semanticTokens: semanticTokens[i], into: firstLine + i)
471+
}
472+
473+
// Request redrawing for those lines
474+
for layoutManager in textStorage.layoutManagers {
475+
layoutManager.invalidateDisplay(forCharacterRange: lineMap.charRangeOf(lines: lines))
476+
}
477+
}
478+
479+
}
480+
} catch let error { logger.error("Failed to get semantic tokens for line range \(lines): \(error.localizedDescription)") }
481+
}
482+
}
483+
484+
/// Merge semantic token information for one line into the line map.
485+
///
486+
/// - Parameters:
487+
/// - semanticTokens: The semntic tokens to merge.
488+
/// - line: The line on which the tokens are located.
489+
///
490+
/// NB: Currently, we only enrich the information of tokens that are already present as syntactic tokens.
491+
///
492+
private func merge(semanticTokens: [(token: LanguageConfiguration.Token, range: NSRange)], into line: Int) {
493+
guard var info = lineMap.lookup(line: line)?.info else { return }
494+
495+
var remainingSemanticTokens = semanticTokens
496+
var tokens = info.tokens
497+
for i in 0..<tokens.count {
498+
499+
let token = tokens[i]
500+
while let semanticToken = remainingSemanticTokens.first,
501+
semanticToken.range.location <= token.range.location
502+
{
503+
remainingSemanticTokens.removeFirst()
504+
505+
// We enrich identifier and operator tokens if the semantic token is an identifier, operator, or keyword.
506+
if semanticToken.range == token.range
507+
&& (token.token.isIdentifier || token.token.isOperator)
508+
&& (semanticToken.token.isIdentifier || semanticToken.token.isOperator || semanticToken.token == .keyword)
509+
{
510+
tokens[i] = LanguageConfiguration.Tokeniser.Token(token: semanticToken.token, range: token.range)
511+
}
512+
}
513+
}
514+
515+
// Store updated token array
516+
info.tokens = tokens
517+
lineMap.setInfoOf(line: line, to: info)
518+
}
443519
}
444520

445521

Sources/CodeEditorView/GutterView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ extension GutterView {
329329
// TODO: CodeEditor needs to be parameterised by message theme
330330
let theme = Message.defaultTheme
331331

332-
for line in lineRange { // NB: These are zero-based line numbers
332+
for line in lineRange { // NB: These are zero-based line numbers
333333

334334
// NB: We adjust the range, so that in case of a trailing empty line that last line break is not included in
335335
// the second to last line (as otherwise, the bounding rect will contain both the second to last and last

0 commit comments

Comments
 (0)