diff --git a/claude.md b/claude.md
index df75dbab..fd3c2228 100644
--- a/claude.md
+++ b/claude.md
@@ -190,6 +190,16 @@ apart.
enrichment on top. Copying is `IViewerWindow.SetClipboard` rather than a `ViewerActions` member,
because a clipboard belongs to a toolkit the way a window does, and it is answered before the
owner link: the text is already in this process.
+- An entry opens at its first change, not line 1: every path that changes what is being read goes
+ through `ViewerSession.Open`, so none resets to row 0 on its own. The minimal view ("Changes
+ only", `SessionState.Minimal`) is a second `DiffView` built with each entry - changes plus
+ `DiffView.Context` rows either side, longer unchanged runs folded into one `RowKind.Folded` row.
+ Scrolling, the scrollbar and navigation count rows of the view on screen; a selection stays in
+ rows of the entry (`ViewerSession.Drag` unfolds the head's rows), so it survives switching views
+ and a fold inside it copies what it stands for. Navigation is defined over where it lands a
+ change - `Context` rows under the top - which is what makes previous undo next. The fold kind and
+ the `m` key are additive ABI values with no `DEVIEW_VERSION` bump, as `DEVIEW_QUEUE_HEADER` was:
+ a stale library draws a fold as a plain row.
- Queue tooltips are composed once in `QueueProjection`, not per head, and are **null when they
would only repeat the row**. Labels are already the shortest distinguishing form, so the tip is
what the label left off — path, test, frameworks, failure text. `QueueTooltipTests` snapshots the
diff --git a/docs/mdsource/viewer.source.md b/docs/mdsource/viewer.source.md
index 0caee6f5..be0ebbc1 100644
--- a/docs/mdsource/viewer.source.md
+++ b/docs/mdsource/viewer.source.md
@@ -70,7 +70,8 @@ Nothing is written to disk for inline review. The patch travels over stdin, or o
| Key | Action |
| --- | --- |
| `Up` `Down` `PgUp` `PgDn` `Home` `End` | Scroll |
-| `n` `p` | Next and previous change |
+| `n` `p` | Next and previous change (also the **Next change** and **Prev change** buttons) |
+| `m` | Show only the changes, or every line (also the **Changes only** button) |
| `Tab` `Shift+Tab` | Next and previous pending item |
| `a` | Accept |
| `Shift+A` | Accept all |
@@ -81,6 +82,15 @@ Nothing is written to disk for inline review. The patch travels over stdin, or o
| `q` `Esc` | Close |
+## Moving between changes
+
+A comparison opens scrolled to its first change, with three lines of context above it, rather than at line 1. **Next change** and **Prev change** move from one change to the next, putting each in the same place under the top of the pane, and each is disabled when no change is left in its direction.
+
+**Changes only** switches to a minimal view: each change with the three lines either side of it, and every longer run of unchanged lines folded into one row saying how many lines it stands for. The button then reads **All lines**, which switches back. Switching keeps the line being read where it is on screen, and the choice holds while moving through the queue. Images are never folded, because their rows are their properties and each is worth reading.
+
+A fold is only a view. A selection that spans one copies the lines it stands for, since those are what lies between the selection's two ends, and the status line names the stretch of the file on screen rather than a count of rows: sixteen rows in the minimal view can read `lines 1-30 of 40`.
+
+
## Selecting and copying
Drag across either pane to select text, and `Ctrl+C` to copy it. `Ctrl+A` selects one whole pane: the one something is already selected in, or the received side when nothing is. On macOS the Edit menu carries both, so `Cmd+C` and `Cmd+A` work there too.
diff --git a/docs/viewer.md b/docs/viewer.md
index 7806896f..eb3c5c98 100644
--- a/docs/viewer.md
+++ b/docs/viewer.md
@@ -77,7 +77,8 @@ Nothing is written to disk for inline review. The patch travels over stdin, or o
| Key | Action |
| --- | --- |
| `Up` `Down` `PgUp` `PgDn` `Home` `End` | Scroll |
-| `n` `p` | Next and previous change |
+| `n` `p` | Next and previous change (also the **Next change** and **Prev change** buttons) |
+| `m` | Show only the changes, or every line (also the **Changes only** button) |
| `Tab` `Shift+Tab` | Next and previous pending item |
| `a` | Accept |
| `Shift+A` | Accept all |
@@ -88,6 +89,15 @@ Nothing is written to disk for inline review. The patch travels over stdin, or o
| `q` `Esc` | Close |
+## Moving between changes
+
+A comparison opens scrolled to its first change, with three lines of context above it, rather than at line 1. **Next change** and **Prev change** move from one change to the next, putting each in the same place under the top of the pane, and each is disabled when no change is left in its direction.
+
+**Changes only** switches to a minimal view: each change with the three lines either side of it, and every longer run of unchanged lines folded into one row saying how many lines it stands for. The button then reads **All lines**, which switches back. Switching keeps the line being read where it is on screen, and the choice holds while moving through the queue. Images are never folded, because their rows are their properties and each is worth reading.
+
+A fold is only a view. A selection that spans one copies the lines it stands for, since those are what lies between the selection's two ends, and the status line names the stretch of the file on screen rather than a count of rows: sixteen rows in the minimal view can read `lines 1-30 of 40`.
+
+
## Selecting and copying
Drag across either pane to select text, and `Ctrl+C` to copy it. `Ctrl+A` selects one whole pane: the one something is already selected in, or the received side when nothing is. On macOS the Edit menu carries both, so `Cmd+C` and `Cmd+A` work there too.
diff --git a/native/include/deview.h b/native/include/deview.h
index 4fa0963c..884625c4 100644
--- a/native/include/deview.h
+++ b/native/include/deview.h
@@ -31,7 +31,13 @@ enum DeviewRowKind {
DEVIEW_ROW_ADDED = 1,
DEVIEW_ROW_REMOVED = 2,
DEVIEW_ROW_MODIFIED = 3,
- DEVIEW_ROW_FILLER = 4
+ DEVIEW_ROW_FILLER = 4,
+ /*
+ * A run of unchanged lines the minimal view left out, as one row: its text says how many, and
+ * it has no line number. Drawn dimmed on a band of its own. A library built before this kind
+ * existed draws it as a plain unchanged row, which still reads.
+ */
+ DEVIEW_ROW_FOLDED = 5
};
enum DeviewButtonFlags {
@@ -47,7 +53,7 @@ enum DeviewQueueFlags {
typedef struct DeviewRow {
int32_t kind;
- /* -1 when the row is filler and has no line number. */
+ /* -1 when the row is filler or folded and has no line number. */
int32_t lineNumber;
int32_t textOffset;
int32_t textLength;
@@ -184,7 +190,9 @@ enum DeviewKey {
/* Ctrl+C, and Cmd+C on macOS. */
DEVIEW_KEY_COPY = 16,
/* Ctrl+A, which is why plain A must be reported as accept only when no modifier is held. */
- DEVIEW_KEY_SELECT_ALL = 17
+ DEVIEW_KEY_SELECT_ALL = 17,
+ /* M: every line, or only the changes and the lines around them. */
+ DEVIEW_KEY_TOGGLE_MINIMAL = 18
};
typedef struct DeviewInput {
diff --git a/native/src/deview.cpp b/native/src/deview.cpp
index 2db13fbe..2b9b1734 100644
--- a/native/src/deview.cpp
+++ b/native/src/deview.cpp
@@ -241,6 +241,9 @@ ImU32 RowColour(int kind)
return IM_COL32(233, 129, 129, 255);
case DEVIEW_ROW_MODIFIED:
return IM_COL32(231, 197, 113, 255);
+ /* Dimmed like the gutter, since what it says is about the file rather than from it. */
+ case DEVIEW_ROW_FOLDED:
+ return IM_COL32(130, 130, 130, 255);
default:
return IM_COL32(212, 212, 212, 255);
}
@@ -256,6 +259,10 @@ ImU32 RowBackground(int kind)
return IM_COL32(84, 40, 40, 255);
case DEVIEW_ROW_MODIFIED:
return IM_COL32(74, 64, 32, 255);
+ /* A shade lighter than filler, so a fold reads as a break in the file rather than as a
+ * line of it or as padding. */
+ case DEVIEW_ROW_FOLDED:
+ return IM_COL32(34, 34, 34, 255);
default:
return 0;
}
@@ -540,6 +547,7 @@ int ReadKey()
if (IsKeyPressed(KEY_END)) return DEVIEW_KEY_END;
if (IsKeyPressed(KEY_N)) return DEVIEW_KEY_NEXT_CHANGE;
if (IsKeyPressed(KEY_P)) return DEVIEW_KEY_PREVIOUS_CHANGE;
+ if (IsKeyPressed(KEY_M)) return DEVIEW_KEY_TOGGLE_MINIMAL;
if (IsKeyPressed(KEY_TAB)) return IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT)
? DEVIEW_KEY_PREVIOUS_ITEM
: DEVIEW_KEY_NEXT_ITEM;
diff --git a/native/swift/Sources/Deview/MainMenu.swift b/native/swift/Sources/Deview/MainMenu.swift
index 89826f11..11044945 100644
--- a/native/swift/Sources/Deview/MainMenu.swift
+++ b/native/swift/Sources/Deview/MainMenu.swift
@@ -105,6 +105,7 @@ enum MainMenu {
.separator(),
command("Next Change (n)", DEVIEW_KEY_NEXT_CHANGE, target),
command("Previous Change (p)", DEVIEW_KEY_PREVIOUS_CHANGE, target),
+ command("Toggle Changes Only (m)", DEVIEW_KEY_TOGGLE_MINIMAL, target),
.separator(),
command("Next Pending (⇥)", DEVIEW_KEY_NEXT_ITEM, target),
command("Previous Pending (⇧⇥)", DEVIEW_KEY_PREVIOUS_ITEM, target)
diff --git a/native/swift/Sources/Deview/Palette.swift b/native/swift/Sources/Deview/Palette.swift
index 410ed5d2..bfa6f954 100644
--- a/native/swift/Sources/Deview/Palette.swift
+++ b/native/swift/Sources/Deview/Palette.swift
@@ -33,6 +33,10 @@ enum Palette {
static let rule = grey(70)
+ /// Behind a folded row, a shade lighter than filler, so the runs the minimal view leaves out
+ /// read as breaks in the file rather than as a line of it or as padding.
+ static let folded = grey(34)
+
/// ImGui draws a selected item as its accent at 31% over the window background. This is that
/// composite, so the queue highlight matches without carrying an alpha channel around.
static let selected = rgb(38, 64, 90)
@@ -60,6 +64,9 @@ enum Palette {
return rgb(233, 129, 129)
case DEVIEW_ROW_MODIFIED.value:
return rgb(231, 197, 113)
+ // Dimmed like the gutter, since what it says is about the file rather than from it.
+ case DEVIEW_ROW_FOLDED.value:
+ return dim
default:
return text
}
@@ -76,6 +83,8 @@ enum Palette {
return rgb(74, 64, 32)
case DEVIEW_ROW_FILLER.value:
return filler
+ case DEVIEW_ROW_FOLDED.value:
+ return folded
default:
return nil
}
diff --git a/native/swift/Sources/Deview/Renderer.swift b/native/swift/Sources/Deview/Renderer.swift
index 9aa7e3d5..3546d3f1 100644
--- a/native/swift/Sources/Deview/Renderer.swift
+++ b/native/swift/Sources/Deview/Renderer.swift
@@ -323,7 +323,8 @@ final class Renderer {
return
}
- let number = String(row.lineNumber)
+ // A folded row has no number, and printing the -1 standing in for one put it in the gutter.
+ let number = row.lineNumber < 0 ? "" : String(row.lineNumber)
let gutter = "\(Palette.marker(row.kind)) \(String(repeating: " ", count: max(0, 4 - number.count)))\(number)"
let width = Renderer.gutterCells * cell.width
diff --git a/native/swift/Sources/Deview/ViewerView.swift b/native/swift/Sources/Deview/ViewerView.swift
index 96ab3a80..72b4208f 100644
--- a/native/swift/Sources/Deview/ViewerView.swift
+++ b/native/swift/Sources/Deview/ViewerView.swift
@@ -315,6 +315,8 @@ final class ViewerView: NSView, NSViewToolTipOwner {
return DEVIEW_KEY_NEXT_CHANGE.value
case "p":
return DEVIEW_KEY_PREVIOUS_CHANGE.value
+ case "m":
+ return DEVIEW_KEY_TOGGLE_MINIMAL.value
case "a":
return shift ? DEVIEW_KEY_ACCEPT_ALL.value : DEVIEW_KEY_ACCEPT.value
case "d":
diff --git a/src/DiffEngine.Tests/ViewerClientUnownedTests.cs b/src/DiffEngine.Tests/ViewerClientUnownedTests.cs
index e7f48cff..de65a449 100644
--- a/src/DiffEngine.Tests/ViewerClientUnownedTests.cs
+++ b/src/DiffEngine.Tests/ViewerClientUnownedTests.cs
@@ -13,8 +13,14 @@ public class ViewerClientUnownedTests
static readonly ViewerMessage settle = new(ViewerVerb.Settle, InlineKey.For("Tests.cs", 1));
// Read before any test can have changed it, so the restore below puts back the real default
- // rather than a copy of it kept here
- static readonly TimeSpan recheckUnownedAfter = ViewerClient.RecheckUnownedAfter;
+ // rather than a copy of it kept here. A hook rather than a static field initializer: with no
+ // static constructor that runs on first touching a static field, and TheMemoryExpires sets the
+ // value before it touches one, so running first it captured Zero for every test after it.
+ static TimeSpan recheckUnownedAfter;
+
+ [Before(Class)]
+ public static void Remember() =>
+ recheckUnownedAfter = ViewerClient.RecheckUnownedAfter;
[Before(Test)]
public void Forget() =>
diff --git a/src/DiffEngineViewer.Linux/runtimes/linux-arm64/native/libdiffengine_viewer.so b/src/DiffEngineViewer.Linux/runtimes/linux-arm64/native/libdiffengine_viewer.so
index e7e62c4d..79c8d649 100644
Binary files a/src/DiffEngineViewer.Linux/runtimes/linux-arm64/native/libdiffengine_viewer.so and b/src/DiffEngineViewer.Linux/runtimes/linux-arm64/native/libdiffengine_viewer.so differ
diff --git a/src/DiffEngineViewer.Linux/runtimes/linux-x64/native/libdiffengine_viewer.so b/src/DiffEngineViewer.Linux/runtimes/linux-x64/native/libdiffengine_viewer.so
index 6bcb719d..183f04e5 100644
Binary files a/src/DiffEngineViewer.Linux/runtimes/linux-x64/native/libdiffengine_viewer.so and b/src/DiffEngineViewer.Linux/runtimes/linux-x64/native/libdiffengine_viewer.so differ
diff --git a/src/DiffEngineViewer.Mac/runtimes/osx-arm64/native/libdiffengine_viewer.dylib b/src/DiffEngineViewer.Mac/runtimes/osx-arm64/native/libdiffengine_viewer.dylib
index 11d27714..5d30f898 100644
Binary files a/src/DiffEngineViewer.Mac/runtimes/osx-arm64/native/libdiffengine_viewer.dylib and b/src/DiffEngineViewer.Mac/runtimes/osx-arm64/native/libdiffengine_viewer.dylib differ
diff --git a/src/DiffEngineViewer.Mac/runtimes/osx-x64/native/libdiffengine_viewer.dylib b/src/DiffEngineViewer.Mac/runtimes/osx-x64/native/libdiffengine_viewer.dylib
index 11d27714..5d30f898 100644
Binary files a/src/DiffEngineViewer.Mac/runtimes/osx-x64/native/libdiffengine_viewer.dylib and b/src/DiffEngineViewer.Mac/runtimes/osx-x64/native/libdiffengine_viewer.dylib differ
diff --git a/src/DiffEngineViewer.Tests/AcceptAllProgressTests.cs b/src/DiffEngineViewer.Tests/AcceptAllProgressTests.cs
index 9d6837ad..ca83743d 100644
--- a/src/DiffEngineViewer.Tests/AcceptAllProgressTests.cs
+++ b/src/DiffEngineViewer.Tests/AcceptAllProgressTests.cs
@@ -176,10 +176,10 @@ public async Task TheWindowOffersNothingThatChangesTheQueueWhileItRuns()
var screen = ScreenBuilder.Build(state);
await Assert.That(screen.Status).IsEqualTo("Accepting 1 of 3");
- await Assert.That(screen.Buttons.Where(_ => _.Enabled)).IsEmpty();
+ await Assert.That(EnabledQueueButtons(screen)).IsEmpty();
var window = new Window();
- foreach (var key in new[] { CommandKind.Accept, CommandKind.Discard, CommandKind.AcceptAll })
+ foreach (var key in queueCommands)
{
var pressed = ViewerProgram.Apply(state, Input(key), null, window);
await Assert.That(pressed.Queue).IsSameReferenceAs(state.Queue);
@@ -206,7 +206,7 @@ public async Task AnAttachedWindowShowsTheOwnersProgress()
var screen = ScreenBuilder.Build(state);
await Assert.That(screen.Status).IsEqualTo("Accepting 5 of 9");
- await Assert.That(screen.Buttons.Where(_ => _.Enabled)).IsEmpty();
+ await Assert.That(EnabledQueueButtons(screen)).IsEmpty();
// And the listing after the batch is what gives the window back
var after = ViewerSession.Sync(state, Fixtures.Pending(Fixtures.Patch()), [], "Accepted 8", null);
@@ -359,6 +359,15 @@ static SessionState Pending() =>
Fixtures.Patch("SampleTests.cs", 88, "\"one\"", "two"),
Fixtures.Patch("OtherTests.cs", 12, null, "brand new"));
+ ///
+ /// What a batch refuses. Moving between changes and switching views only change what is being
+ /// read, so those buttons stay live while one runs.
+ ///
+ static readonly CommandKind[] queueCommands = [CommandKind.Accept, CommandKind.Discard, CommandKind.AcceptAll];
+
+ static IEnumerable EnabledQueueButtons(Screen screen) =>
+ screen.Buttons.Where(_ => _.Enabled && queueCommands.Contains(_.Command));
+
static ViewerInput Input(CommandKind key) =>
new(key, -1, -1, 0, false, Fixtures.Columns, Fixtures.Rows);
diff --git a/src/DiffEngineViewer.Tests/DeviewStructTests.cs b/src/DiffEngineViewer.Tests/DeviewStructTests.cs
index 309d05da..aaf0172e 100644
--- a/src/DiffEngineViewer.Tests/DeviewStructTests.cs
+++ b/src/DiffEngineViewer.Tests/DeviewStructTests.cs
@@ -39,6 +39,23 @@ public async Task FieldsMatchTheHeader(string name, Type managed)
await Assert.That(string.Join(", ", Camel(mirrored))).IsEqualTo(string.Join(", ", declared));
}
+ ///
+ /// A row kind crosses the ABI as a cast rather than through a mapping, so the two enums have to
+ /// agree value for value. A kind added to one side only is drawn as whatever the other side has
+ /// at that number, and nothing fails anywhere.
+ ///
+ [Test]
+ public async Task RowKindsMatchTheHeader() =>
+ await Assert.That(Managed()).IsEqualTo(Declared("DeviewRowKind", "DEVIEW_ROW_"));
+
+ ///
+ /// Keys are mapped rather than cast, because the shim reports only the ones a window can
+ /// produce, but the numbers are still the wire format and both sides have to use the same.
+ ///
+ [Test]
+ public async Task KeysMatchTheHeader() =>
+ await Assert.That(Managed()).IsEqualTo(Declared("DeviewKey", "DEVIEW_KEY_"));
+
public static IEnumerable<(string, Type)> Structs()
{
yield return ("DeviewRow", typeof(DeviewRow));
@@ -83,6 +100,41 @@ static List Fields(string name)
.ToList();
}
+ ///
+ /// An enum as name and value pairs in value order, the names lower cased and run together so
+ /// ScrollUp and DEVIEW_KEY_SCROLL_UP compare as the same member.
+ ///
+ static string Managed()
+ where T : struct, Enum =>
+ string.Join(
+ ", ",
+ Enum.GetValues()
+ .OrderBy(_ => Convert.ToInt32(_))
+ .Select(_ => $"{_.ToString().ToLowerInvariant()}={Convert.ToInt32(_)}"));
+
+ static string Declared(string name, string prefix)
+ {
+ var block = Regex.Match(
+ Header(),
+ $@"enum {name} \{{(?.*?)\}};",
+ RegexOptions.Singleline);
+ if (!block.Success)
+ {
+ throw new($"{name} is not declared in deview.h.");
+ }
+
+ var body = Regex.Replace(block.Groups["body"].Value, @"/\*.*?\*/", "", RegexOptions.Singleline);
+ return string.Join(
+ ", ",
+ Regex
+ .Matches(body, @"(?\w+)\s*=\s*(?\d+)")
+ .Select(_ => (
+ Member: _.Groups["member"].Value[prefix.Length..].Replace("_", "").ToLowerInvariant(),
+ Value: int.Parse(_.Groups["value"].Value)))
+ .OrderBy(_ => _.Value)
+ .Select(_ => $"{_.Member}={_.Value}"));
+ }
+
static string Header()
{
// bin/{configuration}/{tfm} under this test project, so four up is src and five is the
diff --git a/src/DiffEngineViewer.Tests/DiffViewTests.cs b/src/DiffEngineViewer.Tests/DiffViewTests.cs
new file mode 100644
index 00000000..0156b0b2
--- /dev/null
+++ b/src/DiffEngineViewer.Tests/DiffViewTests.cs
@@ -0,0 +1,80 @@
+///
+/// The mapping between a view's rows and the entry's, which scrolling, selection and switching
+/// views all go through. Held as invariants over every row rather than as a few picked cases,
+/// because an off by one here moves a selection by a line without anything looking wrong.
+///
+public class DiffViewTests
+{
+ [Test]
+ public async Task Every_row_of_the_entry_finds_the_row_that_shows_it()
+ {
+ var entry = Entry();
+ var view = entry.View(true);
+
+ for (var line = 0; line < entry.TotalRows; line++)
+ {
+ var row = view.Find(line);
+ await Assert.That(view.First(row)).IsLessThanOrEqualTo(line);
+ await Assert.That(view.Last(row)).IsGreaterThanOrEqualTo(line);
+ }
+ }
+
+ [Test]
+ public async Task A_shown_row_is_the_entrys_own()
+ {
+ var entry = Entry();
+ var view = entry.View(true);
+
+ for (var row = 0; row < view.Count; row++)
+ {
+ if (view.IsFolded(row))
+ {
+ continue;
+ }
+
+ await Assert.That(view.Left[row]).IsSameReferenceAs(entry.LeftRows[view.First(row)]);
+ await Assert.That(view.Right[row]).IsSameReferenceAs(entry.RightRows[view.First(row)]);
+ await Assert.That(view.Last(row)).IsEqualTo(view.First(row));
+ }
+ }
+
+ ///
+ /// Between them the rows cover the entry once each, the last one running to the end, so no
+ /// line is in two rows and none is in no row.
+ ///
+ [Test]
+ public async Task The_rows_cover_the_entry_exactly_once()
+ {
+ var entry = Entry();
+ var view = entry.View(true);
+
+ var next = 0;
+ for (var row = 0; row < view.Count; row++)
+ {
+ await Assert.That(view.First(row)).IsEqualTo(next);
+ next = view.Last(row) + 1;
+ }
+
+ await Assert.That(next).IsEqualTo(entry.TotalRows);
+ }
+
+ ///
+ /// Changes are never folded, so the minimal view has the same runs of them, only closer
+ /// together.
+ ///
+ [Test]
+ public async Task Both_views_have_the_same_changes()
+ {
+ var entry = Entry();
+ var full = entry.View(false);
+ var minimal = entry.View(true);
+
+ var mapped = minimal.Changes.Select(_ => minimal.First(_));
+
+ await Assert.That(string.Join(", ", mapped)).IsEqualTo(string.Join(", ", full.Changes));
+ await Assert.That(string.Join(", ", full.Changes)).IsEqualTo("2, 16, 32");
+ }
+
+ static QueueEntry Entry() =>
+ Fixtures.File(Fixtures.Long(true), Fixtures.Long(false)).Current!;
+}
diff --git a/src/DiffEngineViewer.Tests/FileScreenTests.AtEnd.verified.txt b/src/DiffEngineViewer.Tests/FileScreenTests.AtEnd.verified.txt
index dd174431..0042e37e 100644
--- a/src/DiffEngineViewer.Tests/FileScreenTests.AtEnd.verified.txt
+++ b/src/DiffEngineViewer.Tests/FileScreenTests.AtEnd.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| Sample.received.txt <> Sample.verified.txt diff |
-+----------------------------------------------+-----------------------------------------------+
-| Sample.received.txt | Sample.verified.txt |
-+----------------------------------------------+-----------------------------------------------+
-| 25 line 25 | 25 line 25 |
-| 26 line 26 | 26 line 26 |
-| 27 line 27 | 27 line 27 |
-| 28 line 28 | 28 line 28 |
-| 29 line 29 | 29 line 29 |
-| 30 line 30 | 30 line 30 |
-| 31 line 31 | 31 line 31 |
-| 32 line 32 | 32 line 32 |
-| ~ 33 line 33 changed | ~ 33 line 33 |
-| 34 line 34 | 34 line 34 |
-| 35 line 35 | 35 line 35 |
-| 36 line 36 | 36 line 36 |
-| 37 line 37 | 37 line 37 |
-| 38 line 38 | 38 line 38 |
-| 39 line 39 | 39 line 39 |
-| 40 line 40 | 40 line 40 |
-+----------------------------------------------+-----------------------------------------------+
-| [Accept] [Close] lines 25-40 of 40 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| Sample.received.txt <> Sample.verified.txt diff |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| Sample.received.txt | Sample.verified.txt |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| 25 line 25 | 25 line 25 |
+| 26 line 26 | 26 line 26 |
+| 27 line 27 | 27 line 27 |
+| 28 line 28 | 28 line 28 |
+| 29 line 29 | 29 line 29 |
+| 30 line 30 | 30 line 30 |
+| 31 line 31 | 31 line 31 |
+| 32 line 32 | 32 line 32 |
+| ~ 33 line 33 changed | ~ 33 line 33 |
+| 34 line 34 | 34 line 34 |
+| 35 line 35 | 35 line 35 |
+| 36 line 36 | 36 line 36 |
+| 37 line 37 | 37 line 37 |
+| 38 line 38 | 38 line 38 |
+| 39 line 39 | 39 line 39 |
+| 40 line 40 | 40 line 40 |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| [Accept] [Close] [Prev change] (Next change) [Changes only] lines 25-40 of 40 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/FileScreenTests.Initial.verified.txt b/src/DiffEngineViewer.Tests/FileScreenTests.Initial.verified.txt
index 363d0f55..e3debf29 100644
--- a/src/DiffEngineViewer.Tests/FileScreenTests.Initial.verified.txt
+++ b/src/DiffEngineViewer.Tests/FileScreenTests.Initial.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| Sample.received.txt <> Sample.verified.txt diff |
-+----------------------------------------------+-----------------------------------------------+
-| Sample.received.txt | Sample.verified.txt |
-+----------------------------------------------+-----------------------------------------------+
-| 1 the quick | 1 the quick |
-| ~ 2 brown dog | ~ 2 brown fox |
-| 3 jumps over | 3 jumps over |
-| 4 the lazy | 4 the lazy |
-| 5 dog | 5 dog |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-+----------------------------------------------+-----------------------------------------------+
-| [Accept] [Close] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| Sample.received.txt <> Sample.verified.txt diff |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| Sample.received.txt | Sample.verified.txt |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| 1 the quick | 1 the quick |
+| ~ 2 brown dog | ~ 2 brown fox |
+| 3 jumps over | 3 jumps over |
+| 4 the lazy | 4 the lazy |
+| 5 dog | 5 dog |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| [Accept] [Close] (Prev change) (Next change) [Changes only] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/FileScreenTests.LeftEmpty.verified.txt b/src/DiffEngineViewer.Tests/FileScreenTests.LeftEmpty.verified.txt
index 280b16bd..14a60136 100644
--- a/src/DiffEngineViewer.Tests/FileScreenTests.LeftEmpty.verified.txt
+++ b/src/DiffEngineViewer.Tests/FileScreenTests.LeftEmpty.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| Sample.received.txt <> Sample.verified.txt diff |
-+----------------------------------------------+-----------------------------------------------+
-| Sample.received.txt | Sample.verified.txt |
-+----------------------------------------------+-----------------------------------------------+
-| | - 1 the quick |
-| | - 2 brown fox |
-| | - 3 jumps over |
-| | - 4 the lazy |
-| | - 5 dog |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-+----------------------------------------------+-----------------------------------------------+
-| [Accept] [Close] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| Sample.received.txt <> Sample.verified.txt diff |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| Sample.received.txt | Sample.verified.txt |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| | - 1 the quick |
+| | - 2 brown fox |
+| | - 3 jumps over |
+| | - 4 the lazy |
+| | - 5 dog |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| [Accept] [Close] (Prev change) (Next change) [Changes only] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/FileScreenTests.LongLines.verified.txt b/src/DiffEngineViewer.Tests/FileScreenTests.LongLines.verified.txt
index a130c3a1..9e3bde8d 100644
--- a/src/DiffEngineViewer.Tests/FileScreenTests.LongLines.verified.txt
+++ b/src/DiffEngineViewer.Tests/FileScreenTests.LongLines.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| Sample.received.txt <> Sample.verified.txt diff |
-+----------------------------------------------+-----------------------------------------------+
-| Sample.received.txt | Sample.verified.txt |
-+----------------------------------------------+-----------------------------------------------+
-| 1 start | 1 start |
-| ~ 2 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx> | ~ 2 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx> |
-| 3 end | 3 end |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-+----------------------------------------------+-----------------------------------------------+
-| [Accept] [Close] lines 1-3 of 3 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| Sample.received.txt <> Sample.verified.txt diff |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| Sample.received.txt | Sample.verified.txt |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| 1 start | 1 start |
+| ~ 2 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx> | ~ 2 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx> |
+| 3 end | 3 end |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| [Accept] [Close] (Prev change) (Next change) [Changes only] lines 1-3 of 3 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/FileScreenTests.Minimal.verified.txt b/src/DiffEngineViewer.Tests/FileScreenTests.Minimal.verified.txt
new file mode 100644
index 00000000..7be21e28
--- /dev/null
+++ b/src/DiffEngineViewer.Tests/FileScreenTests.Minimal.verified.txt
@@ -0,0 +1,24 @@
++--------------------------------------------------------------------------------------------------------------------------------------+
+| Sample.received.txt <> Sample.verified.txt diff |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| Sample.received.txt | Sample.verified.txt |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| 1 line 01 | 1 line 01 |
+| 2 line 02 | 2 line 02 |
+| ~ 3 line 03 changed | ~ 3 line 03 |
+| 4 line 04 | 4 line 04 |
+| 5 line 05 | 5 line 05 |
+| 6 line 06 | 6 line 06 |
+| ... 7 unchanged lines ... | ... 7 unchanged lines ... |
+| 14 line 14 | 14 line 14 |
+| 15 line 15 | 15 line 15 |
+| 16 line 16 | 16 line 16 |
+| ~ 17 line 17 changed | ~ 17 line 17 |
+| 18 line 18 | 18 line 18 |
+| 19 line 19 | 19 line 19 |
+| 20 line 20 | 20 line 20 |
+| ... 9 unchanged lines ... | ... 9 unchanged lines ... |
+| 30 line 30 | 30 line 30 |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| [Accept] [Close] (Prev change) [Next change] [All lines] lines 1-30 of 40 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/FileScreenTests.MinimalAtEnd.verified.txt b/src/DiffEngineViewer.Tests/FileScreenTests.MinimalAtEnd.verified.txt
new file mode 100644
index 00000000..5a5b1aac
--- /dev/null
+++ b/src/DiffEngineViewer.Tests/FileScreenTests.MinimalAtEnd.verified.txt
@@ -0,0 +1,24 @@
++--------------------------------------------------------------------------------------------------------------------------------------+
+| Sample.received.txt <> Sample.verified.txt diff |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| Sample.received.txt | Sample.verified.txt |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| 14 line 14 | 14 line 14 |
+| 15 line 15 | 15 line 15 |
+| 16 line 16 | 16 line 16 |
+| ~ 17 line 17 changed | ~ 17 line 17 |
+| 18 line 18 | 18 line 18 |
+| 19 line 19 | 19 line 19 |
+| 20 line 20 | 20 line 20 |
+| ... 9 unchanged lines ... | ... 9 unchanged lines ... |
+| 30 line 30 | 30 line 30 |
+| 31 line 31 | 31 line 31 |
+| 32 line 32 | 32 line 32 |
+| ~ 33 line 33 changed | ~ 33 line 33 |
+| 34 line 34 | 34 line 34 |
+| 35 line 35 | 35 line 35 |
+| 36 line 36 | 36 line 36 |
+| ... 4 unchanged lines ... | ... 4 unchanged lines ... |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| [Accept] [Close] [Prev change] (Next change) [All lines] lines 14-40 of 40 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/FileScreenTests.MinimalNoDifferences.verified.txt b/src/DiffEngineViewer.Tests/FileScreenTests.MinimalNoDifferences.verified.txt
new file mode 100644
index 00000000..4e155635
--- /dev/null
+++ b/src/DiffEngineViewer.Tests/FileScreenTests.MinimalNoDifferences.verified.txt
@@ -0,0 +1,24 @@
++--------------------------------------------------------------------------------------------------------------------------------------+
+| Sample.received.txt <> Sample.verified.txt diff |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| Sample.received.txt | Sample.verified.txt |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| ... 40 unchanged lines ... | ... 40 unchanged lines ... |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| [Accept] [Close] (Prev change) (Next change) [All lines] lines 1-40 of 40 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/FileScreenTests.NextChange.verified.txt b/src/DiffEngineViewer.Tests/FileScreenTests.NextChange.verified.txt
index 02b8032c..dac9f9f6 100644
--- a/src/DiffEngineViewer.Tests/FileScreenTests.NextChange.verified.txt
+++ b/src/DiffEngineViewer.Tests/FileScreenTests.NextChange.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| Sample.received.txt <> Sample.verified.txt diff |
-+----------------------------------------------+-----------------------------------------------+
-| Sample.received.txt | Sample.verified.txt |
-+----------------------------------------------+-----------------------------------------------+
-| ~ 17 line 17 changed | ~ 17 line 17 |
-| 18 line 18 | 18 line 18 |
-| 19 line 19 | 19 line 19 |
-| 20 line 20 | 20 line 20 |
-| 21 line 21 | 21 line 21 |
-| 22 line 22 | 22 line 22 |
-| 23 line 23 | 23 line 23 |
-| 24 line 24 | 24 line 24 |
-| 25 line 25 | 25 line 25 |
-| 26 line 26 | 26 line 26 |
-| 27 line 27 | 27 line 27 |
-| 28 line 28 | 28 line 28 |
-| 29 line 29 | 29 line 29 |
-| 30 line 30 | 30 line 30 |
-| 31 line 31 | 31 line 31 |
-| 32 line 32 | 32 line 32 |
-+----------------------------------------------+-----------------------------------------------+
-| [Accept] [Close] lines 17-32 of 40 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| Sample.received.txt <> Sample.verified.txt diff |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| Sample.received.txt | Sample.verified.txt |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| 14 line 14 | 14 line 14 |
+| 15 line 15 | 15 line 15 |
+| 16 line 16 | 16 line 16 |
+| ~ 17 line 17 changed | ~ 17 line 17 |
+| 18 line 18 | 18 line 18 |
+| 19 line 19 | 19 line 19 |
+| 20 line 20 | 20 line 20 |
+| 21 line 21 | 21 line 21 |
+| 22 line 22 | 22 line 22 |
+| 23 line 23 | 23 line 23 |
+| 24 line 24 | 24 line 24 |
+| 25 line 25 | 25 line 25 |
+| 26 line 26 | 26 line 26 |
+| 27 line 27 | 27 line 27 |
+| 28 line 28 | 28 line 28 |
+| 29 line 29 | 29 line 29 |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| [Accept] [Close] [Prev change] [Next change] [Changes only] lines 14-29 of 40 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/FileScreenTests.NoDifferences.verified.txt b/src/DiffEngineViewer.Tests/FileScreenTests.NoDifferences.verified.txt
index 443facfe..f3a336cb 100644
--- a/src/DiffEngineViewer.Tests/FileScreenTests.NoDifferences.verified.txt
+++ b/src/DiffEngineViewer.Tests/FileScreenTests.NoDifferences.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| Sample.received.txt <> Sample.verified.txt diff |
-+----------------------------------------------+-----------------------------------------------+
-| Sample.received.txt | Sample.verified.txt |
-+----------------------------------------------+-----------------------------------------------+
-| 1 the quick | 1 the quick |
-| 2 brown fox | 2 brown fox |
-| 3 jumps over | 3 jumps over |
-| 4 the lazy | 4 the lazy |
-| 5 dog | 5 dog |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-+----------------------------------------------+-----------------------------------------------+
-| [Accept] [Close] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| Sample.received.txt <> Sample.verified.txt diff |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| Sample.received.txt | Sample.verified.txt |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| 1 the quick | 1 the quick |
+| 2 brown fox | 2 brown fox |
+| 3 jumps over | 3 jumps over |
+| 4 the lazy | 4 the lazy |
+| 5 dog | 5 dog |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| [Accept] [Close] (Prev change) (Next change) [Changes only] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/FileScreenTests.OpensAtTheFirstChange.verified.txt b/src/DiffEngineViewer.Tests/FileScreenTests.OpensAtTheFirstChange.verified.txt
new file mode 100644
index 00000000..6e645a63
--- /dev/null
+++ b/src/DiffEngineViewer.Tests/FileScreenTests.OpensAtTheFirstChange.verified.txt
@@ -0,0 +1,24 @@
++--------------------------------------------------------------------------------------------------------------------------------------+
+| Sample.received.txt <> Sample.verified.txt diff |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| Sample.received.txt | Sample.verified.txt |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| 27 line 27 | 27 line 27 |
+| 28 line 28 | 28 line 28 |
+| 29 line 29 | 29 line 29 |
+| ~ 30 line 30 changed | ~ 30 line 30 |
+| 31 line 31 | 31 line 31 |
+| 32 line 32 | 32 line 32 |
+| 33 line 33 | 33 line 33 |
+| 34 line 34 | 34 line 34 |
+| 35 line 35 | 35 line 35 |
+| 36 line 36 | 36 line 36 |
+| 37 line 37 | 37 line 37 |
+| 38 line 38 | 38 line 38 |
+| 39 line 39 | 39 line 39 |
+| 40 line 40 | 40 line 40 |
+| 41 line 41 | 41 line 41 |
+| 42 line 42 | 42 line 42 |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| [Accept] [Close] (Prev change) [Next change] [Changes only] lines 27-42 of 60 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/FileScreenTests.RightEmpty.verified.txt b/src/DiffEngineViewer.Tests/FileScreenTests.RightEmpty.verified.txt
index 69e30b8d..202e06b8 100644
--- a/src/DiffEngineViewer.Tests/FileScreenTests.RightEmpty.verified.txt
+++ b/src/DiffEngineViewer.Tests/FileScreenTests.RightEmpty.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| Sample.received.txt <> Sample.verified.txt diff |
-+----------------------------------------------+-----------------------------------------------+
-| Sample.received.txt | Sample.verified.txt |
-+----------------------------------------------+-----------------------------------------------+
-| + 1 the quick | |
-| + 2 brown dog | |
-| + 3 jumps over | |
-| + 4 the lazy | |
-| + 5 dog | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-+----------------------------------------------+-----------------------------------------------+
-| [Accept] [Close] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| Sample.received.txt <> Sample.verified.txt diff |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| Sample.received.txt | Sample.verified.txt |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| + 1 the quick | |
+| + 2 brown dog | |
+| + 3 jumps over | |
+| + 4 the lazy | |
+| + 5 dog | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| [Accept] [Close] (Prev change) (Next change) [Changes only] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/FileScreenTests.Scrolled.verified.txt b/src/DiffEngineViewer.Tests/FileScreenTests.Scrolled.verified.txt
index 02b8032c..e416d2d0 100644
--- a/src/DiffEngineViewer.Tests/FileScreenTests.Scrolled.verified.txt
+++ b/src/DiffEngineViewer.Tests/FileScreenTests.Scrolled.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| Sample.received.txt <> Sample.verified.txt diff |
-+----------------------------------------------+-----------------------------------------------+
-| Sample.received.txt | Sample.verified.txt |
-+----------------------------------------------+-----------------------------------------------+
-| ~ 17 line 17 changed | ~ 17 line 17 |
-| 18 line 18 | 18 line 18 |
-| 19 line 19 | 19 line 19 |
-| 20 line 20 | 20 line 20 |
-| 21 line 21 | 21 line 21 |
-| 22 line 22 | 22 line 22 |
-| 23 line 23 | 23 line 23 |
-| 24 line 24 | 24 line 24 |
-| 25 line 25 | 25 line 25 |
-| 26 line 26 | 26 line 26 |
-| 27 line 27 | 27 line 27 |
-| 28 line 28 | 28 line 28 |
-| 29 line 29 | 29 line 29 |
-| 30 line 30 | 30 line 30 |
-| 31 line 31 | 31 line 31 |
-| 32 line 32 | 32 line 32 |
-+----------------------------------------------+-----------------------------------------------+
-| [Accept] [Close] lines 17-32 of 40 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| Sample.received.txt <> Sample.verified.txt diff |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| Sample.received.txt | Sample.verified.txt |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| ~ 17 line 17 changed | ~ 17 line 17 |
+| 18 line 18 | 18 line 18 |
+| 19 line 19 | 19 line 19 |
+| 20 line 20 | 20 line 20 |
+| 21 line 21 | 21 line 21 |
+| 22 line 22 | 22 line 22 |
+| 23 line 23 | 23 line 23 |
+| 24 line 24 | 24 line 24 |
+| 25 line 25 | 25 line 25 |
+| 26 line 26 | 26 line 26 |
+| 27 line 27 | 27 line 27 |
+| 28 line 28 | 28 line 28 |
+| 29 line 29 | 29 line 29 |
+| 30 line 30 | 30 line 30 |
+| 31 line 31 | 31 line 31 |
+| 32 line 32 | 32 line 32 |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| [Accept] [Close] [Prev change] [Next change] [Changes only] lines 17-32 of 40 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/FileScreenTests.WhitespaceOnly.verified.txt b/src/DiffEngineViewer.Tests/FileScreenTests.WhitespaceOnly.verified.txt
index 0f4ad098..2043601c 100644
--- a/src/DiffEngineViewer.Tests/FileScreenTests.WhitespaceOnly.verified.txt
+++ b/src/DiffEngineViewer.Tests/FileScreenTests.WhitespaceOnly.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| Sample.received.txt <> Sample.verified.txt diff |
-+----------------------------------------------+-----------------------------------------------+
-| Sample.received.txt | Sample.verified.txt |
-+----------------------------------------------+-----------------------------------------------+
-| 1 the quick | 1 the quick |
-| ~ 2 brown fox | ~ 2 brown fox |
-| ~ 3 dog | ~ 3 dog |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-+----------------------------------------------+-----------------------------------------------+
-| [Accept] [Close] lines 1-3 of 3 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| Sample.received.txt <> Sample.verified.txt diff |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| Sample.received.txt | Sample.verified.txt |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| 1 the quick | 1 the quick |
+| ~ 2 brown fox | ~ 2 brown fox |
+| ~ 3 dog | ~ 3 dog |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| [Accept] [Close] (Prev change) (Next change) [Changes only] lines 1-3 of 3 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/FileScreenTests.cs b/src/DiffEngineViewer.Tests/FileScreenTests.cs
index ce53bbef..8dea7793 100644
--- a/src/DiffEngineViewer.Tests/FileScreenTests.cs
+++ b/src/DiffEngineViewer.Tests/FileScreenTests.cs
@@ -49,11 +49,51 @@ public Task AtEnd()
return Verify(Fixtures.Render(Apply(state, CommandKind.ScrollEnd)));
}
+ ///
+ /// The second change, with the three rows leading into it above it.
+ ///
[Test]
public Task NextChange()
{
var state = Fixtures.File(Fixtures.Long(true), Fixtures.Long(false));
- return Verify(Fixtures.Render(Apply(state, CommandKind.NextChange, CommandKind.NextChange)));
+ return Verify(Fixtures.Render(Apply(state, CommandKind.NextChange)));
+ }
+
+ ///
+ /// A comparison whose first change is far down the file opens on that change rather than on
+ /// line 1, which is the part of the file nothing is wrong with.
+ ///
+ [Test]
+ public Task OpensAtTheFirstChange() =>
+ Verify(Fixtures.Render(Fixtures.File(Fixtures.Deep(true), Fixtures.Deep(false))));
+
+ ///
+ /// Only the changes and the three rows either side of each, with every longer run of unchanged
+ /// rows folded into one row saying how many it stands for.
+ ///
+ [Test]
+ public Task Minimal()
+ {
+ var state = Fixtures.File(Fixtures.Long(true), Fixtures.Long(false));
+ return Verify(Fixtures.Render(Apply(state, CommandKind.ToggleMinimal)));
+ }
+
+ [Test]
+ public Task MinimalAtEnd()
+ {
+ var state = Fixtures.File(Fixtures.Long(true), Fixtures.Long(false));
+ return Verify(Fixtures.Render(Apply(state, CommandKind.ToggleMinimal, CommandKind.ScrollEnd)));
+ }
+
+ ///
+ /// Nothing differs, so everything folds: one row saying so rather than an empty pane, which
+ /// would read as a comparison with nothing in it.
+ ///
+ [Test]
+ public Task MinimalNoDifferences()
+ {
+ var state = Fixtures.File(Fixtures.Long(false), Fixtures.Long(false));
+ return Verify(Fixtures.Render(Apply(state, CommandKind.ToggleMinimal)));
}
static SessionState Apply(SessionState state, params CommandKind[] commands)
diff --git a/src/DiffEngineViewer.Tests/Fixtures.cs b/src/DiffEngineViewer.Tests/Fixtures.cs
index 9ac8479e..212e860d 100644
--- a/src/DiffEngineViewer.Tests/Fixtures.cs
+++ b/src/DiffEngineViewer.Tests/Fixtures.cs
@@ -2,8 +2,13 @@ static class Fixtures
{
///
/// Fixed so every screen snapshot has the same grid. 24 rows leaves 16 body rows.
+ ///
+ /// Wide enough for the fullest footer the snapshots carry - a conflicted entry's buttons beside
+ /// the accept-all summary - with its status whole. A footer that runs out of room loses the end
+ /// of its status line, and the status line is what half these snapshots are about.
+ ///
///
- public const int Columns = 96;
+ public const int Columns = 136;
public const int Rows = 24;
@@ -50,6 +55,32 @@ public static string Long(bool changed)
return builder.ToString();
}
+ ///
+ /// Sixty lines changed only at 30 and 50, so an entry holding it opens well below its first
+ /// line - on row 26, three above the first change - and the minimal view has a run to fold
+ /// before, between and after the two.
+ ///
+ public static string Deep(bool changed)
+ {
+ var builder = new StringBuilder();
+ for (var index = 1; index <= 60; index++)
+ {
+ if (index > 1)
+ {
+ builder.Append('\n');
+ }
+
+ builder.Append($"line {index:D2}");
+ if (changed &&
+ index is 30 or 50)
+ {
+ builder.Append(" changed");
+ }
+ }
+
+ return builder.ToString();
+ }
+
public static SessionState File(string left = Received, string right = Expected) =>
ViewerSession.EnqueueFile(
SessionState.Start(ViewerMode.File, Columns, Rows),
@@ -70,6 +101,17 @@ public static SessionState Inline(params InlinePatch[] patches)
return state;
}
+ ///
+ /// The source of a raw string literal holding a text, for a patch whose expected side has to be
+ /// that text. A patch with no expression is a new snapshot, every line of which is a change, so
+ /// where its first change is says nothing.
+ ///
+ public static string Literal(string text)
+ {
+ var lines = text.Split('\n').Select(_ => $" {_}");
+ return $"\"\"\"\n{string.Join("\n", lines)}\n \"\"\"";
+ }
+
public static InlinePatch Patch(
string source = "SampleTests.cs",
int line = 42,
diff --git a/src/DiffEngineViewer.Tests/FsharpEntryTests.Screen.verified.txt b/src/DiffEngineViewer.Tests/FsharpEntryTests.Screen.verified.txt
index 5c5b3822..fc813847 100644
--- a/src/DiffEngineViewer.Tests/FsharpEntryTests.Screen.verified.txt
+++ b/src/DiffEngineViewer.Tests/FsharpEntryTests.Screen.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| SampleTests.fs:42 inline 1 of 1 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (1) | received | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| > SampleTests.fs:42 | 1 the quick | 1 the quick |
-| | ~ 2 brown dog | ~ 2 brown fox |
-| | 3 jumps over | 3 jumps over |
-| | 4 the lazy | 4 the lazy |
-| | 5 dog | 5 dog |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| SampleTests.fs:42 inline 1 of 1 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (1) | received | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| > SampleTests.fs:42 | 1 the quick | 1 the quick |
+| | ~ 2 brown dog | ~ 2 brown fox |
+| | 3 jumps over | 3 jumps over |
+| | 4 the lazy | 4 the lazy |
+| | 5 dog | 5 dog |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/ImageScreenTests.DeleteInQueue.verified.txt b/src/DiffEngineViewer.Tests/ImageScreenTests.DeleteInQueue.verified.txt
index b22273e2..642c1be2 100644
--- a/src/DiffEngineViewer.Tests/ImageScreenTests.DeleteInQueue.verified.txt
+++ b/src/DiffEngineViewer.Tests/ImageScreenTests.DeleteInQueue.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| extra.verified.png inline 1 of 1 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (1) | (deleted) | extra.verified.png |
-+----------------------+-----------------------------------+-----------------------------------+
-| > extra.verified.png | | - 1 format PNG |
-| | | - 2 dimensions 800 x 600 |
-| | | - 3 bytes 12,384 |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept delete] [Discard] [Accept all] only extra.verified.png exists |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| extra.verified.png inline 1 of 1 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (1) | (deleted) | extra.verified.png |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| > extra.verified.png | | - 1 format PNG |
+| | | - 2 dimensions 800 x 600 |
+| | | - 3 bytes 12,384 |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept delete] [Discard] [Accept all] (Prev change) (Next change) (Changes only) only extra.verified.png exists |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/ImageScreenTests.Differs.verified.txt b/src/DiffEngineViewer.Tests/ImageScreenTests.Differs.verified.txt
index e0014aca..91d121fe 100644
--- a/src/DiffEngineViewer.Tests/ImageScreenTests.Differs.verified.txt
+++ b/src/DiffEngineViewer.Tests/ImageScreenTests.Differs.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| sample.received.png <> sample.verified.png diff |
-+----------------------------------------------+-----------------------------------------------+
-| sample.received.png | sample.verified.png |
-+----------------------------------------------+-----------------------------------------------+
-| 1 format PNG | 1 format PNG |
-| ~ 2 dimensions 800 x 600 | ~ 2 dimensions 640 x 480 |
-| ~ 3 bytes 12,384 | ~ 3 bytes 9,120 |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-+----------------------------------------------+-----------------------------------------------+
-| [Accept] [Close] images differ |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| sample.received.png <> sample.verified.png diff |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| sample.received.png | sample.verified.png |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| 1 format PNG | 1 format PNG |
+| ~ 2 dimensions 800 x 600 | ~ 2 dimensions 640 x 480 |
+| ~ 3 bytes 12,384 | ~ 3 bytes 9,120 |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| [Accept] [Close] (Prev change) (Next change) (Changes only) images differ |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/ImageScreenTests.FormatChanged.verified.txt b/src/DiffEngineViewer.Tests/ImageScreenTests.FormatChanged.verified.txt
index 1b736a86..21aea71c 100644
--- a/src/DiffEngineViewer.Tests/ImageScreenTests.FormatChanged.verified.txt
+++ b/src/DiffEngineViewer.Tests/ImageScreenTests.FormatChanged.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| sample.received.png <> sample.verified.png diff |
-+----------------------------------------------+-----------------------------------------------+
-| sample.received.png | sample.verified.png |
-+----------------------------------------------+-----------------------------------------------+
-| ~ 1 format PNG | ~ 1 format JPEG |
-| 2 dimensions 800 x 600 | 2 dimensions 800 x 600 |
-| ~ 3 bytes 12,384 | ~ 3 bytes 4,002 |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-+----------------------------------------------+-----------------------------------------------+
-| [Accept] [Close] images differ |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| sample.received.png <> sample.verified.png diff |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| sample.received.png | sample.verified.png |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| ~ 1 format PNG | ~ 1 format JPEG |
+| 2 dimensions 800 x 600 | 2 dimensions 800 x 600 |
+| ~ 3 bytes 12,384 | ~ 3 bytes 4,002 |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| [Accept] [Close] (Prev change) (Next change) (Changes only) images differ |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/ImageScreenTests.Identical.verified.txt b/src/DiffEngineViewer.Tests/ImageScreenTests.Identical.verified.txt
index 026f9157..0472cf35 100644
--- a/src/DiffEngineViewer.Tests/ImageScreenTests.Identical.verified.txt
+++ b/src/DiffEngineViewer.Tests/ImageScreenTests.Identical.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| sample.received.png <> sample.verified.png diff |
-+----------------------------------------------+-----------------------------------------------+
-| sample.received.png | sample.verified.png |
-+----------------------------------------------+-----------------------------------------------+
-| 1 format PNG | 1 format PNG |
-| 2 dimensions 800 x 600 | 2 dimensions 800 x 600 |
-| 3 bytes 12,384 | 3 bytes 12,384 |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-+----------------------------------------------+-----------------------------------------------+
-| [Accept] [Close] images are identical |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| sample.received.png <> sample.verified.png diff |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| sample.received.png | sample.verified.png |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| 1 format PNG | 1 format PNG |
+| 2 dimensions 800 x 600 | 2 dimensions 800 x 600 |
+| 3 bytes 12,384 | 3 bytes 12,384 |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| [Accept] [Close] (Prev change) (Next change) (Changes only) images are identical |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/ImageScreenTests.MoveInQueue.verified.txt b/src/DiffEngineViewer.Tests/ImageScreenTests.MoveInQueue.verified.txt
index 07eb6aa7..87ae79cc 100644
--- a/src/DiffEngineViewer.Tests/ImageScreenTests.MoveInQueue.verified.txt
+++ b/src/DiffEngineViewer.Tests/ImageScreenTests.MoveInQueue.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| Sample.Test (png) inline 1 of 1 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (1) | sample.received.png | sample.verified.png |
-+----------------------+-----------------------------------+-----------------------------------+
-| > Sample.Test (png) | 1 format PNG | 1 format PNG |
-| | ~ 2 dimensions 800 x 600 | ~ 2 dimensions 640 x 480 |
-| | ~ 3 bytes 12,384 | ~ 3 bytes 9,120 |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept move] [Discard] [Accept all] images differ |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| Sample.Test (png) inline 1 of 1 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (1) | sample.received.png | sample.verified.png |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| > Sample.Test (png) | 1 format PNG | 1 format PNG |
+| | ~ 2 dimensions 800 x 600 | ~ 2 dimensions 640 x 480 |
+| | ~ 3 bytes 12,384 | ~ 3 bytes 9,120 |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept move] [Discard] [Accept all] (Prev change) (Next change) (Changes only) images differ |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/ImageScreenTests.NewImage.verified.txt b/src/DiffEngineViewer.Tests/ImageScreenTests.NewImage.verified.txt
index d4c764d1..6c2663a7 100644
--- a/src/DiffEngineViewer.Tests/ImageScreenTests.NewImage.verified.txt
+++ b/src/DiffEngineViewer.Tests/ImageScreenTests.NewImage.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| sample.received.png <> sample.verified.png diff |
-+----------------------------------------------+-----------------------------------------------+
-| sample.received.png | sample.verified.png |
-+----------------------------------------------+-----------------------------------------------+
-| + 1 format PNG | |
-| + 2 dimensions 800 x 600 | |
-| + 3 bytes 12,384 | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-+----------------------------------------------+-----------------------------------------------+
-| [Accept] [Close] only sample.received.png exists |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| sample.received.png <> sample.verified.png diff |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| sample.received.png | sample.verified.png |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| + 1 format PNG | |
+| + 2 dimensions 800 x 600 | |
+| + 3 bytes 12,384 | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| [Accept] [Close] (Prev change) (Next change) (Changes only) only sample.received.png exists |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/ImageScreenTests.NotRecognized.verified.txt b/src/DiffEngineViewer.Tests/ImageScreenTests.NotRecognized.verified.txt
index 4550f57a..19c889a9 100644
--- a/src/DiffEngineViewer.Tests/ImageScreenTests.NotRecognized.verified.txt
+++ b/src/DiffEngineViewer.Tests/ImageScreenTests.NotRecognized.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| sample.received.png <> sample.verified.png diff |
-+----------------------------------------------+-----------------------------------------------+
-| sample.received.png | sample.verified.png |
-+----------------------------------------------+-----------------------------------------------+
-| ~ 1 format not recognized | ~ 1 format PNG |
-| ~ 2 dimensions unknown | ~ 2 dimensions 800 x 600 |
-| ~ 3 bytes 27 | ~ 3 bytes 12,384 |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-+----------------------------------------------+-----------------------------------------------+
-| [Accept] [Close] images differ |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| sample.received.png <> sample.verified.png diff |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| sample.received.png | sample.verified.png |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| ~ 1 format not recognized | ~ 1 format PNG |
+| ~ 2 dimensions unknown | ~ 2 dimensions 800 x 600 |
+| ~ 3 bytes 27 | ~ 3 bytes 12,384 |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| [Accept] [Close] (Prev change) (Next change) (Changes only) images differ |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/ImageScreenTests.Unreadable.verified.txt b/src/DiffEngineViewer.Tests/ImageScreenTests.Unreadable.verified.txt
index 4949702d..6aea74d6 100644
--- a/src/DiffEngineViewer.Tests/ImageScreenTests.Unreadable.verified.txt
+++ b/src/DiffEngineViewer.Tests/ImageScreenTests.Unreadable.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| sample.received.png <> sample.verified.png diff |
-+----------------------------------------------+-----------------------------------------------+
-| sample.received.png | sample.verified.png |
-+----------------------------------------------+-----------------------------------------------+
-| ~ 1 format not recognized | ~ 1 format PNG |
-| ~ 2 dimensions unknown | ~ 2 dimensions 800 x 600 |
-| ~ 3 bytes unreadable | ~ 3 bytes 12,384 |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-+----------------------------------------------+-----------------------------------------------+
-| [Accept] [Close] images could not be compared |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| sample.received.png <> sample.verified.png diff |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| sample.received.png | sample.verified.png |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| ~ 1 format not recognized | ~ 1 format PNG |
+| ~ 2 dimensions unknown | ~ 2 dimensions 800 x 600 |
+| ~ 3 bytes unreadable | ~ 3 bytes 12,384 |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| [Accept] [Close] (Prev change) (Next change) (Changes only) images could not be compared |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.AcceptAllInProgress.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.AcceptAllInProgress.verified.txt
index 93d6f68a..c5e1809c 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.AcceptAllInProgress.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.AcceptAllInProgress.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| OtherTests.cs:12 inline 1 of 3 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (3) | received | expected (new snapshot) |
-+----------------------+-----------------------------------+-----------------------------------+
-| > OtherTests.cs:12 | + 1 brand new | |
-| OtherTests.cs:30 | | |
-| WideNamedTests.cs> | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| (Accept) (Discard) (Accept all) Accepting 3 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| OtherTests.cs:12 inline 1 of 3 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (3) | received | expected (new snapshot) |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| > OtherTests.cs:12 | + 1 brand new | |
+| OtherTests.cs:30 | | |
+| WideNamedTests.cs> | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| (Accept) (Discard) (Accept all) (Prev change) (Next change) [Changes only] Accepting 3 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.AfterAccept.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.AfterAccept.verified.txt
index 869dafc1..4efb8fa2 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.AfterAccept.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.AfterAccept.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| SampleTests.cs:88 inline 1 of 4 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (4) | received | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| > SampleTests.cs:88 | ~ 1 two | ~ 1 one |
-| OtherTests.cs:12 | | |
-| OtherTests.cs:30 | | |
-| WideNamedTests.cs> | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] Applied SampleTests.cs:42 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| SampleTests.cs:88 inline 1 of 4 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (4) | received | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| > SampleTests.cs:88 | ~ 1 two | ~ 1 one |
+| OtherTests.cs:12 | | |
+| OtherTests.cs:30 | | |
+| WideNamedTests.cs> | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] Applied SampleTests.cs:42 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.AfterAcceptAll.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.AfterAcceptAll.verified.txt
index b251b0f5..513ef359 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.AfterAcceptAll.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.AfterAcceptAll.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| nothing pending inline |
-+----------------------------------------------+-----------------------------------------------+
-| received | expected |
-+----------------------------------------------+-----------------------------------------------+
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-+----------------------------------------------+-----------------------------------------------+
-| (Accept) (Discard) (Accept all) Accepted 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| nothing pending inline |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| received | expected |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| (Accept) (Discard) (Accept all) (Prev change) (Next change) (Changes only) Accepted 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.AfterAcceptAllSkipsConflicts.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.AfterAcceptAllSkipsConflicts.verified.txt
index c0821159..3022e37a 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.AfterAcceptAllSkipsConflicts.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.AfterAcceptAllSkipsConflicts.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| SampleTests.cs:42 inline 1 of 1 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (1) | received (net8.0) | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| > * SampleTests.cs:> | 1 the quick | 1 the quick |
-| | ~ 2 brown dog | ~ 2 brown fox |
-| | 3 jumps over | 3 jumps over |
-| | 4 the lazy | 4 the lazy |
-| | 5 dog | 5 dog |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] [Variant 1/2: net8.0] Accepted 1, 1 conflict needs review |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| SampleTests.cs:42 inline 1 of 1 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (1) | received (net8.0) | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| > * SampleTests.cs:> | 1 the quick | 1 the quick |
+| | ~ 2 brown dog | ~ 2 brown fox |
+| | 3 jumps over | 3 jumps over |
+| | 4 the lazy | 4 the lazy |
+| | 5 dog | 5 dog |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] [Variant 1/2: net8.0] Accepted 1, 1 conflict needs review |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.AfterDiscard.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.AfterDiscard.verified.txt
index 7eddcbb0..71f2fc31 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.AfterDiscard.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.AfterDiscard.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| SampleTests.cs:88 inline 1 of 4 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (4) | received | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| > SampleTests.cs:88 | ~ 1 two | ~ 1 one |
-| OtherTests.cs:12 | | |
-| OtherTests.cs:30 | | |
-| WideNamedTests.cs> | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] Discarded SampleTests.cs:42 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| SampleTests.cs:88 inline 1 of 4 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (4) | received | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| > SampleTests.cs:88 | ~ 1 two | ~ 1 one |
+| OtherTests.cs:12 | | |
+| OtherTests.cs:30 | | |
+| WideNamedTests.cs> | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] Discarded SampleTests.cs:42 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.CollapsedSolution.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.CollapsedSolution.verified.txt
index 48d8fb77..bee2c466 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.CollapsedSolution.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.CollapsedSolution.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| BTests.cs:42 inline 3 of 3 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (3) | received (net8.0) | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| + SolutionA (2) | ~ 1 eight | ~ 1 the quick |
-| - SolutionB (1) | | - 2 brown fox |
-| > * BTests.cs:42 | | - 3 jumps over |
-| | | - 4 the lazy |
-| | | - 5 dog |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] [Variant 1/2: net8.0] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| BTests.cs:42 inline 3 of 3 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (3) | received (net8.0) | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| + SolutionA (2) | ~ 1 eight | ~ 1 the quick |
+| - SolutionB (1) | | - 2 brown fox |
+| > * BTests.cs:42 | | - 3 jumps over |
+| | | - 4 the lazy |
+| | | - 5 dog |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] [Variant 1/2: net8.0] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.CollapsedTestGroup.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.CollapsedTestGroup.verified.txt
index 3343592c..8f00e9aa 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.CollapsedTestGroup.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.CollapsedTestGroup.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| BTests.cs:42 inline 3 of 3 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (3) | received (net8.0) | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| - SolutionA (2) | ~ 1 eight | ~ 1 the quick |
-| + Compare handles> | | - 2 brown fox |
-| - SolutionB (1) | | - 3 jumps over |
-| > * BTests.cs:42 | | - 4 the lazy |
-| | | - 5 dog |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] [Variant 1/2: net8.0] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| BTests.cs:42 inline 3 of 3 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (3) | received (net8.0) | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| - SolutionA (2) | ~ 1 eight | ~ 1 the quick |
+| + Compare handles> | | - 2 brown fox |
+| - SolutionB (1) | | - 3 jumps over |
+| > * BTests.cs:42 | | - 4 the lazy |
+| | | - 5 dog |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] [Variant 1/2: net8.0] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.CollidingNames.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.CollidingNames.verified.txt
index 54135cd5..236fb9b3 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.CollidingNames.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.CollidingNames.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| SampleTests.cs:42 inline 1 of 2 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (2) | received | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| > ProjA/SampleTests> | 1 the quick | 1 the quick |
-| ProjB/SampleTests> | ~ 2 brown dog | ~ 2 brown fox |
-| | 3 jumps over | 3 jumps over |
-| | 4 the lazy | 4 the lazy |
-| | 5 dog | 5 dog |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| SampleTests.cs:42 inline 1 of 2 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (2) | received | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| > ProjA/SampleTests> | 1 the quick | 1 the quick |
+| ProjB/SampleTests> | ~ 2 brown dog | ~ 2 brown fox |
+| | 3 jumps over | 3 jumps over |
+| | 4 the lazy | 4 the lazy |
+| | 5 dog | 5 dog |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.ConflictedEntry.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.ConflictedEntry.verified.txt
index 000a6050..3ccce537 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.ConflictedEntry.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.ConflictedEntry.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| SampleTests.cs:42 inline 1 of 2 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (2) | received (net8.0) | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| > * SampleTests.cs:> | 1 the quick | 1 the quick |
-| OtherTests.cs:12 | ~ 2 brown dog | ~ 2 brown fox |
-| | 3 jumps over | 3 jumps over |
-| | 4 the lazy | 4 the lazy |
-| | 5 dog | 5 dog |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] [Variant 1/2: net8.0] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| SampleTests.cs:42 inline 1 of 2 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (2) | received (net8.0) | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| > * SampleTests.cs:> | 1 the quick | 1 the quick |
+| OtherTests.cs:12 | ~ 2 brown dog | ~ 2 brown fox |
+| | 3 jumps over | 3 jumps over |
+| | 4 the lazy | 4 the lazy |
+| | 5 dog | 5 dog |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] [Variant 1/2: net8.0] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.ConflictedSecondVariant.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.ConflictedSecondVariant.verified.txt
index f21ce68f..f650a981 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.ConflictedSecondVariant.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.ConflictedSecondVariant.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| SampleTests.cs:42 inline 1 of 2 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (2) | received (net9.0) | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| > * SampleTests.cs:> | 1 the quick | 1 the quick |
-| OtherTests.cs:12 | ~ 2 brown wolf | ~ 2 brown fox |
-| | 3 jumps over | 3 jumps over |
-| | 4 the lazy | 4 the lazy |
-| | 5 dog | 5 dog |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] [Variant 2/2: net9.0] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| SampleTests.cs:42 inline 1 of 2 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (2) | received (net9.0) | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| > * SampleTests.cs:> | 1 the quick | 1 the quick |
+| OtherTests.cs:12 | ~ 2 brown wolf | ~ 2 brown fox |
+| | 3 jumps over | 3 jumps over |
+| | 4 the lazy | 4 the lazy |
+| | 5 dog | 5 dog |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] [Variant 2/2: net9.0] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.DeleteEntry.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.DeleteEntry.verified.txt
index 42c71362..cdd2868d 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.DeleteEntry.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.DeleteEntry.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| extra.verified.txt inline 1 of 1 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (1) | (deleted) | extra.verified.txt |
-+----------------------+-----------------------------------+-----------------------------------+
-| > extra.verified.txt | | - 1 the quick |
-| | | - 2 brown fox |
-| | | - 3 jumps over |
-| | | - 4 the lazy |
-| | | - 5 dog |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept delete] [Discard] [Accept all] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| extra.verified.txt inline 1 of 1 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (1) | (deleted) | extra.verified.txt |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| > extra.verified.txt | | - 1 the quick |
+| | | - 2 brown fox |
+| | | - 3 jumps over |
+| | | - 4 the lazy |
+| | | - 5 dog |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept delete] [Discard] [Accept all] (Prev change) (Next change) [Changes only] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.EmptyQueue.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.EmptyQueue.verified.txt
index 1bf632de..6d676183 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.EmptyQueue.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.EmptyQueue.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| nothing pending inline |
-+----------------------------------------------+-----------------------------------------------+
-| received | expected |
-+----------------------------------------------+-----------------------------------------------+
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-| | |
-+----------------------------------------------+-----------------------------------------------+
-| (Accept) (Discard) (Accept all) nothing pending |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| nothing pending inline |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| received | expected |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
+| | |
++------------------------------------------------------------------+-------------------------------------------------------------------+
+| (Accept) (Discard) (Accept all) (Prev change) (Next change) (Changes only) nothing pending |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.FailedApply.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.FailedApply.verified.txt
index 8e60f6ef..5e38877f 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.FailedApply.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.FailedApply.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| SampleTests.cs:42 inline 1 of 5 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (5) | received | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| > SampleTests.cs:42> | 1 the quick | 1 the quick |
-| SampleTests.cs:88 | ~ 2 brown dog | ~ 2 brown fox |
-| OtherTests.cs:12 | 3 jumps over | 3 jumps over |
-| OtherTests.cs:30 | 4 the lazy | 4 the lazy |
-| WideNamedTests.cs> | 5 dog | 5 dog |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] Failed to write: SampleTests.cs |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| SampleTests.cs:42 inline 1 of 5 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (5) | received | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| > SampleTests.cs:42> | 1 the quick | 1 the quick |
+| SampleTests.cs:88 | ~ 2 brown dog | ~ 2 brown fox |
+| OtherTests.cs:12 | 3 jumps over | 3 jumps over |
+| OtherTests.cs:30 | 4 the lazy | 4 the lazy |
+| WideNamedTests.cs> | 5 dog | 5 dog |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] Failed to write: SampleTests.cs |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.FailedApplyThenAcceptAll.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.FailedApplyThenAcceptAll.verified.txt
index c35a6505..d9bc7886 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.FailedApplyThenAcceptAll.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.FailedApplyThenAcceptAll.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| SampleTests.cs:42 inline 1 of 5 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (5) | received | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| > SampleTests.cs:42> | 1 the quick | 1 the quick |
-| SampleTests.cs:88> | ~ 2 brown dog | ~ 2 brown fox |
-| OtherTests.cs:12 ! | 3 jumps over | 3 jumps over |
-| OtherTests.cs:30 ! | 4 the lazy | 4 the lazy |
-| WideNamedTests.cs> | 5 dog | 5 dog |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] Accepted 0, 5 failed |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| SampleTests.cs:42 inline 1 of 5 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (5) | received | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| > SampleTests.cs:42> | 1 the quick | 1 the quick |
+| SampleTests.cs:88> | ~ 2 brown dog | ~ 2 brown fox |
+| OtherTests.cs:12 ! | 3 jumps over | 3 jumps over |
+| OtherTests.cs:30 ! | 4 the lazy | 4 the lazy |
+| WideNamedTests.cs> | 5 dog | 5 dog |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] Accepted 0, 5 failed |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.GroupedByTest.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.GroupedByTest.verified.txt
index 460331bd..00b1fa73 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.GroupedByTest.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.GroupedByTest.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| SampleTests.cs:10 inline 1 of 3 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (3) | received | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| - Compare handles n> | 1 the quick | 1 the quick |
-| > SampleTests.cs:> | ~ 2 brown dog | ~ 2 brown fox |
-| SampleTests.cs:> | 3 jumps over | 3 jumps over |
-| Order is stable | 4 the lazy | 4 the lazy |
-| | 5 dog | 5 dog |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| SampleTests.cs:10 inline 1 of 3 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (3) | received | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| - Compare handles n> | 1 the quick | 1 the quick |
+| > SampleTests.cs:> | ~ 2 brown dog | ~ 2 brown fox |
+| SampleTests.cs:> | 3 jumps over | 3 jumps over |
+| Order is stable | 4 the lazy | 4 the lazy |
+| | 5 dog | 5 dog |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.LongQueueScrollsToSelection.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.LongQueueScrollsToSelection.verified.txt
index ab06ac22..77641862 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.LongQueueScrollsToSelection.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.LongQueueScrollsToSelection.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| SampleTests.cs:18 inline 18 of 20 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (20) | received | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| SampleTests.cs:4 | ~ 1 b | ~ 1 a |
-| SampleTests.cs:5 | | |
-| SampleTests.cs:6 | | |
-| SampleTests.cs:7 | | |
-| SampleTests.cs:8 | | |
-| SampleTests.cs:9 | | |
-| SampleTests.cs:10 | | |
-| SampleTests.cs:11 | | |
-| SampleTests.cs:12 | | |
-| SampleTests.cs:13 | | |
-| SampleTests.cs:14 | | |
-| SampleTests.cs:15 | | |
-| SampleTests.cs:16 | | |
-| SampleTests.cs:17 | | |
-| > SampleTests.cs:18 | | |
-| SampleTests.cs:19 | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] lines 1-1 of 1 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| SampleTests.cs:18 inline 18 of 20 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (20) | received | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| SampleTests.cs:4 | ~ 1 b | ~ 1 a |
+| SampleTests.cs:5 | | |
+| SampleTests.cs:6 | | |
+| SampleTests.cs:7 | | |
+| SampleTests.cs:8 | | |
+| SampleTests.cs:9 | | |
+| SampleTests.cs:10 | | |
+| SampleTests.cs:11 | | |
+| SampleTests.cs:12 | | |
+| SampleTests.cs:13 | | |
+| SampleTests.cs:14 | | |
+| SampleTests.cs:15 | | |
+| SampleTests.cs:16 | | |
+| SampleTests.cs:17 | | |
+| > SampleTests.cs:18 | | |
+| SampleTests.cs:19 | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] lines 1-1 of 1 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.LongSolutionNames.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.LongSolutionNames.verified.txt
index 91d6bdef..cc6ebaad 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.LongSolutionNames.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.LongSolutionNames.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| ATests.cs:10 inline 1 of 2 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (2) | received | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| - AVeryLongWindedSo> | 1 the quick | 1 the quick |
-| > ATests.cs:10 | ~ 2 brown dog | ~ 2 brown fox |
-| - SolutionB (1) | 3 jumps over | 3 jumps over |
-| BTests.cs:30 | 4 the lazy | 4 the lazy |
-| | 5 dog | 5 dog |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| ATests.cs:10 inline 1 of 2 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (2) | received | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| - AVeryLongWindedSo> | 1 the quick | 1 the quick |
+| > ATests.cs:10 | ~ 2 brown dog | ~ 2 brown fox |
+| - SolutionB (1) | 3 jumps over | 3 jumps over |
+| BTests.cs:30 | 4 the lazy | 4 the lazy |
+| | 5 dog | 5 dog |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.MenuOnConflictedEntry.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.MenuOnConflictedEntry.verified.txt
index 82726a00..b75c67ca 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.MenuOnConflictedEntry.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.MenuOnConflictedEntry.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| SampleTests.cs:42 inline 1 of 2 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (2) | received (net8.0) | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| > * SampleTests.cs:> | 1 the quick | 1 the quick |
-| +------------------------+ 2 brown dog | ~ 2 brown fox |
-| | Accept | 3 jumps over | 3 jumps over |
-| | Show next variant | 4 the lazy | 4 the lazy |
-| | Discard | 5 dog | 5 dog |
-| | Open source file | | |
-| | Copy received (net8.0) | | |
-| | Copy expected | | |
-| +------------------------+ | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] [Variant 1/2: net8.0] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| SampleTests.cs:42 inline 1 of 2 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (2) | received (net8.0) | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| > * SampleTests.cs:> | 1 the quick | 1 the quick |
+| +------------------------+ 2 brown dog | ~ 2 brown fox |
+| | Accept | 3 jumps over | 3 jumps over |
+| | Show next variant | 4 the lazy | 4 the lazy |
+| | Discard | 5 dog | 5 dog |
+| | Open source file | | |
+| | Copy received (net8.0) | | |
+| | Copy expected | | |
+| +------------------------+ | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] [Variant 1/2: net8.0] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.MenuOnEntry.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.MenuOnEntry.verified.txt
index 8d98e16c..0dcb2759 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.MenuOnEntry.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.MenuOnEntry.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| SampleTests.cs:42 inline 1 of 5 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (5) | received | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| > SampleTests.cs:42 | 1 the quick | 1 the quick |
-| +------------------+| ~ 2 brown dog | ~ 2 brown fox |
-| | Accept || 3 jumps over | 3 jumps over |
-| | Discard || 4 the lazy | 4 the lazy |
-| | Open source file || 5 dog | 5 dog |
-| | Copy received || | |
-| | Copy expected || | |
-| +------------------+| | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| SampleTests.cs:42 inline 1 of 5 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (5) | received | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| > SampleTests.cs:42 | 1 the quick | 1 the quick |
+| +------------------+| ~ 2 brown dog | ~ 2 brown fox |
+| | Accept || 3 jumps over | 3 jumps over |
+| | Discard || 4 the lazy | 4 the lazy |
+| | Open source file || 5 dog | 5 dog |
+| | Copy received || | |
+| | Copy expected || | |
+| +------------------+| | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.MenuOnMove.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.MenuOnMove.verified.txt
index 23e21f96..7bf2994e 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.MenuOnMove.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.MenuOnMove.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| Sample.Test (txt) inline 1 of 2 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (2) | sample.received.txt | sample.verified.txt |
-+----------------------+-----------------------------------+-----------------------------------+
-| > Sample.Test (txt) | 1 the quick | 1 the quick |
-| +--------------------------+ brown dog | ~ 2 brown fox |
-| | Accept move | jumps over | 3 jumps over |
-| | Discard | the lazy | 4 the lazy |
-| | Open target directory | dog | 5 dog |
-| | Copy sample.received.txt | | |
-| | Copy sample.verified.txt | | |
-| +--------------------------+ | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept move] [Discard] [Accept all] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| Sample.Test (txt) inline 1 of 2 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (2) | sample.received.txt | sample.verified.txt |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| > Sample.Test (txt) | 1 the quick | 1 the quick |
+| +--------------------------+ brown dog | ~ 2 brown fox |
+| | Accept move | jumps over | 3 jumps over |
+| | Discard | the lazy | 4 the lazy |
+| | Open target directory | dog | 5 dog |
+| | Copy sample.received.txt | | |
+| | Copy sample.verified.txt | | |
+| +--------------------------+ | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept move] [Discard] [Accept all] (Prev change) (Next change) [Changes only] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.MenuOnSolutionHeader.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.MenuOnSolutionHeader.verified.txt
index 0ec0667b..005b7320 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.MenuOnSolutionHeader.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.MenuOnSolutionHeader.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| ATests.cs:10 inline 1 of 3 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (3) | received | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| - SolutionA (2) | 1 the quick | 1 the quick |
-| >+--------------------------+ brown dog | ~ 2 brown fox |
-| | Collapse | jumps over | 3 jumps over |
-| -| Accept all in SolutionA | the lazy | 4 the lazy |
-| | Discard all in SolutionA | dog | 5 dog |
-| +--------------------------+ | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| ATests.cs:10 inline 1 of 3 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (3) | received | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| - SolutionA (2) | 1 the quick | 1 the quick |
+| >+--------------------------+ brown dog | ~ 2 brown fox |
+| | Collapse | jumps over | 3 jumps over |
+| -| Accept all in SolutionA | the lazy | 4 the lazy |
+| | Discard all in SolutionA | dog | 5 dog |
+| +--------------------------+ | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.MergedOriginLabels.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.MergedOriginLabels.verified.txt
index 94d68690..e1509f22 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.MergedOriginLabels.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.MergedOriginLabels.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| SampleTests.cs:42 inline 1 of 1 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (1) | received (net8.0, net9.0) | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| > SampleTests.cs:42 | 1 the quick | 1 the quick |
-| | ~ 2 brown dog | ~ 2 brown fox |
-| | 3 jumps over | 3 jumps over |
-| | 4 the lazy | 4 the lazy |
-| | 5 dog | 5 dog |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| SampleTests.cs:42 inline 1 of 1 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (1) | received (net8.0, net9.0) | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| > SampleTests.cs:42 | 1 the quick | 1 the quick |
+| | ~ 2 brown dog | ~ 2 brown fox |
+| | 3 jumps over | 3 jumps over |
+| | 4 the lazy | 4 the lazy |
+| | 5 dog | 5 dog |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.MixedKindsGrouped.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.MixedKindsGrouped.verified.txt
index 483998aa..3eee98e1 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.MixedKindsGrouped.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.MixedKindsGrouped.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| ATests.cs:10 inline 1 of 4 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (4) | received | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| - SolutionA (2) | 1 the quick | 1 the quick |
-| > ATests.cs:10 | ~ 2 brown dog | ~ 2 brown fox |
-| OtherTests.cs:20 | 3 jumps over | 3 jumps over |
-| - SolutionB (2) | 4 the lazy | 4 the lazy |
-| Sample.Test (tx> | 5 dog | 5 dog |
-| extra.verified.> | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| ATests.cs:10 inline 1 of 4 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (4) | received | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| - SolutionA (2) | 1 the quick | 1 the quick |
+| > ATests.cs:10 | ~ 2 brown dog | ~ 2 brown fox |
+| OtherTests.cs:20 | 3 jumps over | 3 jumps over |
+| - SolutionB (2) | 4 the lazy | 4 the lazy |
+| Sample.Test (tx> | 5 dog | 5 dog |
+| extra.verified.> | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.MoveEntry.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.MoveEntry.verified.txt
index 6848b68c..094abc3f 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.MoveEntry.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.MoveEntry.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| Sample.Test (txt) inline 1 of 1 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (1) | sample.received.txt | sample.verified.txt |
-+----------------------+-----------------------------------+-----------------------------------+
-| > Sample.Test (txt) | 1 the quick | 1 the quick |
-| | ~ 2 brown dog | ~ 2 brown fox |
-| | 3 jumps over | 3 jumps over |
-| | 4 the lazy | 4 the lazy |
-| | 5 dog | 5 dog |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept move] [Discard] [Accept all] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| Sample.Test (txt) inline 1 of 1 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (1) | sample.received.txt | sample.verified.txt |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| > Sample.Test (txt) | 1 the quick | 1 the quick |
+| | ~ 2 brown dog | ~ 2 brown fox |
+| | 3 jumps over | 3 jumps over |
+| | 4 the lazy | 4 the lazy |
+| | 5 dog | 5 dog |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept move] [Discard] [Accept all] (Prev change) (Next change) [Changes only] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.NewSnapshot.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.NewSnapshot.verified.txt
index e389e11c..1e2fb584 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.NewSnapshot.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.NewSnapshot.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| SampleTests.cs:42 inline 1 of 1 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (1) | received | expected (new snapshot) |
-+----------------------+-----------------------------------+-----------------------------------+
-| > SampleTests.cs:42 | + 1 the quick | |
-| | + 2 brown dog | |
-| | + 3 jumps over | |
-| | + 4 the lazy | |
-| | + 5 dog | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| SampleTests.cs:42 inline 1 of 1 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (1) | received | expected (new snapshot) |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| > SampleTests.cs:42 | + 1 the quick | |
+| | + 2 brown dog | |
+| | + 3 jumps over | |
+| | + 4 the lazy | |
+| | + 5 dog | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.Queue.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.Queue.verified.txt
index c806f6b8..4e06e083 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.Queue.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.Queue.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| SampleTests.cs:42 inline 1 of 5 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (5) | received | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| > SampleTests.cs:42 | 1 the quick | 1 the quick |
-| SampleTests.cs:88 | ~ 2 brown dog | ~ 2 brown fox |
-| OtherTests.cs:12 | 3 jumps over | 3 jumps over |
-| OtherTests.cs:30 | 4 the lazy | 4 the lazy |
-| WideNamedTests.cs> | 5 dog | 5 dog |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| SampleTests.cs:42 inline 1 of 5 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (5) | received | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| > SampleTests.cs:42 | 1 the quick | 1 the quick |
+| SampleTests.cs:88 | ~ 2 brown dog | ~ 2 brown fox |
+| OtherTests.cs:12 | 3 jumps over | 3 jumps over |
+| OtherTests.cs:30 | 4 the lazy | 4 the lazy |
+| WideNamedTests.cs> | 5 dog | 5 dog |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.SecondItemSelected.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.SecondItemSelected.verified.txt
index 51e78be4..a4eb6cfa 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.SecondItemSelected.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.SecondItemSelected.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| SampleTests.cs:88 inline 2 of 5 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (5) | received | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| SampleTests.cs:42 | ~ 1 two | ~ 1 one |
-| > SampleTests.cs:88 | | |
-| OtherTests.cs:12 | | |
-| OtherTests.cs:30 | | |
-| WideNamedTests.cs> | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] lines 1-1 of 1 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| SampleTests.cs:88 inline 2 of 5 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (5) | received | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| SampleTests.cs:42 | ~ 1 two | ~ 1 one |
+| > SampleTests.cs:88 | | |
+| OtherTests.cs:12 | | |
+| OtherTests.cs:30 | | |
+| WideNamedTests.cs> | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] lines 1-1 of 1 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.Single.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.Single.verified.txt
index 41519ebf..1947d79e 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.Single.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.Single.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| SampleTests.cs:42 inline 1 of 1 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (1) | received | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| > SampleTests.cs:42 | 1 the quick | 1 the quick |
-| | ~ 2 brown dog | ~ 2 brown fox |
-| | 3 jumps over | 3 jumps over |
-| | 4 the lazy | 4 the lazy |
-| | 5 dog | 5 dog |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| SampleTests.cs:42 inline 1 of 1 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (1) | received | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| > SampleTests.cs:42 | 1 the quick | 1 the quick |
+| | ~ 2 brown dog | ~ 2 brown fox |
+| | 3 jumps over | 3 jumps over |
+| | 4 the lazy | 4 the lazy |
+| | 5 dog | 5 dog |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.SolutionAndUngrouped.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.SolutionAndUngrouped.verified.txt
index 430a15ca..300f4338 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.SolutionAndUngrouped.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.SolutionAndUngrouped.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| SampleTests.cs:42 inline 2 of 2 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (2) | received | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| - SolutionA (1) | 1 the quick | 1 the quick |
-| ATests.cs:10 | ~ 2 brown dog | ~ 2 brown fox |
-| > SampleTests.cs:42 | 3 jumps over | 3 jumps over |
-| | 4 the lazy | 4 the lazy |
-| | 5 dog | 5 dog |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| SampleTests.cs:42 inline 2 of 2 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (2) | received | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| - SolutionA (1) | 1 the quick | 1 the quick |
+| ATests.cs:10 | ~ 2 brown dog | ~ 2 brown fox |
+| > SampleTests.cs:42 | 3 jumps over | 3 jumps over |
+| | 4 the lazy | 4 the lazy |
+| | 5 dog | 5 dog |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.StalePatch.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.StalePatch.verified.txt
index aac0ab39..f0864ec9 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.StalePatch.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.StalePatch.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| SampleTests.cs:88 inline 1 of 4 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (4) | received | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| > SampleTests.cs:88 | ~ 1 two | ~ 1 one |
-| OtherTests.cs:12 | | |
-| OtherTests.cs:30 | | |
-| WideNamedTests.cs> | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] SampleTests.cs:42 not written. Could not locate the VerifyI> |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| SampleTests.cs:88 inline 1 of 4 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (4) | received | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| > SampleTests.cs:88 | ~ 1 two | ~ 1 one |
+| OtherTests.cs:12 | | |
+| OtherTests.cs:30 | | |
+| WideNamedTests.cs> | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] SampleTests.cs:42 not written. Could not locate the Veri> |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.TestNameLabels.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.TestNameLabels.verified.txt
index 6bb9a404..9e04a0c4 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.TestNameLabels.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.TestNameLabels.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| SampleTests.cs:42 inline 1 of 2 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (2) | received | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| > Compare handles n> | 1 the quick | 1 the quick |
-| OtherTests.cs:12 | ~ 2 brown dog | ~ 2 brown fox |
-| | 3 jumps over | 3 jumps over |
-| | 4 the lazy | 4 the lazy |
-| | 5 dog | 5 dog |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| SampleTests.cs:42 inline 1 of 2 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (2) | received | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| > Compare handles n> | 1 the quick | 1 the quick |
+| OtherTests.cs:12 | ~ 2 brown dog | ~ 2 brown fox |
+| | 3 jumps over | 3 jumps over |
+| | 4 the lazy | 4 the lazy |
+| | 5 dog | 5 dog |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.TwoSolutions.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.TwoSolutions.verified.txt
index c49e58a7..05fc4d7b 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.TwoSolutions.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.TwoSolutions.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| ATests.cs:10 inline 1 of 3 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (3) | received | expected |
-+----------------------+-----------------------------------+-----------------------------------+
-| - SolutionA (2) | 1 the quick | 1 the quick |
-| > ATests.cs:10 | ~ 2 brown dog | ~ 2 brown fox |
-| OtherTests.cs:20 | 3 jumps over | 3 jumps over |
-| - SolutionB (1) | 4 the lazy | 4 the lazy |
-| BTests.cs:30 | 5 dog | 5 dog |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] lines 1-5 of 5 |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| ATests.cs:10 inline 1 of 3 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (3) | received | expected |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| - SolutionA (2) | 1 the quick | 1 the quick |
+| > ATests.cs:10 | ~ 2 brown dog | ~ 2 brown fox |
+| OtherTests.cs:20 | 3 jumps over | 3 jumps over |
+| - SolutionB (1) | 4 the lazy | 4 the lazy |
+| BTests.cs:30 | 5 dog | 5 dog |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] lines 1-5 of 5 |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/InlineScreenTests.UnparsedExpression.verified.txt b/src/DiffEngineViewer.Tests/InlineScreenTests.UnparsedExpression.verified.txt
index ddc1bc7d..51d49255 100644
--- a/src/DiffEngineViewer.Tests/InlineScreenTests.UnparsedExpression.verified.txt
+++ b/src/DiffEngineViewer.Tests/InlineScreenTests.UnparsedExpression.verified.txt
@@ -1,24 +1,24 @@
-+----------------------------------------------------------------------------------------------+
-| SampleTests.cs:42 inline 1 of 1 |
-+----------------------+-----------------------------------+-----------------------------------+
-| Pending (1) | received | expected (literal not parsed) |
-+----------------------+-----------------------------------+-----------------------------------+
-| > SampleTests.cs:42 | ~ 1 the quick | ~ 1 $"the {value} fox" |
-| | + 2 brown dog | |
-| | + 3 jumps over | |
-| | + 4 the lazy | |
-| | + 5 dog | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-| | | |
-+----------------------+-----------------------------------+-----------------------------------+
-| [Accept] [Discard] [Accept all] Existing expected argument is not a plain string literal. S> |
-+----------------------------------------------------------------------------------------------+
\ No newline at end of file
++--------------------------------------------------------------------------------------------------------------------------------------+
+| SampleTests.cs:42 inline 1 of 1 |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| Pending (1) | received | expected (literal not parsed) |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| > SampleTests.cs:42 | ~ 1 the quick | ~ 1 $"the {value} fox" |
+| | + 2 brown dog | |
+| | + 3 jumps over | |
+| | + 4 the lazy | |
+| | + 5 dog | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
+| | | |
++----------------------+-------------------------------------------------------+-------------------------------------------------------+
+| [Accept] [Discard] [Accept all] (Prev change) (Next change) [Changes only] Existing expected argument is not a plain string literal> |
++--------------------------------------------------------------------------------------------------------------------------------------+
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/MinimalViewTests.cs b/src/DiffEngineViewer.Tests/MinimalViewTests.cs
new file mode 100644
index 00000000..d4585cd8
--- /dev/null
+++ b/src/DiffEngineViewer.Tests/MinimalViewTests.cs
@@ -0,0 +1,259 @@
+///
+/// The minimal view: only the changes, each with three rows either side, and every longer run of
+/// unchanged rows folded into one row. changes rows 2, 16 and 32 of 40,
+/// which folds to 23 rows: lines 1-6, a fold of 7, lines 14-20, a fold of 9, lines 30-36 and a fold
+/// of 4.
+///
+public class MinimalViewTests
+{
+ [Test]
+ public async Task Folds_every_unchanged_run_longer_than_one_row()
+ {
+ var view = Minimal(Long()).View!;
+
+ await Assert.That(view.Count).IsEqualTo(23);
+ await Assert.That(Folds(view)).IsEqualTo(
+ "... 7 unchanged lines ... | ... 9 unchanged lines ... | ... 4 unchanged lines ...");
+ }
+
+ ///
+ /// One unchanged row between two changes' context would fold into a row of its own, taking the
+ /// same space as the line and saying less, so it is shown. Nothing else folds here either, and
+ /// then the minimal view is the full one rather than a copy of it.
+ ///
+ [Test]
+ public async Task A_single_row_between_two_contexts_is_shown()
+ {
+ var state = Fixtures.File(Lines(12, 1, 9), Lines(12));
+
+ var entry = state.Current!;
+
+ await Assert.That(entry.View(true)).IsSameReferenceAs(entry.View(false));
+ }
+
+ [Test]
+ public async Task Everything_folds_when_nothing_changed()
+ {
+ var view = Minimal(Fixtures.File(Fixtures.Long(false), Fixtures.Long(false))).View!;
+
+ await Assert.That(view.Count).IsEqualTo(1);
+ await Assert.That(Folds(view)).IsEqualTo("... 40 unchanged lines ...");
+ }
+
+ ///
+ /// A picture's rows are its properties, each worth reading, so there is nothing to fold and the
+ /// button that would is disabled rather than doing nothing.
+ ///
+ [Test]
+ public async Task A_picture_is_never_folded()
+ {
+ var state = Fixtures.Images();
+
+ var entry = state.Current!;
+
+ await Assert.That(entry.View(true)).IsSameReferenceAs(entry.View(false));
+ await Assert.That(Button(state, CommandKind.ToggleMinimal).Enabled).IsFalse();
+ }
+
+ ///
+ /// Switching views keeps the reader's place: the change on screen stays on the same line of
+ /// the screen while what is around it folds away, and comes back to where it was.
+ ///
+ [Test]
+ public async Task Switching_keeps_the_change_being_read_where_it_is_on_screen()
+ {
+ var full = ViewerSession.Apply(Long(), CommandKind.NextChange);
+ await Assert.That(ScreenLineOf(full, 17)).IsEqualTo(3);
+
+ var minimal = ViewerSession.Apply(full, CommandKind.ToggleMinimal);
+ var back = ViewerSession.Apply(minimal, CommandKind.ToggleMinimal);
+
+ await Assert.That(ScreenLineOf(minimal, 17)).IsEqualTo(3);
+ await Assert.That(back.Minimal).IsFalse();
+ await Assert.That(back.ScrollTop).IsEqualTo(full.ScrollTop);
+ }
+
+ ///
+ /// Nothing on screen survives folding when the reader was inside a run the minimal view folds,
+ /// so the fold standing for where they were is what goes at the top.
+ ///
+ [Test]
+ public async Task Folding_from_inside_a_folded_run_puts_that_fold_at_the_top()
+ {
+ var state = ViewerSession.Apply(
+ Fixtures.File(Fixtures.Deep(true), Fixtures.Deep(false)),
+ Command.Scroll(5));
+
+ var minimal = Minimal(state);
+
+ await Assert.That(minimal.ScrollTop).IsEqualTo(0);
+ await Assert.That(ScreenBuilder.Build(minimal).Left.Rows[0].Kind).IsEqualTo(RowKind.Folded);
+ }
+
+ ///
+ /// Navigation counts rows of the view on screen: the second change is row 10 of the minimal
+ /// view, brought in from row 7 with the three rows of its context above it, which is also the
+ /// last page of 23 rows in 16.
+ ///
+ [Test]
+ public async Task Change_navigation_counts_rows_of_the_minimal_view()
+ {
+ var minimal = Minimal(Long());
+
+ var next = ViewerSession.Apply(minimal, CommandKind.NextChange);
+ var past = ViewerSession.Apply(next, CommandKind.NextChange);
+ var back = ViewerSession.Apply(past, CommandKind.PreviousChange);
+
+ await Assert.That(next.ScrollTop).IsEqualTo(7);
+ await Assert.That(ScreenLineOf(next, 17)).IsEqualTo(3);
+ await Assert.That(past.ScrollTop).IsEqualTo(7);
+ await Assert.That(back.ScrollTop).IsEqualTo(0);
+ }
+
+ ///
+ /// The status line names the stretch of the file on screen, folds and all, rather than a
+ /// position in the list of rows the view happens to have: sixteen rows here run from line 1 to
+ /// line 30 of 40.
+ ///
+ [Test]
+ public async Task The_status_line_counts_lines_of_the_file()
+ {
+ var minimal = Minimal(Long());
+
+ await Assert.That(ScreenBuilder.Build(minimal).Status).IsEqualTo("lines 1-30 of 40");
+ }
+
+ ///
+ /// A view setting rather than something one entry has, so it holds for whatever is selected
+ /// next.
+ ///
+ [Test]
+ public async Task The_view_holds_across_entries()
+ {
+ var state = Fixtures.Inline(
+ Fixtures.Patch("A.cs", 1, Fixtures.Literal(Fixtures.Long(false)), Fixtures.Long(true)),
+ Fixtures.Patch("B.cs", 2, Fixtures.Literal(Fixtures.Long(false)), Fixtures.Long(true)));
+
+ var stepped = ViewerSession.Apply(Minimal(state), CommandKind.NextItem);
+
+ await Assert.That(stepped.Minimal).IsTrue();
+ await Assert.That(stepped.Current!.Name).IsEqualTo("B.cs:2");
+ await Assert.That(stepped.View!.Count).IsEqualTo(23);
+ }
+
+ [Test]
+ public async Task The_fold_button_says_what_it_switches_to()
+ {
+ var state = Long();
+
+ await Assert.That(Button(state, CommandKind.ToggleMinimal).Label).IsEqualTo("Changes only");
+ await Assert.That(Button(Minimal(state), CommandKind.ToggleMinimal).Label).IsEqualTo("All lines");
+ }
+
+ ///
+ /// The change buttons say whether there is a change left to go to, which is otherwise only
+ /// found out by scrolling.
+ ///
+ [Test]
+ public async Task The_change_buttons_are_enabled_only_with_somewhere_to_go()
+ {
+ var first = Long();
+ var second = ViewerSession.Apply(first, CommandKind.NextChange);
+ var last = ViewerSession.Apply(second, CommandKind.NextChange);
+
+ await Assert.That(Enabled(first)).IsEqualTo("previous: False, next: True");
+ await Assert.That(Enabled(second)).IsEqualTo("previous: True, next: True");
+ await Assert.That(Enabled(last)).IsEqualTo("previous: True, next: False");
+ }
+
+ ///
+ /// What is on screen is this window's to lay out however it likes, so switching is never
+ /// forwarded to the queue's owner the way accepting is.
+ ///
+ [Test]
+ public async Task Switching_is_local_when_displaying_someone_elses_queue()
+ {
+ var state = Fixtures.Attached(InlineQueue.Empty, Fixtures.Move());
+ var link = new OwnerLink(new(state), port: 1);
+
+ var switched = ViewerProgram.Apply(
+ state,
+ new(CommandKind.ToggleMinimal, -1, -1, 0, false, Fixtures.Columns, Fixtures.Rows),
+ link,
+ new NoWindow());
+
+ await Assert.That(switched.Minimal).IsTrue();
+ await Assert.That(switched.Message).IsNull();
+ }
+
+ static SessionState Long() =>
+ Fixtures.File(Fixtures.Long(true), Fixtures.Long(false));
+
+ static SessionState Minimal(SessionState state) =>
+ ViewerSession.Apply(state, CommandKind.ToggleMinimal);
+
+ ///
+ /// A text of numbered lines with some of them changed.
+ ///
+ static string Lines(int count, params int[] changed) =>
+ string.Join(
+ "\n",
+ Enumerable
+ .Range(1, count)
+ .Select(_ => changed.Contains(_) ? $"line {_:D2} changed" : $"line {_:D2}"));
+
+ static string Folds(DiffView view) =>
+ string.Join(
+ " | ",
+ view.Left
+ .Where(_ => _.Kind == RowKind.Folded)
+ .Select(_ => _.Text));
+
+ static int ScreenLineOf(SessionState state, int line)
+ {
+ var rows = ScreenBuilder.Build(state).Left.Rows;
+ for (var index = 0; index < rows.Count; index++)
+ {
+ if (rows[index].LineNumber == line)
+ {
+ return index;
+ }
+ }
+
+ throw new($"Line {line} is not on screen.");
+ }
+
+ static Button Button(SessionState state, CommandKind command) =>
+ ScreenBuilder.Build(state).Buttons.Single(_ => _.Command == command);
+
+ static string Enabled(SessionState state) =>
+ $"previous: {Button(state, CommandKind.PreviousChange).Enabled}, next: {Button(state, CommandKind.NextChange).Enabled}";
+
+ sealed class NoWindow : IViewerWindow
+ {
+ public bool Present(Screen screen) =>
+ true;
+
+ public ViewerInput Poll() =>
+ default;
+
+ public void SetHidden(bool hidden)
+ {
+ }
+
+ public void Focus()
+ {
+ }
+
+ public void SetClipboard(string text)
+ {
+ }
+
+ public bool Capture(Screen screen, int width, int height, string pngPath) =>
+ false;
+
+ public void Dispose()
+ {
+ }
+ }
+}
diff --git a/src/DiffEngineViewer.Tests/OpeningTests.cs b/src/DiffEngineViewer.Tests/OpeningTests.cs
new file mode 100644
index 00000000..a5747d0f
--- /dev/null
+++ b/src/DiffEngineViewer.Tests/OpeningTests.cs
@@ -0,0 +1,120 @@
+///
+/// Where an entry is scrolled to when it comes on screen: its first change, with three rows above
+/// it, rather than its first line. changes line 30 of 60, row 29, so
+/// that is row 26 - well past the sixteen body rows a top of zero would show.
+///
+/// Every way an entry arrives is here, because each used to reset the scroll on its own and one
+/// left out would be the one entry a reader still has to go looking through.
+///
+///
+public class OpeningTests
+{
+ [Test]
+ public async Task A_file_comparison()
+ {
+ var state = Fixtures.File(Fixtures.Deep(true), Fixtures.Deep(false));
+
+ await Assert.That(state.ScrollTop).IsEqualTo(26);
+ }
+
+ [Test]
+ public async Task An_inline_snapshot()
+ {
+ var state = Fixtures.Inline(Patch("A.cs", 1));
+
+ await Assert.That(state.ScrollTop).IsEqualTo(26);
+ }
+
+ ///
+ /// A pair arriving in an empty owning viewer, which is how a failing file snapshot reaches one.
+ ///
+ [Test]
+ public async Task A_tracked_pair()
+ {
+ var state = ViewerSession.EnqueueTracked(
+ SessionState.Start(ViewerMode.Inline, Fixtures.Columns, Fixtures.Rows),
+ Fixtures.Move(left: Fixtures.Deep(true), right: Fixtures.Deep(false)));
+
+ await Assert.That(state.ScrollTop).IsEqualTo(26);
+ }
+
+ ///
+ /// An attached viewer's first listing from the owner.
+ ///
+ [Test]
+ public async Task A_queue_read_from_its_owner()
+ {
+ var state = Fixtures.Attached(Fixtures.Pending(Patch("A.cs", 1)));
+
+ await Assert.That(state.ScrollTop).IsEqualTo(26);
+ }
+
+ [Test]
+ public async Task Stepping_to_the_next_entry()
+ {
+ var state = Fixtures.Inline(Fixtures.Patch(), Patch("B.cs", 2));
+
+ var stepped = ViewerSession.Apply(state, CommandKind.NextItem);
+
+ await Assert.That(stepped.Current!.Name).IsEqualTo("B.cs:2");
+ await Assert.That(stepped.ScrollTop).IsEqualTo(26);
+ }
+
+ ///
+ /// A variant is different text, so it is met the way a different entry is.
+ ///
+ [Test]
+ public async Task Cycling_to_another_variant()
+ {
+ var shifted = Fixtures.Deep(true).Replace("line 50 changed", "line 50");
+ var state = Fixtures.Inline(
+ Patch("A.cs", 1, Fixtures.Deep(true), "net8.0"),
+ Patch("A.cs", 1, $"line 01 changed{shifted[7..]}", "net9.0"));
+ var scrolled = ViewerSession.Apply(state, CommandKind.PageDown);
+
+ var cycled = ViewerSession.Apply(scrolled, CommandKind.NextVariant);
+
+ await Assert.That(cycled.Current!.SelectedVariant).IsEqualTo(1);
+ // That variant's first change is line 1, which has no rows above it to show.
+ await Assert.That(cycled.ScrollTop).IsEqualTo(0);
+ }
+
+ [Test]
+ public async Task Nothing_changed_opens_at_the_top()
+ {
+ var state = Fixtures.File(Fixtures.Deep(false), Fixtures.Deep(false));
+
+ await Assert.That(state.ScrollTop).IsEqualTo(0);
+ }
+
+ ///
+ /// A first change already on the first page, with fewer than three rows above it, leaves the
+ /// view where it was: there is nothing above it to scroll off.
+ ///
+ [Test]
+ public async Task A_change_near_the_top_opens_at_the_top()
+ {
+ var state = Fixtures.File(Fixtures.Long(true), Fixtures.Long(false));
+
+ await Assert.That(state.ScrollTop).IsEqualTo(0);
+ }
+
+ ///
+ /// A first change on the last line opens on the last page, like any scroll that far.
+ ///
+ [Test]
+ public async Task A_change_on_the_last_line_opens_on_the_last_page()
+ {
+ var state = Fixtures.File($"{Fixtures.Deep(false)}!", Fixtures.Deep(false));
+
+ await Assert.That(state.ScrollTop).IsEqualTo(60 - ScreenBuilder.BodyRows(state));
+ }
+
+ static InlinePatch Patch(string source, int line, string? content = null, string? framework = null) =>
+ Fixtures.Patch(
+ source,
+ line,
+ Fixtures.Literal(Fixtures.Deep(false)),
+ content ?? Fixtures.Deep(true),
+ framework: framework);
+}
diff --git a/src/DiffEngineViewer.Tests/PixelTests.ContextMenu.Linux.verified.png b/src/DiffEngineViewer.Tests/PixelTests.ContextMenu.Linux.verified.png
index 7da61c36..6b33e167 100644
Binary files a/src/DiffEngineViewer.Tests/PixelTests.ContextMenu.Linux.verified.png and b/src/DiffEngineViewer.Tests/PixelTests.ContextMenu.Linux.verified.png differ
diff --git a/src/DiffEngineViewer.Tests/PixelTests.FileDiff.Linux.verified.png b/src/DiffEngineViewer.Tests/PixelTests.FileDiff.Linux.verified.png
index 5c8b5403..1c6a24c5 100644
Binary files a/src/DiffEngineViewer.Tests/PixelTests.FileDiff.Linux.verified.png and b/src/DiffEngineViewer.Tests/PixelTests.FileDiff.Linux.verified.png differ
diff --git a/src/DiffEngineViewer.Tests/PixelTests.FileDiff.OSX.verified.png b/src/DiffEngineViewer.Tests/PixelTests.FileDiff.OSX.verified.png
index a32921a9..59807c96 100644
Binary files a/src/DiffEngineViewer.Tests/PixelTests.FileDiff.OSX.verified.png and b/src/DiffEngineViewer.Tests/PixelTests.FileDiff.OSX.verified.png differ
diff --git a/src/DiffEngineViewer.Tests/PixelTests.GroupedConflictedQueue.Linux.verified.png b/src/DiffEngineViewer.Tests/PixelTests.GroupedConflictedQueue.Linux.verified.png
index 5fe38abb..925c8883 100644
Binary files a/src/DiffEngineViewer.Tests/PixelTests.GroupedConflictedQueue.Linux.verified.png and b/src/DiffEngineViewer.Tests/PixelTests.GroupedConflictedQueue.Linux.verified.png differ
diff --git a/src/DiffEngineViewer.Tests/PixelTests.GroupedConflictedQueue.OSX.verified.png b/src/DiffEngineViewer.Tests/PixelTests.GroupedConflictedQueue.OSX.verified.png
index cd245fac..a7ca83da 100644
Binary files a/src/DiffEngineViewer.Tests/PixelTests.GroupedConflictedQueue.OSX.verified.png and b/src/DiffEngineViewer.Tests/PixelTests.GroupedConflictedQueue.OSX.verified.png differ
diff --git a/src/DiffEngineViewer.Tests/PixelTests.Images.Linux.verified.png b/src/DiffEngineViewer.Tests/PixelTests.Images.Linux.verified.png
index c8f41971..5642a48a 100644
Binary files a/src/DiffEngineViewer.Tests/PixelTests.Images.Linux.verified.png and b/src/DiffEngineViewer.Tests/PixelTests.Images.Linux.verified.png differ
diff --git a/src/DiffEngineViewer.Tests/PixelTests.Images.OSX.verified.png b/src/DiffEngineViewer.Tests/PixelTests.Images.OSX.verified.png
index 4cb1e991..ee224a10 100644
Binary files a/src/DiffEngineViewer.Tests/PixelTests.Images.OSX.verified.png and b/src/DiffEngineViewer.Tests/PixelTests.Images.OSX.verified.png differ
diff --git a/src/DiffEngineViewer.Tests/PixelTests.InlineAccepted.Linux.verified.png b/src/DiffEngineViewer.Tests/PixelTests.InlineAccepted.Linux.verified.png
index b456d1d2..bf9eba54 100644
Binary files a/src/DiffEngineViewer.Tests/PixelTests.InlineAccepted.Linux.verified.png and b/src/DiffEngineViewer.Tests/PixelTests.InlineAccepted.Linux.verified.png differ
diff --git a/src/DiffEngineViewer.Tests/PixelTests.InlineAccepted.OSX.verified.png b/src/DiffEngineViewer.Tests/PixelTests.InlineAccepted.OSX.verified.png
index f8a3ad9f..093ea4e2 100644
Binary files a/src/DiffEngineViewer.Tests/PixelTests.InlineAccepted.OSX.verified.png and b/src/DiffEngineViewer.Tests/PixelTests.InlineAccepted.OSX.verified.png differ
diff --git a/src/DiffEngineViewer.Tests/PixelTests.InlineQueue.Linux.verified.png b/src/DiffEngineViewer.Tests/PixelTests.InlineQueue.Linux.verified.png
index 11b1a443..f9ff801c 100644
Binary files a/src/DiffEngineViewer.Tests/PixelTests.InlineQueue.Linux.verified.png and b/src/DiffEngineViewer.Tests/PixelTests.InlineQueue.Linux.verified.png differ
diff --git a/src/DiffEngineViewer.Tests/PixelTests.InlineQueue.OSX.verified.png b/src/DiffEngineViewer.Tests/PixelTests.InlineQueue.OSX.verified.png
index edbf4c2c..ccff60d3 100644
Binary files a/src/DiffEngineViewer.Tests/PixelTests.InlineQueue.OSX.verified.png and b/src/DiffEngineViewer.Tests/PixelTests.InlineQueue.OSX.verified.png differ
diff --git a/src/DiffEngineViewer.Tests/PixelTests.InlineSingle.Linux.verified.png b/src/DiffEngineViewer.Tests/PixelTests.InlineSingle.Linux.verified.png
index 1ff12460..1a31035a 100644
Binary files a/src/DiffEngineViewer.Tests/PixelTests.InlineSingle.Linux.verified.png and b/src/DiffEngineViewer.Tests/PixelTests.InlineSingle.Linux.verified.png differ
diff --git a/src/DiffEngineViewer.Tests/PixelTests.InlineSingle.OSX.verified.png b/src/DiffEngineViewer.Tests/PixelTests.InlineSingle.OSX.verified.png
index f2b91db9..55834d04 100644
Binary files a/src/DiffEngineViewer.Tests/PixelTests.InlineSingle.OSX.verified.png and b/src/DiffEngineViewer.Tests/PixelTests.InlineSingle.OSX.verified.png differ
diff --git a/src/DiffEngineViewer.Tests/PixelTests.LongQueueLabel.Linux.verified.png b/src/DiffEngineViewer.Tests/PixelTests.LongQueueLabel.Linux.verified.png
index d0be3a93..bd888ac4 100644
Binary files a/src/DiffEngineViewer.Tests/PixelTests.LongQueueLabel.Linux.verified.png and b/src/DiffEngineViewer.Tests/PixelTests.LongQueueLabel.Linux.verified.png differ
diff --git a/src/DiffEngineViewer.Tests/PixelTests.LongQueueLabel.OSX.verified.png b/src/DiffEngineViewer.Tests/PixelTests.LongQueueLabel.OSX.verified.png
index 9d3053dc..7cb96ac5 100644
Binary files a/src/DiffEngineViewer.Tests/PixelTests.LongQueueLabel.OSX.verified.png and b/src/DiffEngineViewer.Tests/PixelTests.LongQueueLabel.OSX.verified.png differ
diff --git a/src/DiffEngineViewer.Tests/PixelTests.Minimal.Linux.verified.png b/src/DiffEngineViewer.Tests/PixelTests.Minimal.Linux.verified.png
new file mode 100644
index 00000000..d34bb225
Binary files /dev/null and b/src/DiffEngineViewer.Tests/PixelTests.Minimal.Linux.verified.png differ
diff --git a/src/DiffEngineViewer.Tests/PixelTests.Minimal.OSX.verified.png b/src/DiffEngineViewer.Tests/PixelTests.Minimal.OSX.verified.png
new file mode 100644
index 00000000..2a01b622
Binary files /dev/null and b/src/DiffEngineViewer.Tests/PixelTests.Minimal.OSX.verified.png differ
diff --git a/src/DiffEngineViewer.Tests/PixelTests.Selection.Linux.verified.png b/src/DiffEngineViewer.Tests/PixelTests.Selection.Linux.verified.png
index 9db521cf..6acef28d 100644
Binary files a/src/DiffEngineViewer.Tests/PixelTests.Selection.Linux.verified.png and b/src/DiffEngineViewer.Tests/PixelTests.Selection.Linux.verified.png differ
diff --git a/src/DiffEngineViewer.Tests/PixelTests.Selection.OSX.verified.png b/src/DiffEngineViewer.Tests/PixelTests.Selection.OSX.verified.png
index ed8e3860..00290059 100644
Binary files a/src/DiffEngineViewer.Tests/PixelTests.Selection.OSX.verified.png and b/src/DiffEngineViewer.Tests/PixelTests.Selection.OSX.verified.png differ
diff --git a/src/DiffEngineViewer.Tests/PixelTests.cs b/src/DiffEngineViewer.Tests/PixelTests.cs
index f8676859..e520c5a4 100644
--- a/src/DiffEngineViewer.Tests/PixelTests.cs
+++ b/src/DiffEngineViewer.Tests/PixelTests.cs
@@ -173,6 +173,16 @@ public Task InlineAccepted()
return Capture(ViewerSession.Apply(state, CommandKind.Accept, Fixtures.Applied));
}
+ ///
+ /// The minimal view, whose folded rows are drawn dimmed on a band of their own and with no
+ /// line number. Mirrored in WindowsPixelTests over the same fixture.
+ ///
+ [Test]
+ [PixelTest]
+ [NotInParallel(nameof(PixelTests), Order = 10)]
+ public Task Minimal() =>
+ Capture(ViewerSession.Apply(Fixtures.File(Fixtures.Long(true), Fixtures.Long(false)), CommandKind.ToggleMinimal));
+
static async Task Capture(SessionState state)
{
var screen = ScreenBuilder.Build(ViewerSession.Resize(state, columns, rows));
diff --git a/src/DiffEngineViewer.Tests/PreviousChangeTests.cs b/src/DiffEngineViewer.Tests/PreviousChangeTests.cs
index f8c5ef43..ca29237f 100644
--- a/src/DiffEngineViewer.Tests/PreviousChangeTests.cs
+++ b/src/DiffEngineViewer.Tests/PreviousChangeTests.cs
@@ -1,6 +1,8 @@
///
/// Stepping back through the changed blocks of the entry on screen. Forty rows with changes at 3,
-/// 17 and 33 - so rows 2, 16 and 32 - and sixteen body rows to show them in.
+/// 17 and 33 - so rows 2, 16 and 32 - and sixteen body rows to show them in. A change is brought in
+/// with three rows above it, so the places the three are shown from are rows 0, 13 and 24, the last
+/// clamped to the final page.
///
public class PreviousChangeTests
{
@@ -15,7 +17,7 @@ public async Task Lands_on_a_block_ending_just_above_the_viewport()
var moved = ViewerSession.Apply(state, CommandKind.PreviousChange);
- await Assert.That(moved.ScrollTop).IsEqualTo(16);
+ await Assert.That(moved.ScrollTop).IsEqualTo(13);
}
///
@@ -29,31 +31,49 @@ public async Task Reaches_the_first_block_from_the_row_below_it()
var moved = ViewerSession.Apply(state, CommandKind.PreviousChange);
- await Assert.That(moved.ScrollTop).IsEqualTo(2);
+ await Assert.That(moved.ScrollTop).IsEqualTo(0);
}
///
- /// A viewport that really is inside a block still steps off it, or previous would never leave
- /// the block it is in.
+ /// A viewport showing a block from where navigation puts it still steps off it, or previous
+ /// would never leave the block it is in.
///
[Test]
public async Task Steps_off_the_block_the_viewport_is_in()
{
- var state = At(16);
+ var state = At(13);
var moved = ViewerSession.Apply(state, CommandKind.PreviousChange);
- await Assert.That(moved.ScrollTop).IsEqualTo(2);
+ await Assert.That(moved.ScrollTop).IsEqualTo(0);
}
[Test]
public async Task Stays_at_the_first_block()
{
- var state = At(2);
+ var state = At(0);
var moved = ViewerSession.Apply(state, CommandKind.PreviousChange);
- await Assert.That(moved.ScrollTop).IsEqualTo(2);
+ await Assert.That(moved.ScrollTop).IsEqualTo(0);
+ }
+
+ ///
+ /// Previous undoes next, from every place next can land. A change is put the same distance
+ /// under the top whichever way it was reached, or the two would drift apart a few rows a trip.
+ ///
+ [Test]
+ public async Task Undoes_next()
+ {
+ var start = At(0);
+ var second = ViewerSession.Apply(start, CommandKind.NextChange);
+ var third = ViewerSession.Apply(second, CommandKind.NextChange);
+
+ var back = ViewerSession.Apply(third, CommandKind.PreviousChange);
+ var home = ViewerSession.Apply(back, CommandKind.PreviousChange);
+
+ await Assert.That(back.ScrollTop).IsEqualTo(second.ScrollTop);
+ await Assert.That(home.ScrollTop).IsEqualTo(start.ScrollTop);
}
static SessionState At(int scrollTop) =>
diff --git a/src/DiffEngineViewer.Tests/QueueRemovalTests.cs b/src/DiffEngineViewer.Tests/QueueRemovalTests.cs
index 024b3576..884ce0cb 100644
--- a/src/DiffEngineViewer.Tests/QueueRemovalTests.cs
+++ b/src/DiffEngineViewer.Tests/QueueRemovalTests.cs
@@ -17,8 +17,8 @@ public async Task Settling_another_entry_leaves_the_one_being_read_where_it_was(
}
///
- /// The entry on screen going is the one case where the reader has to be moved, and the top of
- /// the next entry is where they go.
+ /// The entry on screen going is the one case where the reader has to be moved, and the first
+ /// change of the next entry is where they go.
///
[Test]
public async Task Settling_the_entry_being_read_moves_on_to_the_next()
@@ -28,27 +28,28 @@ public async Task Settling_the_entry_being_read_moves_on_to_the_next()
var settled = ViewerSession.Settle(state, reading);
await Assert.That(settled.Current!.Key).IsNotEqualTo(reading);
- await Assert.That(settled.ScrollTop).IsEqualTo(0);
+ await Assert.That(settled.ScrollTop).IsEqualTo(26);
}
///
/// Three entries, the middle one selected and scrolled into - so that a removal above it moves
- /// every index below, which is the thing being asserted about.
+ /// every index below, which is the thing being asserted about. Scrolled past where it opened,
+ /// so staying put and moving on cannot be mistaken for each other.
///
static SessionState Scrolled(out string reading)
{
- var state = Fixtures.Inline(
- Fixtures.Patch("A.cs", 1, null, Fixtures.Long(true)),
- Fixtures.Patch("B.cs", 2, null, Fixtures.Long(true)),
- Fixtures.Patch("C.cs", 3, null, Fixtures.Long(true)));
+ var state = Fixtures.Inline(Patch("A.cs", 1), Patch("B.cs", 2), Patch("C.cs", 3));
reading = state.Queue[1].Key;
state = ViewerSession.SelectKey(state, reading);
state = ViewerSession.Apply(state, CommandKind.PageDown);
- if (state.ScrollTop == 0)
+ if (state.ScrollTop == 26)
{
throw new("The entry did not scroll, so nothing below asserts anything.");
}
return state;
}
+
+ static InlinePatch Patch(string source, int line) =>
+ Fixtures.Patch(source, line, Fixtures.Literal(Fixtures.Deep(false)), Fixtures.Deep(true));
}
diff --git a/src/DiffEngineViewer.Tests/ReEnqueueTests.cs b/src/DiffEngineViewer.Tests/ReEnqueueTests.cs
index edb45887..4f09894c 100644
--- a/src/DiffEngineViewer.Tests/ReEnqueueTests.cs
+++ b/src/DiffEngineViewer.Tests/ReEnqueueTests.cs
@@ -10,29 +10,34 @@ public async Task An_identical_re_send_leaves_the_scroll_alone()
{
var state = Scrolled();
- var again = ViewerSession.EnqueueInline(state, Patch(Fixtures.Long(true)));
+ var again = ViewerSession.EnqueueInline(state, Patch(Fixtures.Deep(true)));
await Assert.That(again.Queue[0]).IsSameReferenceAs(state.Queue[0]);
await Assert.That(again.ScrollTop).IsEqualTo(state.ScrollTop);
}
///
- /// A re-send that says something else is a new comparison, and that one does start at the top.
+ /// A re-send that says something else is a new comparison, and that one does start again, at
+ /// its first change.
///
[Test]
- public async Task A_re_send_of_different_content_starts_at_the_top()
+ public async Task A_re_send_of_different_content_starts_at_its_first_change()
{
var state = Scrolled();
- var again = ViewerSession.EnqueueInline(state, Patch($"{Fixtures.Long(true)}\nand one more line"));
+ var again = ViewerSession.EnqueueInline(state, Patch($"{Fixtures.Deep(true)}\nand one more line"));
- await Assert.That(again.ScrollTop).IsEqualTo(0);
+ await Assert.That(again.ScrollTop).IsEqualTo(26);
}
+ ///
+ /// Scrolled away from where the entry opened, so neither staying put nor starting again can
+ /// pass by coincidence.
+ ///
static SessionState Scrolled()
{
- var state = ViewerSession.Apply(Fixtures.Inline(Patch(Fixtures.Long(true))), CommandKind.PageDown);
- if (state.ScrollTop == 0)
+ var state = ViewerSession.Apply(Fixtures.Inline(Patch(Fixtures.Deep(true))), CommandKind.PageDown);
+ if (state.ScrollTop == 26)
{
throw new("The entry did not scroll, so nothing below asserts anything.");
}
@@ -41,5 +46,5 @@ static SessionState Scrolled()
}
static InlinePatch Patch(string content) =>
- Fixtures.Patch("A.cs", 1, null, content);
+ Fixtures.Patch("A.cs", 1, Fixtures.Literal(Fixtures.Deep(false)), content);
}
diff --git a/src/DiffEngineViewer.Tests/ReselectTests.cs b/src/DiffEngineViewer.Tests/ReselectTests.cs
index 1556a4cc..36b73d26 100644
--- a/src/DiffEngineViewer.Tests/ReselectTests.cs
+++ b/src/DiffEngineViewer.Tests/ReselectTests.cs
@@ -28,27 +28,30 @@ public async Task Focusing_the_entry_being_read_keeps_the_scroll()
}
///
- /// A different entry is a different comparison, so that one does start at the top.
+ /// A different entry is a different comparison, so that one does start again, at its first
+ /// change.
///
[Test]
- public async Task Selecting_another_entry_starts_at_its_top()
+ public async Task Selecting_another_entry_starts_at_its_first_change()
{
var state = Scrolled();
var selected = ViewerSession.SelectKey(state, state.Queue[0].Key);
await Assert.That(selected.Current!.Key).IsEqualTo(state.Queue[0].Key);
- await Assert.That(selected.ScrollTop).IsEqualTo(0);
+ await Assert.That(selected.ScrollTop).IsEqualTo(26);
}
+ ///
+ /// Scrolled past where the entry opened, so keeping the scroll and starting again cannot be
+ /// mistaken for each other.
+ ///
static SessionState Scrolled()
{
- var state = Fixtures.Inline(
- Fixtures.Patch("A.cs", 1, null, Fixtures.Long(true)),
- Fixtures.Patch("B.cs", 2, null, Fixtures.Long(true)));
+ var state = Fixtures.Inline(Patch("A.cs", 1), Patch("B.cs", 2));
state = ViewerSession.SelectKey(state, state.Queue[1].Key);
state = ViewerSession.Apply(state, CommandKind.PageDown);
- if (state.ScrollTop == 0)
+ if (state.ScrollTop == 26)
{
throw new("The entry did not scroll, so nothing below asserts anything.");
}
@@ -56,6 +59,9 @@ static SessionState Scrolled()
return state;
}
+ static InlinePatch Patch(string source, int line) =>
+ Fixtures.Patch(source, line, Fixtures.Literal(Fixtures.Deep(false)), Fixtures.Deep(true));
+
static int VisibleRowOf(SessionState state, int entry)
{
var visible = QueueProjection.Visible(state, ScreenBuilder.BodyRows(state), out _);
diff --git a/src/DiffEngineViewer.Tests/SelectionTests.ASelectionSurvivesSwitchingViews.verified.txt b/src/DiffEngineViewer.Tests/SelectionTests.ASelectionSurvivesSwitchingViews.verified.txt
new file mode 100644
index 00000000..730e618c
--- /dev/null
+++ b/src/DiffEngineViewer.Tests/SelectionTests.ASelectionSurvivesSwitchingViews.verified.txt
@@ -0,0 +1,50 @@
+status: selected 11 lines, 84 characters
+
+Sample.received.txt
+ 1 line 01
+ 2 line 02
+ 3 line 03 changed
+ 4 line 04
+ 5 [line 05]
+ 6 [line 06]
+ [... 7 unchanged lines ...]
+ 14 [line 14]
+ 15 [line] 15
+ 16 line 16
+ 17 line 17 changed
+ 18 line 18
+ 19 line 19
+ 20 line 20
+ ... 9 unchanged lines ...
+ 30 line 30
+
+Sample.verified.txt
+ 1 line 01
+ 2 line 02
+ 3 line 03
+ 4 line 04
+ 5 line 05
+ 6 line 06
+ ... 7 unchanged lines ...
+ 14 line 14
+ 15 line 15
+ 16 line 16
+ 17 line 17
+ 18 line 18
+ 19 line 19
+ 20 line 20
+ ... 9 unchanged lines ...
+ 30 line 30
+
+copied:
+line 05
+line 06
+line 07
+line 08
+line 09
+line 10
+line 11
+line 12
+line 13
+line 14
+line
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/SelectionTests.AcrossAFoldedRow.verified.txt b/src/DiffEngineViewer.Tests/SelectionTests.AcrossAFoldedRow.verified.txt
new file mode 100644
index 00000000..730e618c
--- /dev/null
+++ b/src/DiffEngineViewer.Tests/SelectionTests.AcrossAFoldedRow.verified.txt
@@ -0,0 +1,50 @@
+status: selected 11 lines, 84 characters
+
+Sample.received.txt
+ 1 line 01
+ 2 line 02
+ 3 line 03 changed
+ 4 line 04
+ 5 [line 05]
+ 6 [line 06]
+ [... 7 unchanged lines ...]
+ 14 [line 14]
+ 15 [line] 15
+ 16 line 16
+ 17 line 17 changed
+ 18 line 18
+ 19 line 19
+ 20 line 20
+ ... 9 unchanged lines ...
+ 30 line 30
+
+Sample.verified.txt
+ 1 line 01
+ 2 line 02
+ 3 line 03
+ 4 line 04
+ 5 line 05
+ 6 line 06
+ ... 7 unchanged lines ...
+ 14 line 14
+ 15 line 15
+ 16 line 16
+ 17 line 17
+ 18 line 18
+ 19 line 19
+ 20 line 20
+ ... 9 unchanged lines ...
+ 30 line 30
+
+copied:
+line 05
+line 06
+line 07
+line 08
+line 09
+line 10
+line 11
+line 12
+line 13
+line 14
+line
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/SelectionTests.EndingOnAFoldedRow.verified.txt b/src/DiffEngineViewer.Tests/SelectionTests.EndingOnAFoldedRow.verified.txt
new file mode 100644
index 00000000..c1bd4afe
--- /dev/null
+++ b/src/DiffEngineViewer.Tests/SelectionTests.EndingOnAFoldedRow.verified.txt
@@ -0,0 +1,50 @@
+status: selected 11 lines, 95 characters
+
+Sample.received.txt
+ 1 line 01
+ 2 line 02
+ 3 [line 03 changed]
+ 4 [line 04]
+ 5 [line 05]
+ 6 [line 06]
+ [... 7 unchanged lines ...]
+ 14 line 14
+ 15 line 15
+ 16 line 16
+ 17 line 17 changed
+ 18 line 18
+ 19 line 19
+ 20 line 20
+ ... 9 unchanged lines ...
+ 30 line 30
+
+Sample.verified.txt
+ 1 line 01
+ 2 line 02
+ 3 line 03
+ 4 line 04
+ 5 line 05
+ 6 line 06
+ ... 7 unchanged lines ...
+ 14 line 14
+ 15 line 15
+ 16 line 16
+ 17 line 17
+ 18 line 18
+ 19 line 19
+ 20 line 20
+ ... 9 unchanged lines ...
+ 30 line 30
+
+copied:
+line 03 changed
+line 04
+line 05
+line 06
+line 07
+line 08
+line 09
+line 10
+line 11
+line 12
+line 13
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/SelectionTests.StartingOnAFoldedRowGoingUp.verified.txt b/src/DiffEngineViewer.Tests/SelectionTests.StartingOnAFoldedRowGoingUp.verified.txt
new file mode 100644
index 00000000..c1bd4afe
--- /dev/null
+++ b/src/DiffEngineViewer.Tests/SelectionTests.StartingOnAFoldedRowGoingUp.verified.txt
@@ -0,0 +1,50 @@
+status: selected 11 lines, 95 characters
+
+Sample.received.txt
+ 1 line 01
+ 2 line 02
+ 3 [line 03 changed]
+ 4 [line 04]
+ 5 [line 05]
+ 6 [line 06]
+ [... 7 unchanged lines ...]
+ 14 line 14
+ 15 line 15
+ 16 line 16
+ 17 line 17 changed
+ 18 line 18
+ 19 line 19
+ 20 line 20
+ ... 9 unchanged lines ...
+ 30 line 30
+
+Sample.verified.txt
+ 1 line 01
+ 2 line 02
+ 3 line 03
+ 4 line 04
+ 5 line 05
+ 6 line 06
+ ... 7 unchanged lines ...
+ 14 line 14
+ 15 line 15
+ 16 line 16
+ 17 line 17
+ 18 line 18
+ 19 line 19
+ 20 line 20
+ ... 9 unchanged lines ...
+ 30 line 30
+
+copied:
+line 03 changed
+line 04
+line 05
+line 06
+line 07
+line 08
+line 09
+line 10
+line 11
+line 12
+line 13
\ No newline at end of file
diff --git a/src/DiffEngineViewer.Tests/SelectionTests.cs b/src/DiffEngineViewer.Tests/SelectionTests.cs
index bebd99ea..1a9cf3de 100644
--- a/src/DiffEngineViewer.Tests/SelectionTests.cs
+++ b/src/DiffEngineViewer.Tests/SelectionTests.cs
@@ -56,6 +56,60 @@ public Task OverAFillerRow() =>
3,
4)));
+ ///
+ /// A drag from one change's context into the next, across the fold between them. A fold stands
+ /// for the lines it leaves out, so it is highlighted whole and they are copied: what lies
+ /// between a selection's two ends is what a selection is, on screen or not.
+ ///
+ [Test]
+ public Task AcrossAFoldedRow() =>
+ Verify(Report(Drag(Minimal(), PaneSide.Left, 4, 0, 8, 4)));
+
+ ///
+ /// A drag finishing on a fold takes in every line it stands for, since part of a label is not
+ /// part of anything.
+ ///
+ [Test]
+ public Task EndingOnAFoldedRow() =>
+ Verify(Report(Drag(Minimal(), PaneSide.Left, 2, 0, 6, 3)));
+
+ ///
+ /// The same fold with the drag going up from it. It is still the end the selection finishes
+ /// at, so it still takes in all of its lines, rather than stopping at the first of them because
+ /// that is where the press landed.
+ ///
+ [Test]
+ public Task StartingOnAFoldedRowGoingUp() =>
+ Verify(Report(Drag(Minimal(), PaneSide.Left, 6, 3, 2, 0)));
+
+ ///
+ /// A selection is held in the entry's rows rather than the view's, so switching views leaves it
+ /// selecting the same text: the middle of this one folds away and is still copied.
+ ///
+ [Test]
+ public Task ASelectionSurvivesSwitchingViews()
+ {
+ var full = Files(Fixtures.Long(true), Fixtures.Long(false));
+ var selected = Drag(full, PaneSide.Left, 4, 0, 14, 4);
+ return Verify(Report(Key(selected, CommandKind.ToggleMinimal)));
+ }
+
+ ///
+ /// A press with no drag behind it is a click whatever it lands on, and a click clears. On a
+ /// fold, taking it for a selection of everything the fold stands for would select seven lines
+ /// with a tap.
+ ///
+ [Test]
+ public async Task AClickOnAFoldedRowClearsTheSelection()
+ {
+ var selected = Drag(Minimal(), PaneSide.Left, 1, 0, 3, 4);
+ await Assert.That(selected.LiveSelection).IsNotNull();
+
+ var clicked = Drag(selected, PaneSide.Left, 6, 3, 6, 3);
+
+ await Assert.That(clicked.LiveSelection!.IsEmpty).IsTrue();
+ }
+
[Test]
public Task SelectAll() =>
Verify(Report(Key(Files(), CommandKind.SelectAll)));
@@ -183,6 +237,13 @@ public async Task SelectAllThenCopyTakesTheWholeSide()
static SessionState Files(string left = Fixtures.Received, string right = Fixtures.Expected) =>
Fixtures.File(left, right);
+ ///
+ /// Forty lines in the minimal view: lines 1-6 on rows 0-5, a fold of lines 7-13 on row 6, and
+ /// lines 14-20 from row 7.
+ ///
+ static SessionState Minimal() =>
+ Key(Files(Fixtures.Long(true), Fixtures.Long(false)), CommandKind.ToggleMinimal);
+
static SessionState Drag(
SessionState state,
PaneSide side,
diff --git a/src/DiffEngineViewer.Tests/ViewerLaunchTests.cs b/src/DiffEngineViewer.Tests/ViewerLaunchTests.cs
index 2ea7189a..fe3349ff 100644
--- a/src/DiffEngineViewer.Tests/ViewerLaunchTests.cs
+++ b/src/DiffEngineViewer.Tests/ViewerLaunchTests.cs
@@ -88,7 +88,7 @@ public async Task FileDiff()
"Two panes, headers Sample.received.txt and Sample.verified.txt",
"Line 2 highlighted on both sides, dog on the left and fox on the right",
"No pending queue column",
- "Buttons are Accept and Close");
+ "Buttons are Accept, Close, Prev change, Next change and Changes only");
var result = await EngineRunner.LaunchAsync(EngineTool.DiffEngineViewer, temp, target);
diff --git a/src/DiffEngineViewer.Tests/ViewerSessionTests.cs b/src/DiffEngineViewer.Tests/ViewerSessionTests.cs
index a5f44795..b6e70524 100644
--- a/src/DiffEngineViewer.Tests/ViewerSessionTests.cs
+++ b/src/DiffEngineViewer.Tests/ViewerSessionTests.cs
@@ -290,15 +290,16 @@ public async Task NextChangeWalksBlocksThenStops()
{
var state = Fixtures.File(Fixtures.Long(true), Fixtures.Long(false));
- var first = ViewerSession.Apply(state, CommandKind.NextChange);
- var second = ViewerSession.Apply(first, CommandKind.NextChange);
+ var second = ViewerSession.Apply(state, CommandKind.NextChange);
var third = ViewerSession.Apply(second, CommandKind.NextChange);
var past = ViewerSession.Apply(third, CommandKind.NextChange);
- // Changes sit on lines 3, 17 and 33, so rows 2, 16 and 32 zero based. Row 32 is past the
- // last full page of 40 rows in a 16 row viewport, so it clamps to 24 and stays there.
- await Assert.That(first.ScrollTop).IsEqualTo(2);
- await Assert.That(second.ScrollTop).IsEqualTo(16);
+ // Changes sit on lines 3, 17 and 33, so rows 2, 16 and 32 zero based, and each is brought
+ // in with three rows above it. The first is already there when the entry opens, so the
+ // walk starts at the second. Row 32 is past the last full page of 40 rows in a 16 row
+ // viewport, so it clamps to 24 and stays there.
+ await Assert.That(state.ScrollTop).IsEqualTo(0);
+ await Assert.That(second.ScrollTop).IsEqualTo(13);
await Assert.That(third.ScrollTop).IsEqualTo(24);
await Assert.That(past.ScrollTop).IsEqualTo(24);
}
diff --git a/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.FileDiff.verified.png b/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.FileDiff.verified.png
index b1656a8d..b6eb6961 100644
Binary files a/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.FileDiff.verified.png and b/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.FileDiff.verified.png differ
diff --git a/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.GroupedConflictedQueue.verified.png b/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.GroupedConflictedQueue.verified.png
index 41d9c252..7b33bff7 100644
Binary files a/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.GroupedConflictedQueue.verified.png and b/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.GroupedConflictedQueue.verified.png differ
diff --git a/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.Images.verified.png b/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.Images.verified.png
index be334206..6b9372bd 100644
Binary files a/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.Images.verified.png and b/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.Images.verified.png differ
diff --git a/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.InlineAccepted.verified.png b/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.InlineAccepted.verified.png
index b7bbc80a..d3368755 100644
Binary files a/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.InlineAccepted.verified.png and b/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.InlineAccepted.verified.png differ
diff --git a/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.InlineQueue.verified.png b/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.InlineQueue.verified.png
index c2d3b5d1..15809042 100644
Binary files a/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.InlineQueue.verified.png and b/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.InlineQueue.verified.png differ
diff --git a/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.InlineSingle.verified.png b/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.InlineSingle.verified.png
index 912c5f06..6d36ebc7 100644
Binary files a/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.InlineSingle.verified.png and b/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.InlineSingle.verified.png differ
diff --git a/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.LongPane.verified.png b/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.LongPane.verified.png
index 9b50d23c..a6d8e8e9 100644
Binary files a/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.LongPane.verified.png and b/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.LongPane.verified.png differ
diff --git a/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.LongQueueLabel.verified.png b/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.LongQueueLabel.verified.png
index cd32dac3..5d650047 100644
Binary files a/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.LongQueueLabel.verified.png and b/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.LongQueueLabel.verified.png differ
diff --git a/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.Minimal.verified.png b/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.Minimal.verified.png
new file mode 100644
index 00000000..5c6ee08c
Binary files /dev/null and b/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.Minimal.verified.png differ
diff --git a/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.Selection.verified.png b/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.Selection.verified.png
index 8a25c7ef..10fe34ac 100644
Binary files a/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.Selection.verified.png and b/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.Selection.verified.png differ
diff --git a/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.cs b/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.cs
index 6a718c5d..96bc3a8a 100644
--- a/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.cs
+++ b/src/DiffEngineViewer.Windows.Tests/WindowsPixelTests.cs
@@ -116,6 +116,14 @@ public Task Images() =>
public Task Selection() =>
Capture(ViewerSession.Drag(Fixtures.File(), PaneSide.Left, 1, 6, 3, 4));
+ ///
+ /// The minimal view, whose folded rows are drawn dimmed on a band of their own. Mirrored in the
+ /// native suite, which is what holds the three heads to one look for a fold.
+ ///
+ [Test]
+ public Task Minimal() =>
+ Capture(ViewerSession.Apply(Fixtures.File(Fixtures.Long(true), Fixtures.Long(false)), CommandKind.ToggleMinimal));
+
// No menu case here: this head shows a real popup, which is a top level window and so cannot
// appear in a capture of the client area. ContextMenuTests renders the strip itself instead.
diff --git a/src/DiffEngineViewer.Windows/Palette.cs b/src/DiffEngineViewer.Windows/Palette.cs
index 26c12bda..73c8ff89 100644
--- a/src/DiffEngineViewer.Windows/Palette.cs
+++ b/src/DiffEngineViewer.Windows/Palette.cs
@@ -16,6 +16,12 @@ static class Palette
public static readonly Color Rule = Color.FromArgb(70, 70, 70);
+ ///
+ /// Behind a folded row, a shade lighter than filler, so the runs the minimal view leaves out
+ /// read as breaks in the file rather than as a line of it or as padding.
+ ///
+ public static readonly Color Folded = Color.FromArgb(34, 34, 34);
+
///
/// The two squares behind a picture. Dark enough not to compete with the image, and different
/// enough from each other that a transparent region is obviously transparent rather than
@@ -45,6 +51,8 @@ public static Color Foreground(RowKind kind) =>
RowKind.Added => Color.FromArgb(126, 214, 139),
RowKind.Removed => Color.FromArgb(233, 129, 129),
RowKind.Modified => Color.FromArgb(231, 197, 113),
+ // Dimmed like the gutter, since what it says is about the file rather than from it.
+ RowKind.Folded => Dim,
_ => Text
};
@@ -58,6 +66,7 @@ public static Color Foreground(RowKind kind) =>
RowKind.Removed => Color.FromArgb(84, 40, 40),
RowKind.Modified => Color.FromArgb(74, 64, 32),
RowKind.Filler => Filler,
+ RowKind.Folded => Folded,
_ => null
};
diff --git a/src/DiffEngineViewer.Windows/ViewerForm.cs b/src/DiffEngineViewer.Windows/ViewerForm.cs
index 0dfe5824..998ac9e7 100644
--- a/src/DiffEngineViewer.Windows/ViewerForm.cs
+++ b/src/DiffEngineViewer.Windows/ViewerForm.cs
@@ -420,6 +420,7 @@ static CommandKind Map(Keys keyData)
Keys.End => CommandKind.ScrollEnd,
Keys.N => CommandKind.NextChange,
Keys.P => CommandKind.PreviousChange,
+ Keys.M => CommandKind.ToggleMinimal,
Keys.Tab => shift ? CommandKind.PreviousItem : CommandKind.NextItem,
Keys.A => shift ? CommandKind.AcceptAll : CommandKind.Accept,
Keys.D => CommandKind.Discard,
diff --git a/src/DiffEngineViewer/CommandKind.cs b/src/DiffEngineViewer/CommandKind.cs
index 53529469..94796f99 100644
--- a/src/DiffEngineViewer/CommandKind.cs
+++ b/src/DiffEngineViewer/CommandKind.cs
@@ -15,6 +15,13 @@ enum CommandKind
ScrollTo,
NextChange,
PreviousChange,
+
+ ///
+ /// Switch the panes between every line and only the changes, each with a few lines either
+ /// side. View only, like : it changes what is being read, never a
+ /// file, and applies locally even when the queue belongs to someone else.
+ ///
+ ToggleMinimal,
NextItem,
PreviousItem,
SelectItem,
diff --git a/src/DiffEngineViewer/DiffView.cs b/src/DiffEngineViewer/DiffView.cs
new file mode 100644
index 00000000..65846e4b
--- /dev/null
+++ b/src/DiffEngineViewer/DiffView.cs
@@ -0,0 +1,394 @@
+///
+/// An entry's two sides as the panes lay them out, and where each shown row sits in the entry.
+///
+/// Every entry has two: all of its rows, and the minimal view, which keeps each change and
+/// unchanged rows either side of it and folds every longer unchanged run into
+/// one row. Both are built with the entry, since they are pure
+/// functions of its rows, so switching between them costs nothing.
+///
+///
+/// A view, never a filter, the way a folded queue group is one. Scrolling, the scrollbar and change
+/// navigation count rows of the view on screen, because those are what is being scrolled. A
+/// selection is held in rows of the entry, because it describes text rather than the screen: it
+/// survives switching views, and one spanning a folded row copies the lines that row stands for,
+/// since those are what lie between its two ends.
+///
+///
+sealed class DiffView
+{
+ ///
+ /// How many unchanged rows the minimal view keeps either side of a change, and how many rows
+ /// navigation leaves showing above a change it brings into view. One number, so a change
+ /// navigated to in the minimal view arrives with exactly the context kept for it above it.
+ ///
+ public const int Context = 3;
+
+ ///
+ /// For each shown row, the first row of the entry it stands for, ascending. A folded row stands
+ /// for every row up to the next one's. Null in the full view, where every row is its own.
+ ///
+ readonly int[]? lines;
+
+ ///
+ /// How many rows the entry has, which only the last folded row needs: it stands for every row
+ /// up to the end.
+ ///
+ readonly int total;
+
+ DiffView(IReadOnlyList left, IReadOnlyList right, int[]? lines, int total)
+ {
+ Left = left;
+ Right = right;
+ this.lines = lines;
+ this.total = total;
+ Changes = FindChanges(left, right);
+ }
+
+ public IReadOnlyList Left { get; }
+
+ public IReadOnlyList Right { get; }
+
+ public int Count => Left.Count;
+
+ ///
+ /// The first row of each run of changed rows, in order.
+ ///
+ public IReadOnlyList Changes { get; }
+
+ ///
+ /// Both views of an entry's rows. The minimal one is the full one itself when nothing folds,
+ /// which lets switching between them tell that nothing on screen would change.
+ ///
+ ///
+ /// False for a picture. Its rows are its properties, three of them and each worth reading, so
+ /// there is nothing to leave out.
+ ///
+ public static (DiffView Full, DiffView Minimal) Build(
+ (IReadOnlyList Left, IReadOnlyList Right) rows,
+ bool fold)
+ {
+ var full = new DiffView(rows.Left, rows.Right, null, rows.Left.Count);
+ if (!fold)
+ {
+ return (full, full);
+ }
+
+ return (full, full.Fold());
+ }
+
+ public IReadOnlyList Side(PaneSide side)
+ {
+ if (side == PaneSide.Left)
+ {
+ return Left;
+ }
+
+ return Right;
+ }
+
+ public bool IsFolded(int row) =>
+ Left[row].Kind == RowKind.Folded;
+
+ ///
+ /// The entry row a shown row is, or for a folded row the first of those it stands for.
+ ///
+ public int First(int row)
+ {
+ if (lines is null)
+ {
+ return row;
+ }
+
+ return lines[row];
+ }
+
+ ///
+ /// The entry row a shown row is, or for a folded row the last of those it stands for.
+ ///
+ public int Last(int row)
+ {
+ if (lines is null)
+ {
+ return row;
+ }
+
+ if (row + 1 < lines.Length)
+ {
+ return lines[row + 1] - 1;
+ }
+
+ return total - 1;
+ }
+
+ ///
+ /// The shown row an entry row is on: itself, or the folded row standing for it.
+ ///
+ public int Find(int line)
+ {
+ if (lines is null)
+ {
+ return line;
+ }
+
+ var index = Array.BinarySearch(lines, line);
+ if (index >= 0)
+ {
+ return index;
+ }
+
+ // The complement is the first row starting after the line, so the one before it is the
+ // folded row the line is inside.
+ return Math.Max(0, ~index - 1);
+ }
+
+ ///
+ /// Where the view opens: at its first change, with rows above it, or at
+ /// the top when there is none. A snapshot that fails on line 200 is about line 200, and a reader
+ /// handed line 1 had to go looking for it.
+ ///
+ public int Opening(int body)
+ {
+ if (Changes.Count == 0)
+ {
+ return 0;
+ }
+
+ return Target(Changes[0], body);
+ }
+
+ ///
+ /// The scroll top that brings the next change into view, or null when none is left below.
+ ///
+ /// Next is the first change whose place is below the current one, rather than the first change
+ /// below the top row. Where a change is put is rows under the top, so
+ /// counting from the top row would take the change already there for the next one and move the
+ /// view three rows to arrive where it started. Clamped like any scroll, so the changes the last
+ /// page already shows are reached together rather than one row at a time.
+ ///
+ ///
+ public int? Next(int top, int body)
+ {
+ foreach (var change in Changes)
+ {
+ var target = Target(change, body);
+ if (target > top)
+ {
+ return target;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// The scroll top that brings the previous change into view, or null when none is left above.
+ /// The mirror of : from a change it put there, that is the one before it, and
+ /// from anywhere else it is the nearest change above the view's own position.
+ ///
+ public int? Previous(int top, int body)
+ {
+ for (var index = Changes.Count - 1; index >= 0; index--)
+ {
+ var target = Target(Changes[index], body);
+ if (target < top)
+ {
+ return target;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// The scroll top in this view that keeps the reader where they were in
+ /// : the first row on screen that both views show stays on the same
+ /// line of the screen. Changes and their context are in both, so it is almost always one of
+ /// those, and the change being read stays put while what is around it folds or unfolds.
+ /// Unclamped, since clamping is the session's.
+ ///
+ public int Follow(DiffView from, int top, int body)
+ {
+ if (from.Count == 0 ||
+ Count == 0)
+ {
+ return 0;
+ }
+
+ for (var offset = 0; offset < body && top + offset < from.Count; offset++)
+ {
+ var row = top + offset;
+ if (from.IsFolded(row))
+ {
+ continue;
+ }
+
+ var shown = Find(from.First(row));
+ if (IsFolded(shown))
+ {
+ continue;
+ }
+
+ return shown - offset;
+ }
+
+ // Nothing on screen is in both: a screen of lines this view folds, or of folded rows
+ // standing for lines this one shows. Whatever this view has for the top line goes at the
+ // top.
+ return Find(from.First(Math.Clamp(top, 0, from.Count - 1)));
+ }
+
+ ///
+ /// A drag's two ends, from rows of this view into rows of the entry, which is what a selection
+ /// is held in. A shown row is the entry's own row, text and all, so a column needs nothing.
+ ///
+ /// A folded row is the lines it stands for, whole: an end on one takes in all of them, from the
+ /// first when it is the end the selection starts at and to the last when it is the one it
+ /// finishes at. Part of a label is not part of anything. A press with no drag behind it is
+ /// still a click, which clears, rather than a selection of everything the row folds.
+ ///
+ ///
+ /// Rows are clamped into the view first, because a head reports where the pointer is rather
+ /// than where the rows are. The column a finishing end is pushed to is left for
+ /// to pull back to its row's length, since that is the one
+ /// step with the text to measure.
+ ///
+ ///
+ public (int AnchorRow, int AnchorColumn, int FocusRow, int FocusColumn) Unfold(
+ int anchorRow,
+ int anchorColumn,
+ int focusRow,
+ int focusColumn)
+ {
+ if (lines is null)
+ {
+ return (anchorRow, anchorColumn, focusRow, focusColumn);
+ }
+
+ var lastRow = Count - 1;
+ anchorRow = Math.Clamp(anchorRow, 0, lastRow);
+ focusRow = Math.Clamp(focusRow, 0, lastRow);
+ if (anchorRow == focusRow &&
+ anchorColumn == focusColumn)
+ {
+ var (line, column) = End(anchorRow, anchorColumn, start: true);
+ return (line, column, line, column);
+ }
+
+ var backwards = focusRow < anchorRow ||
+ (focusRow == anchorRow && focusColumn < anchorColumn);
+ var anchor = End(anchorRow, anchorColumn, start: !backwards);
+ var focus = End(focusRow, focusColumn, start: backwards);
+ return (anchor.Line, anchor.Column, focus.Line, focus.Column);
+ }
+
+ (int Line, int Column) End(int row, int column, bool start)
+ {
+ if (!IsFolded(row))
+ {
+ return (First(row), column);
+ }
+
+ if (start)
+ {
+ return (First(row), 0);
+ }
+
+ return (Last(row), int.MaxValue);
+ }
+
+ int Target(int change, int body) =>
+ Math.Clamp(change - Context, 0, Math.Max(0, Count - body));
+
+ ///
+ /// The minimal view of this one. Every row within of a change is kept,
+ /// and each run of the rest becomes one folded row on both sides, which keeps the two panes
+ /// aligned: an unchanged row always has an unchanged row beside it, so the run is the same
+ /// length on each.
+ ///
+ DiffView Fold()
+ {
+ var count = Count;
+ var kept = new bool[count];
+ for (var index = 0; index < count; index++)
+ {
+ if (!IsChange(Left, Right, index))
+ {
+ continue;
+ }
+
+ var from = Math.Max(0, index - Context);
+ var to = Math.Min(count - 1, index + Context);
+ for (var near = from; near <= to; near++)
+ {
+ kept[near] = true;
+ }
+ }
+
+ var left = new List(count);
+ var right = new List(count);
+ var starts = new List(count);
+ var row = 0;
+ while (row < count)
+ {
+ var end = row;
+ while (end < count && !kept[end])
+ {
+ end++;
+ }
+
+ // A run of one is shown rather than folded: its row would take the same space as the
+ // line it stands for and say less.
+ var run = end - row;
+ if (run > 1)
+ {
+ var folded = new Row(null, RowKind.Folded, $"... {run} unchanged lines ...");
+ left.Add(folded);
+ right.Add(folded);
+ starts.Add(row);
+ row = end;
+ continue;
+ }
+
+ left.Add(Left[row]);
+ right.Add(Right[row]);
+ starts.Add(row);
+ row++;
+ }
+
+ if (starts.Count == count)
+ {
+ return this;
+ }
+
+ return new(left, right, starts.ToArray(), count);
+ }
+
+ static List FindChanges(IReadOnlyList left, IReadOnlyList right)
+ {
+ var changes = new List();
+ var inside = false;
+ for (var index = 0; index < left.Count; index++)
+ {
+ var changed = IsChange(left, right, index);
+ if (changed && !inside)
+ {
+ changes.Add(index);
+ }
+
+ inside = changed;
+ }
+
+ return changes;
+ }
+
+ ///
+ /// Either side, rather than the left alone, which is all a text diff needs. An image's rows
+ /// are coloured per side, and a row is a change if the reader would see one on either.
+ ///
+ static bool IsChange(IReadOnlyList left, IReadOnlyList right, int index) =>
+ IsChange(left[index]) ||
+ (index < right.Count && IsChange(right[index]));
+
+ static bool IsChange(Row row) =>
+ row.Kind is not (RowKind.Unchanged or RowKind.Folded);
+}
diff --git a/src/DiffEngineViewer/Model/RowKind.cs b/src/DiffEngineViewer/Model/RowKind.cs
index 1c3d9377..8f8babba 100644
--- a/src/DiffEngineViewer/Model/RowKind.cs
+++ b/src/DiffEngineViewer/Model/RowKind.cs
@@ -11,5 +11,12 @@ enum RowKind
///
/// No line exists on this side. Rendered blank to keep the two panes vertically aligned.
///
- Filler
+ Filler,
+
+ ///
+ /// A run of unchanged lines the minimal view left out, drawn as the one row standing for all
+ /// of them. Its text says how many rather than being any of them, and it has no line number.
+ /// Never produced by a diff: only makes one.
+ ///
+ Folded
}
diff --git a/src/DiffEngineViewer/Native/DeviewStructs.cs b/src/DiffEngineViewer/Native/DeviewStructs.cs
index 023b61b3..ef3833d2 100644
--- a/src/DiffEngineViewer/Native/DeviewStructs.cs
+++ b/src/DiffEngineViewer/Native/DeviewStructs.cs
@@ -14,7 +14,7 @@ struct DeviewRow
public int Kind;
///
- /// -1 when the row is filler and has no line number.
+ /// -1 when the row is filler or folded and has no line number.
///
public int LineNumber;
@@ -184,5 +184,11 @@ enum DeviewKey
Quit = 14,
NextVariant = 15,
Copy = 16,
- SelectAll = 17
+ SelectAll = 17,
+
+ ///
+ /// m. A shim built before the key existed never reports it, and the footer button still
+ /// reaches the same command.
+ ///
+ ToggleMinimal = 18
}
diff --git a/src/DiffEngineViewer/Native/NativeViewerWindow.cs b/src/DiffEngineViewer/Native/NativeViewerWindow.cs
index 26da667e..a7c72c4c 100644
--- a/src/DiffEngineViewer/Native/NativeViewerWindow.cs
+++ b/src/DiffEngineViewer/Native/NativeViewerWindow.cs
@@ -121,6 +121,7 @@ static CommandKind Key(int key) =>
DeviewKey.End => CommandKind.ScrollEnd,
DeviewKey.NextChange => CommandKind.NextChange,
DeviewKey.PreviousChange => CommandKind.PreviousChange,
+ DeviewKey.ToggleMinimal => CommandKind.ToggleMinimal,
DeviewKey.NextItem => CommandKind.NextItem,
DeviewKey.PreviousItem => CommandKind.PreviousItem,
DeviewKey.Accept => CommandKind.Accept,
diff --git a/src/DiffEngineViewer/QueueEntry.cs b/src/DiffEngineViewer/QueueEntry.cs
index e54c06e5..1888622f 100644
--- a/src/DiffEngineViewer/QueueEntry.cs
+++ b/src/DiffEngineViewer/QueueEntry.cs
@@ -56,14 +56,29 @@ record QueueEntry(
// Computed once, because the diff is a pure function of the two sides and a new entry only
// arrives on stdin or the socket. A `with` expression copies this field rather than
// recomputing, so change the content by building a fresh entry, never by `with`.
- readonly (IReadOnlyList Left, IReadOnlyList Right) rows =
+ readonly (DiffView Full, DiffView Minimal) views = DiffView.Build(
LeftImage is null && RightImage is null
? DiffRows.Build(LeftText, RightText)
- : ImageRows.Build(LeftImage, RightImage);
+ : ImageRows.Build(LeftImage, RightImage),
+ fold: LeftImage is null && RightImage is null);
- public IReadOnlyList LeftRows => rows.Left;
- public IReadOnlyList RightRows => rows.Right;
- public int TotalRows => rows.Left.Count;
+ public IReadOnlyList LeftRows => views.Full.Left;
+ public IReadOnlyList RightRows => views.Full.Right;
+ public int TotalRows => views.Full.Count;
+
+ ///
+ /// The rows the panes show: every one, or only the changes and the lines around them. The
+ /// entry's own rows stay and either way.
+ ///
+ public DiffView View(bool minimal)
+ {
+ if (minimal)
+ {
+ return views.Minimal;
+ }
+
+ return views.Full;
+ }
///
/// A picture on either side makes the whole entry one, because the two sides of a comparison
diff --git a/src/DiffEngineViewer/ScreenBuilder.cs b/src/DiffEngineViewer/ScreenBuilder.cs
index 14bfd256..13cf583f 100644
--- a/src/DiffEngineViewer/ScreenBuilder.cs
+++ b/src/DiffEngineViewer/ScreenBuilder.cs
@@ -16,10 +16,11 @@ public static Screen Build(SessionState state)
{
var body = BodyRows(state);
var current = state.Current;
+ var view = state.View;
var selection = state.LiveSelection;
var left = BuildPane(
current?.LeftHeader ?? "received",
- current?.LeftRows ?? [],
+ view,
state.ScrollTop,
body,
current?.LeftImage,
@@ -27,7 +28,7 @@ public static Screen Build(SessionState state)
PaneSide.Left);
var right = BuildPane(
current?.RightHeader ?? "expected",
- current?.RightRows ?? [],
+ view,
state.ScrollTop,
body,
current?.RightImage,
@@ -42,7 +43,7 @@ public static Screen Build(SessionState state)
Queue: queue,
Left: left,
Right: right,
- Buttons: BuildButtons(state),
+ Buttons: BuildButtons(state, body),
Status: BuildStatus(state, current, body),
Columns: state.Columns,
Rows: state.Rows,
@@ -73,13 +74,19 @@ public static Screen Build(SessionState state)
static Pane BuildPane(
string header,
- IReadOnlyList rows,
+ DiffView? view,
int scrollTop,
int body,
ImageFile? image,
TextSelection? selection,
PaneSide side)
{
+ if (view is null)
+ {
+ return new(header, [], scrollTop, 0, BuildImage(image));
+ }
+
+ var rows = view.Side(side);
var end = Math.Min(scrollTop + body, rows.Count);
var visible = new List(Math.Max(0, end - scrollTop));
for (var index = Math.Max(0, scrollTop); index < end; index++)
@@ -88,7 +95,7 @@ static Pane BuildPane(
// Attached to the visible slice rather than carried beside it, so a head draws a row
// and its highlight from one thing and the frame comparison that decides whether to
// repaint already covers both.
- var span = SelectionText.Span(selection, side, index, row.Text);
+ var span = SelectionText.Span(selection, side, view, index);
visible.Add(span.Length == 0 ? row : row with { Selection = span });
}
@@ -122,7 +129,7 @@ static IReadOnlyList BuildQueue(SessionState state, int body, out int
return QueueProjection.Visible(state, body, out top);
}
- static IReadOnlyList BuildButtons(SessionState state)
+ static IReadOnlyList BuildButtons(SessionState state, int body)
{
var current = state.Current;
// Nothing that changes the queue while an accept-all is working through it. Kept in their
@@ -134,7 +141,8 @@ static IReadOnlyList BuildButtons(SessionState state)
return
[
new("Accept", enabled, CommandKind.Accept),
- new("Close", true, CommandKind.Quit)
+ new("Close", true, CommandKind.Quit),
+ ..ViewButtons(state, body)
];
}
@@ -155,6 +163,10 @@ static IReadOnlyList BuildButtons(SessionState state)
new("Accept all", state.Queue.Count > 0 && idle, CommandKind.AcceptAll)
};
+ // Ahead of the variant button, which comes and goes with the entry selected, so these
+ // keep their place in the footer whatever is on screen.
+ buttons.AddRange(ViewButtons(state, body));
+
if (current is { Kind: QueueEntryKind.Inline, Conflicted: true })
{
var variant = current.Variants[current.SelectedVariant];
@@ -167,6 +179,32 @@ static IReadOnlyList BuildButtons(SessionState state)
return buttons;
}
+ ///
+ /// The buttons that move around the entry on screen rather than act on it, the same in both
+ /// modes. Each change button is enabled only when there is a change to go to, so the pair
+ /// also say whether any are left above or below, which otherwise takes scrolling to find out.
+ ///
+ /// The fold is labelled with what it switches to. A picture's rows are its properties, none of
+ /// which the minimal view leaves out, so for one there is nothing for it to do.
+ ///
+ ///
+ static IEnumerable ViewButtons(SessionState state, int body)
+ {
+ var view = state.View;
+ yield return new(
+ "Prev change",
+ view?.Previous(state.ScrollTop, body) is not null,
+ CommandKind.PreviousChange);
+ yield return new(
+ "Next change",
+ view?.Next(state.ScrollTop, body) is not null,
+ CommandKind.NextChange);
+ yield return new(
+ state.Minimal ? "All lines" : "Changes only",
+ state.Current is { IsImage: false },
+ CommandKind.ToggleMinimal);
+ }
+
static string BuildSubtitle(SessionState state)
{
if (state.Mode == ViewerMode.File)
@@ -222,10 +260,19 @@ static string BuildStatus(SessionState state, QueueEntry? current, int body)
return ImageStatus(current);
}
- var total = current.TotalRows;
- var from = total == 0 ? 0 : state.ScrollTop + 1;
- var to = Math.Min(state.ScrollTop + body, total);
- return $"lines {from}-{to} of {total}";
+ // Rows of the entry rather than of the view. In the minimal view the rows on screen run from
+ // one line to another with folds between, and which stretch of the file that is says more
+ // than how far down a list of rows it is - "lines 1-1 of 1" beside a fold of forty lines
+ // said nothing true. In the full view the two are the same numbers.
+ var view = current.View(state.Minimal);
+ if (view.Count == 0)
+ {
+ return "lines 0-0 of 0";
+ }
+
+ var top = Math.Clamp(state.ScrollTop, 0, view.Count - 1);
+ var bottom = Math.Min(top + body, view.Count) - 1;
+ return $"lines {view.First(top) + 1}-{view.Last(bottom) + 1} of {current.TotalRows}";
}
static string ImageStatus(QueueEntry entry)
diff --git a/src/DiffEngineViewer/SelectionText.cs b/src/DiffEngineViewer/SelectionText.cs
index 5fdd1f78..222b0981 100644
--- a/src/DiffEngineViewer/SelectionText.cs
+++ b/src/DiffEngineViewer/SelectionText.cs
@@ -30,6 +30,32 @@ public static TextSelection Clamp(TextSelection selection, QueueEntry entry)
};
}
+ ///
+ /// What of one row of a view is selected. A shown row is one of the entry's own, so it is
+ /// measured as one. A folded row stands for lines rather than being any, so it is highlighted
+ /// whole when the selection takes in any of them and not at all otherwise, which is also what
+ /// copying it does: the lines between a selection's ends are copied whether or not they are on
+ /// screen.
+ ///
+ public static SelectionSpan Span(TextSelection? selection, PaneSide side, DiffView view, int row)
+ {
+ var shown = view.Side(side)[row];
+ if (shown.Kind != RowKind.Folded)
+ {
+ return Span(selection, side, view.First(row), shown.Text);
+ }
+
+ if (selection is not { IsEmpty: false } range ||
+ range.Side != side ||
+ range.Start.Row > view.Last(row) ||
+ range.End.Row < view.First(row))
+ {
+ return default;
+ }
+
+ return new(0, RowText.Flatten(shown.Text).Length);
+ }
+
///
/// What of one row is selected, for a row of the visible slice. Empty for the other side and
/// for a row outside the selection, which is most of them.
diff --git a/src/DiffEngineViewer/SessionState.cs b/src/DiffEngineViewer/SessionState.cs
index eea96cd9..6b28ba9b 100644
--- a/src/DiffEngineViewer/SessionState.cs
+++ b/src/DiffEngineViewer/SessionState.cs
@@ -44,6 +44,21 @@ record SessionState(
///
public TextSelection? Selection { get; init; }
+ ///
+ /// Whether the panes show only the changes, each with lines
+ /// either side, rather than every line. A view setting like : it holds
+ /// across entries rather than belonging to one, and it is this window's to set even when the
+ /// queue belongs to someone else.
+ ///
+ public bool Minimal { get; init; }
+
+ ///
+ /// The current entry's rows as lays them out. Everything that scrolls
+ /// reads these rather than the entry's own, so the scroll, the scrollbar and change navigation
+ /// all count the rows that are on screen.
+ ///
+ public DiffView? View => Current?.View(Minimal);
+
///
/// The selection, but only while it still describes what is on screen. Everything that reads
/// one goes through this, so a stale selection needs no clearing: the entry it named is gone,
diff --git a/src/DiffEngineViewer/TextSelection.cs b/src/DiffEngineViewer/TextSelection.cs
index 4517920e..609776f0 100644
--- a/src/DiffEngineViewer/TextSelection.cs
+++ b/src/DiffEngineViewer/TextSelection.cs
@@ -13,8 +13,10 @@ enum PaneSide
/// an ordered pair, so extending a selection backwards past its own start keeps working.
///
/// Rows are indexes into the whole side, not into the visible slice: a drag that continues while
-/// the wheel scrolls has to mean the same thing before and after. Columns are characters of the
-/// row's flattened text, which is what is on screen and therefore what was pointed at.
+/// the wheel scrolls has to mean the same thing before and after. The entry's side, too, rather
+/// than the minimal view's, so a selection means the same text whichever view is on screen.
+/// Columns are characters of the row's flattened text, which is what is on screen and therefore
+/// what was pointed at.
///
///
/// and are the entry this describes. Anything
diff --git a/src/DiffEngineViewer/ViewerSession.cs b/src/DiffEngineViewer/ViewerSession.cs
index 4242d3a3..51cc76fb 100644
--- a/src/DiffEngineViewer/ViewerSession.cs
+++ b/src/DiffEngineViewer/ViewerSession.cs
@@ -38,35 +38,51 @@ public static SessionState EnqueueInline(SessionState state, InlinePatch patch)
selected = 0;
}
- // Start the reader at the top again only when the text under them changed. Folding into an
- // entry further down the list is not it, and neither is a re-send of what is already
- // there: Fold reports an identical patch as unchanged and Project hands back the same
- // entry, so a continuous runner re-sending the same failing snapshot every few seconds
- // used to bounce the reader to the top on every run.
+ // Start the reader over, at the first change, only when the text under them changed.
+ // Folding into an entry further down the list is not it, and neither is a re-send of what
+ // is already there: Fold reports an identical patch as unchanged and Project hands back
+ // the same entry, so a continuous runner re-sending the same failing snapshot every few
+ // seconds used to bounce the reader to the top on every run.
var replaced = current is not null &&
current.Key == key &&
!ReferenceEquals(queue[selected], current);
- return Clamp(state with
+ var next = state with
{
Queue = queue,
Selected = selected,
- ScrollTop = replaced ? 0 : state.ScrollTop,
// The open menu indexes the queue it was opened over, which just changed.
Menu = null
- });
+ };
+
+ // Nothing on screen before means nobody has been reading this one yet either.
+ if (current is null ||
+ replaced)
+ {
+ return Open(next);
+ }
+
+ return Clamp(next);
}
///
/// The single entry a file comparison shows. Nothing arrives after it, because file mode runs
/// without a socket.
///
- public static SessionState EnqueueFile(SessionState state, QueueEntry entry) =>
- Clamp(state with
+ public static SessionState EnqueueFile(SessionState state, QueueEntry entry)
+ {
+ var next = state with
{
- Queue = [..state.Queue, entry],
- Selected = state.Selected < 0 ? 0 : state.Selected
- });
+ Queue = [..state.Queue, entry]
+ };
+
+ if (state.Selected < 0)
+ {
+ return Open(next with { Selected = 0 });
+ }
+
+ return Clamp(next);
+ }
///
/// Drops the item for a key, used when a previously failing test starts passing — or, with an
@@ -122,13 +138,20 @@ public static SessionState EnqueueTracked(SessionState state, QueueEntry entry)
var queue = QueueProjection.Order([..kept, entry]);
var currentKey = state.Current?.Key;
var selected = currentKey is null ? 0 : IndexOf(queue, currentKey);
- return Clamp(state with
+ var next = state with
{
Queue = queue,
Selected = selected < 0 ? 0 : selected,
- ScrollTop = replacedCurrent ? 0 : state.ScrollTop,
Menu = null
- });
+ };
+
+ if (currentKey is null ||
+ replacedCurrent)
+ {
+ return Open(next);
+ }
+
+ return Clamp(next);
}
///
@@ -154,18 +177,24 @@ public static SessionState Sync(
var queue = QueueProjection.Order(entries);
var key = state.Current?.Key;
var selected = key is null ? -1 : IndexOf(queue, key);
- return Clamp(state with
+ var next = state with
{
Queue = queue,
Selected = selected < 0 ? state.Selected : selected,
- ScrollTop = selected < 0 ? 0 : state.ScrollTop,
Message = message ?? state.Message,
OwnerProgress = progress,
// Nothing left to show, and this window is not what is holding the queue.
Exit = queue.Count == 0,
// The open menu indexes the queue it was opened over, which was just replaced.
Menu = null
- });
+ };
+
+ if (selected < 0)
+ {
+ return Open(next);
+ }
+
+ return Clamp(next);
}
///
@@ -310,6 +339,11 @@ public static SessionState OpenMenu(SessionState state, int visibleRow)
/// with and a drag that continues across a wheel notch has to mean the same thing either side
/// of it.
///
+ /// Rows of the side the head drew, that is, which in the minimal view are not the entry's. They
+ /// are unfolded here into the entry's own rows, which is what a selection is held in, so the
+ /// heads never learn that a view can leave rows out.
+ ///
+ ///
/// Reported for as long as the button is held, and simply not reported once it is let go: the
/// selection is already here, so there is nothing for a release to say. That is what makes a
/// whole press-drag-release landing inside one frame come out right.
@@ -328,15 +362,18 @@ public static SessionState Drag(
return state;
}
+ var ends = current
+ .View(state.Minimal)
+ .Unfold(anchorRow, anchorColumn, focusRow, focusColumn);
var selection = SelectionText.Clamp(
new(
current.Key,
current.SelectedVariant,
side,
- anchorRow,
- anchorColumn,
- focusRow,
- focusColumn),
+ ends.AnchorRow,
+ ends.AnchorColumn,
+ ends.FocusRow,
+ ends.FocusColumn),
current);
// The identical state when the pointer has not left the cell it was in, which is most
@@ -430,9 +467,11 @@ public static SessionState Apply(SessionState state, Command command, ViewerActi
case CommandKind.ScrollTo:
return Scroll(state, command.Index);
case CommandKind.NextChange:
- return Scroll(state, NextChange(Rows(state), state.ScrollTop));
+ return Scroll(state, state.View?.Next(state.ScrollTop, body) ?? state.ScrollTop);
case CommandKind.PreviousChange:
- return Scroll(state, PreviousChange(Rows(state), state.ScrollTop));
+ return Scroll(state, state.View?.Previous(state.ScrollTop, body) ?? state.ScrollTop);
+ case CommandKind.ToggleMinimal:
+ return ToggleMinimal(state, body);
case CommandKind.NextItem:
return Step(state, 1);
case CommandKind.PreviousItem:
@@ -664,11 +703,7 @@ static SessionState NextVariant(SessionState state)
var queue = state.Queue.ToList();
queue[state.Selected] = rebuilt;
// The text under the reader changed, the same reason a fold resets it.
- return Clamp(state with
- {
- Queue = queue,
- ScrollTop = 0
- });
+ return Open(state with { Queue = queue });
}
///
@@ -697,11 +732,7 @@ public static SessionState SelectVariant(SessionState state, string origin)
var queue = state.Queue.ToList();
queue[state.Selected] = QueueEntry.ForInline(new(current.Variants, current.Status), index);
- return Clamp(state with
- {
- Queue = queue,
- ScrollTop = 0
- });
+ return Open(state with { Queue = queue });
}
return state;
@@ -1355,16 +1386,22 @@ static SessionState Remove(SessionState state, IReadOnlyList queue,
{
var key = state.Current?.Key;
var selected = key is null ? -1 : IndexOf(queue, key);
- return Clamp(state with
+ var next = state with
{
Queue = queue,
Selected = selected < 0 ? state.Selected : selected,
- ScrollTop = selected < 0 ? 0 : state.ScrollTop,
Message = message,
// Nothing left to manage, so the window has no reason to stay open.
Exit = queue.Count == 0,
Menu = null
- });
+ };
+
+ if (selected < 0)
+ {
+ return Open(next);
+ }
+
+ return Clamp(next);
}
static SessionState Select(SessionState state, int index)
@@ -1384,11 +1421,24 @@ static SessionState Select(SessionState state, int index)
return Clamp(state);
}
- return Clamp(state with
+ return Open(state with { Selected = index });
+ }
+
+ ///
+ /// Puts the current entry on screen the way a reader first meets it: scrolled to its first
+ /// change rather than to its first line. Every path that changes what is being read comes
+ /// through here - selecting, stepping, a re-run that rewrote the text, a variant cycled to,
+ /// the entry on screen going - so an entry is met the same way however it got there.
+ ///
+ static SessionState Open(SessionState state)
+ {
+ state = Clamp(state);
+ if (state.View is not { } view)
{
- Selected = index,
- ScrollTop = 0
- });
+ return state;
+ }
+
+ return state with { ScrollTop = view.Opening(ScreenBuilder.BodyRows(state)) };
}
///
@@ -1492,12 +1542,32 @@ static SessionState Reveal(SessionState state, int index)
static SessionState Scroll(SessionState state, int top) =>
Clamp(state with { ScrollTop = top });
+ ///
+ /// Switches between every line and only the changes, keeping the reader's place rather than
+ /// sending them back to the first change: the row they were reading stays where it was on
+ /// screen while what is around it folds away or opens out. A selection is held in the entry's
+ /// own rows, so it carries across untouched.
+ ///
+ static SessionState ToggleMinimal(SessionState state, int body)
+ {
+ var toggled = state with { Minimal = !state.Minimal };
+ if (state.View is not { } from ||
+ toggled.View is not { } to ||
+ ReferenceEquals(from, to))
+ {
+ return Clamp(toggled);
+ }
+
+ return Clamp(toggled with { ScrollTop = to.Follow(from, state.ScrollTop, body) });
+ }
+
static SessionState Clamp(SessionState state)
{
var selected = state.Queue.Count == 0
? -1
: Math.Clamp(state.Selected, 0, state.Queue.Count - 1);
- var total = selected < 0 ? 0 : state.Queue[selected].TotalRows;
+ // The rows on screen, which in the minimal view are fewer than the entry has.
+ var total = selected < 0 ? 0 : state.Queue[selected].View(state.Minimal).Count;
var maxScroll = Math.Max(0, total - ScreenBuilder.BodyRows(state));
return state with
{
@@ -1505,65 +1575,4 @@ static SessionState Clamp(SessionState state)
ScrollTop = Math.Clamp(state.ScrollTop, 0, maxScroll)
};
}
-
- static IReadOnlyList Rows(SessionState state) =>
- state.Current?.LeftRows ?? [];
-
- static bool IsChange(Row row) =>
- row.Kind != RowKind.Unchanged;
-
- static int NextChange(IReadOnlyList rows, int from)
- {
- var index = from;
- // Step off the block currently at the top of the viewport before looking for the next one.
- while (index < rows.Count &&
- IsChange(rows[index]))
- {
- index++;
- }
-
- while (index < rows.Count &&
- !IsChange(rows[index]))
- {
- index++;
- }
-
- return index < rows.Count ? index : from;
- }
-
- static int PreviousChange(IReadOnlyList rows, int from)
- {
- // The top row of the viewport, not the one above it. Stepping off from there took the
- // block ending immediately above the viewport for the block the viewport was in, and
- // skipped past it to the one before - or, with nothing before it, refused to move at all.
- var index = Math.Min(from, rows.Count - 1);
-
- // So step off only when the viewport really is sitting in a block, which is when its top
- // row is itself a change.
- while (index >= 0 &&
- IsChange(rows[index]))
- {
- index--;
- }
-
- while (index >= 0 &&
- !IsChange(rows[index]))
- {
- index--;
- }
-
- if (index < 0)
- {
- return from;
- }
-
- // Land on the first row of the block, not its last.
- while (index > 0 &&
- IsChange(rows[index - 1]))
- {
- index--;
- }
-
- return index;
- }
}