Fix scrolling (#650)

* fix(grid): scroll up on empty line

* fix(grid): line wrapping while scrolling

* style(grid): fix names

* docs(changelog): document fix
This commit is contained in:
Aram Drevekenin 2021-08-19 13:28:08 +02:00 committed by GitHub
parent fde38dcb44
commit e889891604
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 348 additions and 79 deletions

View File

@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
* New pane UI: draw pane frames - can be disabled with ctrl-p + z, or through configuration (https://github.com/zellij-org/zellij/pull/643)
* Terminal compatibility: support changing index colors through OSC 4 and similar (https://github.com/zellij-org/zellij/pull/646)
* Fix various shells (eg. nushell) unexpectedly exiting when the user presses ctrl-c (https://github.com/zellij-org/zellij/pull/648)
* Fix line wrapping while scrolling (https://github.com/zellij-org/zellij/pull/650)
## [0.15.0] - 2021-07-19
* Kill children properly (https://github.com/zellij-org/zellij/pull/601)

20
src/tests/fixtures/scrolling vendored Normal file
View File

@ -0,0 +1,20 @@
line 1aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line 2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line 3aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line 4aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line 5aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line 6aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line 7aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line 8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line 9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line 10aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line 11aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line 12aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line 13aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line 14aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line 15aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line 16aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line 17aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line 18aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line 19aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line 20aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

View File

@ -42,7 +42,7 @@ fn get_top_non_canonical_rows(rows: &mut Vec<Row>) -> Vec<Row> {
}
}
fn get_bottom_canonical_row_and_wraps(rows: &mut VecDeque<Row>) -> Vec<Row> {
fn get_lines_above_bottom_canonical_row_and_wraps(rows: &mut VecDeque<Row>) -> Vec<Row> {
let mut index_of_last_non_canonical_row = None;
for (i, row) in rows.iter().enumerate().rev() {
index_of_last_non_canonical_row = Some(i);
@ -58,100 +58,157 @@ fn get_bottom_canonical_row_and_wraps(rows: &mut VecDeque<Row>) -> Vec<Row> {
}
}
fn transfer_rows_down(
source: &mut VecDeque<Row>,
destination: &mut Vec<Row>,
fn get_viewport_bottom_canonical_row_and_wraps(viewport: &mut Vec<Row>) -> Vec<Row> {
let mut index_of_last_non_canonical_row = None;
for (i, row) in viewport.iter().enumerate().rev() {
index_of_last_non_canonical_row = Some(i);
if row.is_canonical {
break;
}
}
match index_of_last_non_canonical_row {
Some(index_of_last_non_canonical_row) => {
viewport.drain(index_of_last_non_canonical_row..).collect()
}
None => vec![],
}
}
fn get_top_canonical_row_and_wraps(rows: &mut Vec<Row>) -> Vec<Row> {
let mut index_of_first_non_canonical_row = None;
let mut end_index_of_first_canonical_line = None;
for (i, row) in rows.iter().enumerate() {
if row.is_canonical && end_index_of_first_canonical_line.is_none() {
index_of_first_non_canonical_row = Some(i);
end_index_of_first_canonical_line = Some(i);
continue;
}
if row.is_canonical && end_index_of_first_canonical_line.is_some() {
break;
}
if index_of_first_non_canonical_row.is_some() {
end_index_of_first_canonical_line = Some(i);
continue;
}
}
match (
index_of_first_non_canonical_row,
end_index_of_first_canonical_line,
) {
(Some(first_index), Some(last_index)) => rows.drain(first_index..=last_index).collect(),
(Some(first_index), None) => rows.drain(first_index..).collect(),
_ => vec![],
}
}
fn transfer_rows_from_lines_above_to_viewport(
lines_above: &mut VecDeque<Row>,
viewport: &mut Vec<Row>,
count: usize,
max_src_width: Option<usize>,
max_dst_width: Option<usize>,
max_viewport_width: usize,
) {
let mut next_lines: Vec<Row> = vec![];
let mut lines_added_to_destination: isize = 0;
let mut lines_added_to_viewport: isize = 0;
loop {
if lines_added_to_destination as usize == count {
if lines_added_to_viewport as usize == count {
break;
}
if next_lines.is_empty() {
match source.pop_back() {
match lines_above.pop_back() {
Some(next_line) => {
let mut top_non_canonical_rows_in_dst = get_top_non_canonical_rows(destination);
lines_added_to_destination -= top_non_canonical_rows_in_dst.len() as isize;
let mut top_non_canonical_rows_in_dst = get_top_non_canonical_rows(viewport);
lines_added_to_viewport -= top_non_canonical_rows_in_dst.len() as isize;
next_lines.push(next_line);
next_lines.append(&mut top_non_canonical_rows_in_dst);
next_lines = match max_dst_width {
Some(max_row_width) => Row::from_rows(next_lines, max_row_width)
.split_to_rows_of_length(max_row_width),
None => vec![Row::from_rows(next_lines, 0)],
};
next_lines = Row::from_rows(next_lines, max_viewport_width)
.split_to_rows_of_length(max_viewport_width);
if next_lines.is_empty() {
// no more lines at source, the line we popped was probably empty
// no more lines at lines_above, the line we popped was probably empty
break;
}
}
None => break, // no more rows
}
}
destination.insert(0, next_lines.pop().unwrap());
lines_added_to_destination += 1;
viewport.insert(0, next_lines.pop().unwrap());
lines_added_to_viewport += 1;
}
if !next_lines.is_empty() {
match max_src_width {
Some(max_row_width) => {
let excess_rows = Row::from_rows(next_lines, max_row_width)
.split_to_rows_of_length(max_row_width);
source.extend(excess_rows);
}
None => {
let excess_row = Row::from_rows(next_lines, 0);
bounded_push(source, excess_row);
}
}
let excess_row = Row::from_rows(next_lines, 0);
bounded_push(lines_above, excess_row);
}
}
fn transfer_rows_up(
source: &mut Vec<Row>,
destination: &mut VecDeque<Row>,
fn transfer_rows_from_viewport_to_lines_above(
viewport: &mut Vec<Row>,
lines_above: &mut VecDeque<Row>,
count: usize,
max_src_width: Option<usize>,
max_dst_width: Option<usize>,
max_viewport_width: usize,
) {
let mut next_lines: Vec<Row> = vec![];
for _ in 0..count {
if next_lines.is_empty() {
if !source.is_empty() {
let next_line = source.remove(0);
if !viewport.is_empty() {
let next_line = viewport.remove(0);
if !next_line.is_canonical {
let mut bottom_canonical_row_and_wraps_in_dst =
get_bottom_canonical_row_and_wraps(destination);
get_lines_above_bottom_canonical_row_and_wraps(lines_above);
next_lines.append(&mut bottom_canonical_row_and_wraps_in_dst);
}
next_lines.push(next_line);
next_lines = match max_dst_width {
Some(max_row_width) => Row::from_rows(next_lines, max_row_width)
.split_to_rows_of_length(max_row_width),
None => vec![Row::from_rows(next_lines, 0)],
};
next_lines = vec![Row::from_rows(next_lines, 0)];
} else {
break; // no more rows
}
}
bounded_push(destination, next_lines.remove(0));
bounded_push(lines_above, next_lines.remove(0));
}
if !next_lines.is_empty() {
match max_src_width {
Some(max_row_width) => {
let excess_rows = Row::from_rows(next_lines, max_row_width)
.split_to_rows_of_length(max_row_width);
for row in excess_rows {
source.insert(0, row);
let excess_rows = Row::from_rows(next_lines, max_viewport_width)
.split_to_rows_of_length(max_viewport_width);
for row in excess_rows {
viewport.insert(0, row);
}
}
}
fn transfer_rows_from_lines_below_to_viewport(
lines_below: &mut Vec<Row>,
viewport: &mut Vec<Row>,
count: usize,
max_viewport_width: usize,
) {
let mut next_lines: Vec<Row> = vec![];
for _ in 0..count {
let mut lines_pulled_from_viewport = 0;
if next_lines.is_empty() {
if !lines_below.is_empty() {
let mut top_non_canonical_rows_in_lines_below =
get_top_non_canonical_rows(lines_below);
if !top_non_canonical_rows_in_lines_below.is_empty() {
let mut canonical_line = get_viewport_bottom_canonical_row_and_wraps(viewport);
lines_pulled_from_viewport += canonical_line.len();
canonical_line.append(&mut top_non_canonical_rows_in_lines_below);
next_lines = Row::from_rows(canonical_line, max_viewport_width)
.split_to_rows_of_length(max_viewport_width);
} else {
let canonical_row = get_top_canonical_row_and_wraps(lines_below);
next_lines = Row::from_rows(canonical_row, max_viewport_width)
.split_to_rows_of_length(max_viewport_width);
}
}
None => {
let excess_row = Row::from_rows(next_lines, 0);
source.insert(0, excess_row);
} else {
break; // no more rows
}
}
for _ in 0..(lines_pulled_from_viewport + 1) {
if !next_lines.is_empty() {
viewport.push(next_lines.remove(0));
}
}
}
if !next_lines.is_empty() {
let excess_row = Row::from_rows(next_lines, 0);
lines_below.insert(0, excess_row);
}
}
@ -481,12 +538,11 @@ impl Grid {
let line_to_push_down = self.viewport.pop().unwrap();
self.lines_below.insert(0, line_to_push_down);
transfer_rows_down(
transfer_rows_from_lines_above_to_viewport(
&mut self.lines_above,
&mut self.viewport,
1,
None,
Some(self.width),
self.width,
);
self.selection.move_down(1);
@ -503,8 +559,14 @@ impl Grid {
last_line_above.append(&mut line_to_push_up.columns);
bounded_push(&mut self.lines_above, last_line_above);
}
let line_to_insert_at_viewport_bottom = self.lines_below.remove(0);
self.viewport.push(line_to_insert_at_viewport_bottom);
transfer_rows_from_lines_below_to_viewport(
&mut self.lines_below,
&mut self.viewport,
1,
self.width,
);
self.selection.move_up(1);
self.output_buffer.update_all_lines();
}
@ -593,12 +655,12 @@ impl Grid {
match current_viewport_row_count.cmp(&self.height) {
Ordering::Less => {
let row_count_to_transfer = self.height - current_viewport_row_count;
transfer_rows_down(
transfer_rows_from_lines_above_to_viewport(
&mut self.lines_above,
&mut self.viewport,
row_count_to_transfer,
None,
Some(new_columns),
new_columns,
);
let rows_pulled = self.viewport.len() - current_viewport_row_count;
new_cursor_y += rows_pulled;
@ -610,12 +672,11 @@ impl Grid {
} else {
new_cursor_y -= row_count_to_transfer;
}
transfer_rows_up(
transfer_rows_from_viewport_to_lines_above(
&mut self.viewport,
&mut self.lines_above,
row_count_to_transfer,
Some(new_columns),
None,
new_columns,
);
}
Ordering::Equal => {}
@ -628,12 +689,11 @@ impl Grid {
match current_viewport_row_count.cmp(&new_rows) {
Ordering::Less => {
let row_count_to_transfer = new_rows - current_viewport_row_count;
transfer_rows_down(
transfer_rows_from_lines_above_to_viewport(
&mut self.lines_above,
&mut self.viewport,
row_count_to_transfer,
None,
Some(new_columns),
new_columns,
);
let rows_pulled = self.viewport.len() - current_viewport_row_count;
self.cursor.y += rows_pulled;
@ -645,12 +705,11 @@ impl Grid {
} else {
self.cursor.y -= row_count_to_transfer;
}
transfer_rows_up(
transfer_rows_from_viewport_to_lines_above(
&mut self.viewport,
&mut self.lines_above,
row_count_to_transfer,
Some(new_columns),
None,
new_columns,
);
}
Ordering::Equal => {}
@ -794,12 +853,11 @@ impl Grid {
}
if self.cursor.y == self.height - 1 {
let row_count_to_transfer = 1;
transfer_rows_up(
transfer_rows_from_viewport_to_lines_above(
&mut self.viewport,
&mut self.lines_above,
row_count_to_transfer,
Some(self.width),
None,
self.width,
);
self.selection.move_up(1);
self.output_buffer.update_all_lines();
@ -869,12 +927,11 @@ impl Grid {
self.cursor.x = 0;
if self.cursor.y == self.height - 1 {
let row_count_to_transfer = 1;
transfer_rows_up(
transfer_rows_from_viewport_to_lines_above(
&mut self.viewport,
&mut self.lines_above,
row_count_to_transfer,
Some(self.width),
None,
self.width,
);
let wrapped_row = Row::new(self.width);
self.viewport.push(wrapped_row);
@ -2165,6 +2222,9 @@ impl Row {
if !parts.is_empty() && self.is_canonical {
parts.get_mut(0).unwrap().is_canonical = true;
}
if parts.is_empty() {
parts.push(self.clone());
}
parts
}
}

View File

@ -608,9 +608,9 @@ fn copy_selected_text_from_lines_below() {
grid.move_viewport_up(40);
grid.start_selection(&Position::new(35, 6));
grid.start_selection(&Position::new(63, 6));
// check for widechar, 📦 occupies columns 34, 35, and gets selected even if only the first column is selected
grid.end_selection(Some(&Position::new(37, 35)));
grid.end_selection(Some(&Position::new(65, 35)));
let text = grid.get_selected_text();
assert_eq!(
text.unwrap(),
@ -907,3 +907,95 @@ pub fn exa_plus_omf_theme() {
}
assert_snapshot!(format!("{:?}", grid));
}
#[test]
pub fn scroll_up() {
let mut vte_parser = vte::Parser::new();
let mut grid = Grid::new(10, 50, Palette::default());
let fixture_name = "scrolling";
let content = read_fixture(fixture_name);
for byte in content {
vte_parser.advance(&mut grid, byte);
}
grid.scroll_up_one_line();
assert_snapshot!(format!("{:?}", grid));
}
#[test]
pub fn scroll_down() {
let mut vte_parser = vte::Parser::new();
let mut grid = Grid::new(10, 50, Palette::default());
let fixture_name = "scrolling";
let content = read_fixture(fixture_name);
for byte in content {
vte_parser.advance(&mut grid, byte);
}
grid.scroll_up_one_line();
grid.scroll_down_one_line();
assert_snapshot!(format!("{:?}", grid));
}
#[test]
pub fn scroll_up_with_line_wraps() {
let mut vte_parser = vte::Parser::new();
let mut grid = Grid::new(10, 25, Palette::default());
let fixture_name = "scrolling";
let content = read_fixture(fixture_name);
for byte in content {
vte_parser.advance(&mut grid, byte);
}
grid.scroll_up_one_line();
assert_snapshot!(format!("{:?}", grid));
}
#[test]
pub fn scroll_down_with_line_wraps() {
let mut vte_parser = vte::Parser::new();
let mut grid = Grid::new(10, 25, Palette::default());
let fixture_name = "scrolling";
let content = read_fixture(fixture_name);
for byte in content {
vte_parser.advance(&mut grid, byte);
}
grid.scroll_up_one_line();
grid.scroll_down_one_line();
assert_snapshot!(format!("{:?}", grid));
}
#[test]
pub fn scroll_up_decrease_width_and_scroll_down() {
let mut vte_parser = vte::Parser::new();
let mut grid = Grid::new(10, 50, Palette::default());
let fixture_name = "scrolling";
let content = read_fixture(fixture_name);
for byte in content {
vte_parser.advance(&mut grid, byte);
}
for _ in 0..10 {
grid.scroll_up_one_line();
}
grid.change_size(10, 25);
for _ in 0..10 {
grid.scroll_down_one_line();
}
assert_snapshot!(format!("{:?}", grid));
}
#[test]
pub fn scroll_up_increase_width_and_scroll_down() {
let mut vte_parser = vte::Parser::new();
let mut grid = Grid::new(10, 25, Palette::default());
let fixture_name = "scrolling";
let content = read_fixture(fixture_name);
for byte in content {
vte_parser.advance(&mut grid, byte);
}
for _ in 0..10 {
grid.scroll_up_one_line();
}
grid.change_size(10, 50);
for _ in 0..10 {
grid.scroll_down_one_line();
}
assert_snapshot!(format!("{:?}", grid));
}

View File

@ -0,0 +1,16 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
expression: "format!(\"{:?}\", grid)"
---
00 (C): line 11aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
01 (C): line 12aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
02 (C): line 13aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
03 (C): line 14aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
04 (C): line 15aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
05 (C): line 16aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
06 (C): line 17aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
07 (C): line 18aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
08 (C): line 19aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
09 (C): line 20aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

View File

@ -0,0 +1,16 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
expression: "format!(\"{:?}\", grid)"
---
00 (C): line 16aaaaaaaaaaaaaaaaaa
01 (W): aaaaaaaaaaaaaaaaaaaaaaaa
02 (C): line 17aaaaaaaaaaaaaaaaaa
03 (W): aaaaaaaaaaaaaaaaaaaaaaaa
04 (C): line 18aaaaaaaaaaaaaaaaaa
05 (W): aaaaaaaaaaaaaaaaaaaaaaaa
06 (C): line 19aaaaaaaaaaaaaaaaaa
07 (W): aaaaaaaaaaaaaaaaaaaaaaaa
08 (C): line 20aaaaaaaaaaaaaaaaaa
09 (W): aaaaaaaaaaaaaaaaaaaaaaaa

View File

@ -0,0 +1,16 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
expression: "format!(\"{:?}\", grid)"
---
00 (C): line 10aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
01 (C): line 11aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
02 (C): line 12aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
03 (C): line 13aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
04 (C): line 14aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
05 (C): line 15aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
06 (C): line 16aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
07 (C): line 17aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
08 (C): line 18aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
09 (C): line 19aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

View File

@ -0,0 +1,16 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
expression: "format!(\"{:?}\", grid)"
---
00 (C): line 11aaaaaaaaaaaaaaaaaa
01 (W): aaaaaaaaaaaaaaaaaaaaaaaa
02 (C): line 12aaaaaaaaaaaaaaaaaa
03 (W): aaaaaaaaaaaaaaaaaaaaaaaa
04 (C): line 13aaaaaaaaaaaaaaaaaa
05 (W): aaaaaaaaaaaaaaaaaaaaaaaa
06 (C): line 14aaaaaaaaaaaaaaaaaa
07 (W): aaaaaaaaaaaaaaaaaaaaaaaa
08 (C): line 15aaaaaaaaaaaaaaaaaa
09 (W): aaaaaaaaaaaaaaaaaaaaaaaa

View File

@ -0,0 +1,16 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
expression: "format!(\"{:?}\", grid)"
---
00 (C): line 11aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
01 (C): line 12aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
02 (C): line 13aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
03 (C): line 14aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
04 (C): line 15aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
05 (C): line 16aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
06 (C): line 17aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
07 (C): line 18aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
08 (C): line 19aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
09 (C): line 20aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

View File

@ -0,0 +1,16 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
expression: "format!(\"{:?}\", grid)"
---
00 (W): aaaaaaaaaaaaaaaaaaaaaaaa
01 (C): line 16aaaaaaaaaaaaaaaaaa
02 (W): aaaaaaaaaaaaaaaaaaaaaaaa
03 (C): line 17aaaaaaaaaaaaaaaaaa
04 (W): aaaaaaaaaaaaaaaaaaaaaaaa
05 (C): line 18aaaaaaaaaaaaaaaaaa
06 (W): aaaaaaaaaaaaaaaaaaaaaaaa
07 (C): line 19aaaaaaaaaaaaaaaaaa
08 (W): aaaaaaaaaaaaaaaaaaaaaaaa
09 (C): line 20aaaaaaaaaaaaaaaaaa