Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
69d7136
Fix the five bugs from the review: new snapshots, re-runs, and docume…
SimonCropp Oct 3, 2026
a01dfc1
Add the findings of the six per-area reviews to todo.md
SimonCropp Oct 3, 2026
b15c4e7
Fix four bugs in the Windows head: a spinner for good, the first wind…
SimonCropp Oct 3, 2026
4df5e33
Queue the macOS viewer's keys and clicks, and hand one over per poll
SimonCropp Oct 3, 2026
97dd53e
Wrap the macOS footer's buttons onto more rows, and keep the status o…
SimonCropp Oct 3, 2026
d71988a
Follow the macOS scroller's knob without AppKit's tracking loop
SimonCropp Oct 3, 2026
86c947f
Draw the macOS viewer's text without JetBrains Mono's ligatures
SimonCropp Oct 3, 2026
8e011c0
Enable the macOS pane scroller explicitly
SimonCropp Oct 3, 2026
bbe98d4
Stop the tray's Accept all deleting the verified file a move in the s…
SimonCropp Oct 3, 2026
2e939b0
List the tray's deletes as an accept-all begins, and carry out only t…
SimonCropp Oct 3, 2026
915ece2
Hold the tray's deletes when the viewer that owns the queue does not …
SimonCropp Oct 3, 2026
38facd5
Start the tray on an STA thread, so the debug view's Copy copies
SimonCropp Oct 3, 2026
8f59009
Stage the tray's inline queue when the Windows session ends
SimonCropp Oct 3, 2026
6c63f6c
Keep a tracked pair's diff tool when it arrives again without one
SimonCropp Oct 3, 2026
508e04c
Append an inline Snapshot call in front of ConfigureAwait, ToTask and…
SimonCropp Oct 3, 2026
839ed81
Append to the first entry point in the member that has no Snapshot call
SimonCropp Oct 3, 2026
8c91fbf
Indent an F# accept from the column the call's expression starts at
SimonCropp Oct 3, 2026
43fa882
Remove the statement when a Snapshot call is all that is called on a …
SimonCropp Oct 3, 2026
fff265a
Recognise a queued call site that has moved, and ask whose entry a li…
SimonCropp Oct 3, 2026
04d1aa3
Label what a test run stages with its framework, and add a clear scop…
SimonCropp Oct 3, 2026
21d9ca4
Take only the Snapshot call where its value is awaited, assigned, ret…
SimonCropp Oct 3, 2026
36af560
Decide accept-all on Linux by the Shift key, not by the case of the l…
SimonCropp Oct 3, 2026
4a1da8f
Let the right pane's text be selected on Linux when the left pane sho…
SimonCropp Oct 3, 2026
bf7cbed
Wrap the Linux footer's buttons and give its status a line when they …
SimonCropp Oct 3, 2026
f78b783
Draw the characters JetBrains Mono lacks from the machine's fonts on …
SimonCropp Oct 3, 2026
48cf3ea
Start the viewer in its own folder rather than the test host's
SimonCropp Oct 3, 2026
48f4f18
Report a viewer that exits with a failure as not launched
SimonCropp Oct 3, 2026
c9a5e77
Start tools declared without ShellExecute so they inherit nothing fro…
SimonCropp Oct 3, 2026
19843f1
Describe the second round of fixes, and close them in todo.md
SimonCropp Oct 3, 2026
5e5b73a
Approve the macOS Minimal baseline drawn without ligatures
SimonCropp Oct 3, 2026
96da57c
Say in todo.md what the first macOS run showed
SimonCropp Oct 3, 2026
9850231
Rebuild native renderer binaries (#923)
github-actions[bot] Oct 3, 2026
30c7c1c
Drop four macOS baselines for one run, to take them again without lig…
SimonCropp Oct 3, 2026
e2e0a8c
Make the benchmarks runnable again, and give the viewer a project of …
SimonCropp Oct 3, 2026
25308c2
Measure the viewer's model before changing it
SimonCropp Oct 3, 2026
e67b335
Diff only the lines both sides have, and bound what a diff can cost
SimonCropp Oct 3, 2026
c217f5e
Build a screen when the state changes, not once a frame
SimonCropp Oct 3, 2026
1c59b3c
Bound what the watch over an owned queue's files costs
SimonCropp Oct 3, 2026
d9238be
Carry out a group's accept as a batch, off the render thread
SimonCropp Oct 3, 2026
08d8b12
Ask the embedded font which characters draw a cell wide
SimonCropp Oct 3, 2026
b8b1d31
Take the four macOS baselines again, drawn without ligatures
SimonCropp Oct 3, 2026
e340bae
Describe the model's performance changes, and the benchmarks, in clau…
SimonCropp Oct 3, 2026
4d725b4
Measure what a bulk accept costs apart from the applying
SimonCropp Oct 3, 2026
e936d5d
Keep a batch's bookkeeping from growing with the square of the queue
SimonCropp Oct 3, 2026
857dabd
Measure opening a pair of documents, to the right side's first page a…
SimonCropp Oct 3, 2026
ed7499b
Draw both sides of a document at once, each call with a clock of its own
SimonCropp Oct 3, 2026
4eb2242
Say that both sides of a document are drawn at once
SimonCropp Oct 3, 2026
e658eb5
Wait for a decode without holding the thread it needs
SimonCropp Oct 3, 2026
290716e
Benchmark InlineStaging.Clear for a passing inline verification
SimonCropp Oct 3, 2026
4553106
Keep the list of staging directories between clears rather than walki…
SimonCropp Oct 3, 2026
0e6db87
Benchmark finding out that nothing owns the viewer port
SimonCropp Oct 3, 2026
d9813be
Ask the listener table before connecting to a viewer port nobody may …
SimonCropp Oct 3, 2026
45e09c3
Benchmark accepting every inline snapshot in one source file
SimonCropp Oct 3, 2026
458a562
Apply several inline patches to a file with one read and one write
SimonCropp Oct 3, 2026
3bf52bd
Rent the scan's map, and keep its spans in order rather than hashed
SimonCropp Oct 3, 2026
1a63133
Write a file's snapshots together when a batch accepts them
SimonCropp Oct 3, 2026
343e375
Skip what a macOS repaint cannot reach, and keep the lines the last d…
SimonCropp Oct 3, 2026
33d0964
Draw an enlarged macOS picture below its own size from a copy at that…
SimonCropp Oct 3, 2026
3319945
Give the WinForms head a benchmark project of its own
SimonCropp Oct 3, 2026
fa0255a
Measure a paint of rows far longer than their pane
SimonCropp Oct 3, 2026
1633a01
Hand GDI+ no more of a row than its pane has cells for
SimonCropp Oct 3, 2026
dbaa9a9
Measure a paint of a large picture zoomed to less than its own size
SimonCropp Oct 3, 2026
4307204
Copy an enlarged picture out of one scaled copy rather than scaling i…
SimonCropp Oct 3, 2026
088f619
Measure what the WinForms head does with an idle frame's screen
SimonCropp Oct 3, 2026
b67d042
Describe the library's, the inline patcher's and the two heads' perfo…
SimonCropp Oct 3, 2026
7d6b517
Measure what a frame costs the Linux head to draw
SimonCropp Oct 3, 2026
dc265d2
Draw the checkerboard behind a picture as one quad, and only where it…
SimonCropp Oct 3, 2026
8415f3e
Measure what a second costs the Linux head while nothing happens
SimonCropp Oct 3, 2026
2e005cd
Leave a window alone when the frame it would draw is the one on the s…
SimonCropp Oct 3, 2026
caedd49
Say what deview_present does now where three comments still described…
SimonCropp Oct 3, 2026
28f1528
Close the performance items in todo.md, and describe the Linux head's…
SimonCropp Oct 3, 2026
fdde121
Quote the numbers of a quiet pass where todo.md and claude.md give one
SimonCropp Oct 3, 2026
1cda517
Rebuild native renderer binaries (#924)
github-actions[bot] Oct 3, 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
190 changes: 176 additions & 14 deletions claude.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions docs/diff-tool.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Behavior depends on if an [EmptyFiles](https://github.com/SimonCropp/EmptyFiles)
* If an EmptyFiles can be found matching the current extension, then the tool will be launched to compare the input to that empty file.
* If no EmptyFiles can be found no tool will be launched.

The bundled [DiffEngineViewer](/docs/viewer.md) is the exception. It needs no file to compare against, so none is written: the input is shown against an empty side, and stays that way until it is accepted. That holds for every extension, including those with no EmptyFiles.


## Detected difference behavior

Expand Down
13 changes: 8 additions & 5 deletions docs/inline.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,9 @@ DiffEngineViewer --inline --source <source file> --line <number> < the.inlinepat

For the producing side — a test library with a failing inline snapshot:

* `DiffRunner.AddInlineAsync(patch)` queues a patch with whatever owns the port, launching the bundled viewer when nothing does. Returns `Queued`, `Disabled` (build servers, continuous testing and AI CLIs included), or `NoViewerFound` — the caller's cue to stage files and fall back to a text diff.
* `DiffRunner.SettleInline(sourceFile, line)` drops the pending entry for a call site, for when a previously failing test passes. Unknown entries and an absent owner are no-ops, so call it freely. The settle carries the running framework, so a multi-targeted run only settles its own variant of a conflicted entry. That framework is the running process's, which makes this the test run's verb and only the test run's: a surface applying a patch of its own wants `SettleAppliedInline`, [below](#applying-a-patch-from-another-surface). Pass `memberName` and `value` too: `value` is what the passing call's expected argument holds, as the library compared it (for F#, after `SourceLanguage.SnapshotValue`). Once an accept above a call site moves it, its line no longer names its entry and the member is the fallback. `value` narrows that fallback to an entry the value settles, one anchored to it or waiting to become it. Without `value`, a passing call can settle the entry of a failing sibling in the same member.
* `DiffRunner.AddInlineAsync(patch)` queues a patch with whatever owns the port, launching the bundled viewer when nothing does. Returns `Queued`, `Disabled` (build servers, continuous testing and AI CLIs included), or `NoViewerFound` — the caller's cue to stage files and fall back to a text diff. A viewer that was started and exited with a failure before it held the queue is `NoViewerFound` as well: one too old for the launch, or with no runtime to run on, took nothing.
* `DiffRunner.SettleInline(sourceFile, line)` drops the pending entry for a call site, for when a previously failing test passes. Unknown entries and an absent owner are no-ops, so call it freely. The settle carries the running framework, so a multi-targeted run only settles its own variant of a conflicted entry. That framework is the running process's, which makes this the test run's verb and only the test run's: a surface applying a patch of its own wants `SettleAppliedInline`, [below](#applying-a-patch-from-another-surface). Pass `memberName` and `value` too: `value` is what the passing call's expected argument holds, as the library compared it (for F#, after `SourceLanguage.SnapshotValue`). Once an accept above a call site moves it, its line no longer names its entry and the member is the fallback. `value` narrows that fallback to an entry the value settles, one anchored to it or waiting to become it. Without `value`, a passing call can settle the entry of a failing sibling in the same member. The line can also come to name another call's entry, so one found under it that was queued from a different member is left alone unless `value` settles it. A failing re-run of a call site that has moved is recognised the same way, by its member, test and anchor, and updates its entry rather than queueing a second one beside it.
* `InlineStaging.Settle(sourceFile, line, memberName)` clears what the running framework staged for a call site that now passes. `InlinePatchFile.Write` labels a patch that carries no framework with the running one, so a framework that passes does not clear what another one staged. `InlineStaging.Clear` still clears every framework's, which is what retiring a call site wants.
* An absent owner is also remembered. A port found with nothing listening is taken as still unowned for ten minutes, and the sends that only tell the owner something — settle, retire, a move or delete to track, the first attempt to queue a patch — return without connecting while that stands. A refused loopback connection is not free on Windows: the firewall's stealth mode, on by default, drops the reset a closed port would answer with, so each refusal takes two seconds, and a green run settling once per inline verification was spending minutes on them. Anything that has to reach an owner probes for itself before launching a viewer, and that probe, like every listing, always connects and corrects the memory with what it finds.
* `AddInlineAsync` stamps `patch.Framework` with the consuming project's target framework ("net9.0", "net48") unless the caller already set it, which is what lets the owner tell a re-run from another framework disagreeing. The value is the `$(TargetFramework)` the package's build targets stamp into the project's runtimeconfig, read back rather than asked of the process — in a hosted test run the entry assembly is the runner (testhost, ReSharperTestRunner), whose framework is not the project's — with the running runtime's version as the fallback for consumers without the targets. Callers may also set `patch.TestName`, which the viewer uses to group and label the queue; without it, items are labeled by call site.
* Set `patch.OriginalExpression` from `CallerArgumentExpression` where the language supplies one, and `patch.OriginalValue` — the previous expected argument's value — where it does not. One of the two is what stops a patch rewriting the wrong call site when the file has moved since the run. `patch.MemberName` from `CallerMemberName` narrows it further, and is supported everywhere including F#.
Expand All @@ -100,7 +101,7 @@ originalValue: {base64}
memberName: {base64}
```

`lineHint` is a hint: locating the call is content anchored, so a file that shifted since the test run still patches, and one whose call site changed reports rather than corrupts. `mode` is `Set` (replace or insert the expected argument), `Append` (add a Snapshot call where none exists yet), or `Remove` (delete the call, used when migrating a snapshot back to a file). `testName` and `framework` are optional provenance — who produced the patch and under which target framework — parsed tolerantly: absent means unknown, and unknown trailing lines are ignored.
`lineHint` is a hint: locating the call is content anchored, so a file that shifted since the test run still patches, and one whose call site changed reports rather than corrupts. `mode` is `Set` (replace or insert the expected argument), `Append` (add a Snapshot call where none exists yet), or `Remove` (delete the call, used when migrating a snapshot back to a file). A `Remove` of a call that is a statement by itself on a variable, `settings.Snapshot("old");`, takes the statement with it, since the variable alone is no statement, and is `NotFound` where the statement cannot be lifted out whole. Where the call's value is awaited, assigned, returned or passed, the call alone goes. `testName` and `framework` are optional provenance — who produced the patch and under which target framework — parsed tolerantly: absent means unknown, and unknown trailing lines are ignored.

The anchor is `originalExpression`, the source text of the argument the test run saw. A producer whose language does not implement `CallerArgumentExpression` sends `originalValue` instead — the argument's *value* — and the call whose literal parses to it is the one rewritten. Either identifies the call; the expression is used where both arrived, being what the source actually says. With neither, all a patch has is the hint, and a literal that differs is taken as the snapshot that changed rather than as a conflict — otherwise an inline snapshot could be accepted once and never updated.

Expand Down Expand Up @@ -131,7 +132,7 @@ await Verify(value).Snapshot(
""");
```

**Indentation.** The call line's own leading whitespace, plus one level. What a level is comes from two places: the character from the call site, so a tab indented method inside a space indented file stays on tabs, and the width from the file, taken as the most common run of whitespace its lines add to the line above. A file that indents by two spaces gets two; four spaces is a fallback for a file with no indentation to read, not a default. Blank lines inside the content are emitted bare, so the literal carries no trailing whitespace.
**Indentation.** The call line's own leading whitespace, plus one level. In F#, where indentation is syntax, a call with something before it on its line (`do!`, `let! result =`) is measured from the column its expression starts at instead, since a continuation has to sit right of that column to compile. What a level is comes from two places: the character from the call site, so a tab indented method inside a space indented file stays on tabs, and the width from the file, taken as the most common run of whitespace its lines add to the line above. A file that indents by two spaces gets two; four spaces is a fallback for a file with no indentation to read, not a default. Blank lines inside the content are emitted bare, so the literal carries no trailing whitespace.

**Line endings.** The file's dominant ending, with the content normalised to it, so a patch produced on one platform applies cleanly on another. A file that mixes endings keeps every ending it already had: only the spliced span is written, and the rest of the file — encoding, BOM and all — is preserved byte for byte.

Expand Down Expand Up @@ -168,7 +169,7 @@ The alternative, writing content at the left margin so the literal means itself,

One thing C# can do that F# cannot is widen a delimiter (FS1232), so content containing `"""`, or starting or ending with a quote, has no multi-line form at all and takes a regular literal on one source line. Single line content is always a regular literal, escaping what both languages escape (`\` `"` `\a` `\b` `\f` `\t` `\v` `\n` `\r`) and `\uXXXX` for the rest, since F# has no `\0` or `\e`.

Two syntax differences show up in `Append` and in an argument list. F# does not apply the implicit conversion that lets a `SettingsTask` be awaited, so an F# test ends its chain with `ToTask`; `Snapshot` returns the `SettingsTask`, so an appended call goes in front of that rather than after it. And an argument binds to a parameter with `=`, so an inserted named argument is `expected = "..."`.
Two syntax differences show up in `Append` and in an argument list. F# does not apply the implicit conversion that lets a `SettingsTask` be awaited, so an F# test ends its chain with `ToTask`; `Snapshot` returns the `SettingsTask`, so an appended call goes in front of that rather than after it. The same holds in C# for `ConfigureAwait` and `GetAwaiter`, and for `ToTask` there too: none of them returns something `Snapshot` can be called on, so in either language an appended call goes in front of the first of them. And an argument binds to a parameter with `=`, so an inserted named argument is `expected = "..."`.

```fsharp
// before
Expand Down Expand Up @@ -214,6 +215,8 @@ For the staging fallback, where no viewer could be resolved and the patch is a f

`AlreadyApplied` covers an `Append` onto a call that already has a `Snapshot` call holding this same content, which is what a multi-targeted project transitioning to inline meets: every framework fails the call site and queues an append, and whichever is accepted first writes the literal the rest are carrying. Only a chained call holding *different* content is `NotFound` — that one genuinely cannot say what it wants until it has been re-run against the literal now in the source. Accepting one framework's append before the others have run does mean the queue never sees them together, so a real disagreement between frameworks is reported as that `NotFound` rather than as a conflict to pick from.

That is the call on the recorded line. Where the line no longer lands on the call and the patch names its member, an `Append` passes over the calls in that member that already have a `Snapshot` call and takes the first that has none.

`Remove` mode patches are configuration changes with nothing to review: apply them directly; `AddInlineAsync` refuses them.


Expand Down
2 changes: 2 additions & 0 deletions docs/mdsource/diff-tool.source.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Behavior depends on if an [EmptyFiles](https://github.com/SimonCropp/EmptyFiles)
* If an EmptyFiles can be found matching the current extension, then the tool will be launched to compare the input to that empty file.
* If no EmptyFiles can be found no tool will be launched.

The bundled [DiffEngineViewer](/docs/viewer.md) is the exception. It needs no file to compare against, so none is written: the input is shown against an empty side, and stays that way until it is accepted. That holds for every extension, including those with no EmptyFiles.


## Detected difference behavior

Expand Down
Loading
Loading