fix(erase_last_command): erase the most recent command even empty output

erase_last_command selected the region to erase via find_cmd_output(..., -1),
which anchors on OUTPUT_START (OSC, 133;C). Commands that produce no output
(an empty Enter, a comment, cd, export, etc. -- never emit 133;C, so they were
skipped and an older command-with-output was erased instead. "Erase the last
command" therefore did not erase the last command whenever the most recent ones
has no output.

Select the region by prompt marks instead: erase the prompt block immediately
above the current (live) prompt, whatever it contains. Every submittd command
is now one unit, removed newest-first, one prompt block per invocation.

This also fixes two latent defects in the previous implementation:

* The on-screen deletion was anchored at `cursor->y - count`, which
  assumes the region ends exactly one row above the cursor.
  Multi-line prompts and skipped rows broke that assumption and left
  residual lines. Anchor at the top of the region instead.

* When part of the erased region was in the scrollback, the lines
  were removed from the history buffer but no redraw was signalled,
  so the deletion of the off-screen lines only became visible after
  the next scroll event recomputed the history viewport. Clamp
  scrolled_by to the new history length and call dirty_scroll()
  after shrinking the buffer.

include_prompt is retained for API compatibility but is now a no-op: the
unit erased is always the whole prompt block.
This commit is contained in:
amoena
2026-06-28 10:43:43 -03:00
committed by slayerlab
parent 925de17ecb
commit 207b22aa80
2 changed files with 63 additions and 16 deletions

View File

@@ -1627,8 +1627,8 @@ class TestScreen(BaseTest):
s.draw('before\r\n')
draw_prompt('p1'), draw_output(2), mark_prompt(), s.draw('partial')
x = s.cursor.x
s.erase_last_command(False)
self.ae('before\n$ p1\npartial', at().rstrip())
s.erase_last_command(False) # include_prompt is now a no-op: the prompt block is the unit erased
self.ae('before\npartial', at().rstrip())
for scroll in (8, 9, 10):
s.reset()
s.draw('before'), s.carriage_return(), s.linefeed()
@@ -1641,6 +1641,30 @@ class TestScreen(BaseTest):
s.erase_last_command()
self.ae(at().rstrip(), ' a b\npartial')
# the most recent command is erased even when it produced no output (an
# empty Enter, a comment, cd, ...): such commands emit no OSC 133;C and
# must not be skipped in favour of an older command-with-output.
s = self.create_screen(cols=10, lines=12, scrollback=30)
s.draw('before\r\n')
draw_prompt('p1'), draw_output(2), draw_prompt('# note'), mark_prompt(), s.draw('partial')
s.erase_last_command()
self.ae('before\n$ p1\n0\n1\npartial', at().rstrip()) # the output-less command goes first
s.erase_last_command()
self.ae('before\npartial', at().rstrip()) # then the command with output
# consecutive output-less commands are removed newest-first, one per call
s.reset()
s.draw('before\r\n')
draw_prompt('p1'), draw_output(1), draw_prompt('e1'), draw_prompt('e2'), mark_prompt(), s.draw('partial')
s.erase_last_command(); self.ae('before\n$ p1\n0\n$ e1\npartial', at().rstrip())
s.erase_last_command(); self.ae('before\n$ p1\n0\npartial', at().rstrip())
s.erase_last_command(); self.ae('before\npartial', at().rstrip())
# multi-line live prompt: the command region is erased with no residual
s.reset()
s.draw('before\r\n')
draw_prompt('p1'), draw_output(9), mark_prompt(), s.draw('l1'), s.carriage_return(), s.index(), s.draw('partial')
s.erase_last_command()
self.ae('before\nl1\npartial', at().rstrip())
def test_pointer_shapes(self):
from kitty.window import set_pointer_shape
s = self.create_screen()