Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c92016f
initial impl
iSapozhnik Feb 23, 2026
c8d3439
revert changes
iSapozhnik Feb 23, 2026
37e42e2
feat: enhance diff revert by handling word boundary spacing
iSapozhnik Feb 24, 2026
ab3c705
refactor: enhance handling of punctuation replacing whitespace
iSapozhnik Feb 24, 2026
dbc2742
feat: enhance word boundary handling in diff revert actions
iSapozhnik Feb 24, 2026
d5d0f8f
feat: add debug overlay for invisible characters
iSapozhnik Feb 24, 2026
89064d3
infra
iSapozhnik Feb 27, 2026
d545c1c
fixing trailing characters dissapear
iSapozhnik Feb 27, 2026
14bab31
tests baseline
iSapozhnik Mar 25, 2026
8c4f53c
layouter performance improvements
iSapozhnik Mar 25, 2026
90b4ed6
snap chip border to pixels
iSapozhnik Mar 25, 2026
5d23d09
fixing artifacts path for GH actions
iSapozhnik Mar 25, 2026
d8b43a0
ci: upload snapshot reference and diff artifacts
iSapozhnik Mar 25, 2026
478e8c5
proper artifacts collection and a new snapshot test
iSapozhnik Mar 25, 2026
162e3e7
snapshots for the new border snap
iSapozhnik Mar 25, 2026
e58ed59
color space changed to more sable
iSapozhnik Mar 25, 2026
e288c88
new snapshots due to color space change
iSapozhnik Mar 25, 2026
ef1627d
regenerate snapshots on Sequoia
iSapozhnik Mar 25, 2026
ccf8b65
chore: update snapshot test images
iSapozhnik Mar 25, 2026
ee8fe52
lock xcode to 16.4
iSapozhnik Mar 25, 2026
345307c
another reference
iSapozhnik Mar 25, 2026
b44dd00
color profile
iSapozhnik Mar 25, 2026
3f3aef3
Revert "color profile"
iSapozhnik Mar 25, 2026
d1a5518
lock xcode
iSapozhnik Mar 25, 2026
f55ae3c
move test to AppKit
iSapozhnik Mar 25, 2026
6ecff9d
opaque color
iSapozhnik Mar 25, 2026
e08b98b
Fix Unicode-safe revert spacing
iSapozhnik Mar 25, 2026
78cd390
Make equal-segment cursor recovery robust
iSapozhnik Mar 25, 2026
641d367
Clean up review nits
iSapozhnik Mar 25, 2026
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
8 changes: 6 additions & 2 deletions .github/workflows/pr-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Select latest stable Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "latest-stable"
xcode-version: "16.4"

- name: Install xcsift
run: brew install xcsift
Expand All @@ -42,7 +42,7 @@ jobs:
- name: Select latest stable Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "latest-stable"
xcode-version: "16.4"

- name: Install xcsift
run: brew install xcsift
Expand All @@ -53,6 +53,10 @@ jobs:
mkdir -p "$SNAPSHOT_ARTIFACTS"
swift test --filter TextDiffSnapshotTests 2>&1 | tee "$SNAPSHOT_ARTIFACTS/swift-test.log" | xcsift --warnings

- name: Collect snapshot artifact bundle
if: always()
run: swift Scripts/collect_snapshot_artifacts.swift

- name: Upload snapshot artifacts
if: always()
uses: actions/upload-artifact@v4
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>classNames</key>
<dict>
<key>DiffLayouterPerformanceTests</key>
<dict>
<key>testLayoutPerformance1000Words()</key>
<dict>
<key>com.apple.dt.XCTMetric_Clock.time.monotonic</key>
<dict>
<key>baselineAverage</key>
<real>0.0168714</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
<key>testLayoutPerformance200Words()</key>
<dict>
<key>com.apple.dt.XCTMetric_Clock.time.monotonic</key>
<dict>
<key>baselineAverage</key>
<real>0.003373</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
<key>testLayoutPerformance500Words()</key>
<dict>
<key>com.apple.dt.XCTMetric_Clock.time.monotonic</key>
<dict>
<key>baselineAverage</key>
<real>0.0082662</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
</dict>
</dict>
</dict>
</plist>
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,30 @@
<key>targetArchitecture</key>
<string>arm64</string>
</dict>
<key>356ED75C-DC2E-441F-90EF-CD873A212B51</key>
<dict>
<key>localComputer</key>
<dict>
<key>busSpeedInMHz</key>
<integer>0</integer>
<key>cpuCount</key>
<integer>1</integer>
<key>cpuKind</key>
<string>Apple M5 Pro</string>
<key>cpuSpeedInMHz</key>
<integer>0</integer>
<key>logicalCPUCoresPerPackage</key>
<integer>18</integer>
<key>modelCode</key>
<string>Mac17,9</string>
<key>physicalCPUCoresPerPackage</key>
<integer>18</integer>
<key>platformIdentifier</key>
<string>com.apple.platform.macosx</string>
</dict>
<key>targetArchitecture</key>
<string>arm64</string>
</dict>
</dict>
</dict>
</plist>
29 changes: 29 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,32 @@ Use the flags above (e.g., `--coverage-details`, `--coverage-path`) as needed, b
## Code search
- Use [`ripgrep`](https://github.com/BurntSushi/ripgrep) (`rg`) for searching within files—it is much faster than grep/ack/ag, respects `.gitignore`, and has smart defaults.
- Typical commands: `rg "TODO"` (find TODOs), `rg -n --glob '!dist' pattern.swift` (search with line numbers while excluding `dist`).

<!-- BACKLOG.MD MCP GUIDELINES START -->

<CRITICAL_INSTRUCTION>

## BACKLOG WORKFLOW INSTRUCTIONS

This project uses Backlog.md MCP for all task and project management activities.

**CRITICAL GUIDANCE**

- If your client supports MCP resources, read `backlog://workflow/overview` to understand when and how to use Backlog for this project.
- If your client only supports tools or the above request fails, call `backlog.get_workflow_overview()` tool to load the tool-oriented overview (it lists the matching guide tools).

- **First time working here?** Read the overview resource IMMEDIATELY to learn the workflow
- **Already familiar?** You should have the overview cached ("## Backlog.md Overview (MCP)")
- **When to read it**: BEFORE creating tasks, or when you're unsure whether to track work

These guides cover:
- Decision framework for when to create tasks
- Search-first workflow to avoid duplicates
- Links to detailed guides for task creation, execution, and finalization
- MCP tools reference

You MUST read the overview resource to understand the complete workflow. The information is NOT summarized here.

</CRITICAL_INSTRUCTION>

<!-- BACKLOG.MD MCP GUIDELINES END -->
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,17 @@ Update baselines intentionally:
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.

## Performance Testing

- Performance baselines for `DiffLayouterPerformanceTests` are stored under `.swiftpm/xcode/xcshareddata/xcbaselines/TextDiffTests.xcbaseline/`.
- `swift test` runs the performance tests, but it does not surface the committed Xcode baseline values in its output.
- For baseline-aware runs, use the generated SwiftPM workspace and the `TextDiff` scheme.

Run the layouter performance suite with Xcode:

```bash
xcodebuild -workspace .swiftpm/xcode/package.xcworkspace -scheme TextDiff -destination 'platform=macOS' -configuration Debug test -only-testing:TextDiffTests/DiffLayouterPerformanceTests 2>&1 | xcsift
```

If you need the raw measured averages for comparison, run the same command once without `xcsift` because XCTest prints the per-test values directly in the plain `xcodebuild` output.
107 changes: 107 additions & 0 deletions Scripts/collect_snapshot_artifacts.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env swift

import AppKit
import CoreImage
import Foundation

let fileManager = FileManager.default
let repoRoot = URL(fileURLWithPath: fileManager.currentDirectoryPath, isDirectory: true)
let artifactsURL = URL(
fileURLWithPath: ProcessInfo.processInfo.environment["SNAPSHOT_ARTIFACTS"]
?? repoRoot.appendingPathComponent("snapshot-artifacts", isDirectory: true).path,
isDirectory: true
)
let referencesRootURL = repoRoot
.appendingPathComponent("Tests", isDirectory: true)
.appendingPathComponent("TextDiffTests", isDirectory: true)
.appendingPathComponent("__Snapshots__", isDirectory: true)

guard fileManager.fileExists(atPath: artifactsURL.path) else {
print("No snapshot artifacts directory found at \(artifactsURL.path)")
exit(0)
}

let ciContext = CIContext(options: nil)
let pngExtension = "png"

func loadCIImage(from url: URL) -> CIImage? {
guard let image = NSImage(contentsOf: url),
let tiff = image.tiffRepresentation,
let bitmap = NSBitmapImageRep(data: tiff) else {
return nil
}
return CIImage(bitmapImageRep: bitmap)
}

func writePNG(ciImage: CIImage, to url: URL) throws {
let extent = ciImage.extent.integral
guard let colorSpace = CGColorSpace(name: CGColorSpace.sRGB),
let cgImage = ciContext.createCGImage(ciImage, from: extent, format: .RGBA8, colorSpace: colorSpace) else {
throw NSError(domain: "collect_snapshot_artifacts", code: 1)
}

let rep = NSBitmapImageRep(cgImage: cgImage)
guard let data = rep.representation(using: .png, properties: [:]) else {
throw NSError(domain: "collect_snapshot_artifacts", code: 2)
}
try data.write(to: url)
}

func diffImage(referenceURL: URL, failedURL: URL) -> CIImage? {
guard let reference = loadCIImage(from: referenceURL),
let failed = loadCIImage(from: failedURL),
let filter = CIFilter(name: "CIDifferenceBlendMode") else {
return nil
}
filter.setValue(reference, forKey: kCIInputImageKey)
filter.setValue(failed, forKey: kCIInputBackgroundImageKey)
return filter.outputImage
}

let artifactFiles = fileManager.enumerator(
at: artifactsURL,
includingPropertiesForKeys: [.isRegularFileKey],
options: [.skipsHiddenFiles]
) ?? NSEnumerator()

var enrichedCount = 0

for case let fileURL as URL in artifactFiles {
guard fileURL.pathExtension == pngExtension else { continue }

let relativePath = fileURL.path.replacingOccurrences(of: artifactsURL.path + "/", with: "")
guard !relativePath.contains(".reference."),
!relativePath.contains(".failed."),
!relativePath.contains(".diff.") else {
continue
}

let referenceURL = referencesRootURL.appendingPathComponent(relativePath)
guard fileManager.fileExists(atPath: referenceURL.path) else {
continue
}

let baseURL = fileURL.deletingPathExtension()
let failedCopyURL = baseURL.appendingPathExtension("failed").appendingPathExtension(pngExtension)
let referenceCopyURL = baseURL.appendingPathExtension("reference").appendingPathExtension(pngExtension)
let diffURL = baseURL.appendingPathExtension("diff").appendingPathExtension(pngExtension)

if fileManager.fileExists(atPath: failedCopyURL.path) {
try fileManager.removeItem(at: fileURL)
} else {
try fileManager.moveItem(at: fileURL, to: failedCopyURL)
}

if !fileManager.fileExists(atPath: referenceCopyURL.path) {
try fileManager.copyItem(at: referenceURL, to: referenceCopyURL)
}

if !fileManager.fileExists(atPath: diffURL.path),
let diff = diffImage(referenceURL: referenceURL, failedURL: failedCopyURL) {
try writePNG(ciImage: diff, to: diffURL)
}

enrichedCount += 1
}

print("Enriched \(enrichedCount) snapshot artifact bundle(s) in \(artifactsURL.path)")
Loading
Loading