1
1
mirror of https://github.com/wez/wezterm.git synced 2025-01-08 23:17:36 +03:00

term: default to cluster storage

The previous commit added the option to convert the storage to
the cluster format.  That saves memory as rows are moved to scrollback,
but makes scrolling back more expensive due to that conversion.

This commit adds a fast(ish) path for the common case of simply
appending text to a new line before it gets scrolled: the default
format for lines in the screen is now the cluster format and,
provided that the cursor moves from left to right as text is
emitted, can simply append to the cluster storage in-place
and avoids a conversion when the line is moved to scrollback.

This improves the throughput of `time cat enwiki8.wiki` so
that the runtime is typically around 11-12s (compared to 9-10s
before introducing cluster storage).  However, this is often
a deal of variance in the measured time and I believe that
that is due to the renderer triggering conversions back to
the vec storage and introducing slowdowns from the render side.

That's what I'll investigate next.
This commit is contained in:
Wez Furlong 2022-07-23 22:54:43 -07:00
parent 751dd460da
commit 8002a17242
7 changed files with 793 additions and 325 deletions

View File

@ -69,7 +69,7 @@ impl Screen {
let mut lines =
VecDeque::with_capacity(physical_rows + scrollback_size(config, allow_scrollback));
for _ in 0..physical_rows {
let mut line = Line::with_width(physical_cols, seqno);
let mut line = Line::new(seqno);
bidi_mode.apply_to_line(&mut line, seqno);
lines.push_back(line);
}
@ -237,8 +237,7 @@ impl Screen {
// pad us back out to the viewport size
while self.lines.len() < physical_rows {
// FIXME: borrow bidi mode from line
self.lines
.push_back(Line::with_width(self.physical_cols, seqno));
self.lines.push_back(Line::new(seqno));
}
let new_cursor_y;
@ -282,8 +281,7 @@ impl Screen {
let actual_num_rows_after_cursor = self.lines.len().saturating_sub(cursor_y);
for _ in actual_num_rows_after_cursor..required_num_rows_after_cursor {
// FIXME: borrow bidi mode from line
self.lines
.push_back(Line::with_width(self.physical_cols, seqno));
self.lines.push_back(Line::new(seqno));
}
} else {
// Compute the new cursor location; this is logically the inverse
@ -380,18 +378,12 @@ impl Screen {
/// Set a cell. the x and y coordinates are relative to the visible screeen
/// origin. 0,0 is the top left.
pub fn set_cell(
&mut self,
x: usize,
y: VisibleRowIndex,
cell: &Cell,
seqno: SequenceNo,
) -> &Cell {
pub fn set_cell(&mut self, x: usize, y: VisibleRowIndex, cell: &Cell, seqno: SequenceNo) {
let line_idx = self.phys_row(y);
//debug!("set_cell x={} y={} phys={} {:?}", x, y, line_idx, cell);
let line = self.line_mut(line_idx);
line.set_cell(x, cell.clone(), seqno)
line.set_cell(x, cell.clone(), seqno);
}
pub fn cell_mut(&mut self, x: usize, y: VisibleRowIndex) -> Option<&mut Cell> {
@ -657,15 +649,21 @@ impl Screen {
phys_scroll.start
};
let default_blank = CellAttributes::blank();
// To avoid thrashing the heap, prefer to move lines that were
// scrolled off the top and re-use them at the bottom.
let to_move = lines_removed.min(num_rows);
let (to_remove, to_add) = {
for _ in 0..to_move {
let mut line = self.lines.remove(remove_idx).unwrap();
// Make the line like a new one of the appropriate width
line.resize_and_clear(self.physical_cols, seqno, blank_attr.clone());
line.update_last_change_seqno(seqno);
let line = if default_blank == blank_attr {
Line::new(seqno)
} else {
// Make the line like a new one of the appropriate width
line.resize_and_clear(self.physical_cols, seqno, blank_attr.clone());
line.update_last_change_seqno(seqno);
line
};
if scroll_region.end as usize == self.physical_rows {
self.lines.push_back(line);
} else {
@ -689,11 +687,15 @@ impl Screen {
// It's cheaper to push() than it is insert() at the end
let push = scroll_region.end as usize == self.physical_rows;
for _ in 0..to_add {
let mut line = Line::with_width_and_cell(
self.physical_cols,
Cell::blank_with_attrs(blank_attr.clone()),
seqno,
);
let mut line = if default_blank == blank_attr {
Line::new(seqno)
} else {
Line::with_width_and_cell(
self.physical_cols,
Cell::blank_with_attrs(blank_attr.clone()),
seqno,
)
};
bidi_mode.apply_to_line(&mut line, seqno);
if push {
self.lines.push_back(line);
@ -747,12 +749,18 @@ impl Screen {
self.lines.remove(middle);
}
let default_blank = CellAttributes::blank();
for _ in 0..num_rows {
let mut line = Line::with_width_and_cell(
self.physical_cols,
Cell::blank_with_attrs(blank_attr.clone()),
seqno,
);
let mut line = if blank_attr == default_blank {
Line::new(seqno)
} else {
Line::with_width_and_cell(
self.physical_cols,
Cell::blank_with_attrs(blank_attr.clone()),
seqno,
)
};
bidi_mode.apply_to_line(&mut line, seqno);
self.lines.insert(phys_scroll.start, line);
}

View File

@ -136,12 +136,10 @@ impl<'a> Performer<'a> {
// so that we can correctly reflow it if the window is
// resized.
{
let x = self.cursor.x;
let y = self.cursor.y;
let screen = self.screen_mut();
if let Some(cell) = screen.cell_mut(x, y) {
cell.attrs_mut().set_wrapped(true);
}
let y = screen.phys_row(y);
screen.line_mut(y).set_last_cell_was_wrapped(true, seqno);
}
self.new_line(true);
}

View File

@ -7,12 +7,12 @@ fn test_ind() {
let mut term = TestTerm::new(4, 4, 0);
term.print("a\r\nb\x1bD");
term.assert_cursor_pos(1, 2, None, None);
assert_visible_contents(&term, file!(), line!(), &["a ", "b ", " ", " "]);
assert_visible_contents(&term, file!(), line!(), &["a", "b", "", ""]);
term.print("\x1bD");
term.assert_cursor_pos(1, 3, None, None);
term.print("\x1bD");
term.assert_cursor_pos(1, 3, None, Some(term.current_seqno() - 1));
assert_visible_contents(&term, file!(), line!(), &["b ", " ", " ", " "]);
assert_visible_contents(&term, file!(), line!(), &["b", "", "", ""]);
}
#[test]
@ -24,7 +24,7 @@ fn test_nel() {
term.assert_cursor_pos(0, 3, None, None);
term.print("\x1bE");
term.assert_cursor_pos(0, 3, None, None);
assert_visible_contents(&term, file!(), line!(), &["b ", " ", " ", " "]);
assert_visible_contents(&term, file!(), line!(), &["b", "", "", ""]);
}
#[test]
@ -63,7 +63,7 @@ fn test_hts() {
fn test_ri() {
let mut term = TestTerm::new(4, 2, 0);
term.print("a\r\nb\r\nc\r\nd.");
assert_visible_contents(&term, file!(), line!(), &["a ", "b ", "c ", "d."]);
assert_visible_contents(&term, file!(), line!(), &["a", "b", "c", "d."]);
term.assert_cursor_pos(1, 3, None, None);
term.print("\x1bM");
term.assert_cursor_pos(1, 2, None, None);
@ -74,5 +74,5 @@ fn test_ri() {
let seqno = term.current_seqno();
term.print("\x1bM");
term.assert_cursor_pos(1, 0, None, Some(seqno));
assert_visible_contents(&term, file!(), line!(), &[" ", "a ", "b ", "c "]);
assert_visible_contents(&term, file!(), line!(), &["", "a", "b", "c"]);
}

View File

@ -1,5 +1,4 @@
use super::*;
use termwiz::color::AnsiColor;
/// In this issue, the `CSI 2 P` sequence incorrectly removed two
/// cells from the line, leaving them effectively blank, when those
@ -10,21 +9,199 @@ fn test_789() {
let mut term = TestTerm::new(1, 8, 0);
term.print("\x1b[40m\x1b[Kfoo\x1b[2P");
let black = CellAttributes::default()
.set_background(AnsiColor::Black)
.clone();
let mut line = Line::from_text("foo", &black, SEQ_ZERO, None);
line.resize(8, 0);
for x in 3..8 {
line.set_cell(x, Cell::blank_with_attrs(black.clone()), 0);
}
assert_lines_equal(
file!(),
line!(),
&term.screen().visible_lines(),
&[line],
Compare::TEXT | Compare::ATTRS,
k9::snapshot!(
term.screen().visible_lines(),
r#"
[
Line {
cells: V(
VecStorage {
cells: [
Cell {
text: "f",
width: 1,
attrs: CellAttributes {
attributes: 0,
intensity: Normal,
underline: None,
blink: None,
italic: false,
reverse: false,
strikethrough: false,
invisible: false,
wrapped: false,
overline: false,
semantic_type: Output,
foreground: Default,
background: PaletteIndex(
0,
),
fat: None,
},
},
Cell {
text: "o",
width: 1,
attrs: CellAttributes {
attributes: 0,
intensity: Normal,
underline: None,
blink: None,
italic: false,
reverse: false,
strikethrough: false,
invisible: false,
wrapped: false,
overline: false,
semantic_type: Output,
foreground: Default,
background: PaletteIndex(
0,
),
fat: None,
},
},
Cell {
text: "o",
width: 1,
attrs: CellAttributes {
attributes: 0,
intensity: Normal,
underline: None,
blink: None,
italic: false,
reverse: false,
strikethrough: false,
invisible: false,
wrapped: false,
overline: false,
semantic_type: Output,
foreground: Default,
background: PaletteIndex(
0,
),
fat: None,
},
},
Cell {
text: " ",
width: 1,
attrs: CellAttributes {
attributes: 0,
intensity: Normal,
underline: None,
blink: None,
italic: false,
reverse: false,
strikethrough: false,
invisible: false,
wrapped: false,
overline: false,
semantic_type: Output,
foreground: Default,
background: PaletteIndex(
0,
),
fat: None,
},
},
Cell {
text: " ",
width: 1,
attrs: CellAttributes {
attributes: 0,
intensity: Normal,
underline: None,
blink: None,
italic: false,
reverse: false,
strikethrough: false,
invisible: false,
wrapped: false,
overline: false,
semantic_type: Output,
foreground: Default,
background: PaletteIndex(
0,
),
fat: None,
},
},
Cell {
text: " ",
width: 1,
attrs: CellAttributes {
attributes: 0,
intensity: Normal,
underline: None,
blink: None,
italic: false,
reverse: false,
strikethrough: false,
invisible: false,
wrapped: false,
overline: false,
semantic_type: Output,
foreground: Default,
background: PaletteIndex(
0,
),
fat: None,
},
},
Cell {
text: " ",
width: 1,
attrs: CellAttributes {
attributes: 0,
intensity: Normal,
underline: None,
blink: None,
italic: false,
reverse: false,
strikethrough: false,
invisible: false,
wrapped: false,
overline: false,
semantic_type: Output,
foreground: Default,
background: PaletteIndex(
0,
),
fat: None,
},
},
Cell {
text: " ",
width: 1,
attrs: CellAttributes {
attributes: 0,
intensity: Normal,
underline: None,
blink: None,
italic: false,
reverse: false,
strikethrough: false,
invisible: false,
wrapped: false,
overline: false,
semantic_type: Output,
foreground: Default,
background: PaletteIndex(
0,
),
fat: None,
},
},
],
},
),
zones: [],
seqno: 5,
bits: NONE,
},
]
"#
);
}
@ -53,7 +230,7 @@ fn test_rep() {
term.print("h");
term.cup(1, 0);
term.print("\x1b[2ba");
assert_visible_contents(&term, file!(), line!(), &["hhha", " ", " "]);
assert_visible_contents(&term, file!(), line!(), &["hhha", "", ""]);
}
#[test]
@ -62,12 +239,7 @@ fn test_irm() {
term.print("foo");
term.cup(0, 0);
term.print("\x1b[4hBAR");
assert_visible_contents(
&term,
file!(),
line!(),
&["BARfoo ", " ", " "],
);
assert_visible_contents(&term, file!(), line!(), &["BARfoo", "", ""]);
}
#[test]
@ -76,12 +248,12 @@ fn test_ich() {
term.print("hey!wat?");
term.cup(1, 0);
term.print("\x1b[2@");
assert_visible_contents(&term, file!(), line!(), &["h e", "wat?", " "]);
assert_visible_contents(&term, file!(), line!(), &["h e", "wat?", ""]);
// check how we handle overflowing the width
term.print("\x1b[12@");
assert_visible_contents(&term, file!(), line!(), &["h ", "wat?", " "]);
assert_visible_contents(&term, file!(), line!(), &["h ", "wat?", ""]);
term.print("\x1b[-12@");
assert_visible_contents(&term, file!(), line!(), &["h ", "wat?", " "]);
assert_visible_contents(&term, file!(), line!(), &["h ", "wat?", ""]);
}
#[test]
@ -90,12 +262,12 @@ fn test_ech() {
term.print("hey!wat?");
term.cup(1, 0);
term.print("\x1b[2X");
assert_visible_contents(&term, file!(), line!(), &["h !", "wat?", " "]);
assert_visible_contents(&term, file!(), line!(), &["h !", "wat?", ""]);
// check how we handle overflowing the width
term.print("\x1b[12X");
assert_visible_contents(&term, file!(), line!(), &["h ", "wat?", " "]);
assert_visible_contents(&term, file!(), line!(), &["h ", "wat?", ""]);
term.print("\x1b[-12X");
assert_visible_contents(&term, file!(), line!(), &["h ", "wat?", " "]);
assert_visible_contents(&term, file!(), line!(), &["h ", "wat?", ""]);
}
#[test]
@ -104,14 +276,14 @@ fn test_dch() {
term.print("hello world");
term.cup(1, 0);
term.print("\x1b[P");
assert_visible_contents(&term, file!(), line!(), &["hllo world "]);
assert_visible_contents(&term, file!(), line!(), &["hllo world"]);
term.cup(4, 0);
term.print("\x1b[2P");
assert_visible_contents(&term, file!(), line!(), &["hlloorld "]);
assert_visible_contents(&term, file!(), line!(), &["hlloorld"]);
term.print("\x1b[-2P");
assert_visible_contents(&term, file!(), line!(), &["hlloorld "]);
assert_visible_contents(&term, file!(), line!(), &["hlloorld"]);
}
#[test]
@ -151,11 +323,11 @@ fn test_dl() {
term.cup(0, 1);
let seqno = term.current_seqno();
term.delete_lines(1);
assert_visible_contents(&term, file!(), line!(), &["a", "c", " "]);
assert_visible_contents(&term, file!(), line!(), &["a", "c", ""]);
term.assert_cursor_pos(0, 1, None, Some(seqno));
term.cup(0, 0);
term.delete_lines(2);
assert_visible_contents(&term, file!(), line!(), &[" ", " ", " "]);
assert_visible_contents(&term, file!(), line!(), &["", "", ""]);
term.print("1\r\n2\r\n3");
term.cup(0, 1);
term.delete_lines(-2);
@ -218,7 +390,7 @@ fn test_ed() {
fn test_ed_erase_scrollback() {
let mut term = TestTerm::new(3, 3, 3);
term.print("abc\r\ndef\r\nghi\r\n111\r\n222\r\na\x1b[3J");
assert_all_contents(&term, file!(), line!(), &["111", "222", "a "]);
assert_all_contents(&term, file!(), line!(), &["111", "222", "a"]);
term.print("b");
assert_all_contents(&term, file!(), line!(), &["111", "222", "ab "]);
assert_all_contents(&term, file!(), line!(), &["111", "222", "ab"]);
}

View File

@ -322,18 +322,7 @@ fn test_semantic_1539() {
)
));
assert_visible_contents(
&term,
file!(),
line!(),
&[
"prompt ",
"woot ",
" ",
" ",
" ",
],
);
assert_visible_contents(&term, file!(), line!(), &["prompt", "woot", "", "", ""]);
k9::snapshot!(
term.get_semantic_zones().unwrap(),
@ -349,8 +338,8 @@ fn test_semantic_1539() {
SemanticZone {
start_y: 1,
start_x: 0,
end_y: 4,
end_x: 9,
end_y: 1,
end_x: 3,
semantic_type: Output,
},
]
@ -369,18 +358,7 @@ fn test_semantic() {
));
term.print("there");
assert_visible_contents(
&term,
file!(),
line!(),
&[
"hello ",
"there ",
" ",
" ",
" ",
],
);
assert_visible_contents(&term, file!(), line!(), &["hello", "there", "", "", ""]);
term.cup(0, 2);
term.print(format!(
@ -392,13 +370,7 @@ fn test_semantic() {
&term,
file!(),
line!(),
&[
"hello ",
"there ",
"three ",
" ",
" ",
],
&["hello", "there", "three", "", ""],
);
k9::snapshot!(
@ -408,8 +380,8 @@ fn test_semantic() {
SemanticZone {
start_y: 0,
start_x: 0,
end_y: 4,
end_x: 9,
end_y: 2,
end_x: 4,
semantic_type: Output,
},
]
@ -445,7 +417,7 @@ fn test_semantic() {
let mut input = CellAttributes::default();
input.set_semantic_type(SemanticType::Input);
let mut prompt_line = Line::from_text("> ls -l ", &output, SEQ_ZERO, None);
let mut prompt_line = Line::from_text("> ls -l", &output, SEQ_ZERO, None);
for i in 0..2 {
prompt_line.cells_mut()[i]
.attrs_mut()
@ -498,11 +470,11 @@ fn test_semantic() {
line!(),
&term.screen().visible_lines(),
&[
Line::from_text("hello ", &output, SEQ_ZERO, None),
Line::from_text("there ", &output, SEQ_ZERO, None),
Line::from_text("three ", &output, SEQ_ZERO, None),
Line::from_text("hello", &output, SEQ_ZERO, None),
Line::from_text("there", &output, SEQ_ZERO, None),
Line::from_text("three", &output, SEQ_ZERO, None),
prompt_line,
Line::from_text("some file ", &output, SEQ_ZERO, None),
Line::from_text("some file", &output, SEQ_ZERO, None),
],
Compare::TEXT | Compare::ATTRS,
);
@ -518,7 +490,7 @@ fn issue_1161() {
line!(),
&[
// U+3000 is ideographic space, a double-width space
"x\u{3000}x ",
"x\u{3000}x",
],
);
}
@ -531,18 +503,7 @@ fn basic_output() {
term.set_auto_wrap(false);
term.print("hello, world!");
assert_visible_contents(
&term,
file!(),
line!(),
&[
" ",
" hello, w!",
" ",
" ",
" ",
],
);
assert_visible_contents(&term, file!(), line!(), &["", " hello, w!", "", "", ""]);
term.set_auto_wrap(true);
term.erase_in_display(EraseInDisplay::EraseToStartOfDisplay);
@ -552,13 +513,7 @@ fn basic_output() {
&term,
file!(),
line!(),
&[
" ",
" hello, wo",
"rld! ",
" ",
" ",
],
&[" ", " hello, wo", "rld!", "", ""],
);
term.erase_in_display(EraseInDisplay::EraseToStartOfDisplay);
@ -566,13 +521,7 @@ fn basic_output() {
&term,
file!(),
line!(),
&[
" ",
" ",
" ",
" ",
" ",
],
&[" ", " ", " ", "", ""],
);
term.cup(0, 2);
@ -583,7 +532,7 @@ fn basic_output() {
&term,
file!(),
line!(),
&[" ", " ", "wo", " ", " "],
&[" ", " ", "wo", "", ""],
);
term.erase_in_line(EraseInLine::EraseToStartOfLine);
@ -591,13 +540,7 @@ fn basic_output() {
&term,
file!(),
line!(),
&[
" ",
" ",
" ",
" ",
" ",
],
&[" ", " ", " ", "", ""],
);
}
@ -609,7 +552,7 @@ fn cursor_movement_damage() {
let seqno = term.current_seqno();
term.print("fooo.");
assert_visible_contents(&term, file!(), line!(), &["foo", "o. "]);
assert_visible_contents(&term, file!(), line!(), &["foo", "o."]);
term.assert_cursor_pos(2, 1, None, None);
term.assert_dirty_lines(seqno, &[0, 1], None);
@ -656,7 +599,7 @@ fn scroll_up_within_left_and_right_margins() {
&term,
file!(),
line!(),
&["111 ", "222 ", "333 ", "44444", "555 "],
&["111", "222", "333", "44444", "555"],
);
term.set_mode("?69", true); // allow left/right margins to be set
@ -669,11 +612,11 @@ fn scroll_up_within_left_and_right_margins() {
file!(),
line!(),
&[
"111 ",
"222 ",
"111",
"222",
&format!("3{}", "4".repeat(NUM_COLS + 1)),
&format!("4{}", "5".repeat(NUM_COLS - 1)),
&format!("5{} ", " ".repeat(NUM_COLS - 1)),
&format!("5{}", " ".repeat(NUM_COLS - 1)),
],
);
}
@ -702,7 +645,7 @@ fn scroll_down_within_left_and_right_margins() {
&term,
file!(),
line!(),
&["111 ", "222 ", "333 ", "44444", "555 "],
&["111", "222", "333", "44444", "555"],
);
term.set_mode("?69", true); // allow left/right margins to be set
@ -719,9 +662,9 @@ fn scroll_down_within_left_and_right_margins() {
file!(),
line!(),
&[
"111 ",
"222 ",
&format!("3{} ", " ".repeat(NUM_COLS - 1)),
"111",
"222",
&format!("3{}", " ".repeat(NUM_COLS - 1)),
&format!("4{}", "3".repeat(NUM_COLS - 1)),
&format!("5{}", "4".repeat(NUM_COLS + 1)),
],
@ -750,12 +693,7 @@ fn test_delete_lines() {
let seqno = term.current_seqno();
term.assert_dirty_lines(seqno, &[], None);
term.delete_lines(2);
assert_visible_contents(
&term,
file!(),
line!(),
&["111", "444", "555", " ", " "],
);
assert_visible_contents(&term, file!(), line!(), &["111", "444", "555", "", ""]);
term.assert_dirty_lines(seqno, &[1, 2, 3, 4], None);
term.cup(0, 3);
@ -776,12 +714,7 @@ fn test_delete_lines() {
print_all_lines(&term);
term.delete_lines(2);
assert_visible_contents(
&term,
file!(),
line!(),
&["111", "aaa", " ", " ", "bbb"],
);
assert_visible_contents(&term, file!(), line!(), &["111", "aaa", "", "", "bbb"]);
term.assert_dirty_lines(seqno, &[1, 2, 3], None);
// expand the scroll region to fill the screen
@ -791,12 +724,7 @@ fn test_delete_lines() {
print_all_lines(&term);
term.delete_lines(1);
assert_visible_contents(
&term,
file!(),
line!(),
&["aaa", " ", " ", "bbb", " "],
);
assert_visible_contents(&term, file!(), line!(), &["aaa", "", "", "bbb", ""]);
term.assert_dirty_lines(seqno, &[4], None);
}
@ -810,10 +738,7 @@ fn test_dec_special_graphics() {
&term,
file!(),
line!(),
&[
"ABC▒␉␌␍␊°±␤␋┘┐┌└┼⎺⎻─⎼⎽├┤┴┬│≤≥DEF ",
"hello ",
],
&["ABC▒␉␌␍␊°±␤␋┘┐┌└┼⎺⎻─⎼⎽├┤┴┬│≤≥DEF", "hello"],
);
term = TestTerm::new(2, 50, 0);
@ -822,10 +747,7 @@ fn test_dec_special_graphics() {
&term,
file!(),
line!(),
&[
"SO-ABC▒␉␌␍␊°±␤␋┘┐┌└┼⎺⎻─⎼⎽├┤┴┬│≤≥DEF ",
"SI-hello ",
],
&["SO-ABC▒␉␌␍␊°±␤␋┘┐┌└┼⎺⎻─⎼⎽├┤┴┬│≤≥DEF", "SI-hello"],
);
}
@ -839,12 +761,7 @@ fn test_dec_double_width() {
&term,
file!(),
line!(),
&[
"line1 ",
"line2 ",
"line3 ",
"line4 ",
],
&["line1", "line2", "line3", "line4"],
);
let lines = term.screen().visible_lines();
@ -865,9 +782,7 @@ fn test_resize_wrap() {
&term,
file!(),
line!(),
&[
"111 ", "2222", "aa ", "333 ", " ", " ", " ", " ",
],
&["111", "2222", "aa", "333", "", "", "", ""],
);
term.resize(TerminalSize {
rows: LINES,
@ -880,7 +795,7 @@ fn test_resize_wrap() {
&term,
file!(),
line!(),
&["111 ", "2222a", "a", "333 ", " ", " ", " ", " "],
&["111", "2222a", "a", "333", "", "", "", ""],
);
term.resize(TerminalSize {
rows: LINES,
@ -893,9 +808,7 @@ fn test_resize_wrap() {
&term,
file!(),
line!(),
&[
"111 ", "2222aa", "333 ", " ", " ", " ", " ", " ",
],
&["111", "2222aa", "333", "", "", "", "", ""],
);
term.resize(TerminalSize {
rows: LINES,
@ -908,9 +821,7 @@ fn test_resize_wrap() {
&term,
file!(),
line!(),
&[
"111 ", "2222aa", "333 ", " ", " ", " ", " ", " ",
],
&["111", "2222aa", "333", "", "", "", "", ""],
);
term.resize(TerminalSize {
rows: LINES,
@ -921,9 +832,7 @@ fn test_resize_wrap() {
&term,
file!(),
line!(),
&[
"111 ", "2222aa", "333 ", " ", " ", " ", " ", " ",
],
&["111", "2222aa", "333", "", "", "", "", ""],
);
// Resize smaller again
@ -936,9 +845,7 @@ fn test_resize_wrap() {
&term,
file!(),
line!(),
&[
"111 ", "2222aa", "333 ", " ", " ", " ", " ", " ",
],
&["111", "2222aa", "333", "", "", "", "", ""],
);
term.resize(TerminalSize {
rows: LINES,
@ -949,9 +856,7 @@ fn test_resize_wrap() {
&term,
file!(),
line!(),
&[
"111 ", "2222aa", "333 ", " ", " ", " ", " ", " ",
],
&["111", "2222aa", "333", "", "", "", "", ""],
);
term.resize(TerminalSize {
rows: LINES,
@ -962,9 +867,7 @@ fn test_resize_wrap() {
&term,
file!(),
line!(),
&[
"111 ", "2222a", "a", "333 ", " ", " ", " ", " ",
],
&["111", "2222a", "a", "333", "", "", "", ""],
);
term.resize(TerminalSize {
rows: LINES,
@ -975,9 +878,7 @@ fn test_resize_wrap() {
&term,
file!(),
line!(),
&[
"111 ", "2222", "aa", "333 ", " ", " ", " ", " ",
],
&["111", "2222", "aa", "333", "", "", "", ""],
);
}
@ -986,13 +887,13 @@ fn test_resize_wrap_issue_971() {
const LINES: usize = 4;
let mut term = TestTerm::new(LINES, 4, 0);
term.print("====\r\nSS\r\n");
assert_visible_contents(&term, file!(), line!(), &["====", "SS ", " ", " "]);
assert_visible_contents(&term, file!(), line!(), &["====", "SS", "", ""]);
term.resize(TerminalSize {
rows: LINES,
cols: 6,
..Default::default()
});
assert_visible_contents(&term, file!(), line!(), &["====", "SS ", " ", " "]);
assert_visible_contents(&term, file!(), line!(), &["====", "SS", "", ""]);
}
#[test]
@ -1000,13 +901,13 @@ fn test_resize_wrap_sgc_issue_978() {
const LINES: usize = 4;
let mut term = TestTerm::new(LINES, 4, 0);
term.print("\u{1b}(0qqqq\u{1b}(B\r\nSS\r\n");
assert_visible_contents(&term, file!(), line!(), &["────", "SS ", " ", " "]);
assert_visible_contents(&term, file!(), line!(), &["────", "SS", "", ""]);
term.resize(TerminalSize {
rows: LINES,
cols: 6,
..Default::default()
});
assert_visible_contents(&term, file!(), line!(), &["────", "SS ", " ", " "]);
assert_visible_contents(&term, file!(), line!(), &["────", "SS", "", ""]);
}
#[test]
@ -1014,13 +915,13 @@ fn test_resize_wrap_dectcm_issue_978() {
const LINES: usize = 4;
let mut term = TestTerm::new(LINES, 4, 0);
term.print("\u{1b}[?25l====\u{1b}[?25h\r\nSS\r\n");
assert_visible_contents(&term, file!(), line!(), &["====", "SS ", " ", " "]);
assert_visible_contents(&term, file!(), line!(), &["====", "SS", "", ""]);
term.resize(TerminalSize {
rows: LINES,
cols: 6,
..Default::default()
});
assert_visible_contents(&term, file!(), line!(), &["====", "SS ", " ", " "]);
assert_visible_contents(&term, file!(), line!(), &["====", "SS", "", ""]);
}
#[test]
@ -1028,48 +929,48 @@ fn test_resize_wrap_escape_code_issue_978() {
const LINES: usize = 4;
let mut term = TestTerm::new(LINES, 4, 0);
term.print("====\u{1b}[0m\r\nSS\r\n");
assert_visible_contents(&term, file!(), line!(), &["====", "SS ", " ", " "]);
assert_visible_contents(&term, file!(), line!(), &["====", "SS", "", ""]);
term.resize(TerminalSize {
rows: LINES,
cols: 6,
..Default::default()
});
assert_visible_contents(&term, file!(), line!(), &["====", "SS ", " ", " "]);
assert_visible_contents(&term, file!(), line!(), &["====", "SS", "", ""]);
}
#[test]
fn test_scrollup() {
let mut term = TestTerm::new(2, 1, 4);
term.print("1\n");
assert_all_contents(&term, file!(), line!(), &["1", " "]);
assert_all_contents(&term, file!(), line!(), &["1", ""]);
assert_eq!(term.screen().visible_row_to_stable_row(0), 0);
term.print("2\n");
assert_all_contents(&term, file!(), line!(), &["1", "2", " "]);
assert_all_contents(&term, file!(), line!(), &["1", "2", ""]);
assert_eq!(term.screen().visible_row_to_stable_row(0), 1);
term.print("3\n");
assert_all_contents(&term, file!(), line!(), &["1", "2", "3", " "]);
assert_all_contents(&term, file!(), line!(), &["1", "2", "3", ""]);
assert_eq!(term.screen().visible_row_to_stable_row(0), 2);
term.print("4\n");
assert_all_contents(&term, file!(), line!(), &["1", "2", "3", "4", " "]);
assert_all_contents(&term, file!(), line!(), &["1", "2", "3", "4", ""]);
assert_eq!(term.screen().visible_row_to_stable_row(0), 3);
term.print("5\n");
assert_all_contents(&term, file!(), line!(), &["1", "2", "3", "4", "5", " "]);
assert_all_contents(&term, file!(), line!(), &["1", "2", "3", "4", "5", ""]);
assert_eq!(term.screen().visible_row_to_stable_row(0), 4);
term.print("6\n");
assert_all_contents(&term, file!(), line!(), &["2", "3", "4", "5", "6", " "]);
assert_all_contents(&term, file!(), line!(), &["2", "3", "4", "5", "6", ""]);
assert_eq!(term.screen().visible_row_to_stable_row(0), 5);
term.print("7\n");
assert_all_contents(&term, file!(), line!(), &["3", "4", "5", "6", "7", " "]);
assert_all_contents(&term, file!(), line!(), &["3", "4", "5", "6", "7", ""]);
assert_eq!(term.screen().visible_row_to_stable_row(0), 6);
term.print("8\n");
assert_all_contents(&term, file!(), line!(), &["4", "5", "6", "7", "8", " "]);
assert_all_contents(&term, file!(), line!(), &["4", "5", "6", "7", "8", ""]);
assert_eq!(term.screen().visible_row_to_stable_row(0), 7);
}
@ -1077,14 +978,14 @@ fn test_scrollup() {
fn test_ri() {
let mut term = TestTerm::new(3, 1, 10);
term.print("1\n\u{8d}\n");
assert_all_contents(&term, file!(), line!(), &["1", " ", " "]);
assert_all_contents(&term, file!(), line!(), &["1", "", ""]);
}
#[test]
fn test_scroll_margins() {
let mut term = TestTerm::new(3, 1, 10);
term.print("1\n2\n3\n4\n");
assert_all_contents(&term, file!(), line!(), &["1", "2", "3", "4", " "]);
assert_all_contents(&term, file!(), line!(), &["1", "2", "3", "4", ""]);
let margins = CSI::Cursor(termwiz::escape::csi::Cursor::SetTopAndBottomMargins {
top: OneBased::new(1),
@ -1093,19 +994,14 @@ fn test_scroll_margins() {
term.print(format!("{}", margins));
term.print("z\n");
assert_all_contents(&term, file!(), line!(), &["1", "2", "z", "4", " "]);
assert_all_contents(&term, file!(), line!(), &["1", "2", "z", "4", ""]);
term.print("a\n");
assert_all_contents(&term, file!(), line!(), &["1", "2", "z", "a", " ", " "]);
assert_all_contents(&term, file!(), line!(), &["1", "2", "z", "a", "", ""]);
term.cup(0, 1);
term.print("W\n");
assert_all_contents(
&term,
file!(),
line!(),
&["1", "2", "z", "a", "W", " ", " "],
);
assert_all_contents(&term, file!(), line!(), &["1", "2", "z", "a", "W", "", ""]);
}
#[test]
@ -1122,11 +1018,7 @@ fn test_emoji_with_modifier() {
&term,
file!(),
line!(),
&[
&format!("{} ", waving_hand),
&format!("{} ", waving_hand_dark_tone),
" ",
],
&[waving_hand, waving_hand_dark_tone, ""],
);
}
@ -1138,12 +1030,7 @@ fn test_1573() {
term.print(sequence);
term.print("\r\n");
assert_all_contents(
&term,
file!(),
line!(),
&[&format!("{} ", sequence), " "],
);
assert_all_contents(&term, file!(), line!(), &[sequence, ""]);
use unicode_normalization::UnicodeNormalization;
let recomposed: String = sequence.nfc().collect();
@ -1171,8 +1058,8 @@ fn test_hyperlinks() {
&term.screen().visible_lines(),
&[
Line::from_text("hello", &linked, SEQ_ZERO, None),
" ".into(),
" ".into(),
"".into(),
"".into(),
],
Compare::TEXT | Compare::ATTRS,
);
@ -1190,7 +1077,7 @@ fn test_hyperlinks() {
&[
Line::from_text_with_wrapped_last_col("hello", &linked, SEQ_ZERO),
Line::from_text("hey!!", &linked, SEQ_ZERO, None),
" ".into(),
"".into(),
],
Compare::TEXT | Compare::ATTRS,
);

View File

@ -85,7 +85,7 @@ pub struct Line {
#[cfg_attr(feature = "use_serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
enum CellStorage {
V(Vec<Cell>),
V(VecStorage),
C(ClusteredLine),
}
@ -102,7 +102,7 @@ impl Line {
let bits = LineBits::NONE;
Self {
bits,
cells: CellStorage::V(cells),
cells: CellStorage::V(VecStorage::new(cells)),
seqno,
zones: vec![],
}
@ -112,7 +112,20 @@ impl Line {
let bits = LineBits::NONE;
Self {
bits,
cells: CellStorage::V(cells),
cells: CellStorage::V(VecStorage::new(cells)),
seqno,
zones: vec![],
}
}
/// Create a new line using cluster storage, optimized for appending
/// and lower memory utilization.
/// The line will automatically switch to cell storage when necessary
/// to apply edits.
pub fn new(seqno: SequenceNo) -> Self {
Self {
bits: LineBits::NONE,
cells: CellStorage::C(ClusteredLine::default()),
seqno,
zones: vec![],
}
@ -124,7 +137,7 @@ impl Line {
let bits = LineBits::NONE;
Self {
bits,
cells: CellStorage::V(cells),
cells: CellStorage::V(VecStorage::new(cells)),
seqno,
zones: vec![],
}
@ -148,7 +161,7 @@ impl Line {
}
Line {
cells: CellStorage::V(cells),
cells: CellStorage::V(VecStorage::new(cells)),
bits: LineBits::NONE,
seqno,
zones: vec![],
@ -204,7 +217,7 @@ impl Line {
.map(|chunk| {
let chunk_len = chunk.len();
let mut line = Line {
cells: CellStorage::V(chunk.to_vec()),
cells: CellStorage::V(VecStorage::new(chunk.to_vec())),
bits: LineBits::NONE,
seqno: seqno,
zones: vec![],
@ -460,7 +473,7 @@ impl Line {
}
let cells = self.coerce_vec_storage();
for cell in cells {
for cell in cells.iter_mut() {
let replace = match cell.attrs().hyperlink() {
Some(ref link) if link.is_implicit() => Some(Cell::new_grapheme(
cell.str(),
@ -498,39 +511,19 @@ impl Line {
// use this as an opportunity to rebuild HAS_HYPERLINK, skip matching
// cells with existing non-implicit hyperlinks, and avoid matching
// text with zero-width cells.
let line = self.as_str().into_owned();
self.bits |= LineBits::SCANNED_IMPLICIT_HYPERLINKS;
self.bits &= !LineBits::HAS_IMPLICIT_HYPERLINKS;
let line = self.as_str();
let matches = Rule::match_hyperlinks(&line, rules);
if matches.is_empty() {
return;
}
// The capture range is measured in bytes but we need to translate
// that to the index of the column. This is complicated a bit further
// because double wide sequences have a blank column cell after them
// in the cells array, but the string we match against excludes that
// string.
let mut cell_idx = 0;
for (byte_idx, _grapheme) in line.grapheme_indices(true) {
let cells = self.coerce_vec_storage();
let cell = &mut cells[cell_idx];
let mut matched = false;
for m in &matches {
if m.range.contains(&byte_idx) {
let attrs = cell.attrs_mut();
// Don't replace existing links
if attrs.hyperlink().is_none() {
attrs.set_hyperlink(Some(Arc::clone(&m.link)));
matched = true;
}
}
}
cell_idx += cell.width();
if matched {
self.bits |= LineBits::HAS_IMPLICIT_HYPERLINKS;
}
let line = line.into_owned();
let cells = self.coerce_vec_storage();
if cells.scan_and_create_hyperlinks(&line, matches) {
self.bits |= LineBits::HAS_IMPLICIT_HYPERLINKS;
}
}
@ -560,7 +553,7 @@ impl Line {
let cells = my_cells.split_off(idx);
Self {
bits: self.bits,
cells: CellStorage::V(cells),
cells: CellStorage::V(VecStorage::new(cells)),
seqno,
zones: vec![],
}
@ -643,7 +636,7 @@ impl Line {
}
Self {
bits: LineBits::NONE,
cells: CellStorage::V(cells),
cells: CellStorage::V(VecStorage::new(cells)),
seqno: self.current_seqno(),
zones: vec![],
}
@ -654,8 +647,8 @@ impl Line {
/// of cells to avoid partial rendering concerns.
/// Similarly, when we assign a cell, we need to blank out those
/// occluded successor cells.
pub fn set_cell(&mut self, idx: usize, cell: Cell, seqno: SequenceNo) -> &Cell {
self.set_cell_impl(idx, cell, false, seqno)
pub fn set_cell(&mut self, idx: usize, cell: Cell, seqno: SequenceNo) {
self.set_cell_impl(idx, cell, false, seqno);
}
pub fn set_cell_clearing_image_placements(
@ -663,25 +656,16 @@ impl Line {
idx: usize,
cell: Cell,
seqno: SequenceNo,
) -> &Cell {
) {
self.set_cell_impl(idx, cell, true, seqno)
}
fn raw_set_cell(&mut self, idx: usize, mut cell: Cell, clear: bool) {
fn raw_set_cell(&mut self, idx: usize, cell: Cell, clear: bool) {
let cells = self.coerce_vec_storage();
if !clear {
if let Some(images) = cells[idx].attrs().images() {
for image in images {
if image.has_placement_id() {
cell.attrs_mut().attach_image(Box::new(image));
}
}
}
}
cells[idx] = cell;
cells.set_cell(idx, cell, clear);
}
fn set_cell_impl(&mut self, idx: usize, cell: Cell, clear: bool, seqno: SequenceNo) -> &Cell {
fn set_cell_impl(&mut self, idx: usize, cell: Cell, clear: bool, seqno: SequenceNo) {
// The .max(1) stuff is here in case we get called with a
// zero-width cell. That shouldn't happen: those sequences
// should get filtered out in the terminal parsing layer,
@ -690,6 +674,39 @@ impl Line {
// https://github.com/wez/wezterm/issues/768
let width = cell.width().max(1);
self.invalidate_implicit_hyperlinks(seqno);
self.invalidate_zones();
self.update_last_change_seqno(seqno);
if cell.attrs().hyperlink().is_some() {
self.bits |= LineBits::HAS_HYPERLINK;
}
if let CellStorage::C(cl) = &mut self.cells {
if idx == cl.len {
cl.append(cell);
return;
}
if idx > cl.len
&& cell.str() == " "
&& cl
.clusters
.last()
.map(|c| c.attrs == *cell.attrs())
.unwrap_or_else(|| *cell.attrs() == CellAttributes::default())
{
// Appending blank beyond end of line; is already
// implicitly blank
return;
}
/*
log::info!(
"cannot append {cell:?} to {:?} as idx={idx} and cl.len is {}",
cl,
cl.len
);
*/
}
// if the line isn't wide enough, pad it out with the default attributes.
{
let cells = self.coerce_vec_storage();
@ -698,12 +715,6 @@ impl Line {
}
}
self.invalidate_implicit_hyperlinks(seqno);
self.invalidate_zones();
self.update_last_change_seqno(seqno);
if cell.attrs().hyperlink().is_some() {
self.bits |= LineBits::HAS_HYPERLINK;
}
self.invalidate_grapheme_at_or_before(idx);
// For double-wide or wider chars, ensure that the cells that
@ -713,7 +724,6 @@ impl Line {
}
self.raw_set_cell(idx, cell, clear);
&self.cells_mut()[idx]
}
/// Place text starting at the specified column index.
@ -827,6 +837,13 @@ impl Line {
}
pub fn prune_trailing_blanks(&mut self, seqno: SequenceNo) {
if let CellStorage::C(cl) = &self.cells {
if !cl.text.ends_with(' ') {
// There are no trailing blanks
return;
}
}
let def_attr = CellAttributes::blank();
let cells = self.coerce_vec_storage();
if let Some(end_idx) = cells
@ -887,10 +904,10 @@ impl Line {
CellStorage::V(_) => return,
CellStorage::C(cl) => cl.to_cell_vec(),
};
self.cells = CellStorage::V(cells);
self.cells = CellStorage::V(VecStorage::new(cells));
}
fn coerce_vec_storage(&mut self) -> &mut Vec<Cell> {
fn coerce_vec_storage(&mut self) -> &mut VecStorage {
self.make_cells();
match &mut self.cells {
@ -924,8 +941,7 @@ impl Line {
/// indicating that the following line is logically a part of this one.
pub fn last_cell_was_wrapped(&self) -> bool {
self.visible_cells()
.rev()
.next()
.last()
.map(|c| c.attrs().wrapped())
.unwrap_or(false)
}
@ -933,10 +949,17 @@ impl Line {
/// Adjust the value of the wrapped attribute on the last cell of this
/// line.
pub fn set_last_cell_was_wrapped(&mut self, wrapped: bool, seqno: SequenceNo) {
self.update_last_change_seqno(seqno);
if let CellStorage::C(cl) = &mut self.cells {
if cl.len() > 0 {
cl.set_last_cell_was_wrapped(wrapped);
return;
}
}
let cells = self.coerce_vec_storage();
if let Some(cell) = cells.last_mut() {
cell.attrs_mut().set_wrapped(wrapped);
self.update_last_change_seqno(seqno);
}
}
@ -1027,6 +1050,79 @@ impl Line {
}
}
#[cfg_attr(feature = "use_serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
struct VecStorage {
cells: Vec<Cell>,
}
impl VecStorage {
fn new(cells: Vec<Cell>) -> Self {
Self { cells }
}
fn set_cell(&mut self, idx: usize, mut cell: Cell, clear_image_placement: bool) {
if !clear_image_placement {
if let Some(images) = self.cells[idx].attrs().images() {
for image in images {
if image.has_placement_id() {
cell.attrs_mut().attach_image(Box::new(image));
}
}
}
}
self.cells[idx] = cell;
}
fn scan_and_create_hyperlinks(
&mut self,
line: &str,
matches: Vec<crate::hyperlink::RuleMatch>,
) -> bool {
// The capture range is measured in bytes but we need to translate
// that to the index of the column. This is complicated a bit further
// because double wide sequences have a blank column cell after them
// in the cells array, but the string we match against excludes that
// string.
let mut cell_idx = 0;
let mut has_implicit_hyperlinks = false;
for (byte_idx, _grapheme) in line.grapheme_indices(true) {
let cell = &mut self.cells[cell_idx];
let mut matched = false;
for m in &matches {
if m.range.contains(&byte_idx) {
let attrs = cell.attrs_mut();
// Don't replace existing links
if attrs.hyperlink().is_none() {
attrs.set_hyperlink(Some(Arc::clone(&m.link)));
matched = true;
}
}
}
cell_idx += cell.width();
if matched {
has_implicit_hyperlinks = true;
}
}
has_implicit_hyperlinks
}
}
impl std::ops::Deref for VecStorage {
type Target = Vec<Cell>;
fn deref(&self) -> &Vec<Cell> {
&self.cells
}
}
impl std::ops::DerefMut for VecStorage {
fn deref_mut(&mut self) -> &mut Vec<Cell> {
&mut self.cells
}
}
impl<'a> From<&'a str> for Line {
fn from(s: &str) -> Line {
Line::from_text(s, &CellAttributes::default(), SEQ_ZERO, None)
@ -1102,6 +1198,7 @@ impl<'a> DoubleEndedIterator for VisibleCellIter<'a> {
}
}
#[derive(Debug)]
pub enum CellRef<'a> {
CellRef {
cell_index: usize,
@ -1175,7 +1272,7 @@ struct Cluster {
}
#[cfg_attr(feature = "use_serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
#[derive(Default, Debug, Clone, PartialEq)]
struct ClusteredLine {
text: String,
#[cfg_attr(
@ -1316,6 +1413,66 @@ impl ClusteredLine {
line: self,
}
}
fn append(&mut self, cell: Cell) {
let new_cluster = match self.clusters.last() {
Some(cluster) => cluster.attrs != *cell.attrs(),
None => true,
};
let new_cell_index = self.len;
let cell_width = cell.width();
if new_cluster {
self.clusters.push(Cluster {
attrs: (*cell.attrs()).clone(),
cell_width,
});
} else if let Some(cluster) = self.clusters.last_mut() {
cluster.cell_width += cell_width;
}
self.text.push_str(cell.str());
if cell_width > 1 {
let bitset = match self.is_double_wide.take() {
Some(mut bitset) => {
bitset.grow(new_cell_index + 1);
bitset.set(new_cell_index, true);
bitset
}
None => {
let mut bitset = FixedBitSet::with_capacity(new_cell_index + 1);
bitset.set(new_cell_index, true);
bitset
}
};
self.is_double_wide.replace(bitset);
}
self.len += cell_width;
}
fn set_last_cell_was_wrapped(&mut self, wrapped: bool) {
if let Some(last_cell) = self.iter().last() {
if last_cell.attrs().wrapped() == wrapped {
// Nothing to change
//return;
}
let mut attrs = last_cell.attrs().clone();
attrs.set_wrapped(wrapped);
let width = last_cell.width();
let last_cluster = self.clusters.last_mut().unwrap();
if last_cluster.cell_width == width {
// Re-purpose final cluster
last_cluster.attrs = attrs;
} else {
last_cluster.cell_width -= width;
self.clusters.push(Cluster {
cell_width: width,
attrs,
});
}
}
}
}
pub struct ClusterLineCellIter<'a> {
@ -1582,20 +1739,84 @@ C(
}
#[test]
fn cluster_representation_attributes() {
use crate::cell::Intensity;
let bold = {
let mut attr = CellAttributes::default();
attr.set_intensity(Intensity::Bold);
attr
};
fn cluster_wrap_last() {
let mut line: Line = "hello".into();
line.compress_for_scrollback();
line.set_last_cell_was_wrapped(true, 1);
k9::snapshot!(
line,
r#"
Line {
cells: C(
ClusteredLine {
text: "hello",
is_double_wide: None,
clusters: [
Cluster {
cell_width: 4,
attrs: CellAttributes {
attributes: 0,
intensity: Normal,
underline: None,
blink: None,
italic: false,
reverse: false,
strikethrough: false,
invisible: false,
wrapped: false,
overline: false,
semantic_type: Output,
foreground: Default,
background: Default,
fat: None,
},
},
Cluster {
cell_width: 1,
attrs: CellAttributes {
attributes: 2048,
intensity: Normal,
underline: None,
blink: None,
italic: false,
reverse: false,
strikethrough: false,
invisible: false,
wrapped: true,
overline: false,
semantic_type: Output,
foreground: Default,
background: Default,
fat: None,
},
},
],
len: 5,
},
),
zones: [],
seqno: 1,
bits: NONE,
}
"#
);
}
fn bold() -> CellAttributes {
use crate::cell::Intensity;
let mut attr = CellAttributes::default();
attr.set_intensity(Intensity::Bold);
attr
}
#[test]
fn cluster_representation_attributes() {
let line = Line::from_cells(
vec![
Cell::new_grapheme("a", CellAttributes::default(), None),
Cell::new_grapheme("b", bold.clone(), None),
Cell::new_grapheme("b", bold(), None),
Cell::new_grapheme("c", CellAttributes::default(), None),
Cell::new_grapheme("d", bold.clone(), None),
Cell::new_grapheme("d", bold(), None),
],
SEQ_ZERO,
);
@ -1695,4 +1916,185 @@ C(
compressed.coerce_vec_storage();
assert_eq!(line, compressed);
}
#[test]
fn cluster_append() {
let mut cl = ClusteredLine::default();
cl.append(Cell::new_grapheme("h", CellAttributes::default(), None));
cl.append(Cell::new_grapheme("e", CellAttributes::default(), None));
cl.append(Cell::new_grapheme("l", bold(), None));
cl.append(Cell::new_grapheme("l", CellAttributes::default(), None));
cl.append(Cell::new_grapheme("o", CellAttributes::default(), None));
k9::snapshot!(
cl,
r#"
ClusteredLine {
text: "hello",
is_double_wide: None,
clusters: [
Cluster {
cell_width: 2,
attrs: CellAttributes {
attributes: 0,
intensity: Normal,
underline: None,
blink: None,
italic: false,
reverse: false,
strikethrough: false,
invisible: false,
wrapped: false,
overline: false,
semantic_type: Output,
foreground: Default,
background: Default,
fat: None,
},
},
Cluster {
cell_width: 1,
attrs: CellAttributes {
attributes: 1,
intensity: Bold,
underline: None,
blink: None,
italic: false,
reverse: false,
strikethrough: false,
invisible: false,
wrapped: false,
overline: false,
semantic_type: Output,
foreground: Default,
background: Default,
fat: None,
},
},
Cluster {
cell_width: 2,
attrs: CellAttributes {
attributes: 0,
intensity: Normal,
underline: None,
blink: None,
italic: false,
reverse: false,
strikethrough: false,
invisible: false,
wrapped: false,
overline: false,
semantic_type: Output,
foreground: Default,
background: Default,
fat: None,
},
},
],
len: 5,
}
"#
);
}
#[test]
fn cluster_line_new() {
let mut line = Line::new(1);
line.set_cell(
0,
Cell::new_grapheme("h", CellAttributes::default(), None),
1,
);
line.set_cell(
1,
Cell::new_grapheme("e", CellAttributes::default(), None),
2,
);
line.set_cell(2, Cell::new_grapheme("l", bold(), None), 3);
line.set_cell(
3,
Cell::new_grapheme("l", CellAttributes::default(), None),
4,
);
line.set_cell(
4,
Cell::new_grapheme("o", CellAttributes::default(), None),
5,
);
k9::snapshot!(
line,
r#"
Line {
cells: C(
ClusteredLine {
text: "hello",
is_double_wide: None,
clusters: [
Cluster {
cell_width: 2,
attrs: CellAttributes {
attributes: 0,
intensity: Normal,
underline: None,
blink: None,
italic: false,
reverse: false,
strikethrough: false,
invisible: false,
wrapped: false,
overline: false,
semantic_type: Output,
foreground: Default,
background: Default,
fat: None,
},
},
Cluster {
cell_width: 1,
attrs: CellAttributes {
attributes: 1,
intensity: Bold,
underline: None,
blink: None,
italic: false,
reverse: false,
strikethrough: false,
invisible: false,
wrapped: false,
overline: false,
semantic_type: Output,
foreground: Default,
background: Default,
fat: None,
},
},
Cluster {
cell_width: 2,
attrs: CellAttributes {
attributes: 0,
intensity: Normal,
underline: None,
blink: None,
italic: false,
reverse: false,
strikethrough: false,
invisible: false,
wrapped: false,
overline: false,
semantic_type: Output,
foreground: Default,
background: Default,
fat: None,
},
},
],
len: 5,
},
),
zones: [],
seqno: 5,
bits: NONE,
}
"#
);
}
}

View File

@ -193,9 +193,10 @@ impl RenderableInner {
.clone(),
);
let cell = line.set_cell(self.cursor_position.x, cell, SEQ_ZERO);
let width = cell.width();
line.set_cell(self.cursor_position.x, cell, SEQ_ZERO);
// Adjust the cursor to reflect the width of this new cell
self.cursor_position.x += cell.width();
self.cursor_position.x += width;
}
_ => {}
}