From bc97cfa02452b3d304e88b5ee61a57e8c829fc9f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 9 Sep 2017 10:25:03 +0530 Subject: [PATCH] Use a null to represent a blank rather than a space This has performance benefits when clearing (can use a single memset). Also allows detecting trailing whitespace on lines correctly. --- kitty/data-types.h | 2 +- kitty/line-buf.c | 15 ++++++++------- kitty/line.c | 38 ++++++++++++++++++++++++++------------ kitty/rewrap.h | 2 +- kitty_tests/datatypes.py | 22 +++++++++++++--------- kitty_tests/parser.py | 20 ++++++++++---------- kitty_tests/screen.py | 40 ++++++++++++++++++++-------------------- 7 files changed, 79 insertions(+), 60 deletions(-) diff --git a/kitty/data-types.h b/kitty/data-types.h index c0a375c6f..296602678 100644 --- a/kitty/data-types.h +++ b/kitty/data-types.h @@ -38,7 +38,7 @@ typedef uint16_t sprite_index; #define SGR_PROTOCOL 2 #define URXVT_PROTOCOL 3 -#define BLANK_CHAR 32 +#define BLANK_CHAR 0 #define CHAR_MASK 0xFFFFFF #define ATTRS_SHIFT 24 #define ATTRS_MASK_WITHOUT_WIDTH 0xFC000000 diff --git a/kitty/line-buf.c b/kitty/line-buf.c index 46e45cf6f..4012e0a62 100644 --- a/kitty/line-buf.c +++ b/kitty/line-buf.c @@ -19,12 +19,13 @@ clear_chars_to(LineBuf* linebuf, index_type y, char_type ch) { clear_chars_in_line(lineptr(linebuf, y), linebuf->xnum, ch); } -void linebuf_clear(LineBuf *self, char_type ch) { +void +linebuf_clear(LineBuf *self, char_type ch) { memset(self->buf, 0, self->xnum * self->ynum * sizeof(Cell)); memset(self->continued_map, 0, self->ynum * sizeof(bool)); - for (index_type i = 0; i < self->ynum; i++) { - clear_chars_to(self, i, ch); - self->line_map[i] = i; + for (index_type i = 0; i < self->ynum; i++) self->line_map[i] = i; + if (ch != 0) { + for (index_type i = 0; i < self->ynum; i++) clear_chars_to(self, i, ch); } } @@ -69,7 +70,7 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) { self->line->xnum = xnum; for(index_type i = 0; i < ynum; i++) { self->line_map[i] = i; - clear_chars_to(self, i, BLANK_CHAR); + if (BLANK_CHAR != 0) clear_chars_to(self, i, BLANK_CHAR); } } } @@ -151,7 +152,7 @@ allocate_line_storage(Line *line, bool initialize) { if (initialize) { line->cells = PyMem_Calloc(line->xnum, sizeof(Cell)); if (line->cells == NULL) { PyErr_NoMemory(); return false; } - clear_chars_in_line(line->cells, line->xnum, BLANK_CHAR); + if (BLANK_CHAR != 0) clear_chars_in_line(line->cells, line->xnum, BLANK_CHAR); } else { line->cells = PyMem_Malloc(line->xnum * sizeof(Cell)); if (line->cells == NULL) { PyErr_NoMemory(); return false; } @@ -199,7 +200,7 @@ copy_line_to(LineBuf *self, PyObject *args) { static inline void clear_line_(Line *l, index_type xnum) { memset(l->cells, 0, xnum * sizeof(Cell)); - clear_chars_in_line(l->cells, xnum, BLANK_CHAR); + if (BLANK_CHAR != 0) clear_chars_in_line(l->cells, xnum, BLANK_CHAR); } void linebuf_clear_line(LineBuf *self, index_type y) { diff --git a/kitty/line.c b/kitty/line.c index 73c9b351a..73ac4f83c 100644 --- a/kitty/line.c +++ b/kitty/line.c @@ -62,14 +62,22 @@ static PyObject * as_unicode(Line* self) { Py_ssize_t n = 0; Py_UCS4 *buf = PyMem_Malloc(3 * self->xnum * sizeof(Py_UCS4)); - if (buf == NULL) { - PyErr_NoMemory(); - return NULL; + if (buf == NULL) return PyErr_NoMemory(); + index_type xlimit = self->xnum; + if (BLANK_CHAR == 0) { + while (xlimit != 0) { + if ((self->cells[xlimit - 1].ch & CHAR_MASK) != BLANK_CHAR) break; + xlimit--; + } } - for(index_type i = 0; i < self->xnum; i++) { - char_type attrs = self->cells[i].ch >> ATTRS_SHIFT; - if ((attrs & WIDTH_MASK) < 1) continue; - buf[n++] = self->cells[i].ch & CHAR_MASK; + char_type previous_width = 0; + for(index_type i = 0; i < xlimit; i++) { + char_type ch = self->cells[i].ch & CHAR_MASK; + if (ch == 0) { + if (previous_width == 2) { previous_width = 0; continue; }; + ch = ' '; + } + buf[n++] = ch; char_type cc = self->cells[i].cc; Py_UCS4 cc1 = cc & CC_MASK, cc2; if (cc1) { @@ -77,6 +85,7 @@ as_unicode(Line* self) { cc2 = cc >> 16; if (cc2) buf[n++] = cc2; } + previous_width = (self->cells[i].ch >> ATTRS_SHIFT) & WIDTH_MASK; } PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, buf, n); PyMem_Free(buf); @@ -136,7 +145,7 @@ line_as_ansi(Line *self, Py_UCS4 *buf, index_type buflen) { index_type limit = self->xnum, i=0; int r; - if (!self->continued) { // Trim trailing spaces + if (!self->continued) { // Trim trailing blanks for(r = self->xnum - 1; r >= 0; r--) { if ((self->cells[r].ch & CHAR_MASK) != BLANK_CHAR) break; } @@ -144,11 +153,15 @@ line_as_ansi(Line *self, Py_UCS4 *buf, index_type buflen) { } bool bold = false, italic = false, reverse = false, strike = false; uint32_t fg = 0, bg = 0, decoration_fg = 0, decoration = 0; + char_type previous_width = 0; WRITE_SGR(0); for (index_type pos=0; pos < limit; pos++) { char_type attrs = self->cells[pos].ch >> ATTRS_SHIFT, ch = self->cells[pos].ch & CHAR_MASK; - if (ch == 0 || (attrs & WIDTH_MASK) < 1) continue; + if (ch == 0) { + if (previous_width == 2) { previous_width = 0; continue; } + ch = ' '; + } CHECK_BOOL(bold, BOLD_SHIFT, 1, 22); CHECK_BOOL(italic, ITALIC_SHIFT, 3, 23); CHECK_BOOL(reverse, REVERSE_SHIFT, 7, 27); @@ -175,6 +188,7 @@ line_as_ansi(Line *self, Py_UCS4 *buf, index_type buflen) { cc1 = cc >> 16; if (cc1) { WRITE_CH(cc1); } } + previous_width = attrs & WIDTH_MASK; } return i; #undef CHECK_BOOL @@ -305,7 +319,7 @@ cursor_from(Line* self, PyObject *args) { void line_clear_text(Line *self, unsigned int at, unsigned int num, int ch) { - const char_type repl = ((char_type)ch & CHAR_MASK) | (1 << ATTRS_SHIFT); + const char_type repl = ((char_type)ch & CHAR_MASK) | ( (ch ? 1 : 0) << ATTRS_SHIFT); #define PREFIX \ for (index_type i = at; i < MIN(self->xnum, at + num); i++) { \ self->cells[i].ch = (self->cells[i].ch & ATTRS_MASK_WITHOUT_WIDTH) | repl; \ @@ -321,7 +335,7 @@ line_clear_text(Line *self, unsigned int at, unsigned int num, int ch) { static PyObject* clear_text(Line* self, PyObject *args) { -#define clear_text_doc "clear_text(at, num, ch=' ') -> Clear characters in the specified range, preserving formatting." +#define clear_text_doc "clear_text(at, num, ch=BLANK_CHAR) -> Clear characters in the specified range, preserving formatting." unsigned int at, num; int ch = BLANK_CHAR; if (!PyArg_ParseTuple(args, "II|C", &at, &num, &ch)) return NULL; @@ -369,7 +383,7 @@ void line_right_shift(Line *self, unsigned int at, unsigned int num) { // Check if a wide character was split at the right edge char_type w = (self->cells[self->xnum - 1].ch >> ATTRS_SHIFT) & WIDTH_MASK; if (w != 1) { - self->cells[self->xnum - 1].ch = (1 << ATTRS_SHIFT) | BLANK_CHAR; + self->cells[self->xnum - 1].ch = ((BLANK_CHAR ? 1 : 0) << ATTRS_SHIFT) | BLANK_CHAR; clear_sprite_position(self->cells[self->xnum - 1]); } } diff --git a/kitty/rewrap.h b/kitty/rewrap.h index fa830132a..716a25588 100644 --- a/kitty/rewrap.h +++ b/kitty/rewrap.h @@ -58,7 +58,7 @@ rewrap_inner(BufType *src, BufType *dest, const index_type src_limit, HistoryBuf src_line_is_continued = is_src_line_continued(src_y); src_x_limit = src->xnum; if (!src_line_is_continued) { - // Trim trailing white-space since there is a hard line break at the end of this line + // Trim trailing blanks since there is a hard line break at the end of this line while(src_x_limit && (src->line->cells[src_x_limit - 1].ch & CHAR_MASK) == BLANK_CHAR) src_x_limit--; } diff --git a/kitty_tests/datatypes.py b/kitty_tests/datatypes.py index 25e4bc5a1..0de9a547e 100644 --- a/kitty_tests/datatypes.py +++ b/kitty_tests/datatypes.py @@ -34,7 +34,7 @@ class TestDataTypes(BaseTest): new.copy_old(old) self.ae(new.line(0), old.line(1)) new.clear() - self.ae(str(new.line(0)), ' ' * new.xnum) + self.ae(str(new.line(0)), '') old.set_attribute(REVERSE, False) for y in range(old.ynum): for x in range(old.xnum): @@ -140,22 +140,23 @@ class TestDataTypes(BaseTest): lb = LineBuf(2, 3) for y in range(lb.ynum): line = lb.line(y) - self.ae(str(line), ' ' * lb.xnum) + self.ae(str(line), '') for x in range(lb.xnum): - self.ae(line[x], ' ') + self.ae(line[x], '\0') with self.assertRaises(IndexError): lb.line(lb.ynum) with self.assertRaises(IndexError): lb.line(0)[lb.xnum] l = lb.line(0) + l.set_text(' ', 0, len(' '), C()) l.add_combining_char(0, '1') self.ae(l[0], ' 1') l.add_combining_char(0, '2') self.ae(l[0], ' 12') l.add_combining_char(0, '3') self.ae(l[0], ' 13') - self.ae(l[1], ' ') - self.ae(str(l), ' 13 ') + self.ae(l[1], '\0') + self.ae(str(l), ' 13') t = 'Testing with simple text' lb = LineBuf(2, len(t)) l = lb.line(0) @@ -257,18 +258,21 @@ class TestDataTypes(BaseTest): def test_rewrap_wider(self): ' New buffer wider ' lb = create_lbuf('0123 ', '56789') - lb2 = self.line_comparison_rewrap(lb, '0123 5', '6789 ', ' ' * 6) + lb2 = self.line_comparison_rewrap(lb, '0123 5', '6789', '') self.assertContinued(lb2, False, True) lb = create_lbuf('12', 'abc') - lb2 = self.line_comparison_rewrap(lb, '12 ', 'abc ') + lb2 = self.line_comparison_rewrap(lb, '12', 'abc') self.assertContinued(lb2, False, False) def test_rewrap_narrower(self): ' New buffer narrower ' - lb = create_lbuf('123 ', 'abcde') - lb2 = self.line_comparison_rewrap(lb, '123', 'abc', 'de ') + lb = create_lbuf('123', 'abcde') + lb2 = self.line_comparison_rewrap(lb, '123', 'abc', 'de') self.assertContinued(lb2, False, False, True) + lb = create_lbuf('123 ', 'abcde') + lb2 = self.line_comparison_rewrap(lb, '123', ' a', 'bcd', 'e') + self.assertContinued(lb2, False, True, True, True) @skipIf('ANCIENT_WCWIDTH' in os.environ, 'wcwidth() is too old') def test_utils(self): diff --git a/kitty_tests/parser.py b/kitty_tests/parser.py index a627f8eee..585d3385a 100644 --- a/kitty_tests/parser.py +++ b/kitty_tests/parser.py @@ -45,15 +45,15 @@ class TestParser(BaseTest): pb = partial(self.parse_bytes_dump, s) pb('12', '12') - self.ae(str(s.line(0)), '12 ') + self.ae(str(s.line(0)), '12') self.ae(s.cursor.x, 2) pb('3456', '3456') self.ae(str(s.line(0)), '12345') - self.ae(str(s.line(1)), '6 ') + self.ae(str(s.line(1)), '6') pb(b'\n123\n\r45', ('screen_linefeed',), '123', ('screen_linefeed',), ('screen_carriage_return',), '45') - self.ae(str(s.line(1)), '6 ') - self.ae(str(s.line(2)), ' 123 ') - self.ae(str(s.line(3)), '45 ') + self.ae(str(s.line(1)), '6') + self.ae(str(s.line(2)), ' 123') + self.ae(str(s.line(3)), '45') parse_bytes(s, b'\rabcde') self.ae(str(s.line(3)), 'abcde') pb('\rßxyz1', ('screen_carriage_return',), 'ßxyz1') @@ -65,11 +65,11 @@ class TestParser(BaseTest): s = self.create_screen() pb = partial(self.parse_bytes_dump, s) pb('12\033Da', '12', ('screen_index',), 'a') - self.ae(str(s.line(0)), '12 ') - self.ae(str(s.line(1)), ' a ') + self.ae(str(s.line(0)), '12') + self.ae(str(s.line(1)), ' a') pb('\033x', ('Unknown char after ESC: 0x%x' % ord('x'),)) pb('\033c123', ('screen_reset', ), '123') - self.ae(str(s.line(0)), '123 ') + self.ae(str(s.line(0)), '123') def test_charsets(self): s = self.create_screen() @@ -79,14 +79,14 @@ class TestParser(BaseTest): s = self.create_screen() pb = partial(self.parse_bytes_dump, s) pb('\033)0\x0e/_', ('screen_designate_charset', 1, ord('0')), ('screen_change_charset', 1), '/_') - self.ae(str(s.line(0)), '/\xa0 ') + self.ae(str(s.line(0)), '/\xa0') self.assertTrue(s.callbacks.iutf8) pb('\033%@_', ('screen_use_latin1', 1), '_') self.assertFalse(s.callbacks.iutf8) s = self.create_screen() pb = partial(self.parse_bytes_dump, s) pb('\033(0/_', ('screen_designate_charset', 0, ord('0')), '/_') - self.ae(str(s.line(0)), '/\xa0 ') + self.ae(str(s.line(0)), '/\xa0') def test_csi_codes(self): s = self.create_screen() diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py index 8ded76a81..c657c4f40 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -23,7 +23,7 @@ class TestScreen(BaseTest): self.assertTrue(s.linebuf.is_continued(2)) self.ae(str(s.line(0)), 'a' * 5) self.ae(str(s.line(1)), 'b' * 5) - self.ae(str(s.line(2)), 'b' * 2 + ' ' * 3) + self.ae(str(s.line(2)), 'b' * 2) self.ae(s.cursor.x, 2), self.ae(s.cursor.y, 2) s.draw('c' * 15) self.ae(str(s.line(0)), 'b' * 5) @@ -60,17 +60,17 @@ class TestScreen(BaseTest): self.ae(s.cursor.x, 5), self.ae(s.cursor.y, 0) s.draw('ニチハ') self.ae(str(s.line(0)), 'ココx') - self.ae(str(s.line(1)), 'ニチ ') - self.ae(str(s.line(2)), 'ハ ') + self.ae(str(s.line(1)), 'ニチ') + self.ae(str(s.line(2)), 'ハ') self.ae(s.cursor.x, 2), self.ae(s.cursor.y, 2) s.draw('Ƶ̧\u0308') - self.ae(str(s.line(2)), 'ハƵ̧\u0308 ') + self.ae(str(s.line(2)), 'ハƵ̧\u0308') self.ae(s.cursor.x, 3), self.ae(s.cursor.y, 2) s.draw('xy'), s.draw('\u0306') self.ae(str(s.line(2)), 'ハƵ̧\u0308xy\u0306') self.ae(s.cursor.x, 5), self.ae(s.cursor.y, 2) s.draw('c' * 15) - self.ae(str(s.line(0)), 'ニチ ') + self.ae(str(s.line(0)), 'ニチ') # Now test without line-wrap s.reset(), s.reset_dirty() @@ -111,12 +111,12 @@ class TestScreen(BaseTest): self.assertTrue(s.line(0).cursor_from(1).bold) s.cursor_back(1) s.insert_characters(20) - self.ae(str(s.line(0)), ' ') + self.ae(str(s.line(0)), '') s.draw('xココ') s.cursor_back(5) s.reset_dirty() s.insert_characters(1) - self.ae(str(s.line(0)), ' xコ ') + self.ae(str(s.line(0)), ' xコ') c = Cursor() c.italic = True s.line(0).apply_cursor(c, 0, 5) @@ -126,7 +126,7 @@ class TestScreen(BaseTest): init() s.delete_characters(2) - self.ae(str(s.line(0)), 'ade ') + self.ae(str(s.line(0)), 'ade') self.assertTrue(s.line(0).cursor_from(4).bold) self.assertFalse(s.line(0).cursor_from(2).bold) @@ -136,11 +136,11 @@ class TestScreen(BaseTest): self.assertTrue(s.line(0).cursor_from(1).bold) self.assertFalse(s.line(0).cursor_from(4).bold) s.erase_characters(20) - self.ae(str(s.line(0)), 'a ') + self.ae(str(s.line(0)), 'a') init() s.erase_in_line() - self.ae(str(s.line(0)), 'a ') + self.ae(str(s.line(0)), 'a') self.assertTrue(s.line(0).cursor_from(1).bold) self.assertFalse(s.line(0).cursor_from(0).bold) init() @@ -148,7 +148,7 @@ class TestScreen(BaseTest): self.ae(str(s.line(0)), ' cde') init() s.erase_in_line(2) - self.ae(str(s.line(0)), ' ') + self.ae(str(s.line(0)), '') init() s.erase_in_line(2, True) self.ae((False, False, False, False, False), tuple(map(lambda i: s.line(0).cursor_from(i).bold, range(5)))) @@ -168,19 +168,19 @@ class TestScreen(BaseTest): init() s.erase_in_display() - self.ae(all_lines(s), ('12345', '12 ', ' ', ' ', ' ')) + self.ae(all_lines(s), ('12345', '12', '', '', '')) init() s.erase_in_display(1) - self.ae(all_lines(s), (' ', ' 45', '12345', '12345', '12345')) + self.ae(all_lines(s), ('', ' 45', '12345', '12345', '12345')) init() s.erase_in_display(2) - self.ae(all_lines(s), (' ', ' ', ' ', ' ', ' ')) + self.ae(all_lines(s), ('', '', '', '', '')) self.assertTrue(s.line(0).cursor_from(1).bold) init() s.erase_in_display(2, True) - self.ae(all_lines(s), (' ', ' ', ' ', ' ', ' ')) + self.ae(all_lines(s), ('', '', '', '', '')) self.assertFalse(s.line(0).cursor_from(1).bold) def test_cursor_movement(self): @@ -203,13 +203,13 @@ class TestScreen(BaseTest): s = self.create_screen() s.draw('12345' * 5) s.index() - self.ae(str(s.line(4)), ' ' * 5) + self.ae(str(s.line(4)), '') for i in range(4): self.ae(str(s.line(i)), '12345') s.draw('12345' * 5) s.cursor_up(5) s.reverse_index() - self.ae(str(s.line(0)), ' ' * 5) + self.ae(str(s.line(0)), '') for i in range(1, 5): self.ae(str(s.line(i)), '12345') @@ -219,7 +219,7 @@ class TestScreen(BaseTest): s.resize(3, 10) self.ae(str(s.line(0)), '0'*5 + '1'*5) self.ae(str(s.line(1)), '2'*5 + '3'*5) - self.ae(str(s.line(2)), '4'*5 + ' '*5) + self.ae(str(s.line(2)), '4'*5) s.resize(5, 1) self.ae(str(s.line(0)), '4') hb = s.historybuf @@ -229,8 +229,8 @@ class TestScreen(BaseTest): s.draw(''.join([str(i) * s.columns for i in range(s.lines*2)])) self.ae(str(s.line(4)), '9'*5) s.resize(5, 2) - self.ae(str(s.line(3)), '9 ') - self.ae(str(s.line(4)), ' ') + self.ae(str(s.line(3)), '9') + self.ae(str(s.line(4)), '') def test_tab_stops(self): # Taken from vttest/main.c