feat(compatibility): vim working!
This commit is contained in:
parent
1844816f47
commit
5cd365a73c
11 changed files with 452 additions and 1 deletions
|
|
@ -159,6 +159,7 @@ pub struct TerminalOutput {
|
|||
cursor_position: usize,
|
||||
newline_indices: Vec<usize>, // canonical line breaks we get from the vt interpreter
|
||||
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_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
|
||||
|
|
@ -175,6 +176,7 @@ impl TerminalOutput {
|
|||
cursor_position: 0,
|
||||
newline_indices: Vec::new(),
|
||||
linebreak_indices: Vec::new(),
|
||||
scroll_region: (1, ws.ws_row as usize),
|
||||
display_rows: ws.ws_row,
|
||||
display_cols: ws.ws_col,
|
||||
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);
|
||||
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) {
|
||||
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() {
|
||||
self.newline_indices.push(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 {
|
||||
// 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
|
||||
// will be overriden as we print on it
|
||||
self.cursor_position = nearest_line_end;
|
||||
}
|
||||
self.should_render = true;
|
||||
self.pending_foreground_ansi_codes.clear();
|
||||
self.pending_background_ansi_codes.clear();
|
||||
self.pending_misc_ansi_codes.clear();
|
||||
self.should_render = true;
|
||||
}
|
||||
fn move_to_beginning_of_line (&mut self) {
|
||||
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 {
|
||||
fn print(&mut self, c: char) {
|
||||
|
||||
|
||||
// while not ideal that we separate the reset and actual code logic here,
|
||||
// combining them is a question of rendering performance and not refactoring,
|
||||
// so will be addressed separately
|
||||
|
|
@ -673,6 +832,29 @@ impl vte::Perform for TerminalOutput {
|
|||
// TBD
|
||||
} else if c == 'h' {
|
||||
// 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 {
|
||||
println!("unhandled csi: {:?}->{:?}", c, params);
|
||||
panic!("aaa!!!");
|
||||
|
|
|
|||
BIN
src/tests/fixtures/vim_ctrl_d
vendored
Normal file
BIN
src/tests/fixtures/vim_ctrl_d
vendored
Normal file
Binary file not shown.
BIN
src/tests/fixtures/vim_ctrl_u
vendored
Normal file
BIN
src/tests/fixtures/vim_ctrl_u
vendored
Normal file
Binary file not shown.
BIN
src/tests/fixtures/vim_scroll_region_down
vendored
Normal file
BIN
src/tests/fixtures/vim_scroll_region_down
vendored
Normal file
Binary file not shown.
|
|
@ -88,3 +88,80 @@ pub fn fish_select_tab_completion_options() {
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!█
|
||||
|
|
@ -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
|
||||
|
|
@ -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!█
|
||||
|
|
@ -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
|
||||
|
|
@ -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!█
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue