Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions src/XTerm.NET.Tests/ShellIntegrationMarkAnchorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,55 @@ public void Erasing_the_line_leaves_the_mark_alone()
Assert.Equal(ShellIntegrationMark.PromptStart, MarksOn(t, 0)[0].Kind);
}

/// <summary>
/// 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.
/// </summary>
[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));
}

/// <summary>
/// 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.
/// </summary>
[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));
}

/// <summary>A selective erase exists to leave protected text standing; the marks stand with it.</summary>
[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()
{
Expand Down
6 changes: 5 additions & 1 deletion src/XTerm.NET/Buffer/BufferLine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,11 @@ internal void AddMark(LineMark mark)
_marks.Add(mark);
}

/// <summary>Drops every mark. Only line reuse does this; see <see cref="ResetInPlace"/>.</summary>
/// <summary>
/// Drops every mark. Two things do this: line reuse, see <see cref="ResetInPlace"/>, 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.
/// </summary>
internal void ClearMarks() => _marks = null;

/// <summary>Whether this line carries any OSC 8 link span.</summary>
Expand Down
29 changes: 27 additions & 2 deletions src/XTerm.NET/InputHandler.Protection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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);
}

/// <summary>
/// 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 <see cref="EraseWholeScreen"/>
/// -- DECCOLM's clear, and the alternate screen blanked on the way out -- which are the
/// other two acts that empty every row.
/// </summary>
/// <remarks>
/// 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: <c>clear</c> 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.
/// </remarks>
private static void ClearedInFull(BufferLine line)
{
line.LineAttribute = Buffer.LineAttribute.Normal;
line.ClearMarks();
Comment thread
JohnCampionJr marked this conversation as resolved.
}
}
Loading