use crate::console::{ClearType, Console, Key, Position};
use crate::program::Program;
use async_trait::async_trait;
use std::cmp;
use std::io;
const TEXT_COLOR: (Option<u8>, Option<u8>) = (Some(15), None);
const STATUS_COLOR: (Option<u8>, Option<u8>) = (Some(15), Some(4));
fn copy_indent(line: &str) -> String {
let mut indent = String::new();
for ch in line.chars() {
if !ch.is_whitespace() {
break;
}
indent.push(ch);
}
indent
}
pub struct Editor {
content: Vec<String>,
viewport_pos: Position,
file_pos: Position,
insert_col: usize,
}
impl Editor {
pub fn new() -> Self {
Self {
content: vec![],
viewport_pos: Position { row: 0, column: 0 },
file_pos: Position { row: 0, column: 0 },
insert_col: 0,
}
}
fn refresh_status(&self, console: &mut dyn Console, console_size: Position) -> io::Result<()> {
let keys = " ESC Finish editing ";
let pos = format!(" | Ln {}, Col {} ", self.file_pos.row + 1, self.file_pos.column + 1);
let mut status = String::with_capacity(console_size.column);
status.push_str(keys);
while status.len() < console_size.column - pos.len() {
status.push(' ');
}
status.push_str(&pos);
status.truncate(console_size.column);
console.locate(Position { row: console_size.row - 1, column: 0 })?;
console.color(STATUS_COLOR.0, STATUS_COLOR.1)?;
console.write(status.as_bytes())?;
Ok(())
}
fn refresh(&self, console: &mut dyn Console, console_size: Position) -> io::Result<()> {
console.color(TEXT_COLOR.0, TEXT_COLOR.1)?;
console.clear(ClearType::All)?;
self.refresh_status(console, console_size)?;
console.color(TEXT_COLOR.0, TEXT_COLOR.1)?;
console.locate(Position { row: 0, column: 0 })?;
let mut row = self.viewport_pos.row;
let mut printed_rows = 0;
while row < self.content.len() && printed_rows < console_size.row - 1 {
let line = &self.content[row];
if line.len() >= self.viewport_pos.column {
let last = cmp::min(line.len(), self.viewport_pos.column + console_size.column);
let view = &line[self.viewport_pos.column..last];
console.print(view)?;
} else {
console.print("")?;
}
row += 1;
printed_rows += 1;
}
Ok(())
}
async fn edit_interactively(&mut self, console: &mut dyn Console) -> io::Result<()> {
let console_size = console.size()?;
if self.content.is_empty() {
self.content.push(String::new());
}
let mut need_refresh = true;
loop {
if self.file_pos.row < self.viewport_pos.row {
self.viewport_pos.row -= 1;
need_refresh = true;
} else if self.file_pos.row > self.viewport_pos.row + console_size.row - 2 {
self.viewport_pos.row += 1;
need_refresh = true;
}
if self.file_pos.column < self.viewport_pos.column {
self.viewport_pos.column = self.file_pos.column;
need_refresh = true;
} else if self.file_pos.column >= self.viewport_pos.column + console_size.column {
self.viewport_pos.column = self.file_pos.column - console_size.column + 1;
need_refresh = true;
}
console.hide_cursor()?;
if need_refresh {
self.refresh(console, console_size)?;
need_refresh = false;
} else {
self.refresh_status(console, console_size)?;
console.color(TEXT_COLOR.0, TEXT_COLOR.1)?;
}
let cursor_pos = self.file_pos - self.viewport_pos;
console.locate(cursor_pos)?;
console.show_cursor()?;
match console.read_key().await? {
Key::Escape | Key::Eof | Key::Interrupt => break,
Key::ArrowUp => {
if self.file_pos.row > 0 {
self.file_pos.row -= 1;
}
let line = &self.content[self.file_pos.row];
self.file_pos.column = cmp::min(self.insert_col, line.len());
}
Key::ArrowDown => {
if self.file_pos.row < self.content.len() - 1 {
self.file_pos.row += 1;
}
let line = &self.content[self.file_pos.row];
self.file_pos.column = cmp::min(self.insert_col, line.len());
}
Key::ArrowLeft => {
if self.file_pos.column > 0 {
self.file_pos.column -= 1;
self.insert_col = self.file_pos.column;
}
}
Key::ArrowRight => {
if self.file_pos.column < self.content[self.file_pos.row].len() {
self.file_pos.column += 1;
self.insert_col = self.file_pos.column;
}
}
Key::Backspace => {
if self.file_pos.column > 0 {
let line = &mut self.content[self.file_pos.row];
if self.file_pos.column == line.len() {
console.write(b"\x08 \x08")?;
} else {
need_refresh = true;
}
line.remove(self.file_pos.column - 1);
self.file_pos.column -= 1;
} else if self.file_pos.row > 0 {
let line = self.content.remove(self.file_pos.row);
let prev = &mut self.content[self.file_pos.row - 1];
self.file_pos.column = prev.len();
prev.push_str(&line);
self.file_pos.row -= 1;
need_refresh = true;
}
self.insert_col = self.file_pos.column;
}
Key::Char(ch) => {
let mut buf = [0; 4];
let line = &mut self.content[self.file_pos.row];
if self.file_pos.column + 1 < line.len() {
need_refresh = true;
}
line.insert(self.file_pos.column, ch);
self.file_pos.column += 1;
self.insert_col = self.file_pos.column;
if cursor_pos.column < console_size.column - 1 && !need_refresh {
console.write(ch.encode_utf8(&mut buf).as_bytes())?;
}
}
Key::NewLine | Key::CarriageReturn => {
let indent = copy_indent(&self.content[self.file_pos.row]);
let indent_len = indent.len();
if self.file_pos.row < self.content.len() - 1 {
let new = self.content[self.file_pos.row].split_off(self.file_pos.column);
self.content.insert(self.file_pos.row + 1, indent + &new);
need_refresh = true;
} else {
self.content.insert(self.file_pos.row + 1, indent);
}
self.file_pos.column = indent_len;
self.file_pos.row += 1;
self.insert_col = self.file_pos.column;
}
Key::Unknown(_) => (),
}
}
Ok(())
}
}
#[async_trait(?Send)]
impl Program for Editor {
async fn edit(&mut self, console: &mut dyn Console) -> io::Result<()> {
console.enter_alt()?;
let result = self.edit_interactively(console).await;
console.leave_alt()?;
result
}
fn load(&mut self, text: &str) {
self.content = text.lines().map(|l| l.to_owned()).collect();
self.viewport_pos = Position { row: 0, column: 0 };
self.file_pos = Position { row: 0, column: 0 };
self.insert_col = 0;
}
fn text(&self) -> String {
self.content.iter().fold(String::new(), |contents, line| contents + line + "\n")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::console::testutils::*;
use futures_lite::future::block_on;
fn rowcol(row: usize, column: usize) -> Position {
Position { row, column }
}
#[must_use]
struct OutputBuilder {
console_size: Position,
output: Vec<CapturedOut>,
}
impl OutputBuilder {
fn new(console_size: Position) -> Self {
Self { console_size, output: vec![CapturedOut::EnterAlt] }
}
fn refresh_status(mut self, file_pos: Position) -> Self {
let row = file_pos.row + 1;
let column = file_pos.column + 1;
self.output.push(CapturedOut::Locate(rowcol(self.console_size.row - 1, 0)));
self.output.push(CapturedOut::Color(STATUS_COLOR.0, STATUS_COLOR.1));
let mut status = String::from(" ESC Finish editing");
if row < 10 && column < 10 {
status += " ";
} else if row > 10 && column > 10 {
status += " ";
} else {
status += " ";
}
status += &format!("| Ln {}, Col {} ", row, column);
self.output.push(CapturedOut::Write(status.as_bytes().to_owned()));
self
}
fn quick_refresh(mut self, file_pos: Position, cursor: Position) -> Self {
self.output.push(CapturedOut::HideCursor);
self = self.refresh_status(file_pos);
self.output.push(CapturedOut::Color(TEXT_COLOR.0, TEXT_COLOR.1));
self.output.push(CapturedOut::Locate(cursor));
self.output.push(CapturedOut::ShowCursor);
self
}
fn refresh(mut self, file_pos: Position, previous: &[&str], cursor: Position) -> Self {
self.output.push(CapturedOut::HideCursor);
self.output.push(CapturedOut::Color(TEXT_COLOR.0, TEXT_COLOR.1));
self.output.push(CapturedOut::Clear(ClearType::All));
self = self.refresh_status(file_pos);
self.output.push(CapturedOut::Color(TEXT_COLOR.0, TEXT_COLOR.1));
self.output.push(CapturedOut::Locate(rowcol(0, 0)));
for line in previous {
self.output.push(CapturedOut::Print(line.to_string()));
}
self.output.push(CapturedOut::Locate(cursor));
self.output.push(CapturedOut::ShowCursor);
self
}
fn add(mut self, co: CapturedOut) -> Self {
self.output.push(co);
self
}
fn build(self) -> Vec<CapturedOut> {
let mut output = self.output;
output.push(CapturedOut::LeaveAlt);
output
}
}
fn run_editor(previous: &str, exp_text: &str, mut cb: MockConsoleBuilder, ob: OutputBuilder) {
let mut editor = Editor::new();
editor.load(previous);
cb = cb.add_input_keys(&[Key::Escape]);
let mut console = cb.build();
block_on(editor.edit(&mut console)).unwrap();
assert_eq!(exp_text, editor.text());
assert_eq!(ob.build(), console.captured_out());
}
#[test]
fn test_program_behavior() {
let mut editor = Editor::new();
assert!(editor.text().is_empty());
editor.load("some text\n and more\n");
assert_eq!("some text\n and more\n", editor.text());
editor.load("different\n");
assert_eq!("different\n", editor.text());
}
#[test]
fn test_force_trailing_newline() {
let mut editor = Editor::new();
assert!(editor.text().is_empty());
editor.load("missing\nnewline at eof");
assert_eq!("missing\nnewline at eof\n", editor.text());
}
#[test]
fn test_editing_with_previous_content_starts_on_top_left() {
let cb = MockConsoleBuilder::new().with_size(rowcol(10, 40));
let mut ob = OutputBuilder::new(rowcol(10, 40));
ob = ob.refresh(rowcol(0, 0), &["previous content"], rowcol(0, 0));
run_editor("previous content", "previous content\n", cb, ob);
}
#[test]
fn test_insert_in_empty_file() {
let mut cb = MockConsoleBuilder::new().with_size(rowcol(10, 40));
let mut ob = OutputBuilder::new(rowcol(10, 40));
ob = ob.refresh(rowcol(0, 0), &[""], rowcol(0, 0));
cb = cb.add_input_chars("abc");
ob = ob.add(CapturedOut::Write(b"a".to_vec()));
ob = ob.quick_refresh(rowcol(0, 1), rowcol(0, 1));
ob = ob.add(CapturedOut::Write(b"b".to_vec()));
ob = ob.quick_refresh(rowcol(0, 2), rowcol(0, 2));
ob = ob.add(CapturedOut::Write(b"c".to_vec()));
ob = ob.quick_refresh(rowcol(0, 3), rowcol(0, 3));
cb = cb.add_input_keys(&[Key::NewLine]);
ob = ob.quick_refresh(rowcol(1, 0), rowcol(1, 0));
cb = cb.add_input_keys(&[Key::CarriageReturn]);
ob = ob.quick_refresh(rowcol(2, 0), rowcol(2, 0));
cb = cb.add_input_chars("2");
ob = ob.add(CapturedOut::Write(b"2".to_vec()));
ob = ob.quick_refresh(rowcol(2, 1), rowcol(2, 1));
run_editor("", "abc\n\n2\n", cb, ob);
}
#[test]
fn test_insert_before_previous_content() {
let mut cb = MockConsoleBuilder::new().with_size(rowcol(10, 40));
let mut ob = OutputBuilder::new(rowcol(10, 40));
ob = ob.refresh(rowcol(0, 0), &["previous content"], rowcol(0, 0));
cb = cb.add_input_chars("a");
ob = ob.refresh(rowcol(0, 1), &["aprevious content"], rowcol(0, 1));
cb = cb.add_input_chars("b");
ob = ob.refresh(rowcol(0, 2), &["abprevious content"], rowcol(0, 2));
cb = cb.add_input_chars("c");
ob = ob.refresh(rowcol(0, 3), &["abcprevious content"], rowcol(0, 3));
cb = cb.add_input_chars(" ");
ob = ob.refresh(rowcol(0, 4), &["abc previous content"], rowcol(0, 4));
run_editor("previous content", "abc previous content\n", cb, ob);
}
#[test]
fn test_move_in_empty_file() {
let mut cb = MockConsoleBuilder::new().with_size(rowcol(10, 40));
let mut ob = OutputBuilder::new(rowcol(10, 40));
ob = ob.refresh(rowcol(0, 0), &[""], rowcol(0, 0));
for k in &[Key::ArrowUp, Key::ArrowDown, Key::ArrowLeft, Key::ArrowRight] {
cb = cb.add_input_keys(&[k.clone()]);
ob = ob.quick_refresh(rowcol(0, 0), rowcol(0, 0));
}
run_editor("", "\n", cb, ob);
}
#[test]
fn test_move_preserves_insertion_column() {
let mut cb = MockConsoleBuilder::new().with_size(rowcol(10, 40));
let mut ob = OutputBuilder::new(rowcol(10, 40));
ob = ob.refresh(rowcol(0, 0), &["longer", "a", "longer", "b"], rowcol(0, 0));
cb = cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.quick_refresh(rowcol(0, 1), rowcol(0, 1));
cb = cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.quick_refresh(rowcol(0, 2), rowcol(0, 2));
cb = cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.quick_refresh(rowcol(0, 3), rowcol(0, 3));
cb = cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.quick_refresh(rowcol(0, 4), rowcol(0, 4));
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(rowcol(1, 1), rowcol(1, 1));
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(rowcol(2, 4), rowcol(2, 4));
cb = cb.add_input_keys(&[Key::Char('X')]);
ob = ob.refresh(rowcol(2, 5), &["longer", "a", "longXer", "b"], rowcol(2, 5));
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(rowcol(3, 1), rowcol(3, 1));
cb = cb.add_input_keys(&[Key::Char('Z')]);
ob = ob.add(CapturedOut::Write(b"Z".to_vec()));
ob = ob.quick_refresh(rowcol(3, 2), rowcol(3, 2));
run_editor("longer\na\nlonger\nb\n", "longer\na\nlongXer\nbZ\n", cb, ob);
}
#[test]
fn test_move_down_preserves_insertion_column_with_horizontal_scrolling() {
let mut cb = MockConsoleBuilder::new().with_size(rowcol(10, 40));
let mut ob = OutputBuilder::new(rowcol(10, 40));
ob = ob.refresh(
rowcol(0, 0),
&[
"this is a line of text with more than 40",
"short",
"a",
"",
"another line of text with more than 40 c",
],
rowcol(0, 0),
);
for col in 0..39 {
cb = cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.quick_refresh(rowcol(0, col + 1), rowcol(0, col + 1));
}
cb = cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.refresh(
rowcol(0, 40),
&[
"his is a line of text with more than 40 ",
"hort",
"",
"",
"nother line of text with more than 40 ch",
],
rowcol(0, 39),
);
cb = cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.refresh(
rowcol(0, 41),
&[
"is is a line of text with more than 40 c",
"ort",
"",
"",
"other line of text with more than 40 cha",
],
rowcol(0, 39),
);
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(rowcol(1, 5), rowcol(1, 3));
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.refresh(
rowcol(2, 1),
&[
"his is a line of text with more than 40 ",
"hort",
"",
"",
"nother line of text with more than 40 ch",
],
rowcol(2, 0),
);
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.refresh(
rowcol(3, 0),
&[
"this is a line of text with more than 40",
"short",
"a",
"",
"another line of text with more than 40 c",
],
rowcol(3, 0),
);
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.refresh(
rowcol(4, 41),
&[
"is is a line of text with more than 40 c",
"ort",
"",
"",
"other line of text with more than 40 cha",
],
rowcol(4, 39),
);
run_editor(
"this is a line of text with more than 40 characters\nshort\na\n\nanother line of text with more than 40 characters\n",
"this is a line of text with more than 40 characters\nshort\na\n\nanother line of text with more than 40 characters\n",
cb,
ob);
}
#[test]
fn test_move_up_preserves_insertion_column_with_horizontal_scrolling() {
let mut cb = MockConsoleBuilder::new().with_size(rowcol(10, 40));
let mut ob = OutputBuilder::new(rowcol(10, 40));
ob = ob.refresh(
rowcol(0, 0),
&[
"this is a line of text with more than 40",
"",
"a",
"short",
"another line of text with more than 40 c",
],
rowcol(0, 0),
);
for i in 0..4 {
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(rowcol(i + 1, 0), rowcol(i + 1, 0));
}
for col in 0..39 {
cb = cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.quick_refresh(rowcol(4, col + 1), rowcol(4, col + 1));
}
cb = cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.refresh(
rowcol(4, 40),
&[
"his is a line of text with more than 40 ",
"",
"",
"hort",
"nother line of text with more than 40 ch",
],
rowcol(4, 39),
);
cb = cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.refresh(
rowcol(4, 41),
&[
"is is a line of text with more than 40 c",
"",
"",
"ort",
"other line of text with more than 40 cha",
],
rowcol(4, 39),
);
cb = cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.quick_refresh(rowcol(3, 5), rowcol(3, 3));
cb = cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.refresh(
rowcol(2, 1),
&[
"his is a line of text with more than 40 ",
"",
"",
"hort",
"nother line of text with more than 40 ch",
],
rowcol(2, 0),
);
cb = cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.refresh(
rowcol(1, 0),
&[
"this is a line of text with more than 40",
"",
"a",
"short",
"another line of text with more than 40 c",
],
rowcol(1, 0),
);
cb = cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.refresh(
rowcol(0, 41),
&[
"is is a line of text with more than 40 c",
"",
"",
"ort",
"other line of text with more than 40 cha",
],
rowcol(0, 39),
);
run_editor(
"this is a line of text with more than 40 characters\n\na\nshort\nanother line of text with more than 40 characters\n",
"this is a line of text with more than 40 characters\n\na\nshort\nanother line of text with more than 40 characters\n",
cb,
ob);
}
#[test]
fn test_horizontal_scrolling() {
let mut cb = MockConsoleBuilder::new().with_size(rowcol(10, 40));
let mut ob = OutputBuilder::new(rowcol(10, 40));
ob = ob.refresh(rowcol(0, 0), &["ab", "", "xyz"], rowcol(0, 0));
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(rowcol(1, 0), rowcol(1, 0));
for (col, ch) in b"123456789012345678901234567890123456789".iter().enumerate() {
cb = cb.add_input_keys(&[Key::Char(*ch as char)]);
ob = ob.add(CapturedOut::Write([*ch].to_vec()));
ob = ob.quick_refresh(rowcol(1, col + 1), rowcol(1, col + 1));
}
cb = cb.add_input_keys(&[Key::Char('A')]);
ob = ob.refresh(
rowcol(1, 40),
&["b", "23456789012345678901234567890123456789A", "yz"],
rowcol(1, 39),
);
cb = cb.add_input_keys(&[Key::Char('B')]);
ob = ob.refresh(
rowcol(1, 41),
&["", "3456789012345678901234567890123456789AB", "z"],
rowcol(1, 39),
);
cb = cb.add_input_keys(&[Key::Char('C')]);
ob = ob.refresh(
rowcol(1, 42),
&["", "456789012345678901234567890123456789ABC", ""],
rowcol(1, 39),
);
for (file_col, cursor_col) in &[(41, 38), (40, 37), (39, 36)] {
cb = cb.add_input_keys(&[Key::ArrowLeft]);
ob = ob.quick_refresh(rowcol(1, *file_col), rowcol(1, *cursor_col));
}
cb = cb.add_input_keys(&[Key::Char('D')]);
ob = ob.refresh(
rowcol(1, 40),
&["", "456789012345678901234567890123456789DABC", ""],
rowcol(1, 37),
);
cb = cb.add_input_keys(&[Key::Char('E')]);
ob = ob.refresh(
rowcol(1, 41),
&["", "456789012345678901234567890123456789DEAB", ""],
rowcol(1, 38),
);
cb = cb.add_input_keys(&[Key::Backspace]);
ob = ob.refresh(
rowcol(1, 40),
&["", "456789012345678901234567890123456789DABC", ""],
rowcol(1, 37),
);
cb = cb.add_input_keys(&[Key::Backspace]);
ob = ob.refresh(
rowcol(1, 39),
&["", "456789012345678901234567890123456789ABC", ""],
rowcol(1, 36),
);
cb = cb.add_input_keys(&[Key::Backspace]);
ob = ob.refresh(
rowcol(1, 38),
&["", "45678901234567890123456789012345678ABC", ""],
rowcol(1, 35),
);
for col in 0..35 {
cb = cb.add_input_keys(&[Key::ArrowLeft]);
ob = ob.quick_refresh(rowcol(1, 37 - col), rowcol(1, 34 - col));
}
cb = cb.add_input_keys(&[Key::ArrowLeft]);
ob = ob.refresh(
rowcol(1, 2),
&["", "345678901234567890123456789012345678ABC", "z"],
rowcol(1, 0),
);
cb = cb.add_input_keys(&[Key::ArrowLeft]);
ob = ob.refresh(
rowcol(1, 1),
&["b", "2345678901234567890123456789012345678ABC", "yz"],
rowcol(1, 0),
);
cb = cb.add_input_keys(&[Key::ArrowLeft]);
ob = ob.refresh(
rowcol(1, 0),
&["ab", "12345678901234567890123456789012345678AB", "xyz"],
rowcol(1, 0),
);
run_editor("ab\n\nxyz\n", "ab\n12345678901234567890123456789012345678ABC\nxyz\n", cb, ob);
}
#[test]
fn test_vertical_scrolling() {
let mut cb = MockConsoleBuilder::new().with_size(rowcol(5, 40));
let mut ob = OutputBuilder::new(rowcol(5, 40));
ob = ob.refresh(rowcol(0, 0), &["abc", "", "d", "e"], rowcol(0, 0));
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(rowcol(1, 0), rowcol(1, 0));
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(rowcol(2, 0), rowcol(2, 0));
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(rowcol(3, 0), rowcol(3, 0));
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.refresh(rowcol(4, 0), &["", "d", "e", ""], rowcol(3, 0));
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.refresh(rowcol(5, 0), &["d", "e", "", "fg"], rowcol(3, 0));
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.refresh(rowcol(6, 0), &["e", "", "fg", "hij"], rowcol(3, 0));
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(rowcol(6, 0), rowcol(3, 0));
cb = cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.quick_refresh(rowcol(5, 0), rowcol(2, 0));
cb = cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.quick_refresh(rowcol(4, 0), rowcol(1, 0));
cb = cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.quick_refresh(rowcol(3, 0), rowcol(0, 0));
cb = cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.refresh(rowcol(2, 0), &["d", "e", "", "fg"], rowcol(0, 0));
cb = cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.refresh(rowcol(1, 0), &["", "d", "e", ""], rowcol(0, 0));
cb = cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.refresh(rowcol(0, 0), &["abc", "", "d", "e"], rowcol(0, 0));
cb = cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.quick_refresh(rowcol(0, 0), rowcol(0, 0));
run_editor("abc\n\nd\ne\n\nfg\nhij\n", "abc\n\nd\ne\n\nfg\nhij\n", cb, ob);
}
#[test]
fn test_vertical_scrolling_when_splitting_last_visible_line() {
let mut cb = MockConsoleBuilder::new().with_size(rowcol(4, 40));
let mut ob = OutputBuilder::new(rowcol(4, 40));
ob = ob.refresh(rowcol(0, 0), &["first", "second", "thirdfourth"], rowcol(0, 0));
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(rowcol(1, 0), rowcol(1, 0));
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(rowcol(2, 0), rowcol(2, 0));
for i in 0.."third".len() {
cb = cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.quick_refresh(rowcol(2, i + 1), rowcol(2, i + 1));
}
cb = cb.add_input_keys(&[Key::NewLine]);
ob = ob.refresh(rowcol(3, 0), &["second", "third", "fourth"], rowcol(2, 0));
run_editor(
"first\nsecond\nthirdfourth\nfifth\n",
"first\nsecond\nthird\nfourth\nfifth\n",
cb,
ob,
);
}
#[test]
fn test_horizontal_and_vertical_scrolling_when_splitting_last_visible_line() {
let mut cb = MockConsoleBuilder::new().with_size(rowcol(4, 40));
let mut ob = OutputBuilder::new(rowcol(4, 40));
ob = ob.refresh(
rowcol(0, 0),
&["first", "second", "this is a line of text with more than 40"],
rowcol(0, 0),
);
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(rowcol(1, 0), rowcol(1, 0));
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(rowcol(2, 0), rowcol(2, 0));
for i in 0..39 {
cb = cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.quick_refresh(rowcol(2, i + 1), rowcol(2, i + 1));
}
cb = cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.refresh(
rowcol(2, 40),
&["irst", "econd", "his is a line of text with more than 40 "],
rowcol(2, 39),
);
cb = cb.add_input_keys(&[Key::NewLine]);
ob = ob.refresh(
rowcol(3, 0),
&["second", "this is a line of text with more than 40", " characters"],
rowcol(2, 0),
);
run_editor(
"first\nsecond\nthis is a line of text with more than 40 characters\nfifth\n",
"first\nsecond\nthis is a line of text with more than 40\n characters\nfifth\n",
cb,
ob,
);
}
#[test]
fn test_vertical_scrolling_when_joining_first_visible_line() {
let mut cb = MockConsoleBuilder::new().with_size(rowcol(4, 40));
let mut ob = OutputBuilder::new(rowcol(4, 40));
ob = ob.refresh(rowcol(0, 0), &["first", "second", "third"], rowcol(0, 0));
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(rowcol(1, 0), rowcol(1, 0));
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(rowcol(2, 0), rowcol(2, 0));
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.refresh(rowcol(3, 0), &["second", "third", "fourth"], rowcol(2, 0));
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.refresh(rowcol(4, 0), &["third", "fourth", "fifth"], rowcol(2, 0));
cb = cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.quick_refresh(rowcol(3, 0), rowcol(1, 0));
cb = cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.quick_refresh(rowcol(2, 0), rowcol(0, 0));
cb = cb.add_input_keys(&[Key::Backspace]);
ob = ob.refresh(rowcol(1, 6), &["secondthird", "fourth", "fifth"], rowcol(0, 6));
run_editor(
"first\nsecond\nthird\nfourth\nfifth\n",
"first\nsecondthird\nfourth\nfifth\n",
cb,
ob,
);
}
#[test]
fn test_horizontal_and_vertical_scrolling_when_joining_first_visible_line() {
let mut cb = MockConsoleBuilder::new().with_size(rowcol(4, 40));
let mut ob = OutputBuilder::new(rowcol(4, 40));
ob = ob.refresh(
rowcol(0, 0),
&["first", "this is a line of text with more than 40", "third"],
rowcol(0, 0),
);
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(rowcol(1, 0), rowcol(1, 0));
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(rowcol(2, 0), rowcol(2, 0));
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.refresh(
rowcol(3, 0),
&["this is a line of text with more than 40", "third", "fourth"],
rowcol(2, 0),
);
cb = cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.refresh(rowcol(4, 0), &["third", "fourth", "quite a long line"], rowcol(2, 0));
cb = cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.quick_refresh(rowcol(3, 0), rowcol(1, 0));
cb = cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.quick_refresh(rowcol(2, 0), rowcol(0, 0));
cb = cb.add_input_keys(&[Key::Backspace]);
ob = ob.refresh(
rowcol(1, 51),
&["ne of text with more than 40 characterst", "", " line"],
rowcol(0, 39),
);
run_editor(
"first\nthis is a line of text with more than 40 characters\nthird\nfourth\nquite a long line\n",
"first\nthis is a line of text with more than 40 charactersthird\nfourth\nquite a long line\n",
cb,
ob,
);
}
}