mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-06-29 12:21:58 +00:00
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:
parent
925de17ecb
commit
207b22aa80
2 changed files with 63 additions and 16 deletions
|
|
@ -4600,26 +4600,49 @@ find_cmd_output(Screen *self, OutputOffset *oo, index_type start_screen_y, unsig
|
|||
|
||||
static PyObject*
|
||||
erase_last_command(Screen *self, PyObject *args) {
|
||||
int include_prompt = 1;
|
||||
int include_prompt = 1; // retained for API compatibility; the prompt block is the unit erased
|
||||
if (!PyArg_ParseTuple(args, "|p", &include_prompt)) return NULL;
|
||||
OutputOffset oo = {.screen=self};
|
||||
if (self->linebuf != self->main_linebuf || !find_cmd_output(self, &oo, self->cursor->y + self->scrolled_by, self->scrolled_by, -1, false)) Py_RETURN_FALSE;
|
||||
if (include_prompt) {
|
||||
int y = oo.start - 1; Line *line;
|
||||
while ((line = checked_range_line(self, y))) {
|
||||
oo.start--; oo.num_lines++; y--;
|
||||
if (line->attrs.prompt_kind == PROMPT_START) break;
|
||||
}
|
||||
(void)include_prompt;
|
||||
if (self->linebuf != self->main_linebuf) Py_RETURN_FALSE;
|
||||
Line *line;
|
||||
// Erase the most recent command: the prompt block immediately above the
|
||||
// current (live) prompt, regardless of whether that command produced any
|
||||
// output. Commands without output (an empty Enter, a comment, cd, export,
|
||||
// ...) emit no OSC 133;C, so a search anchored on OUTPUT_START would skip
|
||||
// them and erase an older command instead. Selecting by prompt marks makes
|
||||
// every submitted command one unit, removed newest-first.
|
||||
int y = self->cursor->y + self->scrolled_by;
|
||||
bool found = false;
|
||||
for (; (line = checked_range_line(self, y)); y--) {
|
||||
if (line->attrs.prompt_kind == PROMPT_START) { found = true; break; }
|
||||
}
|
||||
index_type num_lines_to_erase_in_screen = oo.start >= 0 ? oo.num_lines : oo.num_lines + oo.start;
|
||||
if (!found) Py_RETURN_FALSE;
|
||||
const int live_prompt_start = y;
|
||||
found = false;
|
||||
for (y = live_prompt_start - 1; (line = checked_range_line(self, y)); y--) {
|
||||
if (line->attrs.prompt_kind == PROMPT_START) { found = true; break; }
|
||||
}
|
||||
if (!found) Py_RETURN_FALSE; // nothing above the current prompt to erase
|
||||
const int start = y;
|
||||
const index_type num_lines = (index_type)(live_prompt_start - start);
|
||||
index_type num_lines_to_erase_in_screen = start >= 0 ? num_lines : num_lines + start;
|
||||
num_lines_to_erase_in_screen = MIN(self->cursor->y, num_lines_to_erase_in_screen);
|
||||
if (num_lines_to_erase_in_screen) {
|
||||
screen_delete_lines_impl(self, self->cursor->y - num_lines_to_erase_in_screen, num_lines_to_erase_in_screen, 0, self->lines - 1);
|
||||
// Anchor the on-screen deletion at the top of the region, not at
|
||||
// cursor->y - count: a multi-line prompt or rows skipped above would
|
||||
// otherwise shift the deletion and leave residual lines on screen.
|
||||
index_type screen_erase_start = start >= 0 ? (index_type)start : 0;
|
||||
screen_delete_lines_impl(self, screen_erase_start, num_lines_to_erase_in_screen, 0, self->lines - 1);
|
||||
self->cursor->y -= num_lines_to_erase_in_screen;
|
||||
}
|
||||
if (oo.num_lines > num_lines_to_erase_in_screen) {
|
||||
index_type num_of_lines_to_erase_from_history = oo.num_lines - num_lines_to_erase_in_screen;
|
||||
historybuf_delete_newest_lines(self->historybuf, num_of_lines_to_erase_from_history);
|
||||
if (num_lines > num_lines_to_erase_in_screen) {
|
||||
historybuf_delete_newest_lines(self->historybuf, num_lines - num_lines_to_erase_in_screen);
|
||||
// The scrollback shrank: clamp the scroll position and signal a redraw,
|
||||
// otherwise the deletion of off-screen lines is not reflected until the
|
||||
// next scroll event forces the history viewport to be recomputed.
|
||||
self->scrolled_by = MIN(self->scrolled_by, self->historybuf->count);
|
||||
self->is_dirty = true;
|
||||
dirty_scroll(self);
|
||||
}
|
||||
Py_RETURN_TRUE;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue