From b410fd16b76146f4116538c5b82330818dd47da9 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Fri, 7 Aug 2026 02:45:31 -0500 Subject: Add multi-language syntax highlighting (#16) Splash (Swift-only, since removed) left code rendering unhighlighted everywhere. Add Highlightr (highlight.js) behind a small SyntaxHighlighter service and use it on both surfaces: - Native file viewer: highlight NSAttributedString by language inferred from the filename, picking a light/dark theme from the trait style and re-highlighting on style changes. Falls back to plain text for unknown languages or very large files. - Markdown/org code blocks in the WKWebView (READMEs, tickets, comments): the web view disables JavaScript, so highlight server-side into inline color-styled spans. The color scheme is now part of the render/cache key since colors are baked into the HTML. Covered by unit tests for language mapping and highlighted output. --- Hutch/Views/Repositories/FileTreeView.swift | 49 +++++++++++++++++++---------- 1 file changed, 32 insertions(+), 17 deletions(-) (limited to 'Hutch/Views/Repositories/FileTreeView.swift') diff --git a/Hutch/Views/Repositories/FileTreeView.swift b/Hutch/Views/Repositories/FileTreeView.swift index 7bfce17..13f72b8 100644 --- a/Hutch/Views/Repositories/FileTreeView.swift +++ b/Hutch/Views/Repositories/FileTreeView.swift @@ -456,12 +456,19 @@ final class CodeFileUIView: UIView { private var wrapLines: Bool private var needsLineLayoutUpdate = true private var lastMeasuredCodeWidth: CGFloat = 0 + private var syntaxHighlighter: SyntaxHighlighter? + private var syntaxHighlighterTheme: SyntaxHighlightTheme? init(text: String, fileName: String, font: UIFont, wrapLines: Bool) { self.currentFont = font self.wrapLines = wrapLines super.init(frame: .zero) setupViews() + registerForTraitChanges([UITraitUserInterfaceStyle.self]) { (view: CodeFileUIView, _: UITraitCollection) in + view.rebuildRows() + view.needsLineLayoutUpdate = true + view.setNeedsLayout() + } updateContent(text: text, fileName: fileName, font: font, wrapLines: wrapLines) } @@ -583,11 +590,7 @@ final class CodeFileUIView: UIView { } rows.removeAll() - let attributedText = CodeSyntaxHighlighter.attributedText( - for: currentText, - fileName: currentFileName, - font: currentFont - ) + let attributedText = highlightedAttributedText() let lines = makeLines(from: attributedText, font: currentFont) gutterWidthConstraint?.constant = Self.gutterWidth(lineCount: lines.count, font: currentFont) @@ -600,6 +603,30 @@ final class CodeFileUIView: UIView { } } + /// Highlighting cap: above this size, skip tokenizing to keep the main + /// thread responsive and render plain, label-colored text instead. + private static let maxHighlightableLength = 100_000 + + private func highlightedAttributedText() -> NSAttributedString { + let plain = NSAttributedString( + string: currentText, + attributes: [.font: currentFont, .foregroundColor: UIColor.label] + ) + + guard currentText.count <= Self.maxHighlightableLength, + let language = SyntaxHighlighter.language(forFileName: currentFileName) else { + return plain + } + + let theme = SyntaxHighlightTheme(userInterfaceStyle: traitCollection.userInterfaceStyle) + if syntaxHighlighterTheme != theme || syntaxHighlighter == nil { + syntaxHighlighter = SyntaxHighlighter(theme: theme) + syntaxHighlighterTheme = theme + } + + return syntaxHighlighter?.attributedText(for: currentText, language: language, font: currentFont) ?? plain + } + private func makeRow(for line: CodeFileLineData) -> LineRow { let row = LineRow() @@ -737,18 +764,6 @@ private struct CodeFileLineData { let text: NSAttributedString } -private enum CodeSyntaxHighlighter { - static func attributedText(for text: String, fileName _: String, font: UIFont) -> NSAttributedString { - NSAttributedString( - string: text, - attributes: [ - .font: font, - .foregroundColor: UIColor.label - ] - ) - } -} - // MARK: - Tree Entry Row private struct TreeEntryRow: View { -- cgit v1.2.3