Skip to content

Commit ccc19f5

Browse files
committed
Add Syntax Highlighting To Source Previews
1 parent 19e484a commit ccc19f5

File tree

4 files changed

+89
-7
lines changed

4 files changed

+89
-7
lines changed

Sources/CodeEditSourceEditor/CodeSuggestion/Model/SuggestionViewModel.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ final class SuggestionViewModel: ObservableObject {
1616

1717
weak var delegate: CodeSuggestionDelegate?
1818

19+
private var syntaxHighlightedCache: [Int: NSAttributedString] = [:]
20+
1921
func showCompletions(
2022
textView: TextViewController,
2123
delegate: CodeSuggestionDelegate,
@@ -56,6 +58,7 @@ final class SuggestionViewModel: ObservableObject {
5658
}
5759

5860
self.items = completionItems.items
61+
self.syntaxHighlightedCache = [:]
5962
showWindowOnParent(targetParentWindow, cursorRect)
6063
}
6164
} catch {
@@ -109,4 +112,26 @@ final class SuggestionViewModel: ObservableObject {
109112
items.removeAll()
110113
activeTextView = nil
111114
}
115+
116+
func syntaxHighlights(forIndex index: Int) -> NSAttributedString? {
117+
if let cached = syntaxHighlightedCache[index] {
118+
return cached
119+
}
120+
121+
if let sourcePreview = items[index].sourcePreview,
122+
let theme = activeTextView?.theme,
123+
let font = activeTextView?.font,
124+
let language = activeTextView?.language {
125+
let string = TreeSitterClient.quickHighlight(
126+
string: sourcePreview,
127+
theme: theme,
128+
font: font,
129+
language: language
130+
)
131+
syntaxHighlightedCache[index] = string
132+
return string
133+
}
134+
135+
return nil
136+
}
112137
}

Sources/CodeEditSourceEditor/CodeSuggestion/TableView/CodeSuggestionPreviewView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import SwiftUI
1010
final class CodeSuggestionPreviewView: NSVisualEffectView {
1111
private let spacing: CGFloat = 5
1212

13-
var sourcePreview: String? {
13+
var sourcePreview: NSAttributedString? {
1414
didSet {
15-
sourcePreviewLabel.stringValue = sourcePreview ?? ""
15+
sourcePreviewLabel.attributedStringValue = sourcePreview ?? NSAttributedString(string: "")
1616
sourcePreviewLabel.isHidden = sourcePreview == nil
1717
}
1818
}

Sources/CodeEditSourceEditor/CodeSuggestion/TableView/SuggestionViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ extension SuggestionViewController: NSTableViewDataSource, NSTableViewDelegate {
333333
// Update our preview view
334334
let selectedItem = model.items[tableView.selectedRow]
335335

336-
previewView.sourcePreview = selectedItem.sourcePreview
336+
previewView.sourcePreview = model.syntaxHighlights(forIndex: tableView.selectedRow)
337337
previewView.documentation = selectedItem.documentation
338338
previewView.pathComponents = selectedItem.pathComponents ?? []
339339
previewView.targetRange = selectedItem.targetPosition

Sources/CodeEditSourceEditor/TreeSitter/TreeSitterClient+Temporary.swift

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,67 @@
55
// Created by Khan Winter on 7/24/25.
66
//
77

8-
import Foundation
8+
import AppKit
9+
import SwiftTreeSitter
10+
import CodeEditLanguages
911

1012
extension TreeSitterClient {
11-
// func temporaryHighlight() -> NSAttributedString {
12-
//
13-
// }
13+
static func quickHighlight(
14+
string: String,
15+
theme: EditorTheme,
16+
font: NSFont,
17+
language: CodeLanguage
18+
) -> NSAttributedString? {
19+
guard let parserLanguage = language.language, let query = TreeSitterModel.shared.query(for: language.id) else {
20+
return nil
21+
}
22+
23+
do {
24+
let parser = Parser()
25+
try parser.setLanguage(parserLanguage)
26+
guard let syntaxTree = parser.parse(string) else {
27+
return nil
28+
}
29+
let queryCursor = query.execute(in: syntaxTree)
30+
var ranges: [NSRange: Int] = [:]
31+
let highlights: [HighlightRange] = queryCursor
32+
.resolve(with: .init(string: string))
33+
.flatMap { $0.captures }
34+
.reversed() // SwiftTreeSitter returns captures in the reverse order of what we need to filter with.
35+
.compactMap { capture in
36+
let range = capture.range
37+
let index = capture.index
38+
39+
// Lower indexed captures are favored over higher, this is why we reverse it above
40+
if let existingLevel = ranges[range], existingLevel <= index {
41+
return nil
42+
}
43+
44+
guard let captureName = CaptureName.fromString(capture.name) else {
45+
return nil
46+
}
47+
48+
// Update the filter level to the current index since it's lower and a 'valid' capture
49+
ranges[range] = index
50+
51+
return HighlightRange(range: range, capture: captureName)
52+
}
53+
54+
var string = NSMutableAttributedString(string: string)
55+
56+
for highlight in highlights {
57+
string.setAttributes(
58+
[
59+
.font: theme.fontFor(for: highlight.capture, from: font),
60+
.foregroundColor: theme.colorFor(highlight.capture)
61+
],
62+
range: highlight.range
63+
)
64+
}
65+
66+
return string
67+
} catch {
68+
return nil
69+
}
70+
}
1471
}

0 commit comments

Comments
 (0)