diff --git a/docs/changelog.rst b/docs/changelog.rst index 98f8b4c4d..f6ce8b2e7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -105,6 +105,8 @@ Detailed list of changes - :opt:`remote_control_password`: Fix using a password without any actions not working (:iss:`8082`) +- Fix enlarging window when a long line is wrapped between the first line of the scrollback buffer and the screen inserting a spurious newline (:iss:`7033`) + 0.37.0 [2024-10-30] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/kitty/history.c b/kitty/history.c index f5bcb0ae4..e7c0eb438 100644 --- a/kitty/history.c +++ b/kitty/history.c @@ -543,6 +543,24 @@ pagerhist_rewrap(HistoryBuf *self, PyObject *xnum) { Py_RETURN_NONE; } +static PyObject* +is_continued(HistoryBuf *self, PyObject *val) { +#define is_continued_doc "is_continued(y) -> Whether the line y is continued or not" + unsigned long y = PyLong_AsUnsignedLong(val); + if (y >= self->count) { PyErr_SetString(PyExc_ValueError, "Out of bounds."); return NULL; } + get_line(self, y, self->line); + if (self->line->attrs.is_continued) { Py_RETURN_TRUE; } + Py_RETURN_FALSE; +} + +static PyObject* +endswith_wrap(HistoryBuf *self, PyObject *val UNUSED) { +#define endswith_wrap_doc "endswith_wrap() -> Whether the last line is wrapped at the end of the buffer" + if (history_buf_endswith_wrap(self)) { Py_RETURN_TRUE; } + Py_RETURN_FALSE; +} + + // Boilerplate {{{ static PyObject* rewrap(HistoryBuf *self, PyObject *args); @@ -550,6 +568,8 @@ static PyObject* rewrap(HistoryBuf *self, PyObject *args); static PyMethodDef methods[] = { METHOD(line, METH_O) + METHOD(is_continued, METH_O) + METHOD(endswith_wrap, METH_NOARGS) METHOD(as_ansi, METH_O) METHODB(pagerhist_write, METH_O), METHODB(pagerhist_rewrap, METH_O), diff --git a/kitty/line-buf.c b/kitty/line-buf.c index 95d1f4e9c..75edf6dd2 100644 --- a/kitty/line-buf.c +++ b/kitty/line-buf.c @@ -581,8 +581,87 @@ copy_old(LineBuf *self, PyObject *y) { #include "rewrap.h" +static index_type +restitch(Line *dest, index_type at, LineBuf *src, const index_type src_y, TrackCursor *cursors) { + index_type y = src_y, num_of_lines_removed = 0, num_cells_removed_on_last_line = 0; + bool last_char_has_wrapped_flag = true; + CPUCell *cp; GPUCell *gp; + // copy the cells into dest + while (at < dest->xnum && y < src->ynum) { + linebuf_init_cells(src, y, &cp, &gp); + index_type x = 0; + bool found_text = false; + while (at < dest->xnum && x < src->xnum) { + if (!found_text && cell_has_text(&cp[x])) found_text = true; + if (gp[x].attrs.width == 2) { + if (dest->xnum - at > 1) { + dest->cpu_cells[at] = cp[x]; + dest->gpu_cells[at++] = gp[x++]; + dest->cpu_cells[at] = cp[x]; + dest->gpu_cells[at] = gp[x]; + at++; x++; + } else { + dest->cpu_cells[at] = (CPUCell){0}; + dest->gpu_cells[at] = (GPUCell){0}; + at++; + } + } else { + dest->cpu_cells[at] = cp[x]; + dest->gpu_cells[at] = gp[x]; + at++; x++; + } + } + if (!found_text) { + last_char_has_wrapped_flag = false; + break; + } + if (x >= src->xnum) { // Entire line was copied + num_of_lines_removed++; + bool line_ends_with_newline = !linebuf_line_ends_with_continuation(src, y); + if (line_ends_with_newline) { + last_char_has_wrapped_flag = false; + break; + } + } else { + num_cells_removed_on_last_line = x; + break; + } + y++; + } + dest->gpu_cells[dest->xnum - 1].attrs.next_char_was_wrapped = last_char_has_wrapped_flag; + // remove the copied lines and cells from src + if (num_of_lines_removed) { + for (index_type i = 0; i < num_of_lines_removed; i++) { + linebuf_clear_line(src, src_y, true); + linebuf_index(src, src_y, src->ynum - 1); + } + for (TrackCursor *tc = cursors; !tc->is_sentinel; tc++) if (tc->y >= src_y) tc->y -= num_of_lines_removed; + } + if (num_cells_removed_on_last_line) { + bool line_is_continued_to_next = linebuf_line_ends_with_continuation(src, src_y); + linebuf_init_cells(src, src_y, &cp, &gp); + for (TrackCursor *tc = cursors; !tc->is_sentinel; tc++) if (tc->y == src_y && tc->x >= num_cells_removed_on_last_line) tc->x -= num_cells_removed_on_last_line; + index_type remaining_cells = src->xnum - num_cells_removed_on_last_line; + memmove(cp, cp + num_cells_removed_on_last_line, remaining_cells * sizeof(cp[0])); + memset(cp + remaining_cells, 0, num_cells_removed_on_last_line * sizeof(cp[0])); + memmove(gp, gp + num_cells_removed_on_last_line, remaining_cells * sizeof(gp[0])); + memset(gp + remaining_cells, 0, num_cells_removed_on_last_line * sizeof(gp[0])); + if (line_is_continued_to_next && remaining_cells < src->xnum && src->ynum > src_y + 1) { + Line next_dest = {.xnum=src->xnum}; + init_line(src, &next_dest, src_y); + num_of_lines_removed += restitch(&next_dest, remaining_cells, src, src_y + 1, cursors); + } + } + return num_of_lines_removed; +} + + void -linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *num_content_lines_before, index_type *num_content_lines_after, HistoryBuf *historybuf, index_type *track_x, index_type *track_y, index_type *track_x2, index_type *track_y2, ANSIBuf *as_ansi_buf) { +linebuf_rewrap( + LineBuf *self, LineBuf *other, index_type *num_content_lines_before, index_type *num_content_lines_after, + HistoryBuf *historybuf, index_type *track_x, index_type *track_y, index_type *track_x2, index_type *track_y2, + ANSIBuf *as_ansi_buf, bool history_buf_last_line_is_split +) { index_type first, i; bool is_empty = true; @@ -617,6 +696,17 @@ linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *num_content_lines_befo *track_x = tcarr[0].x; *track_y = tcarr[0].y; *track_x2 = tcarr[1].x; *track_y2 = tcarr[1].y; *num_content_lines_after = other->line->ynum + 1; + if (history_buf_last_line_is_split && historybuf) { + historybuf_init_line(historybuf, 0, historybuf->line); + index_type xlimit = xlimit_for_line(historybuf->line); + if (xlimit < historybuf->line->xnum) { + TrackCursor tcarr[3] = {{.x = *track_x, .y = *track_y }, {.x = *track_x2, .y = *track_y2}, {.is_sentinel = true}}; + index_type num_of_lines_removed = restitch(historybuf->line, xlimit, other, 0, tcarr); + *track_x = tcarr[0].x; *track_y = tcarr[0].y; + *track_x2 = tcarr[1].x; *track_y2 = tcarr[1].y; + *num_content_lines_after -= num_of_lines_removed; + } + } for (i = 0; i < *num_content_lines_after; i++) other->line_attrs[i].has_dirty_text = true; } @@ -629,7 +719,7 @@ rewrap(LineBuf *self, PyObject *args) { if (!PyArg_ParseTuple(args, "O!O!", &LineBuf_Type, &other, &HistoryBuf_Type, &historybuf)) return NULL; index_type x = 0, y = 0, x2 = 0, y2 = 0; ANSIBuf as_ansi_buf = {0}; - linebuf_rewrap(self, other, &nclb, &ncla, historybuf, &x, &y, &x2, &y2, &as_ansi_buf); + linebuf_rewrap(self, other, &nclb, &ncla, historybuf, &x, &y, &x2, &y2, &as_ansi_buf, false); free(as_ansi_buf.buf); return Py_BuildValue("II", nclb, ncla); diff --git a/kitty/lineops.h b/kitty/lineops.h index f8cdfd76a..514e58d59 100644 --- a/kitty/lineops.h +++ b/kitty/lineops.h @@ -109,7 +109,7 @@ void linebuf_clear_line(LineBuf *self, index_type y, bool clear_attrs); void linebuf_insert_lines(LineBuf *self, unsigned int num, unsigned int y, unsigned int bottom); void linebuf_delete_lines(LineBuf *self, index_type num, index_type y, index_type bottom); void linebuf_copy_line_to(LineBuf *, Line *, index_type); -void linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *, index_type *, HistoryBuf *, index_type *, index_type *, index_type *, index_type *, ANSIBuf*); +void linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *, index_type *, HistoryBuf *, index_type *, index_type *, index_type *, index_type *, ANSIBuf*, bool); void linebuf_mark_line_dirty(LineBuf *self, index_type y); void linebuf_clear_attrs_and_dirty(LineBuf *self, index_type y); void linebuf_mark_line_clean(LineBuf *self, index_type y); diff --git a/kitty/screen.c b/kitty/screen.c index 52f91b6be..a10b46cb0 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -234,12 +234,12 @@ typedef struct CursorTrack { } CursorTrack; static LineBuf* -realloc_lb(LineBuf *old, unsigned int lines, unsigned int columns, index_type *nclb, index_type *ncla, HistoryBuf *hb, CursorTrack *a, CursorTrack *b, ANSIBuf *as_ansi_buf) { +realloc_lb(LineBuf *old, unsigned int lines, unsigned int columns, index_type *nclb, index_type *ncla, HistoryBuf *hb, CursorTrack *a, CursorTrack *b, ANSIBuf *as_ansi_buf, bool history_buf_last_line_is_split) { LineBuf *ans = alloc_linebuf(lines, columns, old->text_cache); if (ans == NULL) { PyErr_NoMemory(); return NULL; } a->temp.x = a->before.x; a->temp.y = a->before.y; b->temp.x = b->before.x; b->temp.y = b->before.y; - linebuf_rewrap(old, ans, nclb, ncla, hb, &a->temp.x, &a->temp.y, &b->temp.x, &b->temp.y, as_ansi_buf); + linebuf_rewrap(old, ans, nclb, ncla, hb, &a->temp.x, &a->temp.y, &b->temp.x, &b->temp.y, as_ansi_buf, history_buf_last_line_is_split); return ans; } @@ -395,6 +395,7 @@ screen_resize(Screen *self, unsigned int lines, unsigned int columns) { if (!init_overlay_line(self, columns, true)) return false; // Resize main linebuf + bool history_buf_last_line_is_split = history_buf_endswith_wrap(self->historybuf); HistoryBuf *nh = realloc_hb(self->historybuf, self->historybuf->ynum, columns, &self->as_ansi_buf); if (nh == NULL) return false; Py_CLEAR(self->historybuf); self->historybuf = nh; @@ -404,7 +405,7 @@ screen_resize(Screen *self, unsigned int lines, unsigned int columns) { prompt_copy = (PyObject*)alloc_linebuf(self->lines, self->columns, self->text_cache); num_of_prompt_lines = prevent_current_prompt_from_rewrapping(self, (LineBuf*)prompt_copy, &num_of_prompt_lines_above_cursor); } - LineBuf *n = realloc_lb(self->main_linebuf, lines, columns, &num_content_lines_before, &num_content_lines_after, self->historybuf, &cursor, &main_saved_cursor, &self->as_ansi_buf); + LineBuf *n = realloc_lb(self->main_linebuf, lines, columns, &num_content_lines_before, &num_content_lines_after, self->historybuf, &cursor, &main_saved_cursor, &self->as_ansi_buf, history_buf_last_line_is_split); if (n == NULL) return false; Py_CLEAR(self->main_linebuf); self->main_linebuf = n; if (is_main) setup_cursor(cursor); @@ -414,7 +415,7 @@ screen_resize(Screen *self, unsigned int lines, unsigned int columns) { grman_resize(self->main_grman, self->lines, lines, self->columns, columns, num_content_lines_before, num_content_lines_after); // Resize alt linebuf - n = realloc_lb(self->alt_linebuf, lines, columns, &num_content_lines_before, &num_content_lines_after, NULL, &cursor, &alt_saved_cursor, &self->as_ansi_buf); + n = realloc_lb(self->alt_linebuf, lines, columns, &num_content_lines_before, &num_content_lines_after, NULL, &cursor, &alt_saved_cursor, &self->as_ansi_buf, false); if (n == NULL) return false; Py_CLEAR(self->alt_linebuf); self->alt_linebuf = n; if (!is_main) setup_cursor(cursor); diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py index 2a17daf55..2b7b4a48c 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -280,6 +280,35 @@ class TestScreen(BaseTest): self.ae(s.cursor.x, 1) def test_resize(self): + from kitty.window import as_text + def at(): + return as_text(s, add_history=True) + # test that a wrapped line split by the history buffer is re-stitched + s = self.create_screen(cols=4, lines=4, scrollback=4) + text = '' + for i in range(s.lines + 1): + text += str(i + 1) * s.columns + s.draw(text) + self.assertTrue(s.historybuf.endswith_wrap()) + s.resize(s.lines, s.columns + 2) + self.assertTrue(s.historybuf.endswith_wrap()) + self.ae(str(s.historybuf), '111122') + self.ae(at(), text + '\n') + s = self.create_screen(cols=4, lines=4, scrollback=4) + s.draw('1111222'), s.linefeed(), s.carriage_return() + s.draw('333344445555') + s.resize(s.lines, s.columns + 2) + self.ae(str(s.historybuf), '111122') + self.ae(str(s.line(0)), '2') + self.ae(at(), '1111222\n333344445555\n') + s = self.create_screen(cols=4, lines=4, scrollback=4) + s.draw('1111😸2'), s.linefeed(), s.carriage_return() + s.index(), s.index() + s.resize(s.lines, s.columns + 1) + self.ae(str(s.historybuf), '1111') + self.assertTrue(s.historybuf.endswith_wrap()) + self.ae(at(), '1111😸2\n\n\n') + # test that trailing blank line is preserved on resize s = self.create_screen(cols=5, lines=5, scrollback=15) for i in range(3):