A Swift package for rendering beautiful, syntax-highlighted code diffs in macOS applications using the @pierre/diffs JavaScript library.
This is how we use it in Claw
- Rich syntax highlighting via Shiki (supports 40+ languages)
- Split and unified diff view modes
- Inline word-level change highlighting
- Dark/light theme support (auto-detects system preference)
- Scroll or wrap overflow modes
- Line click callbacks for custom interactions
- Multi-line drag selection with range callbacks
- SwiftUI-native views wrapping WKWebView
- macOS 14.0+
- Swift 6.0+
- Xcode 16.0+
Add PierreDiffsSwift to your project using Swift Package Manager:
dependencies: [
.package(url: "https://github.com/jamesrochabrun/PierreDiffsSwift.git", from: "1.0.0")
]Or in Xcode: File > Add Package Dependencies... and enter the repository URL.
import SwiftUI
import PierreDiffsSwift
struct ContentView: View {
@State private var diffStyle: DiffStyle = .split
@State private var overflowMode: OverflowMode = .scroll
var body: some View {
PierreDiffView(
oldContent: "let x = 1\nlet y = 2",
newContent: "let x = 1\nlet y = 3\nlet z = 4",
fileName: "example.swift",
diffStyle: $diffStyle,
overflowMode: $overflowMode
)
.frame(height: 400)
}
}Low-level SwiftUI view that renders diffs using WKWebView and the @pierre/diffs library.
PierreDiffView(
oldContent: String, // Original file content
newContent: String, // Updated file content
fileName: String, // Filename for syntax detection
diffStyle: Binding<DiffStyle>,
overflowMode: Binding<OverflowMode>,
onLineClick: ((Int, String) -> Void)? = nil,
onLineClickWithPosition: ((LineClickPosition, CGPoint) -> Void)? = nil,
onLineSelectionChange: ((LineSelectionRange) -> Void)? = nil,
onExpandRequest: (() -> Void)? = nil
)High-level view that processes edit tool responses (Edit, MultiEdit, Write) and renders the resulting diff.
DiffEditsView(
messageID: UUID,
editTool: EditTool,
toolParameters: [String: String],
projectPath: String? = nil,
onExpandRequest: (() -> Void)? = nil,
diffStore: DiffStateManager? = nil,
diffLifecycleState: DiffLifecycleState? = nil
)Full-screen modal wrapper for displaying diffs with a close button.
DiffModalView(
messageID: UUID,
editTool: EditTool,
toolParameters: [String: String],
projectPath: String? = nil,
diffStore: DiffStateManager? = nil,
diffLifecycleState: DiffLifecycleState? = nil,
onDismiss: @escaping () -> Void
)Compact view showing that changes have been reviewed, with tap-to-expand.
CompactDiffStatusView(
fileName: String,
timestamp: Date?,
onTapToExpand: @escaping () -> Void
)enum DiffStyle: String, CaseIterable {
case split // Side-by-side view
case unified // Single column view
}enum OverflowMode: String, CaseIterable {
case scroll // Horizontal scrolling for long lines
case wrap // Word wrap long lines
}Context about a clicked line in the diff view.
struct LineClickPosition {
let lineNumber: Int // The line number clicked
let side: String // "left", "right", or "unified"
let lineY: CGFloat // Y position in view coordinates
let lineHeight: CGFloat // Estimated line height
}Data about a multi-line drag selection in the diff view.
struct LineSelectionRange {
let startLine: Int // First line in the selection
let endLine: Int // Last line in the selection
let side: String // "left", "right", or "unified"
}enum EditTool: String {
case edit // Single edit operation
case multiEdit // Multiple edits in one file
case write // Write entire file content
}struct DiffResult: Equatable, Codable {
var filePath: String
var fileName: String
var original: String
var updated: String
var isInitial: Bool
}Observable class for managing diff state across your application.
@Observable
class DiffStateManager {
func getState(for messageID: UUID) -> DiffState
func process(diffs: [DiffResult], for messageID: UUID) async
func removeState(for messageID: UUID)
func clearAllStates()
}Processes edit tool responses to generate diff results.
struct DiffResultProcessor {
init(fileDataReader: FileDataReader)
func processEditTool(
response: String,
tool: EditTool
) async -> [DiffResult]?
}Protocol for reading file contents. Default implementation provided.
protocol FileDataReader {
var projectPath: String? { get }
func readFileContent(in paths: [String], maxTasks: Int) async throws -> [String: String]
func cancelCurrentTask()
}
// Default implementation
class DefaultFileDataReader: FileDataReaderPierreDiffView(
oldContent: oldText,
newContent: newText,
fileName: "example.swift",
diffStyle: $diffStyle,
overflowMode: $overflowMode,
onLineClickWithPosition: { position, localPoint in
// localPoint is in SwiftUI coordinates (origin top-left)
// Use to position floating editors, popovers, tooltips, etc.
print("Clicked line \(position.lineNumber) at \(localPoint)")
}
)PierreDiffView(
oldContent: oldText,
newContent: newText,
fileName: "example.swift",
diffStyle: $diffStyle,
overflowMode: $overflowMode,
onLineSelectionChange: { selection in
// Fired when the user drags across line numbers to select a range
print("Selected lines \(selection.startLine)-\(selection.endLine) on \(selection.side)")
}
)import PierreDiffsSwift
// Create processor with file reader
let processor = DiffResultProcessor(
fileDataReader: DefaultFileDataReader(projectPath: "/path/to/project")
)
// Process an edit response
let toolResponse = """
{
"file_path": "/path/to/file.swift",
"old_string": "let x = 1",
"new_string": "let x = 2"
}
"""
if let results = await processor.processEditTool(response: toolResponse, tool: .edit) {
// Use results with DiffStateManager or display directly
}DiffEditsView(
messageID: UUID(),
editTool: .edit,
toolParameters: [
"file_path": "/path/to/file.swift",
"old_string": "let x = 1",
"new_string": "let x = 2"
],
projectPath: "/path/to/project"
)struct MyView: View {
@State private var diffStore = DiffStateManager()
var body: some View {
ForEach(messages) { message in
DiffEditsView(
messageID: message.id,
editTool: message.editTool,
toolParameters: message.parameters,
diffStore: diffStore
)
}
}
}The package includes a pre-built JavaScript bundle. To rebuild it:
cd scripts
npm install
npm run buildThis generates pierre-diffs-bundle.js which should be copied to Sources/PierreDiffsSwift/Resources/.
The package supports syntax highlighting for 40+ languages including: Swift, JavaScript, TypeScript, Python, Go, Rust, Java, Kotlin, C, C++, Ruby, PHP, SQL, HTML, CSS, JSON, YAML, Markdown, and more.
Language is auto-detected from the filename extension.
MIT