Switch to tracking linewrap on the last cell in a line

This allows us to have newline not affect the wrap status of a line.

Now a lines wrapping status is changed only when the last cell
in the line is changed. This actually matches the behavior of many other
terminal emulators so is probably a good thing from a ecosystem
compatibility perspective.

The fish shell expects this weird behavior of newline not changing
wrapping status, for unknown reasons, which is the actual motivation for
doing all this work.

Fixes #5766
This commit is contained in:
Kovid Goyal 2022-12-26 20:26:21 +05:30
parent 4556f5b8f1
commit 68cf9f7514
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
10 changed files with 156 additions and 91 deletions

View File

@ -158,6 +158,7 @@ typedef union CellAttrs {
uint16_t strike : 1;
uint16_t dim : 1;
uint16_t mark : 2;
uint16_t next_char_was_wrapped : 1;
};
uint16_t val;
} CellAttrs;
@ -165,7 +166,7 @@ typedef union CellAttrs {
#define WIDTH_MASK (3u)
#define DECORATION_MASK (7u)
#define NUM_UNDERLINE_STYLES (5u)
#define SGR_MASK (~(((CellAttrs){.width=WIDTH_MASK, .mark=MARK_MASK}).val))
#define SGR_MASK (~(((CellAttrs){.width=WIDTH_MASK, .mark=MARK_MASK, .next_char_was_wrapped=1}).val))
typedef struct {
color_type fg, bg, decoration_fg;
@ -184,7 +185,7 @@ static_assert(sizeof(CPUCell) == 12, "Fix the ordering of CPUCell");
typedef enum { UNKNOWN_PROMPT_KIND = 0, PROMPT_START = 1, SECONDARY_PROMPT = 2, OUTPUT_START = 3 } PromptKind;
typedef union LineAttrs {
struct {
uint8_t continued : 1;
uint8_t is_continued : 1;
uint8_t has_dirty_text : 1;
PromptKind prompt_kind : 2;
};

View File

@ -164,6 +164,16 @@ init_line(HistoryBuf *self, index_type num, Line *l) {
l->cpu_cells = cpu_lineptr(self, num);
l->gpu_cells = gpu_lineptr(self, num);
l->attrs = *attrptr(self, num);
if (num > 0) {
l->attrs.is_continued = gpu_lineptr(self, num - 1)[self->xnum-1].attrs.next_char_was_wrapped;
} else {
l->attrs.is_continued = false;
size_t sz;
if (self->pagerhist && self->pagerhist->ringbuf && (sz = ringbuf_bytes_used(self->pagerhist->ringbuf)) > 0) {
size_t pos = ringbuf_findchr(self->pagerhist->ringbuf, '\n', sz - 1);
if (pos >= sz) l->attrs.is_continued = true; // ringbuf does not end with a newline
}
}
}
void
@ -171,6 +181,11 @@ historybuf_init_line(HistoryBuf *self, index_type lnum, Line *l) {
init_line(self, index_of(self, lnum), l);
}
bool
history_buf_endswith_wrap(HistoryBuf *self) {
return gpu_lineptr(self, index_of(self, 0))[self->xnum-1].attrs.next_char_was_wrapped;
}
CPUCell*
historybuf_cpu_cells(HistoryBuf *self, index_type lnum) {
return cpu_lineptr(self, index_of(self, lnum));
@ -243,9 +258,13 @@ pagerhist_push(HistoryBuf *self, ANSIBuf *as_ansi_buf) {
Line l = {.xnum=self->xnum};
init_line(self, self->start_of_data, &l);
line_as_ansi(&l, as_ansi_buf, &prev_cell, 0, l.xnum, 0);
if (ringbuf_bytes_used(ph->ringbuf) && !l.attrs.continued) pagerhist_write_bytes(ph, (const uint8_t*)"\n", 1);
pagerhist_write_bytes(ph, (const uint8_t*)"\x1b[m", 3);
if (pagerhist_write_ucs4(ph, as_ansi_buf->buf, as_ansi_buf->len)) pagerhist_write_bytes(ph, (const uint8_t*)"\r", 1);
if (pagerhist_write_ucs4(ph, as_ansi_buf->buf, as_ansi_buf->len)) {
char line_end[2]; size_t num = 0;
line_end[num++] = '\r';
if (!l.gpu_cells[l.xnum - 1].attrs.next_char_was_wrapped) line_end[num++] = '\n';
pagerhist_write_bytes(ph, (const uint8_t*)line_end, num);
}
}
static index_type
@ -275,6 +294,13 @@ historybuf_pop_line(HistoryBuf *self, Line *line) {
return true;
}
static void
history_buf_set_last_char_as_continuation(HistoryBuf *self, index_type y, bool wrapped) {
if (self->count > 0) {
gpu_lineptr(self, index_of(self, y))[self->xnum-1].attrs.next_char_was_wrapped = wrapped;
}
}
static PyObject*
line(HistoryBuf *self, PyObject *val) {
#define line_doc "Return the line with line number val. This buffer grows upwards, i.e. 0 is the most recently added line"
@ -321,13 +347,10 @@ as_ansi(HistoryBuf *self, PyObject *callback) {
ANSIBuf output = {0};
for(unsigned int i = 0; i < self->count; i++) {
init_line(self, i, &l);
if (i < self->count - 1) {
l.attrs.continued = attrptr(self, index_of(self, i + 1))->continued;
} else l.attrs.continued = false;
line_as_ansi(&l, &output, &prev_cell, 0, l.xnum, 0);
if (!l.attrs.continued) {
if (!l.gpu_cells[l.xnum - 1].attrs.next_char_was_wrapped) {
ensure_space_for(&output, buf, Py_UCS4, output.len + 1, capacity, 2048, false);
output.buf[output.len++] = 10; // 10 = \n
output.buf[output.len++] = '\n';
}
PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, output.buf, output.len);
if (ans == NULL) { PyErr_NoMemory(); goto end; }
@ -438,14 +461,11 @@ pagerhist_as_bytes(HistoryBuf *self, PyObject *args) {
pagerhist_ensure_start_is_valid_utf8(ph);
if (ph->rewrap_needed) pagerhist_rewrap_to(self, self->xnum);
Line l = {.xnum=self->xnum}; get_line(self, 0, &l);
size_t sz = ringbuf_bytes_used(ph->ringbuf);
if (!l.attrs.continued) sz += 1;
PyObject *ans = PyBytes_FromStringAndSize(NULL, sz);
if (!ans) return NULL;
uint8_t *buf = (uint8_t*)PyBytes_AS_STRING(ans);
ringbuf_memcpy_from(buf, ph->ringbuf, sz);
if (!l.attrs.continued) buf[sz-1] = '\n';
if (upto_output_start) {
const uint8_t *p = reverse_find(buf, sz, (const uint8_t*)"\x1b]133;C\x1b\\");
if (p) {
@ -484,7 +504,7 @@ PyObject*
as_text_history_buf(HistoryBuf *self, PyObject *args, ANSIBuf *output) {
GetLineWrapper glw = {.self=self};
glw.line.xnum = self->xnum;
PyObject *ans = as_text_generic(args, &glw, get_line_wrapper, self->count, output);
PyObject *ans = as_text_generic(args, &glw, get_line_wrapper, self->count, output, true);
return ans;
}
@ -560,9 +580,7 @@ HistoryBuf *alloc_historybuf(unsigned int lines, unsigned int columns, unsigned
#define init_src_line(src_y) init_line(src, map_src_index(src_y), src->line);
#define is_src_line_continued(src_y) (map_src_index(src_y) < src->ynum - 1 ? (attrptr(src, map_src_index(src_y + 1))->continued) : false)
#define next_dest_line(cont) { LineAttrs *lap = attrptr(dest, historybuf_push(dest, as_ansi_buf)); *lap = src->line->attrs; if (cont) lap->continued = true; dest->line->attrs.continued = cont; }
#define next_dest_line(cont) { history_buf_set_last_char_as_continuation(dest, 0, cont); LineAttrs *lap = attrptr(dest, historybuf_push(dest, as_ansi_buf)); *lap = src->line->attrs; }
#define first_dest_line next_dest_line(false);

View File

@ -130,6 +130,7 @@ linebuf_init_line(LineBuf *self, index_type idx) {
self->line->ynum = idx;
self->line->xnum = self->xnum;
self->line->attrs = self->line_attrs[idx];
self->line->attrs.is_continued = idx > 0 ? gpu_lineptr(self, self->line_map[idx - 1])[self->xnum - 1].attrs.next_char_was_wrapped : false;
init_line(self, self->line, self->line_map[idx]);
}
@ -151,6 +152,19 @@ linebuf_char_width_at(LineBuf *self, index_type x, index_type y) {
return gpu_lineptr(self, self->line_map[y])[x].attrs.width;
}
bool
linebuf_line_ends_with_continuation(LineBuf *self, index_type y) {
return y < self->ynum ? gpu_lineptr(self, self->line_map[y])[self->xnum - 1].attrs.next_char_was_wrapped : false;
}
void
linebuf_set_last_char_as_continuation(LineBuf *self, index_type y, bool continued) {
if (y < self->ynum) {
gpu_lineptr(self, self->line_map[y])[self->xnum - 1].attrs.next_char_was_wrapped = continued;
}
}
static PyObject*
set_attribute(LineBuf *self, PyObject *args) {
#define set_attribute_doc "set_attribute(which, val) -> Set the attribute on all cells in the line."
@ -172,8 +186,8 @@ set_continued(LineBuf *self, PyObject *args) {
unsigned int y;
int val;
if (!PyArg_ParseTuple(args, "Ip", &y, &val)) return NULL;
if (y >= self->ynum) { PyErr_SetString(PyExc_ValueError, "Out of bounds."); return NULL; }
self->line_attrs[y].continued = val;
if (y > self->ynum || y < 1) { PyErr_SetString(PyExc_ValueError, "Out of bounds."); return NULL; }
linebuf_set_last_char_as_continuation(self, y-1, val);
Py_RETURN_NONE;
}
@ -316,7 +330,7 @@ is_continued(LineBuf *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->ynum) { PyErr_SetString(PyExc_ValueError, "Out of bounds."); return NULL; }
if (self->line_attrs[y].continued) { Py_RETURN_TRUE; }
if (y > 0 && linebuf_line_ends_with_continuation(self, y-1)) { Py_RETURN_TRUE; }
Py_RETURN_FALSE;
}
@ -334,7 +348,6 @@ linebuf_insert_lines(LineBuf *self, unsigned int num, unsigned int y, unsigned i
self->line_map[i] = self->line_map[i - num];
self->line_attrs[i] = self->line_attrs[i - num];
}
if (y + num < self->ynum) self->line_attrs[y + num].continued = false;
for (i = 0; i < num; i++) {
self->line_map[y + i] = self->scratch[ylimit - num + i];
}
@ -369,7 +382,6 @@ linebuf_delete_lines(LineBuf *self, index_type num, index_type y, index_type bot
self->line_map[i] = self->line_map[i + num];
self->line_attrs[i] = self->line_attrs[i + num];
}
self->line_attrs[y].continued = false;
for (i = 0; i < num; i++) {
self->line_map[ylimit - num + i] = self->scratch[y + i];
}
@ -414,10 +426,10 @@ as_ansi(LineBuf *self, PyObject *callback) {
} while(ylimit > 0);
for(index_type i = 0; i <= ylimit; i++) {
l.attrs.continued = self->line_attrs[(i + 1 < self->ynum) ? i+1 : i].continued;
bool output_newline = !linebuf_line_ends_with_continuation(self, i);
init_line(self, (&l), self->line_map[i]);
line_as_ansi(&l, &output, &prev_cell, 0, l.xnum, 0);
if (!l.attrs.continued) {
if (output_newline) {
ensure_space_for(&output, buf, Py_UCS4, output.len + 1, capacity, 2048, false);
output.buf[output.len++] = 10; // 10 = \n
}
@ -444,7 +456,7 @@ get_line(void *x, int y) {
static PyObject*
as_text(LineBuf *self, PyObject *args) {
ANSIBuf output = {0};
PyObject* ans = as_text_generic(args, self, get_line, self->ynum, &output);
PyObject* ans = as_text_generic(args, self, get_line, self->ynum, &output, false);
free(output.buf);
return ans;
}

View File

@ -229,10 +229,9 @@ cell_as_utf8_for_fallback(CPUCell *cell, char *buf) {
PyObject*
unicode_in_range(const Line *self, const index_type start, const index_type limit, const bool include_cc, const char leading_char, const bool skip_zero_cells) {
unicode_in_range(const Line *self, const index_type start, const index_type limit, const bool include_cc, const bool add_trailing_newline, const bool skip_zero_cells) {
size_t n = 0;
static Py_UCS4 buf[4096];
if (leading_char) buf[n++] = leading_char;
char_type previous_width = 0;
for(index_type i = start; i < limit && n < arraysz(buf) - 2 - arraysz(self->cpu_cells->cc_idx); i++) {
char_type ch = self->cpu_cells[i].ch;
@ -252,12 +251,15 @@ unicode_in_range(const Line *self, const index_type start, const index_type limi
}
previous_width = self->gpu_cells[i].attrs.width;
}
if (add_trailing_newline && !self->gpu_cells[self->xnum-1].attrs.next_char_was_wrapped && n < arraysz(buf)) {
buf[n++] = '\n';
}
return PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, buf, n);
}
PyObject *
line_as_unicode(Line* self, bool skip_zero_cells) {
return unicode_in_range(self, 0, xlimit_for_line(self), true, 0, skip_zero_cells);
return unicode_in_range(self, 0, xlimit_for_line(self), true, false, skip_zero_cells);
}
static PyObject*
@ -401,11 +403,10 @@ as_ansi(Line* self, PyObject *a UNUSED) {
}
static PyObject*
is_continued(Line* self, PyObject *a UNUSED) {
#define is_continued_doc "Return the line's continued flag"
PyObject *ans = self->attrs.continued ? Py_True : Py_False;
Py_INCREF(ans);
return ans;
last_char_has_wrapped_flag(Line* self, PyObject *a UNUSED) {
#define last_char_has_wrapped_flag_doc "Return True if the last cell of this line has the wrapped flags set"
if (self->gpu_cells[self->xnum - 1].attrs.next_char_was_wrapped) { Py_RETURN_TRUE; }
Py_RETURN_FALSE;
}
static PyObject*
@ -839,7 +840,7 @@ mark_text_in_line(PyObject *marker, Line *line) {
}
PyObject*
as_text_generic(PyObject *args, void *container, get_line_func get_line, index_type lines, ANSIBuf *ansibuf) {
as_text_generic(PyObject *args, void *container, get_line_func get_line, index_type lines, ANSIBuf *ansibuf, bool add_trailing_newline) {
#define APPEND(x) { PyObject* retval = PyObject_CallFunctionObjArgs(callback, x, NULL); if (!retval) return NULL; Py_DECREF(retval); }
#define APPEND_AND_DECREF(x) { if (x == NULL) { if (PyErr_Occurred()) return NULL; Py_RETURN_NONE; } PyObject* retval = PyObject_CallFunctionObjArgs(callback, x, NULL); Py_CLEAR(x); if (!retval) return NULL; Py_DECREF(retval); }
PyObject *callback;
@ -852,10 +853,11 @@ as_text_generic(PyObject *args, void *container, get_line_func get_line, index_t
if (nl == NULL || cr == NULL || sgr_reset == NULL) return NULL;
const GPUCell *prev_cell = NULL;
ansibuf->active_hyperlink_id = 0;
bool need_newline = false;
for (index_type y = 0; y < lines; y++) {
Line *line = get_line(container, y);
if (!line) { if (PyErr_Occurred()) return NULL; break; }
if (!line->attrs.continued && y > 0) APPEND(nl);
if (need_newline) APPEND(nl);
if (as_ansi) {
// less has a bug where it resets colors when it sees a \r, so work
// around it by resetting SGR at the start of every line. This is
@ -871,7 +873,9 @@ as_text_generic(PyObject *args, void *container, get_line_func get_line, index_t
}
APPEND_AND_DECREF(t);
if (insert_wrap_markers) APPEND(cr);
need_newline = !line->gpu_cells[line->xnum-1].attrs.next_char_was_wrapped;
}
if (need_newline && add_trailing_newline) APPEND(nl);
if (ansibuf->active_hyperlink_id) {
ansibuf->active_hyperlink_id = 0;
t = PyUnicode_FromString("\x1b]8;;\x1b\\");
@ -919,7 +923,7 @@ static PyMethodDef methods[] = {
METHOD(set_char, METH_VARARGS)
METHOD(set_attribute, METH_VARARGS)
METHOD(as_ansi, METH_NOARGS)
METHOD(is_continued, METH_NOARGS)
METHOD(last_char_has_wrapped_flag, METH_NOARGS)
METHOD(hyperlink_ids, METH_NOARGS)
METHOD(width, METH_O)
METHOD(url_start_at, METH_O)

View File

@ -97,7 +97,7 @@ size_t cell_as_unicode(CPUCell *cell, bool include_cc, Py_UCS4 *buf, char_type);
size_t cell_as_unicode_for_fallback(CPUCell *cell, Py_UCS4 *buf);
size_t cell_as_utf8(CPUCell *cell, bool include_cc, char *buf, char_type);
size_t cell_as_utf8_for_fallback(CPUCell *cell, char *buf);
PyObject* unicode_in_range(const Line *self, const index_type start, const index_type limit, const bool include_cc, const char leading_char, const bool skip_zero_cells);
PyObject* unicode_in_range(const Line *self, const index_type start, const index_type limit, const bool include_cc, const bool add_trailing_newline, const bool skip_zero_cells);
PyObject* line_as_unicode(Line *, bool);
void linebuf_init_line(LineBuf *, index_type);
@ -113,11 +113,14 @@ 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);
unsigned int linebuf_char_width_at(LineBuf *self, index_type x, index_type y);
void linebuf_set_last_char_as_continuation(LineBuf *self, index_type y, bool continued);
bool linebuf_line_ends_with_continuation(LineBuf *self, index_type y);
void linebuf_refresh_sprite_positions(LineBuf *self);
void historybuf_add_line(HistoryBuf *self, const Line *line, ANSIBuf*);
bool historybuf_pop_line(HistoryBuf *, Line *);
void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other, ANSIBuf*);
void historybuf_init_line(HistoryBuf *self, index_type num, Line *l);
bool history_buf_endswith_wrap(HistoryBuf *self);
CPUCell* historybuf_cpu_cells(HistoryBuf *self, index_type num);
void historybuf_mark_line_clean(HistoryBuf *self, index_type y);
void historybuf_mark_line_dirty(HistoryBuf *self, index_type y);
@ -125,5 +128,5 @@ void historybuf_refresh_sprite_positions(HistoryBuf *self);
void historybuf_clear(HistoryBuf *self);
void mark_text_in_line(PyObject *marker, Line *line);
bool line_has_mark(Line *, uint16_t mark);
PyObject* as_text_generic(PyObject *args, void *container, get_line_func get_line, index_type lines, ANSIBuf *ansibuf);
PyObject* as_text_generic(PyObject *args, void *container, get_line_func get_line, index_type lines, ANSIBuf *ansibuf, bool add_trailing_newline);
bool colors_for_cell(Line *self, ColorProfile *cp, index_type *x, color_type *fg, color_type *bg);

View File

@ -15,14 +15,15 @@
#define init_src_line(src_y) linebuf_init_line(src, src_y);
#endif
#define set_dest_line_attrs(dest_y, continued_) dest->line_attrs[dest_y] = src->line->attrs; if (continued_) dest->line_attrs[dest_y].continued = true; src->line->attrs.prompt_kind = UNKNOWN_PROMPT_KIND;
#define set_dest_line_attrs(dest_y) dest->line_attrs[dest_y] = src->line->attrs; src->line->attrs.prompt_kind = UNKNOWN_PROMPT_KIND;
#ifndef first_dest_line
#define first_dest_line linebuf_init_line(dest, 0); set_dest_line_attrs(0, false)
#define first_dest_line linebuf_init_line(dest, 0); set_dest_line_attrs(0)
#endif
#ifndef next_dest_line
#define next_dest_line(continued) \
linebuf_set_last_char_as_continuation(dest, dest_y, continued); \
if (dest_y >= dest->ynum - 1) { \
linebuf_index(dest, 0, dest->ynum - 1); \
if (historybuf != NULL) { \
@ -33,11 +34,11 @@
linebuf_clear_line(dest, dest->ynum - 1, true); \
} else dest_y++; \
linebuf_init_line(dest, dest_y); \
set_dest_line_attrs(dest_y, continued);
set_dest_line_attrs(dest_y);
#endif
#ifndef is_src_line_continued
#define is_src_line_continued(src_y) (src_y + 1 < src->ynum ? (src->line_attrs[src_y + 1].continued) : false)
#define is_src_line_continued() (src->line->gpu_cells[src->xnum-1].attrs.next_char_was_wrapped)
#endif
static inline void
@ -54,7 +55,7 @@ typedef struct TrackCursor {
static void
rewrap_inner(BufType *src, BufType *dest, const index_type src_limit, HistoryBuf UNUSED *historybuf, TrackCursor *track, ANSIBuf *as_ansi_buf) {
bool src_line_is_continued = false, is_first_line = true;
bool is_first_line = true;
index_type src_y = 0, src_x = 0, dest_x = 0, dest_y = 0, num = 0, src_x_limit = 0;
TrackCursor tc_end = {.is_sentinel = true };
if (!track) track = &tc_end;
@ -62,11 +63,13 @@ rewrap_inner(BufType *src, BufType *dest, const index_type src_limit, HistoryBuf
do {
for (TrackCursor *t = track; !t->is_sentinel; t++) t->is_tracked_line = src_y == t->y;
init_src_line(src_y);
src_line_is_continued = is_src_line_continued(src_y);
const bool src_line_is_continued = is_src_line_continued();
src_x_limit = src->xnum;
if (!src_line_is_continued) {
// Trim trailing blanks since there is a hard line break at the end of this line
while(src_x_limit && (src->line->cpu_cells[src_x_limit - 1].ch) == BLANK_CHAR) src_x_limit--;
} else {
src->line->gpu_cells[src->xnum-1].attrs.next_char_was_wrapped = false;
}
for (TrackCursor *t = track; !t->is_sentinel; t++) {
if (t->is_tracked_line && t->x >= src_x_limit) t->x = MAX(1u, src_x_limit) - 1;

View File

@ -317,7 +317,6 @@ found:
// so when resizing, simply blank all lines after the current
// prompt and trust the shell to redraw them.
for (; y < (int)self->main_linebuf->ynum; y++) {
self->main_linebuf->line_attrs[y].continued = false;
linebuf_clear_line(self->main_linebuf, y, false);
linebuf_init_line(self->main_linebuf, y);
if (y <= (int)self->cursor->y) {
@ -506,9 +505,9 @@ move_widened_char(Screen *self, CPUCell* cpu_cell, GPUCell *gpu_cell, index_type
line_clear_text(self->linebuf->line, xpos, 1, BLANK_CHAR);
if (self->modes.mDECAWM) { // overflow goes onto next line
linebuf_set_last_char_as_continuation(self->linebuf, self->cursor->y, true);
screen_carriage_return(self);
screen_linefeed(self);
self->linebuf->line_attrs[self->cursor->y].continued = true;
linebuf_init_line(self->linebuf, self->cursor->y);
dest_cpu = self->linebuf->line->cpu_cells;
dest_gpu = self->linebuf->line->gpu_cells;
@ -680,9 +679,9 @@ draw_codepoint(Screen *self, char_type och, bool from_input_stream) {
if (from_input_stream) self->last_graphic_char = ch;
if (UNLIKELY(self->columns - self->cursor->x < (unsigned int)char_width)) {
if (self->modes.mDECAWM) {
linebuf_set_last_char_as_continuation(self->linebuf, self->cursor->y, true);
screen_carriage_return(self);
screen_linefeed(self);
self->linebuf->line_attrs[self->cursor->y].continued = true;
} else {
self->cursor->x = self->columns - char_width;
}
@ -740,7 +739,7 @@ get_overlay_text(Screen *self) {
if (ol.ynum >= self->lines || ol.xnum >= self->columns || !ol.xnum) return NULL;
Line *line = range_line_(self, ol.ynum);
if (!line) return NULL;
return unicode_in_range(line, ol.xstart, ol.xstart + ol.xnum, true, 0, true);
return unicode_in_range(line, ol.xstart, ol.xstart + ol.xnum, true, false, true);
#undef ol
}
@ -1368,7 +1367,6 @@ screen_linefeed(Screen *self) {
bool in_margins = cursor_within_margins(self);
screen_index(self);
if (self->modes.mLNM) screen_carriage_return(self);
if (self->cursor->y < self->lines) self->linebuf->line_attrs[self->cursor->y].continued = false;
screen_ensure_bounds(self, false, in_margins);
}
@ -1674,6 +1672,7 @@ screen_erase_in_display(Screen *self, unsigned int how, bool private) {
linebuf_init_line(self->linebuf, i);
if (private) {
line_clear_text(self->linebuf->line, 0, self->columns, BLANK_CHAR);
linebuf_set_last_char_as_continuation(self->linebuf, i, false);
} else {
line_apply_cursor(self->linebuf->line, self->cursor, 0, self->columns, true);
}
@ -2312,6 +2311,15 @@ num_lines_between_selection_boundaries(const SelectionBoundary *a, const Selecti
typedef Line*(linefunc_t)(Screen*, int);
static Line*
init_line(Screen *self, index_type y) {
linebuf_init_line(self->linebuf, y);
if (y == 0 && self->linebuf == self->main_linebuf) {
if (history_buf_endswith_wrap(self->historybuf)) self->linebuf->line->attrs.is_continued = true;
}
return self->linebuf->line;
}
static Line*
visual_line_(Screen *self, int y_) {
index_type y = MAX(0, y_);
@ -2322,8 +2330,7 @@ visual_line_(Screen *self, int y_) {
}
y -= self->scrolled_by;
}
linebuf_init_line(self->linebuf, y);
return self->linebuf->line;
return init_line(self, y);
}
static Line*
@ -2332,8 +2339,7 @@ range_line_(Screen *self, int y) {
historybuf_init_line(self->historybuf, -(y + 1), self->historybuf->line);
return self->historybuf->line;
}
linebuf_init_line(self->linebuf, y);
return self->linebuf->line;
return init_line(self, y);
}
static Line*
@ -2512,7 +2518,6 @@ text_for_range(Screen *self, const Selection *sel, bool insert_newlines, bool st
for (int i = 0, y = idata.y; y < limit; y++, i++) {
Line *line = range_line_(self, y);
XRange xr = xrange_for_iteration(&idata, y, line);
char leading_char = (i > 0 && insert_newlines && !line->attrs.continued) ? '\n' : 0;
index_type x_limit = xr.x_limit;
if (strip_trailing_whitespace) {
index_type new_limit = limit_without_trailing_whitespace(line, x_limit);
@ -2526,7 +2531,7 @@ text_for_range(Screen *self, const Selection *sel, bool insert_newlines, bool st
}
}
}
PyObject *text = unicode_in_range(line, xr.x, x_limit, true, leading_char, false);
PyObject *text = unicode_in_range(line, xr.x, x_limit, true, insert_newlines && y != limit-1, false);
if (text == NULL) { Py_DECREF(ans); return PyErr_NoMemory(); }
PyTuple_SET_ITEM(ans, i, text);
}
@ -2544,12 +2549,12 @@ ansi_for_range(Screen *self, const Selection *sel, bool insert_newlines, bool st
ANSIBuf output = {0};
const GPUCell *prev_cell = NULL;
bool has_escape_codes = false;
bool need_newline = false;
for (int i = 0, y = idata.y; y < limit; y++, i++) {
Line *line = range_line_(self, y);
XRange xr = xrange_for_iteration(&idata, y, line);
output.len = 0;
char_type prefix_char = 0;
if (i > 0 && insert_newlines && !line->attrs.continued) prefix_char = '\n';
char_type prefix_char = need_newline ? '\n' : 0;
index_type x_limit = xr.x_limit;
if (strip_trailing_whitespace) {
index_type new_limit = limit_without_trailing_whitespace(line, x_limit);
@ -2562,6 +2567,7 @@ ansi_for_range(Screen *self, const Selection *sel, bool insert_newlines, bool st
}
}
if (line_as_ansi(line, &output, &prev_cell, xr.x, x_limit, prefix_char)) has_escape_codes = true;
need_newline = insert_newlines && !line->gpu_cells[line->xnum-1].attrs.next_char_was_wrapped;
PyObject *t = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, output.buf, output.len);
if (!t) return NULL;
PyTuple_SET_ITEM(ans, i, t);
@ -2792,12 +2798,12 @@ static Line* get_range_line(void *x, int y) { return range_line_(x, y); }
static PyObject*
as_text(Screen *self, PyObject *args) {
return as_text_generic(args, self, get_visual_line, self->lines, &self->as_ansi_buf);
return as_text_generic(args, self, get_visual_line, self->lines, &self->as_ansi_buf, false);
}
static PyObject*
as_text_non_visual(Screen *self, PyObject *args) {
return as_text_generic(args, self, get_range_line, self->lines, &self->as_ansi_buf);
return as_text_generic(args, self, get_range_line, self->lines, &self->as_ansi_buf, false);
}
static PyObject*
@ -2807,7 +2813,7 @@ as_text_for_history_buf(Screen *self, PyObject *args) {
static PyObject*
as_text_generic_wrapper(Screen *self, PyObject *args, get_line_func get_line) {
return as_text_generic(args, self, get_line, self->lines, &self->as_ansi_buf);
return as_text_generic(args, self, get_line, self->lines, &self->as_ansi_buf, false);
}
static PyObject*
@ -2849,7 +2855,7 @@ find_cmd_output(Screen *self, OutputOffset *oo, index_type start_screen_y, unsig
found_prompt = true;
// change direction to downwards to find command output
direction = 1;
} else if (line && line->attrs.prompt_kind == OUTPUT_START && !line->attrs.continued) {
} else if (line && line->attrs.prompt_kind == OUTPUT_START && !line->attrs.is_continued) {
found_output = true; start = y1;
found_prompt = true;
// keep finding the first output start upwards
@ -2863,14 +2869,14 @@ find_cmd_output(Screen *self, OutputOffset *oo, index_type start_screen_y, unsig
// find upwards: find prompt after the output, and the first output
while (y1 >= upward_limit) {
line = checked_range_line(self, y1);
if (line && line->attrs.prompt_kind == PROMPT_START && !line->attrs.continued) {
if (line && line->attrs.prompt_kind == PROMPT_START && !line->attrs.is_continued) {
if (direction == 0) {
// find around: stop at prompt start
start = y1 + 1;
break;
}
found_next_prompt = true; end = y1;
} else if (line && line->attrs.prompt_kind == OUTPUT_START && !line->attrs.continued) {
} else if (line && line->attrs.prompt_kind == OUTPUT_START && !line->attrs.is_continued) {
start = y1;
break;
}
@ -2941,7 +2947,7 @@ cmd_output(Screen *self, PyObject *args) {
bool reached_upper_limit = false;
while (!found && !reached_upper_limit) {
line = checked_range_line(self, y);
if (!line || (line->attrs.prompt_kind == OUTPUT_START && !line->attrs.continued)) {
if (!line || (line->attrs.prompt_kind == OUTPUT_START && !line->attrs.is_continued)) {
int start = line ? y : y + 1; reached_upper_limit = !line;
int y2 = start; unsigned int num_lines = 0;
bool found_content = false;
@ -2964,7 +2970,7 @@ cmd_output(Screen *self, PyObject *args) {
return NULL;
}
if (found) {
DECREF_AFTER_FUNCTION PyObject *ret = as_text_generic(as_text_args, &oo, get_line_from_offset, oo.num_lines, &self->as_ansi_buf);
DECREF_AFTER_FUNCTION PyObject *ret = as_text_generic(as_text_args, &oo, get_line_from_offset, oo.num_lines, &self->as_ansi_buf, false);
if (!ret) return NULL;
}
if (oo.reached_upper_limit && self->linebuf == self->main_linebuf && OPT(scrollback_pager_history_size) > 0) Py_RETURN_TRUE;
@ -3364,7 +3370,7 @@ screen_selection_range_for_word(Screen *self, const index_type x, const index_ty
start = x; end = x;
while(true) {
while(start > 0 && is_ok(start - 1, false)) start--;
if (start > 0 || !line->attrs.continued || *y1 == 0) break;
if (start > 0 || !line->attrs.is_continued || *y1 == 0) break;
line = visual_line_(self, *y1 - 1);
if (!is_ok(self->columns - 1, false)) break;
(*y1)--; start = self->columns - 1;
@ -3374,7 +3380,7 @@ screen_selection_range_for_word(Screen *self, const index_type x, const index_ty
while(end < self->columns - 1 && is_ok(end + 1, true)) end++;
if (end < self->columns - 1 || *y2 >= self->lines - 1) break;
line = visual_line_(self, *y2 + 1);
if (!line->attrs.continued || !is_ok(0, true)) break;
if (!line->attrs.is_continued || !is_ok(0, true)) break;
(*y2)++; end = 0;
}
*s = start; *e = end;
@ -3542,7 +3548,7 @@ screen_mark_hyperlink(Screen *self, index_type x, index_type y) {
static index_type
continue_line_upwards(Screen *self, index_type top_line, SelectionBoundary *start, SelectionBoundary *end) {
while (top_line > 0 && visual_line_(self, top_line)->attrs.continued) {
while (top_line > 0 && visual_line_(self, top_line)->attrs.is_continued) {
if (!screen_selection_range_for_line(self, top_line - 1, &start->x, &end->x)) break;
top_line--;
}
@ -3551,7 +3557,7 @@ continue_line_upwards(Screen *self, index_type top_line, SelectionBoundary *star
static index_type
continue_line_downwards(Screen *self, index_type bottom_line, SelectionBoundary *start, SelectionBoundary *end) {
while (bottom_line < self->lines - 1 && visual_line_(self, bottom_line + 1)->attrs.continued) {
while (bottom_line < self->lines - 1 && visual_line_(self, bottom_line + 1)->attrs.is_continued) {
if (!screen_selection_range_for_line(self, bottom_line + 1, &start->x, &end->x)) break;
bottom_line++;
}
@ -3971,7 +3977,7 @@ dump_lines_with_attrs(Screen *self, PyObject *accum) {
PyObject_CallFunction(accum, "s", "\x1b[33moutput \x1b[39m");
break;
}
if (line->attrs.continued) PyObject_CallFunction(accum, "s", "continued ");
if (line->attrs.is_continued) PyObject_CallFunction(accum, "s", "continued ");
if (line->attrs.has_dirty_text) PyObject_CallFunction(accum, "s", "dirty ");
PyObject_CallFunction(accum, "s", "\n");
t = line_as_unicode(line, false);

View File

@ -278,8 +278,6 @@ def as_text(
h: List[str] = [pht] if pht else []
screen.as_text_for_history_buf(h.append, as_ansi, add_wrap_markers)
if h:
if not screen.linebuf.is_continued(0):
h[-1] += '\n'
if as_ansi:
h[-1] += '\x1b[m'
ans = ''.join(chain(h, lines))

View File

@ -20,11 +20,10 @@
def create_lbuf(*lines):
maxw = max(map(len, lines))
ans = LineBuf(len(lines), maxw)
prev_full_length = False
for i, l0 in enumerate(lines):
ans.line(i).set_text(l0, 0, len(l0), C())
ans.set_continued(i, prev_full_length)
prev_full_length = len(l0) == maxw
if i > 0:
ans.set_continued(i, len(lines[i-1]) == maxw)
return ans
@ -73,9 +72,9 @@ def test_linebuf(self):
self.assertFalse(c.reverse)
self.assertTrue(c.bold)
self.assertFalse(old.is_continued(0))
old.set_continued(0, True)
self.assertTrue(old.is_continued(0))
self.assertFalse(old.is_continued(1))
old.set_continued(1, True)
self.assertTrue(old.is_continued(1))
self.assertFalse(old.is_continued(0))
lb = filled_line_buf(5, 5, filled_cursor())
lb2 = LineBuf(5, 5)
@ -518,7 +517,7 @@ def test_ansi_repr(self):
self.ae(l2.as_ansi(), '\x1b[1;2;3;7;9;34;48:2:1:2:3;58:5:5m' '1'
'\x1b[22;23;27;29;39;49;59m' '0000')
lb = filled_line_buf()
for i in range(lb.ynum):
for i in range(1, lb.ynum + 1):
lb.set_continued(i, True)
a = []
lb.as_ansi(a.append)

View File

@ -210,22 +210,23 @@ def init():
s.reset_dirty()
s.cursor.x, s.cursor.y = 2, 1
s.cursor.bold = True
self.ae(continuations(s), (True, True, True, True, False))
def all_lines(s):
return tuple(str(s.line(i)) for i in range(s.lines))
def continuations(s):
return tuple(s.line(i).is_continued() for i in range(s.lines))
return tuple(s.line(i).last_char_has_wrapped_flag() for i in range(s.lines))
init()
s.erase_in_display(0)
self.ae(all_lines(s), ('12345', '12', '', '', ''))
self.ae(continuations(s), (False, True, False, False, False))
self.ae(continuations(s), (True, False, False, False, False))
init()
s.erase_in_display(1)
self.ae(all_lines(s), ('', ' 45', '12345', '12345', '12345'))
self.ae(continuations(s), (False, False, True, True, True))
self.ae(continuations(s), (False, True, True, True, False))
init()
s.erase_in_display(2)
@ -547,16 +548,20 @@ def test_selection_as_text(self):
s.draw(str(i) * s.columns)
s.start_selection(0, 0)
s.update_selection(4, 4)
expected = ('55555', '\n66666', '\n77777', '\n88888', '\n99999')
self.ae(s.text_for_selection(), expected)
def ts(*args):
return ''.join(s.text_for_selection(*args))
expected = ''.join(('55555', '\n66666', '\n77777', '\n88888', '\n99999'))
self.ae(ts(), expected)
s.scroll(2, True)
self.ae(s.text_for_selection(), expected)
self.ae(ts(), expected)
s.reset()
s.draw('ab cd')
s.start_selection(0, 0)
s.update_selection(1, 3)
self.ae(s.text_for_selection(), ('ab ', 'cd'))
self.ae(s.text_for_selection(False, True), ('ab', 'cd'))
self.ae(ts(), ''.join(('ab ', 'cd')))
self.ae(ts(False, True), ''.join(('ab', 'cd')))
s.reset()
s.draw('ab cd')
s.start_selection(0, 0)
@ -630,6 +635,22 @@ def set_link(url=None, id=None):
s.draw('bcdef')
self.ae(as_text(s, True), '\x1b[ma\x1b]8;;moo\x1b\\bcde\x1b[mf\n\n\n\x1b]8;;\x1b\\')
def test_wrapping_serialization(self):
from kitty.window import as_text
s = self.create_screen(cols=2, lines=2, scrollback=2, options={'scrollback_pager_history_size': 128})
s.draw('aabbccddeeff')
self.ae(as_text(s, add_history=True), 'aabbccddeeff')
self.assertNotIn('\n', as_text(s, add_history=True, as_ansi=True))
s = self.create_screen(cols=2, lines=2, scrollback=2, options={'scrollback_pager_history_size': 128})
s.draw('1'), s.carriage_return(), s.linefeed()
s.draw('2'), s.carriage_return(), s.linefeed()
s.draw('3'), s.carriage_return(), s.linefeed()
s.draw('4'), s.carriage_return(), s.linefeed()
s.draw('5'), s.carriage_return(), s.linefeed()
s.draw('6'), s.carriage_return(), s.linefeed()
s.draw('7')
self.ae(as_text(s, add_history=True), '1\n2\n3\n4\n5\n6\n7')
def test_pagerhist(self):
hsz = 8
s = self.create_screen(cols=2, lines=2, scrollback=2, options={'scrollback_pager_history_size': hsz})
@ -666,17 +687,17 @@ def test():
s = self.create_screen(options={'scrollback_pager_history_size': 2048})
text = '\x1b[msoft\r\x1b[mbreak\nnext😼cat'
w(text)
self.ae(contents(), text + '\n')
self.ae(contents(), text)
s.historybuf.pagerhist_rewrap(2)
self.ae(contents(), '\x1b[mso\rft\x1b[m\rbr\rea\rk\nne\rxt\r😼\rca\rt\n')
self.ae(contents(), '\x1b[mso\rft\x1b[m\rbr\rea\rk\nne\rxt\r😼\rca\rt')
s = self.create_screen(options={'scrollback_pager_history_size': 8})
w('😼')
self.ae(contents(), '😼\n')
self.ae(contents(), '😼')
w('abcd')
self.ae(contents(), '😼abcd\n')
self.ae(contents(), '😼abcd')
w('e')
self.ae(contents(), 'abcde\n')
self.ae(contents(), 'abcde')
def test_user_marking(self):