Skip to content

Commit cf9181c

Browse files
committed
Improve code highlighting performance in chat
1 parent 2effc39 commit cf9181c

9 files changed

Lines changed: 315 additions & 264 deletions

File tree

Core/Sources/ChatGPTChatTab/ChatPanel.swift

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -286,43 +286,39 @@ struct ChatHistory: View {
286286
struct ChatHistoryItem: View {
287287
let chat: StoreOf<Chat>
288288
let message: DisplayedChatMessage
289-
@State var height: CGFloat = 0
290-
@State var codeHighlightCacheController = CodeBlockHighlighterCacheController()
291289

292290
var body: some View {
293291
let text = message.text
294292

295-
Group {
296-
switch message.role {
297-
case .user:
298-
UserMessage(id: message.id, text: text, chat: chat)
299-
.listRowInsets(EdgeInsets(
300-
top: 0,
301-
leading: -8,
302-
bottom: 0,
303-
trailing: -8
304-
))
305-
.padding(.vertical, 4)
306-
case .assistant:
307-
BotMessage(
308-
id: message.id,
309-
text: text,
310-
references: message.references,
311-
chat: chat
312-
)
293+
switch message.role {
294+
case .user:
295+
UserMessage(id: message.id, text: text, chat: chat)
313296
.listRowInsets(EdgeInsets(
314297
top: 0,
315298
leading: -8,
316299
bottom: 0,
317300
trailing: -8
318301
))
319302
.padding(.vertical, 4)
320-
case .tool:
321-
FunctionMessage(id: message.id, text: text)
322-
case .ignored:
323-
EmptyView()
324-
}
325-
}.environment(\.codeHighlightCacheController, codeHighlightCacheController)
303+
case .assistant:
304+
BotMessage(
305+
id: message.id,
306+
text: text,
307+
references: message.references,
308+
chat: chat
309+
)
310+
.listRowInsets(EdgeInsets(
311+
top: 0,
312+
leading: -8,
313+
bottom: 0,
314+
trailing: -8
315+
))
316+
.padding(.vertical, 4)
317+
case .tool:
318+
FunctionMessage(id: message.id, text: text)
319+
case .ignored:
320+
EmptyView()
321+
}
326322
}
327323
}
328324

Lines changed: 85 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,104 @@
1+
import Combine
2+
import DebounceFunction
13
import Foundation
24
import MarkdownUI
35
import SharedUIComponents
46
import SwiftUI
57

6-
final class CodeBlockHighlighterCacheController {
7-
private var cache: [String: AttributedString] = [:]
8-
9-
func get(_ key: String) -> AttributedString? {
10-
cache[key]
11-
}
12-
13-
func set(_ key: String, _ value: AttributedString) {
14-
cache[key] = value
15-
}
16-
}
8+
/// Use this instead of the built in ``CodeBlockView`` to highlight code blocks asynchronously,
9+
/// so that the UI doesn't freeze when rendering large code blocks.
10+
struct AsyncCodeBlockView: View {
11+
class Storage: ObservableObject {
12+
static let queue = DispatchQueue(
13+
label: "chat-code-block-highlight",
14+
qos: .userInteractive
15+
)
1716

18-
struct CodeHighlightCacheEnvironmentKey: EnvironmentKey {
19-
static var defaultValue: CodeBlockHighlighterCacheController = .init()
20-
}
17+
@Published var highlighted: AttributedString?
18+
var debounceFunction: DebounceFunction<AsyncCodeBlockView>?
19+
20+
init() {
21+
self.debounceFunction = .init(duration: 0.5, block: { [weak self] view in
22+
self?.highlight(for: view)
23+
})
24+
}
2125

22-
extension EnvironmentValues {
23-
var codeHighlightCacheController: CodeBlockHighlighterCacheController {
24-
get { self[CodeHighlightCacheEnvironmentKey.self] }
25-
set { self[CodeHighlightCacheEnvironmentKey.self] = newValue }
26+
func highlight(debounce: Bool, for view: AsyncCodeBlockView) {
27+
if debounce {
28+
Task { await debounceFunction?(view) }
29+
} else {
30+
highlight(for: view)
31+
}
32+
}
33+
34+
func highlight(for view: AsyncCodeBlockView) {
35+
let content = view.content
36+
let language = view.fenceInfo ?? ""
37+
let brightMode = view.colorScheme != .dark
38+
let font = view.font
39+
Self.queue.async {
40+
let content = highlightedCodeBlock(
41+
code: content,
42+
language: language,
43+
scenario: "chat",
44+
brightMode: brightMode,
45+
font: font
46+
)
47+
let string = AttributedString(content)
48+
DispatchQueue.main.async {
49+
self.highlighted = string
50+
}
51+
}
52+
}
2653
}
27-
}
2854

29-
struct ChatCodeSyntaxHighlighter: CodeSyntaxHighlighter {
30-
let brightMode: Bool
55+
let fenceInfo: String?
56+
let content: String
3157
let font: NSFont
32-
let colorChange: Color?
33-
var cacheController: CodeBlockHighlighterCacheController
3458

35-
init(
36-
brightMode: Bool,
37-
font: NSFont,
38-
colorChange: Color?,
39-
cacheController: CodeBlockHighlighterCacheController
40-
) {
41-
self.brightMode = brightMode
59+
@Environment(\.colorScheme) var colorScheme
60+
@StateObject var storage = Storage()
61+
@AppStorage(\.syncChatCodeHighlightTheme) var syncCodeHighlightTheme
62+
@AppStorage(\.codeForegroundColorLight) var codeForegroundColorLight
63+
@AppStorage(\.codeBackgroundColorLight) var codeBackgroundColorLight
64+
@AppStorage(\.codeForegroundColorDark) var codeForegroundColorDark
65+
@AppStorage(\.codeBackgroundColorDark) var codeBackgroundColorDark
66+
67+
init(fenceInfo: String?, content: String, font: NSFont) {
68+
self.fenceInfo = fenceInfo
69+
self.content = content.hasSuffix("\n") ? String(content.dropLast()) : content
4270
self.font = font
43-
self.colorChange = colorChange
44-
self.cacheController = cacheController
4571
}
4672

47-
func highlightCode(_ code: String, language: String?) -> Text {
48-
let key = "\(language ?? "unknown") - \(code)"
49-
if let text = cacheController.get(key) {
50-
return Text(text)
73+
var body: some View {
74+
Group {
75+
if let highlighted = storage.highlighted {
76+
Text(highlighted)
77+
} else {
78+
Text(content).font(.init(font))
79+
}
5180
}
52-
53-
let content = highlightedCodeBlock(
54-
code: code,
55-
language: language ?? "",
56-
scenario: "chat",
57-
brightMode: brightMode,
58-
font: font
59-
)
60-
let string = AttributedString(content)
61-
Task { @MainActor in
62-
cacheController.set(key, string)
81+
.onAppear {
82+
storage.highlight(debounce: false, for: self)
83+
}
84+
.onChange(of: colorScheme) { _ in
85+
storage.highlight(debounce: false, for: self)
86+
}
87+
.onChange(of: syncCodeHighlightTheme) { _ in
88+
storage.highlight(debounce: true, for: self)
89+
}
90+
.onChange(of: codeForegroundColorLight) { _ in
91+
storage.highlight(debounce: true, for: self)
92+
}
93+
.onChange(of: codeBackgroundColorLight) { _ in
94+
storage.highlight(debounce: true, for: self)
95+
}
96+
.onChange(of: codeForegroundColorDark) { _ in
97+
storage.highlight(debounce: true, for: self)
98+
}
99+
.onChange(of: codeBackgroundColorDark) { _ in
100+
storage.highlight(debounce: true, for: self)
63101
}
64-
return Text(string)
65102
}
66103
}
67104

0 commit comments

Comments
 (0)