Skip to content

jamesrochabrun/PierreDiffsSwift

Repository files navigation

PierreDiffsSwift

Buy me a coffee

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

Dark Mode

Image Image

Light Mode

Image Image

Features

  • 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

Requirements

  • macOS 14.0+
  • Swift 6.0+
  • Xcode 16.0+

Installation

Swift Package Manager

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.

Quick Start

Basic Diff Rendering

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)
    }
}

API Reference

Views

PierreDiffView

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
)

DiffEditsView

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
)

DiffModalView

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
)

CompactDiffStatusView

Compact view showing that changes have been reviewed, with tap-to-expand.

CompactDiffStatusView(
    fileName: String,
    timestamp: Date?,
    onTapToExpand: @escaping () -> Void
)

Types

DiffStyle

enum DiffStyle: String, CaseIterable {
    case split    // Side-by-side view
    case unified  // Single column view
}

OverflowMode

enum OverflowMode: String, CaseIterable {
    case scroll  // Horizontal scrolling for long lines
    case wrap    // Word wrap long lines
}

LineClickPosition

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
}

LineSelectionRange

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"
}

EditTool

enum EditTool: String {
    case edit      // Single edit operation
    case multiEdit // Multiple edits in one file
    case write     // Write entire file content
}

DiffResult

struct DiffResult: Equatable, Codable {
    var filePath: String
    var fileName: String
    var original: String
    var updated: String
    var isInitial: Bool
}

State Management

DiffStateManager

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()
}

Processing

DiffResultProcessor

Processes edit tool responses to generate diff results.

struct DiffResultProcessor {
    init(fileDataReader: FileDataReader)

    func processEditTool(
        response: String,
        tool: EditTool
    ) async -> [DiffResult]?
}

Protocols

FileDataReader

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: FileDataReader

Examples

Line Click with Position

PierreDiffView(
    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)")
    }
)

Multi-Line Drag Selection

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)")
    }
)

Processing Edit Tool Response

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
}

Using DiffEditsView with Parameters

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"
)

Managing Multiple Diffs

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
            )
        }
    }
}

Rebuilding the JavaScript Bundle

The package includes a pre-built JavaScript bundle. To rebuild it:

cd scripts
npm install
npm run build

This generates pierre-diffs-bundle.js which should be copied to Sources/PierreDiffsSwift/Resources/.

Supported Languages

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.

License

MIT

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors