diff --git a/src/XTerm.NET.Tests/ShellIntegrationMarkAnchorTests.cs b/src/XTerm.NET.Tests/ShellIntegrationMarkAnchorTests.cs index 59172cf..473b0d2 100644 --- a/src/XTerm.NET.Tests/ShellIntegrationMarkAnchorTests.cs +++ b/src/XTerm.NET.Tests/ShellIntegrationMarkAnchorTests.cs @@ -104,6 +104,55 @@ public void Erasing_the_line_leaves_the_mark_alone() Assert.Equal(ShellIntegrationMark.PromptStart, MarksOn(t, 0)[0].Kind); } + /// + /// What pwsh's Clear-Host sends, and what tput clear sends on any modern terminfo: the + /// scrollback discarded, the cursor homed, the screen blanked in place. The rows come back + /// empty and so must their marks, or a gutter drawn from them shows bars beside nothing. + /// + [Fact] + public void Clearing_the_screen_drops_the_marks_on_the_rows_it_blanks() + { + var t = Fresh(); + t.Write(Mark("A") + "$ " + Mark("B") + "ls\r\n" + Mark("C") + "file\r\n" + Mark("D;0")); + Assert.NotEmpty(MarksOn(t, 0)); + Assert.NotEmpty(MarksOn(t, 1)); + Assert.NotEmpty(MarksOn(t, 2)); + + t.Write($"{Esc}[3J{Esc}[H{Esc}[2J"); + + for (var row = 0; row < 5; row++) + Assert.Empty(MarksOn(t, row)); + } + + /// + /// ED 0 erases the cursor row through EL, and EL keeps marks: a shell that homes the cursor, + /// erases below and then prints its prompt -- zsh redrawing -- keeps the prompt's mark. The + /// rows below were erased whole and lose theirs. + /// + [Fact] + public void Erasing_below_keeps_the_cursor_rows_mark_and_drops_the_rest() + { + var t = Fresh(); + t.Write(Mark("A") + "$ ls\r\n" + Mark("C") + "file\r\n" + Mark("D;0")); + t.Write($"{Esc}[1;1H{Esc}[J"); + + Assert.Equal(ShellIntegrationMark.PromptStart, Assert.Single(MarksOn(t, 0)).Kind); + Assert.Empty(MarksOn(t, 1)); + Assert.Empty(MarksOn(t, 2)); + } + + /// A selective erase exists to leave protected text standing; the marks stand with it. + [Fact] + public void A_selective_screen_erase_leaves_the_marks_alone() + { + var t = Fresh(); + t.Write(Mark("A") + "$ ls\r\n" + Mark("C") + "file\r\n"); + t.Write($"{Esc}[?2J"); + + Assert.Single(MarksOn(t, 0)); + Assert.Single(MarksOn(t, 1)); + } + [Fact] public void Reflow_moves_a_mark_to_the_row_and_column_owning_its_position() { diff --git a/src/XTerm.NET/Buffer/BufferLine.cs b/src/XTerm.NET/Buffer/BufferLine.cs index 7ef6afe..b1b5d4e 100644 --- a/src/XTerm.NET/Buffer/BufferLine.cs +++ b/src/XTerm.NET/Buffer/BufferLine.cs @@ -487,7 +487,11 @@ internal void AddMark(LineMark mark) _marks.Add(mark); } - /// Drops every mark. Only line reuse does this; see . + /// + /// Drops every mark. Two things do this: line reuse, see , and a + /// whole-screen clear -- ED, DECCOLM, the alternate screen blanked on the way out -- erasing + /// the line in full. Reflow drops and re-anchors them too, but that is a move, not a loss. + /// internal void ClearMarks() => _marks = null; /// Whether this line carries any OSC 8 link span. diff --git a/src/XTerm.NET/InputHandler.Protection.cs b/src/XTerm.NET/InputHandler.Protection.cs index c12e54c..897c617 100644 --- a/src/XTerm.NET/InputHandler.Protection.cs +++ b/src/XTerm.NET/InputHandler.Protection.cs @@ -112,7 +112,7 @@ private void EraseLineCells(BufferLine? line, int start, int end, bool selective line.Fill(blank, start, end); if (wholeLine) - line.LineAttribute = Buffer.LineAttribute.Normal; + ClearedInFull(line); return; } @@ -136,6 +136,31 @@ private void EraseLineCells(BufferLine? line, int start, int end, bool selective } if (wholeLine && !survived) - line.LineAttribute = Buffer.LineAttribute.Normal; + ClearedInFull(line); + } + + /// + /// What a line loses when a display erase has cleared it whole: its double-size attribute, + /// and its shell integration marks. Reached from ED, and from + /// -- DECCOLM's clear, and the alternate screen blanked on the way out -- which are the + /// other two acts that empty every row. + /// + /// + /// The marks are the deliberate exception to "a mark survives the erasing of the cells it sits + /// among". That rule exists for EL, which shells use to redraw a prompt they have just marked. + /// A display erase is a different act: clear is CSI 3 J, CUP, CSI 2 J -- the scrollback + /// discarded and the screen blanked in place -- and the rows that come back blank still carried + /// the marks of the commands that were on them. A host drawing a gutter from those marks kept + /// painting prompt and exit bars beside empty rows. Kitty and Ghostty both drop a row's prompt + /// flag when ED clears it; a shell that clears the screen from inside its own prompt loses that + /// one mark under all three, and prints the prompt it marked onto a screen it just emptied + /// either way. The cursor row of ED 0 and ED 1 goes through EL and keeps its marks, so a shell + /// that homes the cursor and erases below -- zsh, after a redraw -- keeps the mark of the + /// prompt it is about to print there. + /// + private static void ClearedInFull(BufferLine line) + { + line.LineAttribute = Buffer.LineAttribute.Normal; + line.ClearMarks(); } }