terminal: handle consecutive .input's in clearPrompt (#2990)

Our semantic prompts are row-based, so the last prompt marker set on a
row "wins". In the case of at least our bash shell integration, this
means that consecutive prompt lines will all be marked as .input (OSC
133;B -- end-of-prompt, start of input).

Previously, clearPrompt() identified the current prompt's "area" by
searching upward from the current row until it encounters a .prompt
marker or some command output. In the bash case, .prompt is never the
dominant ("last") marker, so clearPrompt() would aggressively clear all
immediately preceding consecutive prompts.

With this change, we'll stop searching upwards when we encounter some
command output, a .prompt marker, _or another .input marker_. That last
case prevents clearPrompt() from unintentionally clearing earlier prompt
lines.

There may be improvements we can make to the way that our bash shell
integration emits semantic prompt markers, but I think this logic is
generally sound for all cases, and it specifically improves the current
bash prompt-clearing experience.

---

Before and after, after resizing the terminal window to trigger a
reflow:

<img width="504"
src="https://github.com/user-attachments/assets/91aa652e-e262-445a-8eed-7268c0d66428"
/>
<img width="510"
src="https://github.com/user-attachments/assets/b43e0aa4-0e24-4f4e-9b2c-b6b07c8f8c77"
/>
This commit is contained in:
Mitchell Hashimoto
2024-12-16 21:08:21 -08:00
committed by GitHub

View File

@ -1315,8 +1315,13 @@ pub fn clearPrompt(self: *Screen) void {
switch (row.semantic_prompt) { switch (row.semantic_prompt) {
// We are at a prompt but we're not at the start of the prompt. // We are at a prompt but we're not at the start of the prompt.
// We mark our found value and continue because the prompt // We mark our found value and continue because the prompt
// may be multi-line. // may be multi-line, unless this is the second time we've
.input => found = p, // seen an .input marker, in which case we've run into an
// earlier prompt.
.input => {
if (found != null) break;
found = p;
},
// If we find the prompt then we're done. We are also done // If we find the prompt then we're done. We are also done
// if we find any prompt continuation, because the shells // if we find any prompt continuation, because the shells
@ -3565,6 +3570,32 @@ test "Screen: clearPrompt continuation" {
} }
} }
test "Screen: clearPrompt consecutive prompts" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 0);
defer s.deinit();
const str = "1ABCD\n2EFGH\n3IJKL";
try s.testWriteString(str);
// Set both rows to be prompts
{
s.cursorAbsolute(0, 1);
s.cursor.page_row.semantic_prompt = .input;
s.cursorAbsolute(0, 2);
s.cursor.page_row.semantic_prompt = .input;
}
s.clearPrompt();
{
const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} });
defer alloc.free(contents);
try testing.expectEqualStrings("1ABCD\n2EFGH", contents);
}
}
test "Screen: clearPrompt no prompt" { test "Screen: clearPrompt no prompt" {
const testing = std.testing; const testing = std.testing;
const alloc = testing.allocator; const alloc = testing.allocator;