Repository navigation
iOS native diff viewer (ndv2): GitHub Files-changed design over mobile.workspace.diffs RPC #8154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
12ba42d
Mac diff engine + mobile.workspace.diffs RPC (ndv2 slice 1)
bb4f4d9
iOS diffs data layer: DTOs + live-client service (ndv2 slice 2)
79d2c75
iOS diff rendering core: GitHub information design (ndv2 slice 3)
azooz2003-bit 1d5ac3c
iOS diff screens: live data, nav models, split + iPad (ndv2 slice 4)
5c19124
iOS diff extras: send-to-agent, base picker, entry points (ndv2 slice 5)
5457af4
Merge commit '01cf2175dc' into feat-ios-diff-ndv2
e61f235
Build fix: explicit self in NSImage tint closure for Xcode 26.6
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"), | ||
| ] | ||
| ), | ||
| ] | ||
| ) |
59 changes: 59 additions & 0 deletions
59
Packages/iOS/CmuxDiffUI/Sources/CmuxDiffUI/CodeHighlightBatcher.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| } | ||
| } | ||
4 changes: 4 additions & 0 deletions
4
Packages/iOS/CmuxDiffUI/Sources/CmuxDiffUI/CodeHighlightCacheEntry.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| struct CodeHighlightCacheEntry: Sendable { | ||
| let value: HighlightedCode | ||
| var lastAccess: UInt64 | ||
| } |
5 changes: 5 additions & 0 deletions
5
Packages/iOS/CmuxDiffUI/Sources/CmuxDiffUI/CodeHighlightCacheKey.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } |
7 changes: 7 additions & 0 deletions
7
Packages/iOS/CmuxDiffUI/Sources/CmuxDiffUI/CodeHighlightColor+SwiftUI.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| } | ||
| } |
24 changes: 24 additions & 0 deletions
24
Packages/iOS/CmuxDiffUI/Sources/CmuxDiffUI/CodeHighlightColor.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } | ||
| } |
6 changes: 6 additions & 0 deletions
6
Packages/iOS/CmuxDiffUI/Sources/CmuxDiffUI/CodeHighlightRequest.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
16
Packages/iOS/CmuxDiffUI/Sources/CmuxDiffUI/CodeHighlightSpan.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
14
Packages/iOS/CmuxDiffUI/Sources/CmuxDiffUI/CodeHighlighting.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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? | ||
| } |
30 changes: 30 additions & 0 deletions
30
Packages/iOS/CmuxDiffUI/Sources/CmuxDiffUI/Color+DiffAdaptive.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } | ||
| } |
18 changes: 18 additions & 0 deletions
18
Packages/iOS/CmuxDiffUI/Sources/CmuxDiffUI/DiffClipboardWriter.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
32
Packages/iOS/CmuxDiffUI/Sources/CmuxDiffUI/DiffCodeText.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } | ||
| } |
7 changes: 7 additions & 0 deletions
7
Packages/iOS/CmuxDiffUI/Sources/CmuxDiffUI/DiffColorScheme+SwiftUI.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } | ||
| } |
7 changes: 7 additions & 0 deletions
7
Packages/iOS/CmuxDiffUI/Sources/CmuxDiffUI/DiffColorScheme.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } |
75 changes: 75 additions & 0 deletions
75
Packages/iOS/CmuxDiffUI/Sources/CmuxDiffUI/DiffComponentPreviews.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
🤖 Prompt for AI Agents
Source: Path instructions