Skip to content
Merged
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
6 changes: 5 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ let package = Package(
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "TextDiff"),
name: "TextDiff",
swiftSettings: [
.define("TESTING", .when(configuration: .debug))
]
),
.testTarget(
name: "TextDiffTests",
dependencies: [
Expand Down
33 changes: 29 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# TextDiff

TextDiff is a macOS Swift package that computes token-level diffs and renders a merged, display-only SwiftUI view backed by a custom AppKit renderer.
TextDiff is a macOS Swift package that computes token-level diffs and renders a merged, display-only diff view for both SwiftUI (`TextDiffView`) and AppKit (`NSTextDiffView`) via the same custom AppKit renderer.

![TextDiff preview](Resources/textdiff-preview.png)

Expand Down Expand Up @@ -43,6 +43,30 @@ struct DemoView: View {
}
```

## AppKit Usage

```swift
import AppKit
import TextDiff

let diffView = NSTextDiffView(
original: "This is teh old sentence.",
updated: "This is the updated sentence!",
mode: .token
)

// Constrain width in your layout. Height is intrinsic and computed from width.
diffView.translatesAutoresizingMaskIntoConstraints = false
```

You can update content in place:

```swift
diffView.mode = .character
diffView.original = "Add a diff"
diffView.updated = "Added a diff"
```

## Comparison Modes

```swift
Expand Down Expand Up @@ -101,14 +125,15 @@ struct StyledDemoView: View {
- No synthetic spacer characters are inserted into the rendered text stream.
- Chip top/bottom clipping is prevented internally via explicit line-height and vertical content insets.
- Moved text is not detected as a move; it appears as delete + insert.
- Rendering uses a custom AppKit draw view bridged into SwiftUI.
- Rendering uses a custom AppKit draw view shared by both `TextDiffView` and `NSTextDiffView`.

## Snapshot Testing

Snapshot coverage uses [Point-Free SnapshotTesting](https://github.com/pointfreeco/swift-snapshot-testing) with `swift-testing`.

- Snapshot tests live in `Tests/TextDiffTests/TextDiffSnapshotTests.swift`.
- Baselines are stored under `Tests/TextDiffTests/__Snapshots__/TextDiffSnapshotTests/`.
- AppKit snapshot tests live in `Tests/TextDiffTests/NSTextDiffSnapshotTests.swift`.
- Baselines are stored under `Tests/TextDiffTests/__Snapshots__/TextDiffSnapshotTests/` and `Tests/TextDiffTests/__Snapshots__/NSTextDiffSnapshotTests/`.
- The suite uses `@Suite(.snapshots(record: .missing))` to record only missing baselines.

Run all tests:
Expand All @@ -119,7 +144,7 @@ swift test 2>&1 | xcsift --quiet

Update baselines intentionally:

1. Temporarily switch the suite trait in `Tests/TextDiffTests/TextDiffSnapshotTests.swift` from `.missing` to `.all`.
1. Temporarily switch the suite trait in snapshot suites (for example, `Tests/TextDiffTests/TextDiffSnapshotTests.swift` and `Tests/TextDiffTests/NSTextDiffSnapshotTests.swift`) from `.missing` to `.all`.
2. Run `swift test 2>&1 | xcsift --quiet` once to rewrite baselines.
3. Switch the suite trait back to `.missing`.
4. Review snapshot image diffs in your PR before merging.
104 changes: 0 additions & 104 deletions Sources/TextDiff/AppKit/DiffCanvasView.swift

This file was deleted.

13 changes: 13 additions & 0 deletions Sources/TextDiff/AppKit/DiffTextLayoutMetrics.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import AppKit

enum DiffTextLayoutMetrics {
static func verticalTextInset(for style: TextDiffStyle) -> CGFloat {
ceil(max(2, style.chipInsets.top + 2, style.chipInsets.bottom + 2))
}

static func lineHeight(for style: TextDiffStyle) -> CGFloat {
let textHeight = ceil(style.font.ascender - style.font.descender + style.font.leading)
let chipHeight = textHeight + style.chipInsets.top + style.chipInsets.bottom
return ceil(chipHeight + max(0, style.lineSpacing))
}
}
35 changes: 17 additions & 18 deletions Sources/TextDiff/AppKit/DiffTextViewRepresentable.swift
Original file line number Diff line number Diff line change
@@ -1,31 +1,30 @@
import AppKit
import SwiftUI

enum DiffTextLayoutMetrics {
static func verticalTextInset(for style: TextDiffStyle) -> CGFloat {
ceil(max(2, style.chipInsets.top + 2, style.chipInsets.bottom + 2))
}

static func lineHeight(for style: TextDiffStyle) -> CGFloat {
let textHeight = ceil(style.font.ascender - style.font.descender + style.font.leading)
let chipHeight = textHeight + style.chipInsets.top + style.chipInsets.bottom
return ceil(chipHeight + max(0, style.lineSpacing))
}
}

struct DiffTextViewRepresentable: NSViewRepresentable {
let segments: [DiffSegment]
let original: String
let updated: String
let style: TextDiffStyle
let mode: TextDiffComparisonMode

func makeNSView(context: Context) -> DiffCanvasView {
let view = DiffCanvasView()
func makeNSView(context: Context) -> NSTextDiffView {
let view = NSTextDiffView(
original: original,
updated: updated,
style: style,
mode: mode
)
view.setContentCompressionResistancePriority(.required, for: .vertical)
view.setContentHuggingPriority(.required, for: .vertical)
view.update(segments: segments, style: style)
return view
}

func updateNSView(_ view: DiffCanvasView, context: Context) {
view.update(segments: segments, style: style)
func updateNSView(_ view: NSTextDiffView, context: Context) {
view.setContent(
original: original,
updated: updated,
style: style,
mode: mode
)
}
}
Loading
Loading