diff --git a/src/DiffEngineViewer.Tests/ReEnqueueTests.cs b/src/DiffEngineViewer.Tests/ReEnqueueTests.cs
new file mode 100644
index 00000000..edb45887
--- /dev/null
+++ b/src/DiffEngineViewer.Tests/ReEnqueueTests.cs
@@ -0,0 +1,45 @@
+///
+/// A patch arriving for the entry already on screen. A continuous test runner sends one every few
+/// seconds for as long as the test keeps failing, and most of them say exactly what the last one
+/// said.
+///
+public class ReEnqueueTests
+{
+ [Test]
+ public async Task An_identical_re_send_leaves_the_scroll_alone()
+ {
+ var state = Scrolled();
+
+ var again = ViewerSession.EnqueueInline(state, Patch(Fixtures.Long(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.
+ ///
+ [Test]
+ public async Task A_re_send_of_different_content_starts_at_the_top()
+ {
+ var state = Scrolled();
+
+ var again = ViewerSession.EnqueueInline(state, Patch($"{Fixtures.Long(true)}\nand one more line"));
+
+ await Assert.That(again.ScrollTop).IsEqualTo(0);
+ }
+
+ static SessionState Scrolled()
+ {
+ var state = ViewerSession.Apply(Fixtures.Inline(Patch(Fixtures.Long(true))), CommandKind.PageDown);
+ if (state.ScrollTop == 0)
+ {
+ throw new("The entry did not scroll, so nothing below asserts anything.");
+ }
+
+ return state;
+ }
+
+ static InlinePatch Patch(string content) =>
+ Fixtures.Patch("A.cs", 1, null, content);
+}
diff --git a/src/DiffEngineViewer/ViewerSession.cs b/src/DiffEngineViewer/ViewerSession.cs
index d59969e7..5b6b805f 100644
--- a/src/DiffEngineViewer/ViewerSession.cs
+++ b/src/DiffEngineViewer/ViewerSession.cs
@@ -29,19 +29,29 @@ public static SessionState Resize(SessionState state, int columns, int rows) =>
public static SessionState EnqueueInline(SessionState state, InlinePatch patch)
{
var key = InlineKey.For(patch.SourceFile, patch.LineHint);
- var replacedCurrent = state.Current?.Key == key;
+ var current = state.Current;
var queue = Rebuild(state, Pending(state).Enqueue(patch));
// Grouping can reorder the list, so the selection follows its key rather than its index.
- var currentKey = state.Current?.Key;
- var selected = currentKey is null ? 0 : IndexOf(queue, currentKey);
+ var selected = current is null ? 0 : IndexOf(queue, current.Key);
+ if (selected < 0)
+ {
+ 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.
+ var replaced = current is not null &&
+ current.Key == key &&
+ !ReferenceEquals(queue[selected], current);
+
return Clamp(state with
{
Queue = queue,
- Selected = selected < 0 ? 0 : selected,
- // The text under the reader just changed, so start it at the top again. Only when it
- // is the item on screen; folding into one further down the list should not move
- // anything.
- ScrollTop = replacedCurrent ? 0 : state.ScrollTop,
+ Selected = selected,
+ ScrollTop = replaced ? 0 : state.ScrollTop,
// The open menu indexes the queue it was opened over, which just changed.
Menu = null
});