Skip to content

Commit 167064a

Browse files
authored
Merge pull request #5 from iSapozhnik/feature/revert-changes
Add revertable diff interactions and hover affordance controls
2 parents eabfa87 + 641d367 commit 167064a

42 files changed

Lines changed: 2361 additions & 94 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/pr-tests.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
- name: Select latest stable Xcode
2121
uses: maxim-lobanov/setup-xcode@v1
2222
with:
23-
xcode-version: "latest-stable"
23+
xcode-version: "16.4"
2424

2525
- name: Install xcsift
2626
run: brew install xcsift
@@ -42,7 +42,7 @@ jobs:
4242
- name: Select latest stable Xcode
4343
uses: maxim-lobanov/setup-xcode@v1
4444
with:
45-
xcode-version: "latest-stable"
45+
xcode-version: "16.4"
4646

4747
- name: Install xcsift
4848
run: brew install xcsift
@@ -53,6 +53,10 @@ jobs:
5353
mkdir -p "$SNAPSHOT_ARTIFACTS"
5454
swift test --filter TextDiffSnapshotTests 2>&1 | tee "$SNAPSHOT_ARTIFACTS/swift-test.log" | xcsift --warnings
5555
56+
- name: Collect snapshot artifact bundle
57+
if: always()
58+
run: swift Scripts/collect_snapshot_artifacts.swift
59+
5660
- name: Upload snapshot artifacts
5761
if: always()
5862
uses: actions/upload-artifact@v4
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>classNames</key>
6+
<dict>
7+
<key>DiffLayouterPerformanceTests</key>
8+
<dict>
9+
<key>testLayoutPerformance1000Words()</key>
10+
<dict>
11+
<key>com.apple.dt.XCTMetric_Clock.time.monotonic</key>
12+
<dict>
13+
<key>baselineAverage</key>
14+
<real>0.0168714</real>
15+
<key>baselineIntegrationDisplayName</key>
16+
<string>Local Baseline</string>
17+
</dict>
18+
</dict>
19+
<key>testLayoutPerformance200Words()</key>
20+
<dict>
21+
<key>com.apple.dt.XCTMetric_Clock.time.monotonic</key>
22+
<dict>
23+
<key>baselineAverage</key>
24+
<real>0.003373</real>
25+
<key>baselineIntegrationDisplayName</key>
26+
<string>Local Baseline</string>
27+
</dict>
28+
</dict>
29+
<key>testLayoutPerformance500Words()</key>
30+
<dict>
31+
<key>com.apple.dt.XCTMetric_Clock.time.monotonic</key>
32+
<dict>
33+
<key>baselineAverage</key>
34+
<real>0.0082662</real>
35+
<key>baselineIntegrationDisplayName</key>
36+
<string>Local Baseline</string>
37+
</dict>
38+
</dict>
39+
</dict>
40+
</dict>
41+
</dict>
42+
</plist>

.swiftpm/xcode/xcshareddata/xcbaselines/TextDiffTests.xcbaseline/Info.plist

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,30 @@
2828
<key>targetArchitecture</key>
2929
<string>arm64</string>
3030
</dict>
31+
<key>356ED75C-DC2E-441F-90EF-CD873A212B51</key>
32+
<dict>
33+
<key>localComputer</key>
34+
<dict>
35+
<key>busSpeedInMHz</key>
36+
<integer>0</integer>
37+
<key>cpuCount</key>
38+
<integer>1</integer>
39+
<key>cpuKind</key>
40+
<string>Apple M5 Pro</string>
41+
<key>cpuSpeedInMHz</key>
42+
<integer>0</integer>
43+
<key>logicalCPUCoresPerPackage</key>
44+
<integer>18</integer>
45+
<key>modelCode</key>
46+
<string>Mac17,9</string>
47+
<key>physicalCPUCoresPerPackage</key>
48+
<integer>18</integer>
49+
<key>platformIdentifier</key>
50+
<string>com.apple.platform.macosx</string>
51+
</dict>
52+
<key>targetArchitecture</key>
53+
<string>arm64</string>
54+
</dict>
3155
</dict>
3256
</dict>
3357
</plist>

AGENTS.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,32 @@ Use the flags above (e.g., `--coverage-details`, `--coverage-path`) as needed, b
3434
## Code search
3535
- 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.
3636
- Typical commands: `rg "TODO"` (find TODOs), `rg -n --glob '!dist' pattern.swift` (search with line numbers while excluding `dist`).
37+
38+
<!-- BACKLOG.MD MCP GUIDELINES START -->
39+
40+
<CRITICAL_INSTRUCTION>
41+
42+
## BACKLOG WORKFLOW INSTRUCTIONS
43+
44+
This project uses Backlog.md MCP for all task and project management activities.
45+
46+
**CRITICAL GUIDANCE**
47+
48+
- If your client supports MCP resources, read `backlog://workflow/overview` to understand when and how to use Backlog for this project.
49+
- 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).
50+
51+
- **First time working here?** Read the overview resource IMMEDIATELY to learn the workflow
52+
- **Already familiar?** You should have the overview cached ("## Backlog.md Overview (MCP)")
53+
- **When to read it**: BEFORE creating tasks, or when you're unsure whether to track work
54+
55+
These guides cover:
56+
- Decision framework for when to create tasks
57+
- Search-first workflow to avoid duplicates
58+
- Links to detailed guides for task creation, execution, and finalization
59+
- MCP tools reference
60+
61+
You MUST read the overview resource to understand the complete workflow. The information is NOT summarized here.
62+
63+
</CRITICAL_INSTRUCTION>
64+
65+
<!-- BACKLOG.MD MCP GUIDELINES END -->

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,17 @@ Update baselines intentionally:
154154
2. Run `swift test 2>&1 | xcsift --quiet` once to rewrite baselines.
155155
3. Switch the suite trait back to `.missing`.
156156
4. Review snapshot image diffs in your PR before merging.
157+
158+
## Performance Testing
159+
160+
- Performance baselines for `DiffLayouterPerformanceTests` are stored under `.swiftpm/xcode/xcshareddata/xcbaselines/TextDiffTests.xcbaseline/`.
161+
- `swift test` runs the performance tests, but it does not surface the committed Xcode baseline values in its output.
162+
- For baseline-aware runs, use the generated SwiftPM workspace and the `TextDiff` scheme.
163+
164+
Run the layouter performance suite with Xcode:
165+
166+
```bash
167+
xcodebuild -workspace .swiftpm/xcode/package.xcworkspace -scheme TextDiff -destination 'platform=macOS' -configuration Debug test -only-testing:TextDiffTests/DiffLayouterPerformanceTests 2>&1 | xcsift
168+
```
169+
170+
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.
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#!/usr/bin/env swift
2+
3+
import AppKit
4+
import CoreImage
5+
import Foundation
6+
7+
let fileManager = FileManager.default
8+
let repoRoot = URL(fileURLWithPath: fileManager.currentDirectoryPath, isDirectory: true)
9+
let artifactsURL = URL(
10+
fileURLWithPath: ProcessInfo.processInfo.environment["SNAPSHOT_ARTIFACTS"]
11+
?? repoRoot.appendingPathComponent("snapshot-artifacts", isDirectory: true).path,
12+
isDirectory: true
13+
)
14+
let referencesRootURL = repoRoot
15+
.appendingPathComponent("Tests", isDirectory: true)
16+
.appendingPathComponent("TextDiffTests", isDirectory: true)
17+
.appendingPathComponent("__Snapshots__", isDirectory: true)
18+
19+
guard fileManager.fileExists(atPath: artifactsURL.path) else {
20+
print("No snapshot artifacts directory found at \(artifactsURL.path)")
21+
exit(0)
22+
}
23+
24+
let ciContext = CIContext(options: nil)
25+
let pngExtension = "png"
26+
27+
func loadCIImage(from url: URL) -> CIImage? {
28+
guard let image = NSImage(contentsOf: url),
29+
let tiff = image.tiffRepresentation,
30+
let bitmap = NSBitmapImageRep(data: tiff) else {
31+
return nil
32+
}
33+
return CIImage(bitmapImageRep: bitmap)
34+
}
35+
36+
func writePNG(ciImage: CIImage, to url: URL) throws {
37+
let extent = ciImage.extent.integral
38+
guard let colorSpace = CGColorSpace(name: CGColorSpace.sRGB),
39+
let cgImage = ciContext.createCGImage(ciImage, from: extent, format: .RGBA8, colorSpace: colorSpace) else {
40+
throw NSError(domain: "collect_snapshot_artifacts", code: 1)
41+
}
42+
43+
let rep = NSBitmapImageRep(cgImage: cgImage)
44+
guard let data = rep.representation(using: .png, properties: [:]) else {
45+
throw NSError(domain: "collect_snapshot_artifacts", code: 2)
46+
}
47+
try data.write(to: url)
48+
}
49+
50+
func diffImage(referenceURL: URL, failedURL: URL) -> CIImage? {
51+
guard let reference = loadCIImage(from: referenceURL),
52+
let failed = loadCIImage(from: failedURL),
53+
let filter = CIFilter(name: "CIDifferenceBlendMode") else {
54+
return nil
55+
}
56+
filter.setValue(reference, forKey: kCIInputImageKey)
57+
filter.setValue(failed, forKey: kCIInputBackgroundImageKey)
58+
return filter.outputImage
59+
}
60+
61+
let artifactFiles = fileManager.enumerator(
62+
at: artifactsURL,
63+
includingPropertiesForKeys: [.isRegularFileKey],
64+
options: [.skipsHiddenFiles]
65+
) ?? NSEnumerator()
66+
67+
var enrichedCount = 0
68+
69+
for case let fileURL as URL in artifactFiles {
70+
guard fileURL.pathExtension == pngExtension else { continue }
71+
72+
let relativePath = fileURL.path.replacingOccurrences(of: artifactsURL.path + "/", with: "")
73+
guard !relativePath.contains(".reference."),
74+
!relativePath.contains(".failed."),
75+
!relativePath.contains(".diff.") else {
76+
continue
77+
}
78+
79+
let referenceURL = referencesRootURL.appendingPathComponent(relativePath)
80+
guard fileManager.fileExists(atPath: referenceURL.path) else {
81+
continue
82+
}
83+
84+
let baseURL = fileURL.deletingPathExtension()
85+
let failedCopyURL = baseURL.appendingPathExtension("failed").appendingPathExtension(pngExtension)
86+
let referenceCopyURL = baseURL.appendingPathExtension("reference").appendingPathExtension(pngExtension)
87+
let diffURL = baseURL.appendingPathExtension("diff").appendingPathExtension(pngExtension)
88+
89+
if fileManager.fileExists(atPath: failedCopyURL.path) {
90+
try fileManager.removeItem(at: fileURL)
91+
} else {
92+
try fileManager.moveItem(at: fileURL, to: failedCopyURL)
93+
}
94+
95+
if !fileManager.fileExists(atPath: referenceCopyURL.path) {
96+
try fileManager.copyItem(at: referenceURL, to: referenceCopyURL)
97+
}
98+
99+
if !fileManager.fileExists(atPath: diffURL.path),
100+
let diff = diffImage(referenceURL: referenceURL, failedURL: failedCopyURL) {
101+
try writePNG(ciImage: diff, to: diffURL)
102+
}
103+
104+
enrichedCount += 1
105+
}
106+
107+
print("Enriched \(enrichedCount) snapshot artifact bundle(s) in \(artifactsURL.path)")

0 commit comments

Comments
 (0)