diff --git a/kitty/line.h b/kitty/line.h index 70289dbe5..a5cece6e8 100644 --- a/kitty/line.h +++ b/kitty/line.h @@ -59,7 +59,8 @@ typedef union CPUCell { char_type y : SCALE_BITS + 1; char_type width: WIDTH_BITS; char_type vertical_align: 3; - char_type : 15; + char_type temp_flag: 1; + char_type : 14; }; struct { char_type ch_and_idx: sizeof(char_type) * 8; diff --git a/kitty/screen.c b/kitty/screen.c index f8a03b071..7d95d371a 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -766,17 +766,13 @@ range_line(Screen *self, int y, Line *line) { static Line* checked_range_line(Screen *self, int y) { - if ( - (y < 0 && -(y + 1) >= (int)self->historybuf->count) || y >= (int)self->lines - ) return NULL; - return range_line_(self, y); + if (-(int)self->historybuf->count <= y && y < (int)self->lines) return range_line_(self, y); + return NULL; } static bool range_line_is_continued(Screen *self, int y) { - if ( - (y < 0 && -(y + 1) >= (int)self->historybuf->count) || y >= (int)self->lines - ) return false; + if (-(int)self->historybuf->count <= y && y < (int)self->lines) return false; if (y < 0) return historybuf_is_line_continued(self->historybuf, -(y + 1)); return visual_line_is_continued(self, y); } @@ -3495,6 +3491,7 @@ limit_without_trailing_whitespace(const Line *line, index_type limit) { if (limit > line->xnum) limit = line->xnum; while (limit > 0) { const CPUCell *cell = line->cpu_cells + limit - 1; + if (cell->is_multicell && (cell->x || cell->y)) { limit--; continue; } if (cell->ch_is_idx) break; switch(cell->ch_or_idx) { case ' ': case '\t': case '\n': case '\r': case 0: break; @@ -3506,130 +3503,136 @@ limit_without_trailing_whitespace(const Line *line, index_type limit) { return limit; } -static PyObject* -text_for_range(Screen *self, const Selection *sel, bool insert_newlines, bool strip_trailing_whitespace) { +static void +flag_selection_to_extract_text(Screen *self, const Selection *s, int *miny, int *y_limit) { IterationData idata; - iteration_data(sel, &idata, self->columns, -self->historybuf->count, 0); - Line line = {.xnum=self->columns, .text_cache=self->text_cache}, next_line = line; XRange xr, xrn = {0}; CPUCell *c; - size_t before = self->as_ansi_buf.len; - const int limit = MIN((int)self->lines, idata.y_limit); - if (idata.y >= limit) return PyTuple_New(0); - RAII_PyObject(ans, PyTuple_New(limit - idata.y)); - RAII_PyObject(nl, PyUnicode_FromString("\n")); - RAII_PyObject(empty, PyUnicode_FromString("")); - if (!ans || !nl || !empty) return NULL; - range_line(self, idata.y, &next_line); - xrn = xrange_for_iteration_with_multicells(&idata, idata.y, &next_line); - for (int i = 0, y = idata.y; y < limit; y++, i++) { - const bool is_first_line = y == idata.y, is_last_line = y + 1 >= limit; - xr = xrn; - line = next_line; - index_type x_limit = xr.x_limit; - bool is_only_whitespace_line = false; - if (strip_trailing_whitespace) { - index_type new_limit = limit_without_trailing_whitespace(&line, x_limit); - if (new_limit != x_limit) { - x_limit = new_limit; - is_only_whitespace_line = new_limit <= xr.x; - } - } - const bool add_trailing_newline = insert_newlines && !is_last_line; - self->as_ansi_buf.len = before; - if (!is_last_line) { - range_line(self, y + 1, &next_line); - xrn = xrange_for_iteration_with_multicells(&idata, y+1, &next_line); - for (index_type x = 0; x < xr.x; x++) { - if ((c = &line.cpu_cells[x])->is_multicell && c->scale > 1 && c->y + 1 < c->scale && x <= xrn.x) { - index_type mc_limit = x + mcd_x_limit(c); - if (!unicode_in_range(&line, x, mc_limit, true, false, false, false, &self->as_ansi_buf)) return PyErr_NoMemory(); - x = mc_limit - 1; - } - } - } - PyObject *text = NULL; - if (x_limit <= xr.x && is_only_whitespace_line) { // we want a newline on only whitespace lines even if they are continued - text = add_trailing_newline ? nl : empty; - text = Py_NewRef(text); - } else { - if (!unicode_in_range(&line, xr.x, x_limit, true, add_trailing_newline, false, !is_first_line, &self->as_ansi_buf)) return PyErr_NoMemory(); - if (!is_last_line) { - for (index_type x = x_limit; x < MIN(line.xnum, xrn.x_limit); x++) { - if ((c = &line.cpu_cells[x])->is_multicell && c->scale > 1 && c->y + 1 < c->scale) { - index_type mc_limit = x + mcd_x_limit(c); - if (!unicode_in_range(&line, x, mc_limit, true, false, false, false, &self->as_ansi_buf)) return PyErr_NoMemory(); - x = mc_limit - 1; + bool has_history = self->linebuf == self->main_linebuf; + iteration_data(s, &idata, self->columns, has_history ? -self->historybuf->count : 0, 0); + Line *line; + *miny = idata.y; *y_limit = MIN(idata.y_limit, (int)self->lines); + if (*miny >= *y_limit) return; + static const int max_scale = ( (1u << SCALE_BITS) - 1u); + for (int y = idata.y - max_scale; y < *y_limit; y++) { + line = checked_range_line(self, y); + if (line) for (index_type x = 0; x < line->xnum; x++) line->cpu_cells[x].temp_flag = 0; + } + Line temp = {.xnum=self->columns, .text_cache=self->text_cache}; + for (int y = idata.y; y < *y_limit; y++) { + range_line(self, y, &temp); + CPUCell *c; + XRange xr = xrange_for_iteration_with_multicells(&idata, y, &temp); + for (index_type x = xr.x; x < xr.x_limit; x++) { + c = temp.cpu_cells + x; + c->temp_flag = 1; + if (c->is_multicell && c->y) { + for (int ym = y - c->y; ym < y; ym++) { + line = checked_range_line(self, ym); + if (line) { + line->cpu_cells[x].temp_flag = 1; + *miny = MIN(*miny, ym); } } } + } + } + // remove lines from bottom that contain only y > 0 cells from multicell + while (*y_limit > *miny) { + range_line(self, *y_limit - 1, &temp); + for (index_type x = 0; x < temp.xnum; x++) { + if (temp.cpu_cells[x].temp_flag && temp.cpu_cells[x].ch_and_idx && (!temp.cpu_cells[x].is_multicell || !temp.cpu_cells[x].y)) return; + } + (*y_limit)--; + } +} + +static PyObject* +text_for_range(Screen *self, const Selection *sel, bool insert_newlines, bool strip_trailing_whitespace) { + int min_y, y_limit; + flag_selection_to_extract_text(self, sel, &min_y, &y_limit); + if (min_y >= y_limit) return PyTuple_New(0); + size_t before = self->as_ansi_buf.len; + RAII_PyObject(ans, PyTuple_New(y_limit - min_y)); + RAII_PyObject(nl, PyUnicode_FromString("\n")); + RAII_PyObject(empty, PyUnicode_FromString("")); + if (!ans || !nl || !empty) return NULL; + for (int i = 0, y = min_y; y < y_limit; y++, i++) { + Line *line = range_line_(self, y); + index_type x_limit = line->xnum, x_start = 0; + while (x_limit && !line->cpu_cells[x_limit - 1].temp_flag) x_limit--; + while (x_start < x_limit && !line->cpu_cells[x_start].temp_flag) x_start++; + bool is_only_whitespace_line = false; + if (strip_trailing_whitespace) { + index_type new_limit = limit_without_trailing_whitespace(line, x_limit); + if (new_limit != x_limit) { + x_limit = new_limit; + is_only_whitespace_line = new_limit <= x_start; + } + } + const bool is_first_line = y == min_y, is_last_line = y + 1 >= y_limit; + const bool add_trailing_newline = insert_newlines && !is_last_line; + PyObject *text = NULL; + if (x_limit <= x_start && is_only_whitespace_line) { // we want a newline on only whitespace lines even if they are continued + text = add_trailing_newline ? nl : empty; + text = Py_NewRef(text); + } else { + while (x_start < x_limit) { + index_type end = x_start; + while (end < x_limit && line->cpu_cells[end].temp_flag) end++; + if (!unicode_in_range(line, x_start, end, true, add_trailing_newline, false, !is_first_line, &self->as_ansi_buf)) return PyErr_NoMemory(); + x_start = MAX(x_start + 1, end); + } text = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, self->as_ansi_buf.buf + before, self->as_ansi_buf.len - before); } + self->as_ansi_buf.len = before; if (!text) return NULL; PyTuple_SET_ITEM(ans, i, text); } - self->as_ansi_buf.len = before; return Py_NewRef(ans); } static PyObject* ansi_for_range(Screen *self, const Selection *sel, bool insert_newlines, bool strip_trailing_whitespace) { - IterationData idata; - iteration_data(sel, &idata, self->columns, -self->historybuf->count, 0); - int limit = MIN((int)self->lines, idata.y_limit); - RAII_PyObject(ans, PyTuple_New(limit - idata.y + 1)); + int min_y, y_limit; + flag_selection_to_extract_text(self, sel, &min_y, &y_limit); + if (min_y >= y_limit) return PyTuple_New(0); + ANSILineState s = {.output_buf=&self->as_ansi_buf}; + s.output_buf->active_hyperlink_id = 0; s.output_buf->len = 0; + RAII_PyObject(ans, PyTuple_New(y_limit - min_y + 1)); RAII_PyObject(nl, PyUnicode_FromString("\n")); if (!ans || !nl) return NULL; bool has_escape_codes = false; bool need_newline = false; - ANSILineState s = {.output_buf=&self->as_ansi_buf}; - Line line = {.xnum=self->columns, .text_cache=self->text_cache}, next_line = line; XRange xr, xrn = {0}; CPUCell *c; - range_line(self, idata.y, &next_line); - xrn = xrange_for_iteration_with_multicells(&idata, idata.y, &next_line); - s.output_buf->active_hyperlink_id = 0; s.output_buf->len = 0; - for (int i = 0, y = idata.y; y < limit; y++, i++) { - const bool is_first_line = y == idata.y, is_last_line = y + 1 >= limit; - xr = xrn; - line = next_line; + for (int i = 0, y = min_y; y < y_limit; y++, i++) { + const bool is_first_line = y == min_y; s.output_buf->len = 0; - char_type prefix_char = need_newline ? '\n' : 0; - index_type x_limit = xr.x_limit; + Line *line = range_line_(self, y); + index_type x_limit = line->xnum, x_start = 0; + while (x_limit && !line->cpu_cells[x_limit - 1].temp_flag) x_limit--; + while (x_start < x_limit && !line->cpu_cells[x_start].temp_flag) x_start++; bool is_only_whitespace_line = false; if (strip_trailing_whitespace) { - index_type new_limit = limit_without_trailing_whitespace(&line, x_limit); + index_type new_limit = limit_without_trailing_whitespace(line, x_limit); if (new_limit != x_limit) { x_limit = new_limit; - is_only_whitespace_line = new_limit <= xr.x; + is_only_whitespace_line = new_limit <= x_start; } } - if (!is_last_line) { - range_line(self, y + 1, &next_line); - xrn = xrange_for_iteration_with_multicells(&idata, y+1, &next_line); - for (index_type x = 0; x < xr.x; x++) { - if ((c = &line.cpu_cells[x])->is_multicell && c->scale > 1 && c->y + 1 < c->scale && x <= xrn.x) { - index_type mc_limit = x + mcd_x_limit(c); - if (line_as_ansi(&line, &s, x, mc_limit, prefix_char, false)) has_escape_codes = true; - x = mc_limit - 1; - prefix_char = 0; - } - } - } - if (x_limit <= xr.x && is_only_whitespace_line) { // we want a newline on only whitespace lines even if they are continued + + if (x_limit <= x_start && is_only_whitespace_line) { // we want a newline on only whitespace lines even if they are continued if (insert_newlines) need_newline = true; } else { - if (line_as_ansi(&line, &s, xr.x, x_limit, prefix_char, !is_first_line)) has_escape_codes = true; - need_newline = insert_newlines && !line.cpu_cells[line.xnum-1].next_char_was_wrapped; - if (!is_last_line) { - for (index_type x = x_limit; x < MIN(line.xnum, xrn.x_limit); x++) { - if ((c = &line.cpu_cells[x])->is_multicell && c->scale > 1 && c->y + 1 < c->scale) { - index_type mc_limit = x + mcd_x_limit(c); - if (line_as_ansi(&line, &s, x, mc_limit, 0, false)) has_escape_codes = true; - x = mc_limit - 1; - } - } + char_type prefix_char = need_newline ? '\n' : 0; + while (x_start < x_limit) { + index_type end = x_start; + while (end < x_limit && line->cpu_cells[end].temp_flag) end++; + if (line_as_ansi(line, &s, x_start, end, prefix_char, !is_first_line)) has_escape_codes = true; + prefix_char = 0; + x_start = MAX(x_start + 1, end); } + PyObject *t = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, s.output_buf->buf, s.output_buf->len); + if (!t) return NULL; + PyTuple_SET_ITEM(ans, i, t); } - PyObject *t = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, s.output_buf->buf, s.output_buf->len); - if (!t) return NULL; - PyTuple_SET_ITEM(ans, i, t); } PyObject *t = PyUnicode_FromFormat("%s%s", has_escape_codes ? "\x1b[m" : "", s.output_buf->active_hyperlink_id ? "\x1b]8;;\x1b\\" : ""); if (!t) return NULL; diff --git a/kitty_tests/multicell.py b/kitty_tests/multicell.py index e6844b203..f9db69c98 100644 --- a/kitty_tests/multicell.py +++ b/kitty_tests/multicell.py @@ -667,8 +667,8 @@ def test_multicell(self: TestMulticell) -> None: asa(f'\x1b]{TEXT_SIZE_CODE};w=1:s=2;b\x07', '\x1b[m') ss(p(y=1, x=0), p(y=1, x=1, in_left_half_of_cell=False)) # empty leading cell before multiline on y=1 asl((0, 1, 2), (1, 0, 2)) - ast(' b') - asa(f' \x1b]{TEXT_SIZE_CODE};w=1:s=2;b\x07', '\x1b[m') + ast('b') + asa(f'\x1b]{TEXT_SIZE_CODE};w=1:s=2;b\x07', '\x1b[m') s.reset() multicell(s, 'X', scale=2), s.draw('123456abcd') @@ -679,10 +679,18 @@ def test_multicell(self: TestMulticell) -> None: asa(f'\x1b]{TEXT_SIZE_CODE};w=1:s=2;X\x07123456', 'ab', '\x1b[m') ss(p(y=1), p(y=1, x=3, in_left_half_of_cell=False)) asl((0, 0, 1), (1, 0, 3)) - ast('Xab') - asa(f'\x1b]{TEXT_SIZE_CODE};w=1:s=2;X\x07ab', '\x1b[m') + ast('X', 'ab') + asa(f'\x1b]{TEXT_SIZE_CODE};w=1:s=2;X\x07', 'ab', '\x1b[m') + + s = self.create_screen(lines=5, cols=24) + + s.reset() + multicell(s, 'ab cd ef', scale=2) + ss(p(6, 1), p(9, 0, in_left_half_of_cell=False)) + ast('ab ef') # Hyperlinks + s = self.create_screen(lines=5, cols=8) asu = partial(asl, bp=2) def set_link(url=None, id=None): parse_bytes(s, '\x1b]8;id={};{}\x1b\\'.format(id or '', url or '').encode('utf-8'))