Skip to content

Add DiffParsing protocol for custom diff formats#9

Merged
tornikegomareli merged 1 commit into
mainfrom
feat/diff-parsing-protocol
May 13, 2026
Merged

Add DiffParsing protocol for custom diff formats#9
tornikegomareli merged 1 commit into
mainfrom
feat/diff-parsing-protocol

Conversation

@tornikegomareli
Copy link
Copy Markdown
Owner

@tornikegomareli tornikegomareli commented May 13, 2026

Summary

  • Introduces public protocol DiffParsing so consumers can plug in formats other than standard unified diff (annotated diffs, server-side payloads, JSON patches, language-server output, …) without forking the library.
  • Makes DiffFile, DiffHunk, DiffLine, and DiffLine.LineType public with public initialisers, so custom parsers can build the renderer's domain model directly.
  • Adds .diffParser(_:) view modifier following the existing .diffTheme / .diffLineNumbers env-driven pattern.
  • UnifiedDiffParser wraps today's parser and remains the environment default — no behaviour change for existing callers.

Why

Today the renderer is hard-wired to DiffParser.parse(...) so any consumer with a non-unified format has to either convert their data to unified diff first (lossy, fragile) or fork the library. A small extension point keeps the library focused on rendering while letting downstream apps own their own parsing logic.

Usage

struct MyAnnotatedDiffParser: DiffParsing {
  let filePath: String
  func parse(_ diffText: String) async throws -> [DiffFile] {
    // map your format → [DiffFile] using the public initialisers
  }
}

DiffRenderer(diffText: rawText)
  .diffParser(MyAnnotatedDiffParser(filePath: "foo.swift"))
  .diffTheme(.dark)

Test plan

  • swift build clean
  • swift test — 11/11 pass, including new DiffParsingTests (parity with legacy parser, custom-parser construction via public inits, env default, empty-result path)
  • Existing DiffParserTests (7) still pass unchanged
  • Benchmark suite unchanged
  • Source-compatible: existing DiffRenderer(diffText:).diffTheme(.dark) / .diffLineNumbers(...) calls work as before

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for custom diff format parsing through a new .diffParser() modifier, allowing injection of custom parsing implementations.
    • Non-standard diff formats can now be handled via custom implementations.
  • Documentation

    • Updated README with a comprehensive guide and practical Swift example for implementing custom diff formats.

Review Change Stack

Make DiffFile / DiffHunk / DiffLine / LineType public with public
initializers so consumers can construct the renderer's domain model
directly. Introduce a public `DiffParsing` protocol; `DiffRenderer` now
reads the active parser from the environment via a new
`.diffParser(_:)` view modifier. The built-in unified-diff parser ships
as `UnifiedDiffParser` and remains the environment default, so existing
callers see no change.

This lets downstream apps render formats other than standard unified
diff (annotated diffs, server-side payloads, JSON patches, language-
server output, ...) without modifying the library: implement
`DiffParsing`, map your format to `[DiffFile]`, inject it. Tests cover
parity with the legacy parser, custom-parser construction via the
public initializers, the env default, and the empty-result path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

📝 Walkthrough

Walkthrough

This PR enables custom diff parsing in gitdiff by exposing DiffFile, DiffHunk, and DiffLine models as public Sendable types, introducing a DiffParsing protocol for parsing custom formats, injecting the parser into DiffRenderer via SwiftUI environment, and providing comprehensive tests and documentation for the extensibility feature.

Changes

Custom Diff Parser Extensibility

Layer / File(s) Summary
Public data models for custom parser construction
Sources/gitdiff/Models/DiffFile.swift, Sources/gitdiff/Models/DiffHunk.swift, Sources/gitdiff/Models/DiffLine.swift
DiffFile, DiffHunk, DiffLine, and LineType are now public and Sendable with parameterized initializers, allowing external code to construct diff domain models directly via public let properties and explicit init signatures.
Parsing protocol and default implementation
Sources/gitdiff/Core/DiffParsing.swift
DiffParsing protocol defines async/throwing parse contract for converting diff text to [DiffFile]; UnifiedDiffParser provides the default implementation by delegating to the existing DiffParser.parse logic.
Environment key, accessor, and view modifier
Sources/gitdiff/DiffEnvironment.swift
DiffParserKey environment key defaults to UnifiedDiffParser, EnvironmentValues exposes diffParser getter/setter, and View.diffParser(_:) modifier allows setting the active parser in the environment hierarchy.
Renderer parser injection
Sources/gitdiff/Views/DiffRenderer.swift
DiffRenderer reads @Environment(\.diffParser) and calls parser.parse(diffText) in its async task instead of the hardcoded DiffParser.parse call, decoupling parsing from rendering.
Tests validating protocol and documentation
Tests/gitdiffTests/DiffParsingTests.swift, README.md
Comprehensive test suite verifies UnifiedDiffParser equivalence, custom parser direct construction, environment defaults, and empty results; README documents .diffParser(_:) modifier and provides a complete custom-format example.

Sequence Diagram

sequenceDiagram
  participant SwiftUI as SwiftUI App
  participant DiffRenderer
  participant Environment as EnvironmentValues
  participant Parser as DiffParsing
  participant UnifiedDiffParser
  participant DiffParser
  SwiftUI->>DiffRenderer: diffText provided
  DiffRenderer->>Environment: read \.diffParser
  Environment-->>DiffRenderer: active parser (default: UnifiedDiffParser)
  DiffRenderer->>Parser: parser.parse(diffText)
  alt Custom Parser
    Parser->>Parser: custom parse logic
  else Unified Default
    Parser->>UnifiedDiffParser: delegate
    UnifiedDiffParser->>DiffParser: parse(diffText)
    DiffParser-->>UnifiedDiffParser: [DiffFile]
  end
  Parser-->>DiffRenderer: [DiffFile]
  DiffRenderer->>DiffRenderer: update parsedFiles state
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A parser once fixed to one way,
Now swaps like a switcheroo in the fray,
With public models and protocol's call,
Custom diff formats? We handle them all!
SwiftUI injects, the renderer receives,
A pluggable dream—the extension achieves! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: introducing a DiffParsing protocol to enable custom diff format support, which is the primary objective of this pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/diff-parsing-protocol

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/gitdiff/Models/DiffLine.swift (1)

34-39: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Move documentation comments above each case; trailing /// are not recognized as DocC documentation.

With LineType now public, documentation comments need to precede the declaration to be picked up by DocC and appear in generated docs and Quick Help. Trailing /// comments on the same line as each case are treated as regular comments and will be invisible to both documentation generation and IDE Quick Help.

📝 Proposed fix
   /// Type of diff line.
   public enum LineType: Sendable {
-    case added    /// Line was added (+)
-    case removed  /// Line was removed (-)
-    case context  /// Unchanged context line
-    case header   /// Section header
+    /// Line was added (+).
+    case added
+    /// Line was removed (-).
+    case removed
+    /// Unchanged context line.
+    case context
+    /// Section header.
+    case header
   }
🤖 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 `@Sources/gitdiff/Models/DiffLine.swift` around lines 34 - 39, The trailing
inline comments on the LineType cases are not picked up by DocC; move each
documentation comment to its own line above the corresponding enum case so
DocC/Quick Help recognizes them — update the enum LineType so that /// Line was
added (+) precedes case added, /// Line was removed (-) precedes case removed,
/// Unchanged context line precedes case context, and /// Section header
precedes case header (preserving wording).
🧹 Nitpick comments (1)
Sources/gitdiff/Models/DiffFile.swift (1)

18-49: ⚡ Quick win

Consider conforming public models to Equatable/Hashable.

Now that DiffFile, DiffHunk, DiffLine, and DiffLine.LineType are part of the public surface, callers (and the new DiffParsingTests parity assertions) will likely benefit from value-equality and hashing. All stored properties are value types and Equatable-by-default, so the conformances are essentially free.

Note: the synthesized Equatable includes id, which means two DiffFiles with identical content but different UUIDs would compare unequal. If you want content-based equality (useful for legacy-parser parity tests), provide an explicit == that ignores id.

♻️ Proposed conformance additions
-public struct DiffFile: Identifiable, Sendable {
+public struct DiffFile: Identifiable, Sendable, Hashable {

Apply the same Hashable (which implies Equatable) addition to DiffHunk, DiffLine, and DiffLine.LineType.

🤖 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 `@Sources/gitdiff/Models/DiffFile.swift` around lines 18 - 49, Add synthesized
Equatable/Hashable conformances for the public models DiffFile, DiffHunk,
DiffLine, and DiffLine.LineType by making each type conform to Hashable (which
also provides Equatable); ensure all stored value-type properties are included
so callers and DiffParsingTests can compare/hash instances. For DiffFile decide
equality semantics: the synthesized Equatable will include id (UUID) which makes
files with identical content but different ids unequal — if you need
content-based equality for parity tests, implement an explicit static func == in
DiffFile that compares oldPath, newPath, hunks, isBinary, and isRenamed but
ignores id, and also implement Hashable accordingly so hashing matches the
chosen equality.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@Sources/gitdiff/Views/DiffRenderer.swift`:
- Around line 70-72: The Task is currently keyed only by diffText so updating
the .diffParser at runtime won’t retrigger parsing; change the .task(id:) key to
include the parser as well (e.g., use a tuple of diffText and the parser
identity) so that when the parser instance changes the task restarts and
parsedFiles is refreshed; you can either make the parser Hashable and use id:
(diffText, parser) or use id: (diffText, ObjectIdentifier(parser as AnyObject))
and keep the body calling self.parsedFiles = try? await parser.parse(diffText).

---

Outside diff comments:
In `@Sources/gitdiff/Models/DiffLine.swift`:
- Around line 34-39: The trailing inline comments on the LineType cases are not
picked up by DocC; move each documentation comment to its own line above the
corresponding enum case so DocC/Quick Help recognizes them — update the enum
LineType so that /// Line was added (+) precedes case added, /// Line was
removed (-) precedes case removed, /// Unchanged context line precedes case
context, and /// Section header precedes case header (preserving wording).

---

Nitpick comments:
In `@Sources/gitdiff/Models/DiffFile.swift`:
- Around line 18-49: Add synthesized Equatable/Hashable conformances for the
public models DiffFile, DiffHunk, DiffLine, and DiffLine.LineType by making each
type conform to Hashable (which also provides Equatable); ensure all stored
value-type properties are included so callers and DiffParsingTests can
compare/hash instances. For DiffFile decide equality semantics: the synthesized
Equatable will include id (UUID) which makes files with identical content but
different ids unequal — if you need content-based equality for parity tests,
implement an explicit static func == in DiffFile that compares oldPath, newPath,
hunks, isBinary, and isRenamed but ignores id, and also implement Hashable
accordingly so hashing matches the chosen equality.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1e3296d4-021d-4797-a6b2-a961e796daf0

📥 Commits

Reviewing files that changed from the base of the PR and between 7d86dec and e0110bb.

📒 Files selected for processing (8)
  • README.md
  • Sources/gitdiff/Core/DiffParsing.swift
  • Sources/gitdiff/DiffEnvironment.swift
  • Sources/gitdiff/Models/DiffFile.swift
  • Sources/gitdiff/Models/DiffHunk.swift
  • Sources/gitdiff/Models/DiffLine.swift
  • Sources/gitdiff/Views/DiffRenderer.swift
  • Tests/gitdiffTests/DiffParsingTests.swift

Comment on lines 70 to 72
.task(id: diffText) {
self.parsedFiles = try? await DiffParser.parse(diffText)
self.parsedFiles = try? await parser.parse(diffText)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Re-parse when parser changes, not only when diffText changes.

Line 70 keys the task only by diffText, so changing .diffParser(...) at runtime won’t trigger a new parse and can show stale output.

Suggested adjustment
-    .task(id: diffText) {
+    .task(id: "\(diffText)|\(String(reflecting: type(of: parser)))") {
       self.parsedFiles = try? await parser.parse(diffText)
     }
📝 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
.task(id: diffText) {
self.parsedFiles = try? await DiffParser.parse(diffText)
self.parsedFiles = try? await parser.parse(diffText)
}
.task(id: "\(diffText)|\(String(reflecting: type(of: parser)))") {
self.parsedFiles = try? await parser.parse(diffText)
}
🤖 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 `@Sources/gitdiff/Views/DiffRenderer.swift` around lines 70 - 72, The Task is
currently keyed only by diffText so updating the .diffParser at runtime won’t
retrigger parsing; change the .task(id:) key to include the parser as well
(e.g., use a tuple of diffText and the parser identity) so that when the parser
instance changes the task restarts and parsedFiles is refreshed; you can either
make the parser Hashable and use id: (diffText, parser) or use id: (diffText,
ObjectIdentifier(parser as AnyObject)) and keep the body calling
self.parsedFiles = try? await parser.parse(diffText).

@tornikegomareli tornikegomareli merged commit bb75a93 into main May 13, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant