Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Packages/iOS/CmuxDiffUI/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 48 additions & 0 deletions Packages/iOS/CmuxDiffUI/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// swift-tools-version: 6.0

import PackageDescription

let package = Package(
name: "CmuxDiffUI",
defaultLocalization: "en",
platforms: [
.iOS(.v18),
.macOS(.v14),
],
products: [
.library(name: "CmuxDiffUI", targets: ["CmuxDiffUI"]),
],
dependencies: [
.package(path: "../CmuxMobileRPC"),
.package(path: "../CmuxMobileShell"),
.package(
url: "https://github.com/smittytone/HighlighterSwift",
exact: "3.1.0"
),
],
targets: [
.target(
name: "CmuxDiffUI",
dependencies: [
"CmuxMobileRPC",
"CmuxMobileShell",
.product(name: "Highlighter", package: "HighlighterSwift"),
],
resources: [.process("Resources")],
swiftSettings: [
.swiftLanguageMode(.v6),
.enableUpcomingFeature("ExistentialAny"),
.enableUpcomingFeature("InternalImportsByDefault"),
]
),
.testTarget(
name: "CmuxDiffUITests",
dependencies: ["CmuxDiffUI", "CmuxMobileRPC"],
swiftSettings: [
.swiftLanguageMode(.v6),
.enableUpcomingFeature("ExistentialAny"),
.enableUpcomingFeature("InternalImportsByDefault"),
]
),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
actor CodeHighlightBatcher {
private let highlighter: any CodeHighlighting
private let capacity: Int
private let batchSize: Int
private var cache: [CodeHighlightCacheKey: CodeHighlightCacheEntry] = [:]
private var accessCounter: UInt64 = 0

init(highlighter: any CodeHighlighting, capacity: Int = 512, batchSize: Int = 24) {
self.highlighter = highlighter
self.capacity = max(1, capacity)
self.batchSize = max(1, batchSize)
}

func highlights(for requests: [CodeHighlightRequest]) async -> [String: HighlightedCode] {
var result: [String: HighlightedCode] = [:]
for batchStart in stride(from: 0, to: requests.count, by: batchSize) {
let batchEnd = min(requests.count, batchStart + batchSize)
for request in requests[batchStart..<batchEnd] {
let key = CodeHighlightCacheKey(
language: request.language,
line: request.line,
colorScheme: request.colorScheme
)
if let cached = cachedValue(for: key) {
result[request.id] = cached
continue
}
if let highlighted = await highlighter.highlight(
line: request.line,
language: request.language,
colorScheme: request.colorScheme
) {
insert(highlighted, for: key)
result[request.id] = highlighted
}
}
await Task.yield()
}
return result
}

private func cachedValue(for key: CodeHighlightCacheKey) -> HighlightedCode? {
guard var entry = cache[key] else { return nil }
accessCounter &+= 1
entry.lastAccess = accessCounter
cache[key] = entry
return entry.value
}

private func insert(_ value: HighlightedCode, for key: CodeHighlightCacheKey) {
accessCounter &+= 1
cache[key] = CodeHighlightCacheEntry(value: value, lastAccess: accessCounter)
guard cache.count > capacity,
let leastRecent = cache.min(by: { $0.value.lastAccess < $1.value.lastAccess })?.key else {
return
}
cache.removeValue(forKey: leastRecent)
}
Comment on lines +50 to +58

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Amortize LRU cache eviction to prevent O(N) scaling per insertion.

Finding the minimum value in the dictionary takes O(N) time. Once the cache reaches its capacity, every single insertion triggers this O(N) scan. During a large file load where thousands of lines are highlighted, this results in O(L × N) complexity on a hot path, violating the algorithmic complexity guidelines.

Consider evicting a batch of the oldest entries at once (e.g., 10%) to amortize the eviction cost. As per path instructions, avoid repeated hot-path sorting, filtering, or unbenchmarked slower algorithms for scalable collections.

⚡ Proposed fix to amortize eviction
     private func insert(_ value: HighlightedCode, for key: CodeHighlightCacheKey) {
         accessCounter &+= 1
         cache[key] = CodeHighlightCacheEntry(value: value, lastAccess: accessCounter)
-        guard cache.count > capacity,
-              let leastRecent = cache.min(by: { $0.value.lastAccess < $1.value.lastAccess })?.key else {
+        guard cache.count > capacity else {
             return
         }
-        cache.removeValue(forKey: leastRecent)
+        // Evict the oldest 10% to amortize the scan cost
+        let evictCount = max(1, capacity / 10)
+        let oldestKeys = cache
+            .sorted(by: { $0.value.lastAccess < $1.value.lastAccess })
+            .prefix(evictCount)
+            .map(\.key)
+        
+        for oldestKey in oldestKeys {
+            cache.removeValue(forKey: oldestKey)
+        }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private func insert(_ value: HighlightedCode, for key: CodeHighlightCacheKey) {
accessCounter &+= 1
cache[key] = CodeHighlightCacheEntry(value: value, lastAccess: accessCounter)
guard cache.count > capacity,
let leastRecent = cache.min(by: { $0.value.lastAccess < $1.value.lastAccess })?.key else {
return
}
cache.removeValue(forKey: leastRecent)
}
private func insert(_ value: HighlightedCode, for key: CodeHighlightCacheKey) {
accessCounter &+= 1
cache[key] = CodeHighlightCacheEntry(value: value, lastAccess: accessCounter)
guard cache.count > capacity else {
return
}
// Evict the oldest 10% to amortize the scan cost
let evictCount = max(1, capacity / 10)
let oldestKeys = cache
.sorted(by: { $0.value.lastAccess < $1.value.lastAccess })
.prefix(evictCount)
.map(\.key)
for oldestKey in oldestKeys {
cache.removeValue(forKey: oldestKey)
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Packages/iOS/CmuxDiffUI/Sources/CmuxDiffUI/CodeHighlightBatcher.swift` around
lines 50 - 58, Update CodeHighlightBatcher.insert(_:for:) to amortize LRU
eviction instead of scanning for and removing only one entry on every
over-capacity insertion. When capacity is exceeded, identify and remove a
bounded batch of the oldest entries (such as roughly 10% of capacity), while
preserving the cache capacity limit and avoiding repeated full-cache eviction
work on the hot path.

Source: Path instructions

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
struct CodeHighlightCacheEntry: Sendable {
let value: HighlightedCode
var lastAccess: UInt64
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
struct CodeHighlightCacheKey: Sendable, Hashable {
let language: String?
let line: String
let colorScheme: DiffColorScheme
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import SwiftUI

extension CodeHighlightColor {
var swiftUIColor: Color {
Color(red: red, green: green, blue: blue, opacity: alpha)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/// A platform-neutral syntax color safe to cross concurrency boundaries.
public struct CodeHighlightColor: Sendable, Equatable {
/// Red component in the range zero through one.
public let red: Double
/// Green component in the range zero through one.
public let green: Double
/// Blue component in the range zero through one.
public let blue: Double
/// Alpha component in the range zero through one.
public let alpha: Double

/// Creates a platform-neutral RGBA color.
/// - Parameters:
/// - red: Red component.
/// - green: Green component.
/// - blue: Blue component.
/// - alpha: Alpha component.
public init(red: Double, green: Double, blue: Double, alpha: Double) {
self.red = red
self.green = green
self.blue = blue
self.alpha = alpha
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
struct CodeHighlightRequest: Identifiable, Sendable, Hashable {
let id: String
let language: String?
let line: String
let colorScheme: DiffColorScheme
}
16 changes: 16 additions & 0 deletions Packages/iOS/CmuxDiffUI/Sources/CmuxDiffUI/CodeHighlightSpan.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/// One foreground-color run produced by syntax highlighting.
public struct CodeHighlightSpan: Sendable, Equatable {
/// Text covered by this run.
public let text: String
/// Highlighted foreground color, or `nil` for the view's primary color.
public let foreground: CodeHighlightColor?

/// Creates a syntax-highlight run.
/// - Parameters:
/// - text: Text covered by the run.
/// - foreground: Optional syntax foreground color.
public init(text: String, foreground: CodeHighlightColor?) {
self.text = text
self.foreground = foreground
}
}
14 changes: 14 additions & 0 deletions Packages/iOS/CmuxDiffUI/Sources/CmuxDiffUI/CodeHighlighting.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/// Asynchronously converts one source line into sendable syntax-color runs.
public protocol CodeHighlighting: Sendable {
/// Highlights one line without blocking the main actor.
/// - Parameters:
/// - line: Plain source text.
/// - language: Highlight.js language identifier, when known.
/// - colorScheme: Appearance whose GitHub theme should be used.
/// - Returns: Highlighted runs, or `nil` when highlighting is unavailable.
func highlight(
line: String,
language: String?,
colorScheme: DiffColorScheme
) async -> HighlightedCode?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
public import SwiftUI

#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif

extension Color {
/// Creates a dynamic color from explicit light and dark tokens.
/// - Parameters:
/// - light: Color used in light appearance.
/// - dark: Color used in dark appearance.
/// - Returns: A platform-adaptive SwiftUI color.
public static func diffAdaptive(light: Color, dark: Color) -> Color {
#if canImport(UIKit)
return Color(uiColor: UIColor { traits in
traits.userInterfaceStyle == .dark ? UIColor(dark) : UIColor(light)
})
#elseif canImport(AppKit)
return Color(nsColor: NSColor(name: nil) { appearance in
appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua
? NSColor(dark)
: NSColor(light)
})
#else
return dark
#endif
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Foundation

#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif

struct DiffClipboardWriter: Sendable {
@MainActor func copy(_ value: String) {
#if canImport(UIKit)
UIPasteboard.general.string = value
#elseif canImport(AppKit)
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(value, forType: .string)
#endif
}
}
32 changes: 32 additions & 0 deletions Packages/iOS/CmuxDiffUI/Sources/CmuxDiffUI/DiffCodeText.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import SwiftUI

struct DiffCodeText: View {
@Environment(\.diffTheme) private var theme
let row: DiffRowSnapshot
let highlighted: HighlightedCode?

var body: some View {
HStack(spacing: 0) {
if !row.intralineSpans.isEmpty {
ForEach(Array(row.intralineSpans.enumerated()), id: \.offset) { _, span in
Text(span.text)
.background(span.isEmphasized ? emphasisFill : Color.clear)
}
} else if let highlighted, !highlighted.spans.isEmpty {
ForEach(Array(highlighted.spans.enumerated()), id: \.offset) { _, span in
Text(span.text)
.foregroundStyle(span.foreground?.swiftUIColor ?? Color.primary)
}
} else {
Text(row.text.isEmpty ? " " : row.text)
}
}
.font(.system(size: 12, design: .monospaced))
.fixedSize(horizontal: true, vertical: false)
.textSelection(.enabled)
}

private var emphasisFill: Color {
row.kind == .deletion ? theme.deletionEmphasisFill : theme.additionEmphasisFill
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import SwiftUI

extension DiffColorScheme {
init(_ colorScheme: ColorScheme) {
self = colorScheme == .dark ? .dark : .light
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/// A sendable appearance value used by background highlighting work.
public enum DiffColorScheme: String, Sendable, Hashable {
/// Light interface appearance.
case light
/// Dark interface appearance.
case dark
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#if DEBUG
import SwiftUI

#Preview("Full diff fixture") {
NavigationStack {
DiffFixtureScreen(
defaults: UserDefaults(suiteName: "DiffFixturePreview") ?? .standard
)
}
}

#Preview("Summary header") {
DiffSummaryHeaderView(
fileCount: 11,
additions: 3215,
deletions: 23,
viewedCount: 3,
baseLabel: "main · working tree",
baseKind: .workingTree,
ignoreWhitespace: false,
selectBase: { _ in },
setIgnoreWhitespace: { _ in }
)
.padding()
}

#Preview("File list row") {
let patchSet = DiffFixtureFactory().patchSet()
let file = patchSet.files[0]
let rows = DiffRowBuilder().rows(
path: file.summary.path,
hunks: {
if case let .loaded(hunks) = file.content { hunks } else { [] }
}()
)
DiffFileListRow(
state: DiffFilePresentationState(
file: file,
isViewed: false,
isCollapsed: false,
rows: rows,
splitRows: SplitDiffPairer().pair(rows: rows)
),
toggleViewed: {},
toggleCollapsed: {}
)
.padding()
}

#Preview("Unified diff row") {
let row = DiffRowSnapshot(
id: "preview",
kind: .addition,
oldLine: nil,
newLine: 42,
text: "let greeting = \"Hello\"",
hunkIndex: 0
)
DiffUnifiedRowView(
row: row,
highlighted: nil,
expand: { _ in },
quickNoteTarget: DiffQuickNoteTarget(
id: "preview",
path: "App.swift",
oldLineRange: nil,
newLineRange: 42...42,
hunkHeader: nil,
excerpt: "+let greeting = \"Hello\""
),
quickNoteAvailable: false,
openQuickNote: { _ in }
)
}
#endif
Loading