feat(compatibility): vim working!

This commit is contained in:
Aram Drevekenin 2020-10-05 16:46:40 +02:00
parent 1844816f47
commit 5cd365a73c
11 changed files with 452 additions and 1 deletions

View file

@ -159,6 +159,7 @@ pub struct TerminalOutput {
cursor_position: usize, cursor_position: usize,
newline_indices: Vec<usize>, // canonical line breaks we get from the vt interpreter newline_indices: Vec<usize>, // canonical line breaks we get from the vt interpreter
linebreak_indices: Vec<usize>, // linebreaks from line wrapping linebreak_indices: Vec<usize>, // linebreaks from line wrapping
scroll_region: (usize, usize), // top line index / bottom line index
reset_foreground_ansi_code: bool, // this is a performance optimization, rather than placing and looking for the ansi reset code in pending_ansi_codes reset_foreground_ansi_code: bool, // this is a performance optimization, rather than placing and looking for the ansi reset code in pending_ansi_codes
reset_background_ansi_code: bool, // this is a performance optimization, rather than placing and looking for the ansi reset code in pending_ansi_codes reset_background_ansi_code: bool, // this is a performance optimization, rather than placing and looking for the ansi reset code in pending_ansi_codes
reset_misc_ansi_code: bool, // this is a performance optimization, rather than placing and looking for the ansi reset code in pending_ansi_codes reset_misc_ansi_code: bool, // this is a performance optimization, rather than placing and looking for the ansi reset code in pending_ansi_codes
@ -175,6 +176,7 @@ impl TerminalOutput {
cursor_position: 0, cursor_position: 0,
newline_indices: Vec::new(), newline_indices: Vec::new(),
linebreak_indices: Vec::new(), linebreak_indices: Vec::new(),
scroll_region: (1, ws.ws_row as usize),
display_rows: ws.ws_row, display_rows: ws.ws_row,
display_cols: ws.ws_col, display_cols: ws.ws_col,
should_render: true, should_render: true,
@ -440,21 +442,168 @@ impl TerminalOutput {
let last_linebreak_index = self.linebreak_indices.iter().rev().find(|&&l_i| l_i <= index_in_line).unwrap_or(&0); let last_linebreak_index = self.linebreak_indices.iter().rev().find(|&&l_i| l_i <= index_in_line).unwrap_or(&0);
max(*last_newline_index, *last_linebreak_index) max(*last_newline_index, *last_linebreak_index)
} }
fn scroll_region_line_indices (&self) -> (usize, usize) {
let mut newline_indices = self.newline_indices.iter().rev();
let mut linebreak_indices = self.linebreak_indices.iter().rev();
let mut next_newline = newline_indices.next();
let mut next_linebreak = linebreak_indices.next();
let mut lines_from_end = 0;
let scroll_end_index_from_screen_bottom = self.display_rows as usize - self.scroll_region.1;
let scroll_start_index_from_screen_bottom = scroll_end_index_from_screen_bottom + (self.scroll_region.1 - self.scroll_region.0);
let mut scroll_region_start_index = None;
let mut scroll_region_end_index = None;
loop {
match max(next_newline, next_linebreak) {
Some(next_line_index) => {
if lines_from_end == scroll_start_index_from_screen_bottom {
scroll_region_start_index = Some(next_line_index);
}
if lines_from_end == scroll_end_index_from_screen_bottom {
scroll_region_end_index = Some(next_line_index);
}
if scroll_region_start_index.is_some() && scroll_region_end_index.is_some() {
break;
}
if Some(next_line_index) == next_newline {
next_newline = newline_indices.next();
} else if Some(next_line_index) == next_linebreak {
next_linebreak = linebreak_indices.next();
}
lines_from_end += 1;
},
None => break,
}
}
(*scroll_region_start_index.unwrap_or(&0), *scroll_region_end_index.unwrap_or(&0))
}
fn index_of_next_line_after(&self, index_in_line: usize) -> usize {
let last_newline_index = self.newline_indices.iter().find(|&&n_i| n_i >= index_in_line).unwrap_or(&0);
let last_linebreak_index = self.linebreak_indices.iter().find(|&&l_i| l_i >= index_in_line).unwrap_or(&0);
max(*last_newline_index, *last_linebreak_index)
}
fn insert_empty_lines_at_cursor(&mut self, count: usize) {
for _ in 0..count {
self.delete_last_line_in_scroll_region();
let start_of_current_line = self.index_of_beginning_of_line(self.cursor_position);
let end_of_current_line = self.index_of_next_line_after(self.cursor_position);
for i in 0..end_of_current_line - start_of_current_line {
self.characters.insert(start_of_current_line + i, EMPTY_TERMINAL_CHARACTER.clone())
}
}
}
fn delete_last_line_in_scroll_region(&mut self) {
if let Some(newline_index_of_scroll_region_end) = self.get_line_position_on_screen(self.scroll_region.1) {
let end_of_last_scroll_region_line = self.get_line_position_on_screen(self.scroll_region.1 + 1).unwrap();
&self.characters.drain(newline_index_of_scroll_region_end..end_of_last_scroll_region_line);
}
}
fn delete_first_line_in_scroll_region(&mut self) {
if let Some(newline_index_of_scroll_region_start) = self.get_line_position_on_screen(self.scroll_region.0) {
let end_of_first_scroll_region_line = self.get_line_position_on_screen(self.scroll_region.0 + 1).unwrap();
let removed_count = {
let removed_line = &self.characters.drain(newline_index_of_scroll_region_start..end_of_first_scroll_region_line);
removed_line.len()
};
let newline_index_of_scroll_region_end = self.get_line_position_on_screen(self.scroll_region.1).unwrap();
for i in 0..removed_count {
self.characters.insert(newline_index_of_scroll_region_end + i, EMPTY_TERMINAL_CHARACTER.clone())
}
// TODO: if removed_count is larger than the line it was inserted it, recalculate all
// newline_indices after it
}
}
fn get_line_position_on_screen(&self, index_on_screen: usize) -> Option<usize> {
let mut newline_indices = self.newline_indices.iter().rev();
let mut linebreak_indices = self.linebreak_indices.iter().rev();
let mut next_newline = newline_indices.next();
let mut next_linebreak = linebreak_indices.next();
let mut lines_from_end = 0; // 1 because we're counting and not indexing TODO: fix this
loop {
match max(next_newline, next_linebreak) {
Some(next_line_index) => {
if index_on_screen == self.display_rows as usize - lines_from_end {
return Some(*next_line_index);
} else {
lines_from_end += 1;
}
if lines_from_end > self.display_rows as usize {
return None;
}
if Some(next_line_index) == next_newline {
next_newline = newline_indices.next();
} else if Some(next_line_index) == next_linebreak {
next_linebreak = linebreak_indices.next();
}
},
None => {
if index_on_screen == self.display_rows as usize - lines_from_end {
return Some(0);
} else {
return None;
}
}
}
}
}
// TODO: better naming of these two functions
fn get_line_index_on_screen (&self, position_in_characters: usize) -> Option<usize> {
let mut newline_indices = self.newline_indices.iter().rev();
let mut linebreak_indices = self.linebreak_indices.iter().rev();
let mut next_newline = newline_indices.next();
let mut next_linebreak = linebreak_indices.next();
let mut lines_from_end = 0;
loop {
match max(next_newline, next_linebreak) {
Some(next_line_index) => {
if *next_line_index <= position_in_characters {
break;
} else {
lines_from_end += 1;
if Some(next_line_index) == next_newline {
next_newline = newline_indices.next();
} else if Some(next_line_index) == next_linebreak {
next_linebreak = linebreak_indices.next();
}
}
},
None => break,
}
}
if lines_from_end > self.display_rows as usize {
None
} else {
Some(self.display_rows as usize - lines_from_end)
}
}
fn add_newline (&mut self) { fn add_newline (&mut self) {
let nearest_line_end = self.index_of_end_of_canonical_line(self.cursor_position); let nearest_line_end = self.index_of_end_of_canonical_line(self.cursor_position);
let current_line_index_on_screen = self.get_line_index_on_screen(self.cursor_position);
if nearest_line_end == self.characters.len() { if nearest_line_end == self.characters.len() {
self.newline_indices.push(nearest_line_end); self.newline_indices.push(nearest_line_end);
self.cursor_position = nearest_line_end; self.cursor_position = nearest_line_end;
} else if current_line_index_on_screen == Some(self.scroll_region.1) { // end of scroll region
// shift all lines in scroll region up
self.delete_first_line_in_scroll_region();
} else { } else {
// we shouldn't add a new line in the middle of the text // we shouldn't add a new line in the middle of the text
// in this case, we'll move to the next existing line and it // in this case, we'll move to the next existing line and it
// will be overriden as we print on it // will be overriden as we print on it
self.cursor_position = nearest_line_end; self.cursor_position = nearest_line_end;
} }
self.should_render = true;
self.pending_foreground_ansi_codes.clear(); self.pending_foreground_ansi_codes.clear();
self.pending_background_ansi_codes.clear(); self.pending_background_ansi_codes.clear();
self.pending_misc_ansi_codes.clear(); self.pending_misc_ansi_codes.clear();
self.should_render = true;
} }
fn move_to_beginning_of_line (&mut self) { fn move_to_beginning_of_line (&mut self) {
let last_newline_index = self.index_of_beginning_of_line(self.cursor_position); let last_newline_index = self.index_of_beginning_of_line(self.cursor_position);
@ -463,8 +612,18 @@ impl TerminalOutput {
} }
} }
fn debug_log_to_file (message: String) {
use std::fs::OpenOptions;
use std::io::prelude::*;
let mut file = OpenOptions::new().append(true).create(true).open("/tmp/mosaic-log.txt").unwrap();
file.write_all(message.as_bytes()).unwrap();
file.write_all("\n".as_bytes()).unwrap();
}
impl vte::Perform for TerminalOutput { impl vte::Perform for TerminalOutput {
fn print(&mut self, c: char) { fn print(&mut self, c: char) {
// while not ideal that we separate the reset and actual code logic here, // while not ideal that we separate the reset and actual code logic here,
// combining them is a question of rendering performance and not refactoring, // combining them is a question of rendering performance and not refactoring,
// so will be addressed separately // so will be addressed separately
@ -673,6 +832,29 @@ impl vte::Perform for TerminalOutput {
// TBD // TBD
} else if c == 'h' { } else if c == 'h' {
// TBD // TBD
} else if c == 'r' {
debug_log_to_file(format!("\rparams {:?}", params));
if params.len() > 1 {
// TODO: why do we need this if? what does a 1 parameter 'r' mean?
self.scroll_region = (params[0] as usize, params[1] as usize);
}
} else if c == 't' {
// TBD - title?
} else if c == 'n' {
// TBD - device status report
} else if c == 'c' {
// TBD - identify terminal
} else if c == 'M' {
// delete lines if currently inside scroll region
let line_count_to_delete = params[0];
for _ in 0..line_count_to_delete {
// TODO: better, do this in bulk
self.delete_first_line_in_scroll_region()
}
} else if c == 'L' {
// insert blank lines if inside scroll region
let line_count_to_add = params[0];
self.insert_empty_lines_at_cursor(line_count_to_add as usize);
} else { } else {
println!("unhandled csi: {:?}->{:?}", c, params); println!("unhandled csi: {:?}->{:?}", c, params);
panic!("aaa!!!"); panic!("aaa!!!");

BIN
src/tests/fixtures/vim_ctrl_d vendored Normal file

Binary file not shown.

BIN
src/tests/fixtures/vim_ctrl_u vendored Normal file

Binary file not shown.

Binary file not shown.

View file

@ -88,3 +88,80 @@ pub fn fish_select_tab_completion_options() {
assert_snapshot!(snapshot); assert_snapshot!(snapshot);
} }
} }
#[test]
pub fn vim_scroll_region_down () {
// here we test a case where vim defines the scroll region as lesser than the screen row count
// and then scrolls down
// the region is defined here by vim as 1-26 (there are 28 rows)
// then the cursor is moved to line 26 and a new line is added
// what should happen is that the first line in the scroll region (1) is deleted
// and an empty line is inserted in the last scroll region line (26)
// this tests also has other steps afterwards that fills the line with the next line in the
// file
// experience appear to the user
let fake_win_size = Winsize {
ws_col: 116,
ws_row: 28,
ws_xpixel: 0,
ws_ypixel: 0,
};
let fixture_name = "vim_scroll_region_down";
let mut fake_input_output = get_fake_os_input(&fake_win_size, fixture_name);
fake_input_output.add_terminal_input(&[17]); // quit (ctrl-q)
start(Box::new(fake_input_output.clone()));
let output_frames = fake_input_output.stdout_writer.output_frames.lock().unwrap();
let snapshots = get_output_frame_snapshots(&output_frames, &fake_win_size);
for snapshot in snapshots {
assert_snapshot!(snapshot);
}
}
#[test]
pub fn vim_ctrl_d() {
// in vim ctrl-d moves down half a page
// in this case, it sends the terminal the csi 'M' directive, which tells it to delete X (13 in
// this case) lines inside the scroll region and push the other lines up
// what happens here is that 13 lines are deleted and instead 13 empty lines are added at the
// end of the scroll region
// vim makes sure to fill these empty lines with the rest of the file
let fake_win_size = Winsize {
ws_col: 116,
ws_row: 28,
ws_xpixel: 0,
ws_ypixel: 0,
};
let fixture_name = "vim_ctrl_d";
let mut fake_input_output = get_fake_os_input(&fake_win_size, fixture_name);
fake_input_output.add_terminal_input(&[17]); // quit (ctrl-q)
start(Box::new(fake_input_output.clone()));
let output_frames = fake_input_output.stdout_writer.output_frames.lock().unwrap();
let snapshots = get_output_frame_snapshots(&output_frames, &fake_win_size);
for snapshot in snapshots {
assert_snapshot!(snapshot);
}
}
#[test]
pub fn vim_ctrl_u() {
// in vim ctrl-u moves up half a page
// in this case, it sends the terminal the csi 'L' directive, which tells it to insert X (13 in
// this case) lines at the cursor, pushing away (deleting) the last line in the scroll region
// this causes the effect of scrolling up X lines (vim replaces the lines with the ones in the
// file above the current content)
let fake_win_size = Winsize {
ws_col: 116,
ws_row: 28,
ws_xpixel: 0,
ws_ypixel: 0,
};
let fixture_name = "vim_ctrl_u";
let mut fake_input_output = get_fake_os_input(&fake_win_size, fixture_name);
fake_input_output.add_terminal_input(&[17]); // quit (ctrl-q)
start(Box::new(fake_input_output.clone()));
let output_frames = fake_input_output.stdout_writer.output_frames.lock().unwrap();
let snapshots = get_output_frame_snapshots(&output_frames, &fake_win_size);
for snapshot in snapshots {
assert_snapshot!(snapshot);
}
}

View file

@ -0,0 +1,32 @@
---
source: src/tests/integration/compatibility.rs
expression: snapshot
---
2 line15
1 line16
17 line17
1 line18
2 line19
3 line20
4 line21
5 line22
6 line23
7 line24
8 line25
9 line26
10 line27
11 line28
12 line29
13 line30
14 line31
15 line32
16 line33
17 line34
18 line35
19 line36
20 line37
21 line38
22 line39
NORMAL testfile.rs unix | utf-8 | rust 40% 17:1
"/tmp/testfile.rs" 42L, 285C
Bye from Mosaic!█

View file

@ -0,0 +1,32 @@
---
source: src/tests/integration/compatibility.rs
expression: snapshot
---
3 line14
2 line15
1 line16
17 █ine17
1 line18
2 line19
3 line20
4 line21
5 line22
6 line23
7 line24
8 line25
9 line26
10 line27
11 line28
12 line29
13 line30
14 line31
15 line32
16 line33
17 line34
18 line35
19 line36
20 line37
21 line38
22 line39
NORMAL testfile.rs unix | utf-8 | rust 40% 17:1
"/tmp/testfile.rs" 42L, 285C

View file

@ -0,0 +1,32 @@
---
source: src/tests/integration/compatibility.rs
expression: snapshot
---
21 line5
20 line6
19 line7
18 line8
17 line9
16 line10
15 line11
14 line12
13 line13
12 line14
11 line15
10 line16
9 line17
8 line18
7 line19
6 line20
5 line21
4 line22
3 line23
2 line24
1 line25
26 line26
1 line27
2 line28
3 line29
NORMAL testfile.rs unix | utf-8 | rust 61% 26:1
"/tmp/testfile.rs" 42L, 285C
Bye from Mosaic!█

View file

@ -0,0 +1,32 @@
---
source: src/tests/integration/compatibility.rs
expression: snapshot
---
22 line4
21 line5
20 line6
19 line7
18 line8
17 line9
16 line10
15 line11
14 line12
13 line13
12 line14
11 line15
10 line16
9 line17
8 line18
7 line19
6 line20
5 line21
4 line22
3 line23
2 line24
1 line25
26 █ine26
1 line27
2 line28
3 line29
NORMAL testfile.rs unix | utf-8 | rust 61% 26:1
"/tmp/testfile.rs" 42L, 285C

View file

@ -0,0 +1,32 @@
---
source: src/tests/integration/compatibility.rs
expression: snapshot
---
21 line3
20 line4
19 line5
18 line6
17 line7
16 line8
15 line9
14 line10
13 line11
12 line12
11 line13
10 line14
9 line15
8 line16
7 line17
6 line18
5 line19
4 line20
3 line21
2 line22
1 line23
24 line24
1 line25
2 line26
3 line27
NORMAL testfile.rs unix | utf-8 | rust 57% 24:1
"/tmp/testfile.rs" 42L, 285C
Bye from Mosaic!█

View file

@ -0,0 +1,32 @@
---
source: src/tests/integration/compatibility.rs
expression: snapshot
---
22 line2
21 line3
20 line4
19 line5
18 line6
17 line7
16 line8
15 line9
14 line10
13 line11
12 line12
11 line13
10 line14
9 line15
8 line16
7 line17
6 line18
5 line19
4 line20
3 line21
2 line22
1 line23
24 █ine24
1 line25
2 line26
3 line27
NORMAL testfile.rs unix | utf-8 | rust 57% 24:1
"/tmp/testfile.rs" 42L, 285C