import Combine import ComposableArchitecture import DebounceFunction import Foundation import MarkdownUI import Perception import SharedUIComponents import SwiftUI /// Use this instead of the built in ``CodeBlockView`` to highlight code blocks asynchronously, /// so that the UI doesn't freeze when rendering large code blocks. struct AsyncCodeBlockView: View { @Perceptible class Storage { static let queue = DispatchQueue( label: "chat-code-block-highlight", qos: .userInteractive, attributes: .concurrent ) var highlighted: AttributedString? @PerceptionIgnored var debounceFunction: DebounceFunction? @PerceptionIgnored private var highlightTask: Task? init() { debounceFunction = .init(duration: 0.5, block: { [weak self] view in self?.highlight(for: view) }) } func highlight(debounce: Bool, for view: AsyncCodeBlockView) { if debounce { Task { await debounceFunction?(view) } } else { highlight(for: view) } } func highlight(for view: AsyncCodeBlockView) { highlightTask?.cancel() let content = view.content let language = view.fenceInfo ?? "" let brightMode = view.colorScheme != .dark let font = view.font highlightTask = Task { let string = await withUnsafeContinuation { continuation in Self.queue.async { let content = CodeHighlighting.highlightedCodeBlock( code: content, language: language, scenario: "chat", brightMode: brightMode, font: font ) continuation.resume(returning: AttributedString(content)) } } try Task.checkCancellation() await MainActor.run { self.highlighted = string } } } } let fenceInfo: String? let content: String let font: NSFont @Environment(\.colorScheme) var colorScheme @State var storage = Storage() @AppStorage(\.syncChatCodeHighlightTheme) var syncCodeHighlightTheme @AppStorage(\.codeForegroundColorLight) var codeForegroundColorLight @AppStorage(\.codeBackgroundColorLight) var codeBackgroundColorLight @AppStorage(\.codeForegroundColorDark) var codeForegroundColorDark @AppStorage(\.codeBackgroundColorDark) var codeBackgroundColorDark init(fenceInfo: String?, content: String, font: NSFont) { self.fenceInfo = fenceInfo self.content = content.hasSuffix("\n") ? String(content.dropLast()) : content self.font = font } var body: some View { WithPerceptionTracking { Group { if let highlighted = storage.highlighted { Text(highlighted) } else { Text(content).font(.init(font)) } } .lineLimit(nil) .fixedSize(horizontal: false, vertical: true) .frame(maxWidth: .infinity, alignment: .leading) .onAppear { storage.highlight(debounce: false, for: self) } .onChange(of: colorScheme) { _ in storage.highlight(debounce: false, for: self) } .onChange(of: syncCodeHighlightTheme) { _ in storage.highlight(debounce: true, for: self) } .onChange(of: codeForegroundColorLight) { _ in storage.highlight(debounce: true, for: self) } .onChange(of: codeBackgroundColorLight) { _ in storage.highlight(debounce: true, for: self) } .onChange(of: codeForegroundColorDark) { _ in storage.highlight(debounce: true, for: self) } .onChange(of: codeBackgroundColorDark) { _ in storage.highlight(debounce: true, for: self) } } } }