use crate::console::{CharsXY, ClearType, Console, Key};
use async_trait::async_trait;
use endbasic_std::console::{AnsiColor, LineBuffer};
use endbasic_std::program::Program;
use std::cmp;
use std::convert::TryFrom;
use std::io;
const TEXT_COLOR: (Option<u8>, Option<u8>) = (Some(AnsiColor::White as u8), None);
const STATUS_COLOR: (Option<u8>, Option<u8>) =
(Some(AnsiColor::BrightWhite as u8), Some(AnsiColor::Blue as u8));
const INDENT_WIDTH: usize = 4;
const KEYS_SUMMARY: &str = " ESC Exit ";
fn copy_indent(line: &LineBuffer) -> String {
let mut indent = String::new();
for ch in line.chars() {
if !ch.is_whitespace() {
break;
}
indent.push(ch);
}
indent
}
fn find_indent_end(line: &LineBuffer) -> usize {
let mut pos = 0;
for ch in line.chars() {
if ch != ' ' {
break;
}
pos += 1;
}
debug_assert!(pos <= line.len());
pos
}
#[derive(Clone, Copy, Default)]
struct FilePos {
line: usize,
col: usize,
}
pub struct Editor {
name: Option<String>,
content: Vec<LineBuffer>,
dirty: bool,
viewport_pos: FilePos,
file_pos: FilePos,
insert_col: usize,
}
impl Default for Editor {
fn default() -> Self {
Self {
name: None,
content: vec![],
dirty: false,
viewport_pos: FilePos::default(),
file_pos: FilePos::default(),
insert_col: 0,
}
}
}
impl Editor {
fn refresh_status(&self, console: &mut dyn Console, console_size: CharsXY) -> io::Result<()> {
let dirty_marker = if self.dirty { "*" } else { "" };
let long_details = format!(
" | {}{} | Ln {}, Col {} ",
self.name.as_deref().unwrap_or("<NO NAME>"),
dirty_marker,
self.file_pos.line + 1,
self.file_pos.col + 1
);
let width = usize::from(console_size.x);
let mut status = String::with_capacity(width);
if KEYS_SUMMARY.len() + long_details.len() >= width {
let short_details = format!(" {}:{} ", self.file_pos.line + 1, self.file_pos.col + 1);
if short_details.len() < width {
while status.len() < width - short_details.len() {
status.push(' ');
}
}
status.push_str(&short_details);
} else {
status.push_str(KEYS_SUMMARY);
while status.len() < width - long_details.len() {
status.push(' ');
}
status.push_str(&long_details);
}
status.truncate(width);
console.locate(CharsXY::new(0, console_size.y - 1))?;
console.set_color(STATUS_COLOR.0, STATUS_COLOR.1)?;
console.write(&status)?;
Ok(())
}
fn refresh(&self, console: &mut dyn Console, console_size: CharsXY) -> io::Result<()> {
console.set_color(TEXT_COLOR.0, TEXT_COLOR.1)?;
console.clear(ClearType::All)?;
self.refresh_status(console, console_size)?;
console.set_color(TEXT_COLOR.0, TEXT_COLOR.1)?;
console.locate(CharsXY::default())?;
let mut row = self.viewport_pos.line;
let mut printed_rows = 0;
while row < self.content.len() && printed_rows < console_size.y - 1 {
let line = &self.content[row];
let line_len = line.len();
if line_len > self.viewport_pos.col {
console.print(&line.range(
self.viewport_pos.col,
self.viewport_pos.col + usize::from(console_size.x),
))?;
} else {
console.print("")?;
}
row += 1;
printed_rows += 1;
}
Ok(())
}
fn move_down(&mut self, nlines: usize) {
if self.file_pos.line + nlines < self.content.len() {
self.file_pos.line += nlines;
} else {
self.file_pos.line = self.content.len() - 1;
}
let line = &self.content[self.file_pos.line];
self.file_pos.col = cmp::min(self.insert_col, line.len());
}
fn move_up(&mut self, nlines: usize) {
if self.file_pos.line > nlines {
self.file_pos.line -= nlines;
} else {
self.file_pos.line = 0;
}
let line = &self.content[self.file_pos.line];
self.file_pos.col = cmp::min(self.insert_col, line.len());
}
async fn edit_interactively(&mut self, console: &mut dyn Console) -> io::Result<()> {
let console_size = console.size_chars()?;
if self.content.is_empty() {
self.content.push(LineBuffer::default());
}
let mut need_refresh = true;
loop {
let width = usize::from(console_size.x);
let height = usize::from(console_size.y);
if self.file_pos.line < self.viewport_pos.line {
self.viewport_pos.line = self.file_pos.line;
need_refresh = true;
} else if self.file_pos.line > self.viewport_pos.line + height - 2 {
if self.file_pos.line > height - 2 {
self.viewport_pos.line = self.file_pos.line - (height - 2);
} else {
self.viewport_pos.line = 0;
}
need_refresh = true;
}
if self.file_pos.col < self.viewport_pos.col {
self.viewport_pos.col = self.file_pos.col;
need_refresh = true;
} else if self.file_pos.col >= self.viewport_pos.col + width {
self.viewport_pos.col = self.file_pos.col - width + 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.set_color(TEXT_COLOR.0, TEXT_COLOR.1)?;
}
let cursor_pos = {
let x = self.file_pos.col - self.viewport_pos.col;
let y = self.file_pos.line - self.viewport_pos.line;
if cfg!(debug_assertions) {
CharsXY::new(
u16::try_from(x).expect("Computed x must have fit on screen"),
u16::try_from(y).expect("Computed y must have fit on screen"),
)
} else {
CharsXY::new(x as u16, y as u16)
}
};
console.locate(cursor_pos)?;
console.show_cursor()?;
console.sync_now()?;
match console.read_key().await? {
Key::Escape | Key::Eof | Key::Interrupt => break,
Key::ArrowUp => self.move_up(1),
Key::ArrowDown => self.move_down(1),
Key::ArrowLeft => {
if self.file_pos.col > 0 {
self.file_pos.col -= 1;
self.insert_col = self.file_pos.col;
}
}
Key::ArrowRight => {
if self.file_pos.col < self.content[self.file_pos.line].len() {
self.file_pos.col += 1;
self.insert_col = self.file_pos.col;
}
}
Key::Backspace => {
if self.file_pos.col > 0 {
let line = &mut self.content[self.file_pos.line];
let indent_pos = find_indent_end(line);
let is_indent = indent_pos >= self.file_pos.col;
let nremove = if is_indent {
let new_pos = if self.file_pos.col >= INDENT_WIDTH {
(self.file_pos.col - 1) / INDENT_WIDTH * INDENT_WIDTH
} else {
0
};
self.file_pos.col - new_pos
} else {
1
};
if self.file_pos.col == line.len() {
if nremove > 0 {
console.hide_cursor()?;
}
for _ in 0..nremove {
console.clear(ClearType::PreviousChar)?;
}
if nremove > 0 {
console.show_cursor()?;
}
} else {
need_refresh = true;
}
for _ in 0..nremove {
line.remove(self.file_pos.col - 1);
self.file_pos.col -= 1;
}
if nremove > 0 {
self.dirty = true;
}
} else if self.file_pos.line > 0 {
let line = self.content.remove(self.file_pos.line);
let prev = &mut self.content[self.file_pos.line - 1];
self.file_pos.col = prev.len();
prev.push_str(&line);
self.file_pos.line -= 1;
need_refresh = true;
self.dirty = true;
}
self.insert_col = self.file_pos.col;
}
Key::Char(ch) => {
let mut buf = [0; 4];
let line = &mut self.content[self.file_pos.line];
if self.file_pos.col < line.len() {
need_refresh = true;
}
line.insert(self.file_pos.col, ch);
self.file_pos.col += 1;
self.insert_col = self.file_pos.col;
if cursor_pos.x < console_size.x - 1 && !need_refresh {
console.write(ch.encode_utf8(&mut buf))?;
}
self.dirty = true;
}
Key::End => {
self.file_pos.col = self.content[self.file_pos.line].len();
self.insert_col = self.file_pos.col;
}
Key::Home => {
let indent_pos = find_indent_end(&self.content[self.file_pos.line]);
if self.file_pos.col == indent_pos {
self.file_pos.col = 0;
} else {
self.file_pos.col = indent_pos;
}
self.insert_col = self.file_pos.col;
}
Key::NewLine | Key::CarriageReturn => {
let indent = copy_indent(&self.content[self.file_pos.line]);
let indent_len = indent.len();
let appending = (self.file_pos.line + 1 == self.content.len())
&& (self.file_pos.col == self.content[self.file_pos.line].len());
let new = self.content[self.file_pos.line].split_off(self.file_pos.col);
self.content.insert(
self.file_pos.line + 1,
LineBuffer::from(indent + &new.into_inner()),
);
need_refresh = !appending;
self.file_pos.col = indent_len;
self.file_pos.line += 1;
self.insert_col = self.file_pos.col;
self.dirty = true;
}
Key::PageDown => self.move_down(usize::from(console_size.y - 2)),
Key::PageUp => self.move_up(usize::from(console_size.y - 2)),
Key::Tab => {
let line = &mut self.content[self.file_pos.line];
if self.file_pos.col < line.len() {
need_refresh = true;
}
let new_pos = (self.file_pos.col + INDENT_WIDTH) / INDENT_WIDTH * INDENT_WIDTH;
let mut new_text = String::with_capacity(new_pos - self.file_pos.col);
for _ in 0..new_text.capacity() {
new_text.push(' ');
}
line.insert_str(self.file_pos.col, &new_text);
self.file_pos.col = new_pos;
self.insert_col = self.file_pos.col;
if !need_refresh {
console.write(&new_text)?;
}
self.dirty = true;
}
Key::Unknown(_) => (),
}
}
Ok(())
}
}
#[async_trait(?Send)]
impl Program for Editor {
fn is_dirty(&self) -> bool {
self.dirty
}
async fn edit(&mut self, console: &mut dyn Console) -> io::Result<()> {
console.enter_alt()?;
let previous = console.set_sync(false)?;
let result = self.edit_interactively(console).await;
console.set_sync(previous)?;
console.leave_alt()?;
result
}
fn load(&mut self, name: Option<&str>, text: &str) {
self.name = name.map(str::to_owned);
self.content = text.lines().map(LineBuffer::from).collect();
self.dirty = false;
self.viewport_pos = FilePos::default();
self.file_pos = FilePos::default();
self.insert_col = 0;
}
fn name(&self) -> Option<&str> {
self.name.as_deref()
}
fn set_name(&mut self, name: &str) {
self.name = Some(name.to_owned());
self.dirty = false;
}
fn text(&self) -> String {
self.content
.iter()
.fold(String::new(), |contents, line| contents + &line.to_string() + "\n")
}
}
#[cfg(test)]
mod tests {
use super::*;
use endbasic_std::testutils::*;
use futures_lite::future::block_on;
const TEST_FILENAME: &str = "X";
fn yx(y: u16, x: u16) -> CharsXY {
CharsXY::new(x, y)
}
fn linecol(line: usize, col: usize) -> FilePos {
FilePos { line, col }
}
#[must_use]
struct OutputBuilder {
console_size: CharsXY,
output: Vec<CapturedOut>,
dirty: bool,
}
impl OutputBuilder {
fn new(console_size: CharsXY) -> Self {
Self {
console_size,
output: vec![CapturedOut::EnterAlt, CapturedOut::SetSync(false)],
dirty: false,
}
}
fn refresh_status(mut self, file_pos: FilePos) -> Self {
let row = file_pos.line + 1;
let column = file_pos.col + 1;
self.output.push(CapturedOut::Locate(yx(self.console_size.y - 1, 0)));
self.output.push(CapturedOut::SetColor(STATUS_COLOR.0, STATUS_COLOR.1));
if self.console_size.x < 30 {
let details = &format!(" {}:{} ", row, column);
let mut status = String::new();
while status.len() + details.len() < usize::from(self.console_size.x) {
status.push(' ');
}
status += details;
status.truncate(usize::from(self.console_size.x));
self.output.push(CapturedOut::Write(status));
} else {
let dirty_marker = if self.dirty { "*" } else { "" };
let details =
&format!("| {}{} | Ln {}, Col {} ", TEST_FILENAME, dirty_marker, row, column);
let mut status = String::from(KEYS_SUMMARY);
while status.len() + details.len() < usize::from(self.console_size.x) {
status.push(' ');
}
status += details;
self.output.push(CapturedOut::Write(status));
}
self
}
fn quick_refresh(mut self, file_pos: FilePos, cursor: CharsXY) -> Self {
self.output.push(CapturedOut::HideCursor);
self = self.refresh_status(file_pos);
self.output.push(CapturedOut::SetColor(TEXT_COLOR.0, TEXT_COLOR.1));
self.output.push(CapturedOut::Locate(cursor));
self.output.push(CapturedOut::ShowCursor);
self.output.push(CapturedOut::SyncNow);
self
}
fn refresh(mut self, file_pos: FilePos, previous: &[&str], cursor: CharsXY) -> Self {
self.output.push(CapturedOut::HideCursor);
self.output.push(CapturedOut::SetColor(TEXT_COLOR.0, TEXT_COLOR.1));
self.output.push(CapturedOut::Clear(ClearType::All));
self = self.refresh_status(file_pos);
self.output.push(CapturedOut::SetColor(TEXT_COLOR.0, TEXT_COLOR.1));
self.output.push(CapturedOut::Locate(yx(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.output.push(CapturedOut::SyncNow);
self
}
fn add(mut self, co: CapturedOut) -> Self {
self.output.push(co);
self
}
fn set_dirty(mut self) -> Self {
self.dirty = true;
self
}
fn build(self) -> Vec<CapturedOut> {
let mut output = self.output;
output.push(CapturedOut::SetSync(true));
output.push(CapturedOut::LeaveAlt);
output
}
}
fn run_editor(previous: &str, exp_text: &str, mut console: MockConsole, ob: OutputBuilder) {
let mut editor = Editor::default();
editor.load(Some(TEST_FILENAME), previous);
console.add_input_keys(&[Key::Escape]);
block_on(editor.edit(&mut console)).unwrap();
assert_eq!(exp_text, editor.text());
assert_eq!(ob.dirty, editor.is_dirty());
assert_eq!(ob.build(), console.captured_out());
}
#[test]
fn test_program_behavior() {
let mut editor = Editor::default();
assert!(editor.text().is_empty());
assert!(!editor.is_dirty());
let mut console = MockConsole::default();
console.set_size_chars(yx(10, 40));
block_on(editor.edit(&mut console)).unwrap();
assert!(!editor.is_dirty());
console.add_input_keys(&[Key::Char('x')]);
block_on(editor.edit(&mut console)).unwrap();
assert!(editor.is_dirty());
editor.load(Some(TEST_FILENAME), "some text\n and more\n");
assert_eq!("some text\n and more\n", editor.text());
assert!(!editor.is_dirty());
editor.load(Some(TEST_FILENAME), "different\n");
assert_eq!("different\n", editor.text());
assert!(!editor.is_dirty());
console.add_input_keys(&[Key::Char('x')]);
block_on(editor.edit(&mut console)).unwrap();
assert!(editor.is_dirty());
editor.set_name("SAVED");
assert!(!editor.is_dirty());
}
#[test]
fn test_force_trailing_newline() {
let mut editor = Editor::default();
assert!(editor.text().is_empty());
editor.load(Some(TEST_FILENAME), "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 mut cb = MockConsole::default();
cb.set_size_chars(yx(10, 40));
let mut ob = OutputBuilder::new(yx(10, 40));
ob = ob.refresh(linecol(0, 0), &["previous content"], yx(0, 0));
run_editor("previous content", "previous content\n", cb, ob);
}
#[test]
fn test_insert_in_empty_file() {
let mut cb = MockConsole::default();
cb.set_size_chars(yx(10, 40));
let mut ob = OutputBuilder::new(yx(10, 40));
ob = ob.refresh(linecol(0, 0), &[""], yx(0, 0));
cb.add_input_chars("abcéà ");
ob = ob.set_dirty();
ob = ob.add(CapturedOut::Write("a".to_string()));
ob = ob.quick_refresh(linecol(0, 1), yx(0, 1));
ob = ob.add(CapturedOut::Write("b".to_string()));
ob = ob.quick_refresh(linecol(0, 2), yx(0, 2));
ob = ob.add(CapturedOut::Write("c".to_string()));
ob = ob.quick_refresh(linecol(0, 3), yx(0, 3));
ob = ob.add(CapturedOut::Write("é".to_string()));
ob = ob.quick_refresh(linecol(0, 4), yx(0, 4));
ob = ob.add(CapturedOut::Write("Ã ".to_string()));
ob = ob.quick_refresh(linecol(0, 5), yx(0, 5));
cb.add_input_keys(&[Key::NewLine]);
ob = ob.quick_refresh(linecol(1, 0), yx(1, 0));
cb.add_input_keys(&[Key::CarriageReturn]);
ob = ob.quick_refresh(linecol(2, 0), yx(2, 0));
cb.add_input_chars("2");
ob = ob.add(CapturedOut::Write("2".to_string()));
ob = ob.quick_refresh(linecol(2, 1), yx(2, 1));
run_editor("", "abcéà \n\n2\n", cb, ob);
}
#[test]
fn test_insert_before_previous_content() {
let mut cb = MockConsole::default();
cb.set_size_chars(yx(10, 40));
let mut ob = OutputBuilder::new(yx(10, 40));
ob = ob.refresh(linecol(0, 0), &["previous content"], yx(0, 0));
cb.add_input_chars("a");
ob = ob.set_dirty();
ob = ob.refresh(linecol(0, 1), &["aprevious content"], yx(0, 1));
cb.add_input_chars("b");
ob = ob.refresh(linecol(0, 2), &["abprevious content"], yx(0, 2));
cb.add_input_chars("c");
ob = ob.refresh(linecol(0, 3), &["abcprevious content"], yx(0, 3));
cb.add_input_chars(" ");
ob = ob.refresh(linecol(0, 4), &["abc previous content"], yx(0, 4));
run_editor("previous content", "abc previous content\n", cb, ob);
}
#[test]
fn test_insert_before_last_character() {
let mut cb = MockConsole::default();
cb.set_size_chars(yx(10, 40));
let mut ob = OutputBuilder::new(yx(10, 40));
ob = ob.refresh(linecol(0, 0), &[""], yx(0, 0));
cb.add_input_chars("abc");
ob = ob.set_dirty();
ob = ob.add(CapturedOut::Write("a".to_string()));
ob = ob.quick_refresh(linecol(0, 1), yx(0, 1));
ob = ob.add(CapturedOut::Write("b".to_string()));
ob = ob.quick_refresh(linecol(0, 2), yx(0, 2));
ob = ob.add(CapturedOut::Write("c".to_string()));
ob = ob.quick_refresh(linecol(0, 3), yx(0, 3));
cb.add_input_keys(&[Key::ArrowLeft]);
ob = ob.quick_refresh(linecol(0, 2), yx(0, 2));
cb.add_input_chars("d");
ob = ob.refresh(linecol(0, 3), &["abdc"], yx(0, 3));
run_editor("", "abdc\n", cb, ob);
}
#[test]
fn test_insert_newline_in_middle() {
let mut cb = MockConsole::default();
cb.set_size_chars(yx(10, 40));
let mut ob = OutputBuilder::new(yx(10, 40));
ob = ob.refresh(linecol(0, 0), &[""], yx(0, 0));
cb.add_input_chars("abc");
ob = ob.set_dirty();
ob = ob.add(CapturedOut::Write("a".to_string()));
ob = ob.quick_refresh(linecol(0, 1), yx(0, 1));
ob = ob.add(CapturedOut::Write("b".to_string()));
ob = ob.quick_refresh(linecol(0, 2), yx(0, 2));
ob = ob.add(CapturedOut::Write("c".to_string()));
ob = ob.quick_refresh(linecol(0, 3), yx(0, 3));
cb.add_input_keys(&[Key::ArrowLeft]);
ob = ob.quick_refresh(linecol(0, 2), yx(0, 2));
cb.add_input_keys(&[Key::NewLine]);
ob = ob.refresh(linecol(1, 0), &["ab", "c"], yx(1, 0));
cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.quick_refresh(linecol(0, 0), yx(0, 0));
cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.quick_refresh(linecol(0, 1), yx(0, 1));
cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.quick_refresh(linecol(0, 2), yx(0, 2));
cb.add_input_keys(&[Key::NewLine]);
ob = ob.refresh(linecol(1, 0), &["ab", "", "c"], yx(1, 0));
run_editor("", "ab\n\nc\n", cb, ob);
}
#[test]
fn test_split_last_line() {
let mut cb = MockConsole::default();
cb.set_size_chars(yx(10, 40));
let mut ob = OutputBuilder::new(yx(10, 40));
ob = ob.refresh(linecol(0, 0), &[""], yx(0, 0));
cb.add_input_chars(" abcd");
ob = ob.set_dirty();
ob = ob.add(CapturedOut::Write(" ".to_string()));
ob = ob.quick_refresh(linecol(0, 1), yx(0, 1));
ob = ob.add(CapturedOut::Write(" ".to_string()));
ob = ob.quick_refresh(linecol(0, 2), yx(0, 2));
ob = ob.add(CapturedOut::Write("a".to_string()));
ob = ob.quick_refresh(linecol(0, 3), yx(0, 3));
ob = ob.add(CapturedOut::Write("b".to_string()));
ob = ob.quick_refresh(linecol(0, 4), yx(0, 4));
ob = ob.add(CapturedOut::Write("c".to_string()));
ob = ob.quick_refresh(linecol(0, 5), yx(0, 5));
ob = ob.add(CapturedOut::Write("d".to_string()));
ob = ob.quick_refresh(linecol(0, 6), yx(0, 6));
cb.add_input_keys(&[Key::ArrowLeft]);
ob = ob.quick_refresh(linecol(0, 5), yx(0, 5));
cb.add_input_keys(&[Key::ArrowLeft]);
ob = ob.quick_refresh(linecol(0, 4), yx(0, 4));
cb.add_input_keys(&[Key::NewLine]);
ob = ob.refresh(linecol(1, 2), &[" ab", " cd"], yx(1, 2));
run_editor("", " ab\n cd\n", cb, ob);
}
#[test]
fn test_move_in_empty_file() {
let mut cb = MockConsole::default();
cb.set_size_chars(yx(10, 40));
let mut ob = OutputBuilder::new(yx(10, 40));
ob = ob.refresh(linecol(0, 0), &[""], yx(0, 0));
for k in &[
Key::ArrowUp,
Key::ArrowDown,
Key::ArrowLeft,
Key::ArrowRight,
Key::PageUp,
Key::PageDown,
] {
cb.add_input_keys(&[k.clone()]);
ob = ob.quick_refresh(linecol(0, 0), yx(0, 0));
}
run_editor("", "\n", cb, ob);
}
#[test]
fn test_move_end() {
let mut cb = MockConsole::default();
cb.set_size_chars(yx(10, 40));
let mut ob = OutputBuilder::new(yx(10, 40));
ob = ob.refresh(linecol(0, 0), &["text"], yx(0, 0));
cb.add_input_keys(&[Key::End]);
ob = ob.quick_refresh(linecol(0, 4), yx(0, 4));
cb.add_input_chars(".");
ob = ob.set_dirty();
ob = ob.add(CapturedOut::Write(".".to_string()));
ob = ob.quick_refresh(linecol(0, 5), yx(0, 5));
run_editor("text", "text.\n", cb, ob);
}
#[test]
fn test_move_home_no_indent() {
let mut cb = MockConsole::default();
cb.set_size_chars(yx(10, 40));
let mut ob = OutputBuilder::new(yx(10, 40));
ob = ob.refresh(linecol(0, 0), &["text"], yx(0, 0));
cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.quick_refresh(linecol(0, 1), yx(0, 1));
cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.quick_refresh(linecol(0, 2), yx(0, 2));
cb.add_input_keys(&[Key::Home]);
ob = ob.quick_refresh(linecol(0, 0), yx(0, 0));
cb.add_input_chars(".");
ob = ob.set_dirty();
ob = ob.refresh(linecol(0, 1), &[".text"], yx(0, 1));
cb.add_input_keys(&[Key::Home]);
ob = ob.quick_refresh(linecol(0, 0), yx(0, 0));
cb.add_input_chars(",");
ob = ob.refresh(linecol(0, 1), &[",.text"], yx(0, 1));
run_editor("text", ",.text\n", cb, ob);
}
#[test]
fn test_move_home_with_indent() {
let mut cb = MockConsole::default();
cb.set_size_chars(yx(10, 40));
let mut ob = OutputBuilder::new(yx(10, 40));
ob = ob.refresh(linecol(0, 0), &[" text"], yx(0, 0));
cb.add_input_keys(&[Key::Home]);
ob = ob.quick_refresh(linecol(0, 2), yx(0, 2));
cb.add_input_keys(&[Key::Home]);
ob = ob.quick_refresh(linecol(0, 0), yx(0, 0));
cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.quick_refresh(linecol(0, 1), yx(0, 1));
cb.add_input_keys(&[Key::Home]);
ob = ob.quick_refresh(linecol(0, 2), yx(0, 2));
cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.quick_refresh(linecol(0, 3), yx(0, 3));
cb.add_input_keys(&[Key::Home]);
ob = ob.quick_refresh(linecol(0, 2), yx(0, 2));
cb.add_input_chars(".");
ob = ob.set_dirty();
ob = ob.refresh(linecol(0, 3), &[" .text"], yx(0, 3));
run_editor(" text", " .text\n", cb, ob);
}
#[test]
fn test_move_page_down_up() {
let mut cb = MockConsole::default();
cb.set_size_chars(yx(10, 40));
let mut ob = OutputBuilder::new(yx(10, 40));
ob = ob.refresh(linecol(0, 0), &["1", "2", "3", "4", "5", "6", "7", "8", "9"], yx(0, 0));
cb.add_input_keys(&[Key::PageDown]);
ob = ob.quick_refresh(linecol(8, 0), yx(8, 0));
cb.add_input_keys(&[Key::PageDown]);
ob = ob.refresh(
linecol(16, 0),
&["9", "10", "11", "12", "13", "14", "15", "16", "17"],
yx(8, 0),
);
cb.add_input_keys(&[Key::PageDown]);
ob = ob.refresh(
linecol(19, 0),
&["12", "13", "14", "15", "16", "17", "18", "19", "20"],
yx(8, 0),
);
cb.add_input_keys(&[Key::PageDown]);
ob = ob.quick_refresh(linecol(19, 0), yx(8, 0));
cb.add_input_keys(&[Key::PageUp]);
ob = ob.quick_refresh(linecol(11, 0), yx(0, 0));
cb.add_input_keys(&[Key::PageUp]);
ob = ob.refresh(linecol(3, 0), &["4", "5", "6", "7", "8", "9", "10", "11", "12"], yx(0, 0));
cb.add_input_keys(&[Key::PageUp]);
ob = ob.refresh(linecol(0, 0), &["1", "2", "3", "4", "5", "6", "7", "8", "9"], yx(0, 0));
cb.add_input_keys(&[Key::PageUp]);
ob = ob.quick_refresh(linecol(0, 0), yx(0, 0));
run_editor(
"1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n",
"1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n",
cb,
ob,
);
}
#[test]
fn test_tab_append() {
let mut cb = MockConsole::default();
cb.set_size_chars(yx(10, 40));
let mut ob = OutputBuilder::new(yx(10, 40));
ob = ob.refresh(linecol(0, 0), &[""], yx(0, 0));
cb.add_input_keys(&[Key::Tab]);
ob = ob.set_dirty();
ob = ob.add(CapturedOut::Write(" ".to_string()));
ob = ob.quick_refresh(linecol(0, 4), yx(0, 4));
cb.add_input_chars("x");
ob = ob.add(CapturedOut::Write("x".to_string()));
ob = ob.quick_refresh(linecol(0, 5), yx(0, 5));
cb.add_input_keys(&[Key::Tab]);
ob = ob.add(CapturedOut::Write(" ".to_string()));
ob = ob.quick_refresh(linecol(0, 8), yx(0, 8));
run_editor("", " x \n", cb, ob);
}
#[test]
fn test_tab_existing_content() {
let mut cb = MockConsole::default();
cb.set_size_chars(yx(10, 40));
let mut ob = OutputBuilder::new(yx(10, 40));
ob = ob.refresh(linecol(0, 0), &["."], yx(0, 0));
cb.add_input_keys(&[Key::Tab]);
ob = ob.set_dirty();
ob = ob.refresh(linecol(0, 4), &[" ."], yx(0, 4));
cb.add_input_keys(&[Key::Tab]);
ob = ob.refresh(linecol(0, 8), &[" ."], yx(0, 8));
run_editor(".", " .\n", cb, ob);
}
#[test]
fn test_tab_remove_empty_line() {
let mut cb = MockConsole::default();
cb.set_size_chars(yx(10, 40));
let mut ob = OutputBuilder::new(yx(10, 40));
ob = ob.refresh(linecol(0, 0), &[" "], yx(0, 0));
cb.add_input_keys(&[Key::End]);
ob = ob.quick_refresh(linecol(0, 10), yx(0, 10));
cb.add_input_keys(&[Key::Backspace]);
ob = ob.set_dirty();
ob = ob.add(CapturedOut::HideCursor);
ob = ob.add(CapturedOut::Clear(ClearType::PreviousChar));
ob = ob.add(CapturedOut::Clear(ClearType::PreviousChar));
ob = ob.add(CapturedOut::ShowCursor);
ob = ob.quick_refresh(linecol(0, 8), yx(0, 8));
cb.add_input_keys(&[Key::Backspace]);
ob = ob.add(CapturedOut::HideCursor);
ob = ob.add(CapturedOut::Clear(ClearType::PreviousChar));
ob = ob.add(CapturedOut::Clear(ClearType::PreviousChar));
ob = ob.add(CapturedOut::Clear(ClearType::PreviousChar));
ob = ob.add(CapturedOut::Clear(ClearType::PreviousChar));
ob = ob.add(CapturedOut::ShowCursor);
ob = ob.quick_refresh(linecol(0, 4), yx(0, 4));
cb.add_input_keys(&[Key::Backspace]);
ob = ob.add(CapturedOut::HideCursor);
ob = ob.add(CapturedOut::Clear(ClearType::PreviousChar));
ob = ob.add(CapturedOut::Clear(ClearType::PreviousChar));
ob = ob.add(CapturedOut::Clear(ClearType::PreviousChar));
ob = ob.add(CapturedOut::Clear(ClearType::PreviousChar));
ob = ob.add(CapturedOut::ShowCursor);
ob = ob.quick_refresh(linecol(0, 0), yx(0, 0));
cb.add_input_keys(&[Key::Backspace]);
ob = ob.quick_refresh(linecol(0, 0), yx(0, 0));
run_editor(" ", "\n", cb, ob);
}
#[test]
fn test_tab_remove_before_some_text() {
let mut cb = MockConsole::default();
cb.set_size_chars(yx(10, 40));
let mut ob = OutputBuilder::new(yx(10, 40));
ob = ob.refresh(linecol(0, 0), &[" aligned"], yx(0, 0));
for i in 0..10 {
cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.quick_refresh(linecol(0, i + 1), yx(0, u16::try_from(i + 1).unwrap()));
}
cb.add_input_keys(&[Key::Backspace]);
ob = ob.set_dirty();
ob = ob.refresh(linecol(0, 8), &[" aligned"], yx(0, 8));
cb.add_input_keys(&[Key::Backspace]);
ob = ob.refresh(linecol(0, 4), &[" aligned"], yx(0, 4));
cb.add_input_keys(&[Key::Backspace]);
ob = ob.refresh(linecol(0, 0), &["aligned"], yx(0, 0));
cb.add_input_keys(&[Key::Backspace]);
ob = ob.quick_refresh(linecol(0, 0), yx(0, 0));
run_editor(" aligned", "aligned\n", cb, ob);
}
#[test]
fn test_move_preserves_insertion_column() {
let mut cb = MockConsole::default();
cb.set_size_chars(yx(10, 40));
let mut ob = OutputBuilder::new(yx(10, 40));
ob = ob.refresh(linecol(0, 0), &["longer", "a", "longer", "b"], yx(0, 0));
cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.quick_refresh(linecol(0, 1), yx(0, 1));
cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.quick_refresh(linecol(0, 2), yx(0, 2));
cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.quick_refresh(linecol(0, 3), yx(0, 3));
cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.quick_refresh(linecol(0, 4), yx(0, 4));
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(linecol(1, 1), yx(1, 1));
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(linecol(2, 4), yx(2, 4));
cb.add_input_keys(&[Key::Char('X')]);
ob = ob.set_dirty();
ob = ob.refresh(linecol(2, 5), &["longer", "a", "longXer", "b"], yx(2, 5));
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(linecol(3, 1), yx(3, 1));
cb.add_input_keys(&[Key::Char('Z')]);
ob = ob.add(CapturedOut::Write("Z".to_string()));
ob = ob.quick_refresh(linecol(3, 2), yx(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 = MockConsole::default();
cb.set_size_chars(yx(10, 40));
let mut ob = OutputBuilder::new(yx(10, 40));
ob = ob.refresh(
linecol(0, 0),
&[
"this is a line of text with more than 40",
"short",
"a",
"",
"another line of text with more than 40 c",
],
yx(0, 0),
);
for col in 0u16..39u16 {
cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.quick_refresh(linecol(0, usize::from(col) + 1), yx(0, col + 1));
}
cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.refresh(
linecol(0, 40),
&[
"his is a line of text with more than 40 ",
"hort",
"",
"",
"nother line of text with more than 40 ch",
],
yx(0, 39),
);
cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.refresh(
linecol(0, 41),
&[
"is is a line of text with more than 40 c",
"ort",
"",
"",
"other line of text with more than 40 cha",
],
yx(0, 39),
);
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(linecol(1, 5), yx(1, 3));
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.refresh(
linecol(2, 1),
&[
"his is a line of text with more than 40 ",
"hort",
"",
"",
"nother line of text with more than 40 ch",
],
yx(2, 0),
);
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.refresh(
linecol(3, 0),
&[
"this is a line of text with more than 40",
"short",
"a",
"",
"another line of text with more than 40 c",
],
yx(3, 0),
);
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.refresh(
linecol(4, 41),
&[
"is is a line of text with more than 40 c",
"ort",
"",
"",
"other line of text with more than 40 cha",
],
yx(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 = MockConsole::default();
cb.set_size_chars(yx(10, 40));
let mut ob = OutputBuilder::new(yx(10, 40));
ob = ob.refresh(
linecol(0, 0),
&[
"this is a line of text with more than 40",
"",
"a",
"short",
"another line of text with more than 40 c",
],
yx(0, 0),
);
for i in 0u16..4u16 {
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(linecol(usize::from(i + 1), 0), yx(i + 1, 0));
}
for col in 0u16..39u16 {
cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.quick_refresh(linecol(4, usize::from(col + 1)), yx(4, col + 1));
}
cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.refresh(
linecol(4, 40),
&[
"his is a line of text with more than 40 ",
"",
"",
"hort",
"nother line of text with more than 40 ch",
],
yx(4, 39),
);
cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.refresh(
linecol(4, 41),
&[
"is is a line of text with more than 40 c",
"",
"",
"ort",
"other line of text with more than 40 cha",
],
yx(4, 39),
);
cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.quick_refresh(linecol(3, 5), yx(3, 3));
cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.refresh(
linecol(2, 1),
&[
"his is a line of text with more than 40 ",
"",
"",
"hort",
"nother line of text with more than 40 ch",
],
yx(2, 0),
);
cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.refresh(
linecol(1, 0),
&[
"this is a line of text with more than 40",
"",
"a",
"short",
"another line of text with more than 40 c",
],
yx(1, 0),
);
cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.refresh(
linecol(0, 41),
&[
"is is a line of text with more than 40 c",
"",
"",
"ort",
"other line of text with more than 40 cha",
],
yx(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 = MockConsole::default();
cb.set_size_chars(yx(10, 40));
let mut ob = OutputBuilder::new(yx(10, 40));
ob = ob.refresh(linecol(0, 0), &["ab", "", "xyz"], yx(0, 0));
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(linecol(1, 0), yx(1, 0));
for (col, ch) in "123456789012345678901234567890123456789".chars().enumerate() {
cb.add_input_keys(&[Key::Char(ch)]);
ob = ob.set_dirty();
let mut buf = [0u8; 4];
ob = ob.add(CapturedOut::Write(ch.encode_utf8(&mut buf).to_string()));
ob = ob.quick_refresh(linecol(1, col + 1), yx(1, u16::try_from(col + 1).unwrap()));
}
cb.add_input_keys(&[Key::Char('A')]);
ob = ob.refresh(
linecol(1, 40),
&["b", "23456789012345678901234567890123456789A", "yz"],
yx(1, 39),
);
cb.add_input_keys(&[Key::Char('B')]);
ob = ob.refresh(
linecol(1, 41),
&["", "3456789012345678901234567890123456789AB", "z"],
yx(1, 39),
);
cb.add_input_keys(&[Key::Char('C')]);
ob = ob.refresh(
linecol(1, 42),
&["", "456789012345678901234567890123456789ABC", ""],
yx(1, 39),
);
for (file_col, cursor_col) in &[(41, 38), (40, 37), (39, 36)] {
cb.add_input_keys(&[Key::ArrowLeft]);
ob = ob.quick_refresh(linecol(1, *file_col), yx(1, *cursor_col));
}
cb.add_input_keys(&[Key::Char('D')]);
ob = ob.refresh(
linecol(1, 40),
&["", "456789012345678901234567890123456789DABC", ""],
yx(1, 37),
);
cb.add_input_keys(&[Key::Char('E')]);
ob = ob.refresh(
linecol(1, 41),
&["", "456789012345678901234567890123456789DEAB", ""],
yx(1, 38),
);
cb.add_input_keys(&[Key::Backspace]);
ob = ob.refresh(
linecol(1, 40),
&["", "456789012345678901234567890123456789DABC", ""],
yx(1, 37),
);
cb.add_input_keys(&[Key::Backspace]);
ob = ob.refresh(
linecol(1, 39),
&["", "456789012345678901234567890123456789ABC", ""],
yx(1, 36),
);
cb.add_input_keys(&[Key::Backspace]);
ob = ob.refresh(
linecol(1, 38),
&["", "45678901234567890123456789012345678ABC", ""],
yx(1, 35),
);
for col in 0u16..35u16 {
cb.add_input_keys(&[Key::ArrowLeft]);
ob = ob.quick_refresh(linecol(1, usize::from(37 - col)), yx(1, 34 - col));
}
cb.add_input_keys(&[Key::ArrowLeft]);
ob = ob.refresh(
linecol(1, 2),
&["", "345678901234567890123456789012345678ABC", "z"],
yx(1, 0),
);
cb.add_input_keys(&[Key::ArrowLeft]);
ob = ob.refresh(
linecol(1, 1),
&["b", "2345678901234567890123456789012345678ABC", "yz"],
yx(1, 0),
);
cb.add_input_keys(&[Key::ArrowLeft]);
ob = ob.refresh(
linecol(1, 0),
&["ab", "12345678901234567890123456789012345678AB", "xyz"],
yx(1, 0),
);
run_editor("ab\n\nxyz\n", "ab\n12345678901234567890123456789012345678ABC\nxyz\n", cb, ob);
}
#[test]
fn test_vertical_scrolling() {
let mut cb = MockConsole::default();
cb.set_size_chars(yx(5, 40));
let mut ob = OutputBuilder::new(yx(5, 40));
ob = ob.refresh(linecol(0, 0), &["abc", "", "d", "e"], yx(0, 0));
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(linecol(1, 0), yx(1, 0));
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(linecol(2, 0), yx(2, 0));
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(linecol(3, 0), yx(3, 0));
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.refresh(linecol(4, 0), &["", "d", "e", ""], yx(3, 0));
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.refresh(linecol(5, 0), &["d", "e", "", "fg"], yx(3, 0));
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.refresh(linecol(6, 0), &["e", "", "fg", "hij"], yx(3, 0));
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(linecol(6, 0), yx(3, 0));
cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.quick_refresh(linecol(5, 0), yx(2, 0));
cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.quick_refresh(linecol(4, 0), yx(1, 0));
cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.quick_refresh(linecol(3, 0), yx(0, 0));
cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.refresh(linecol(2, 0), &["d", "e", "", "fg"], yx(0, 0));
cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.refresh(linecol(1, 0), &["", "d", "e", ""], yx(0, 0));
cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.refresh(linecol(0, 0), &["abc", "", "d", "e"], yx(0, 0));
cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.quick_refresh(linecol(0, 0), yx(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 = MockConsole::default();
cb.set_size_chars(yx(4, 40));
let mut ob = OutputBuilder::new(yx(4, 40));
ob = ob.refresh(linecol(0, 0), &["first", "second", "thirdfourth"], yx(0, 0));
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(linecol(1, 0), yx(1, 0));
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(linecol(2, 0), yx(2, 0));
for i in 0.."third".len() {
cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.quick_refresh(linecol(2, i + 1), yx(2, u16::try_from(i + 1).unwrap()));
}
cb.add_input_keys(&[Key::NewLine]);
ob = ob.set_dirty();
ob = ob.refresh(linecol(3, 0), &["second", "third", "fourth"], yx(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 = MockConsole::default();
cb.set_size_chars(yx(4, 40));
let mut ob = OutputBuilder::new(yx(4, 40));
ob = ob.refresh(
linecol(0, 0),
&["first", "second", "this is a line of text with more than 40"],
yx(0, 0),
);
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(linecol(1, 0), yx(1, 0));
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(linecol(2, 0), yx(2, 0));
for i in 0u16..39u16 {
cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.quick_refresh(linecol(2, usize::from(i + 1)), yx(2, i + 1));
}
cb.add_input_keys(&[Key::ArrowRight]);
ob = ob.refresh(
linecol(2, 40),
&["irst", "econd", "his is a line of text with more than 40 "],
yx(2, 39),
);
cb.add_input_keys(&[Key::NewLine]);
ob = ob.set_dirty();
ob = ob.refresh(
linecol(3, 0),
&["second", "this is a line of text with more than 40", " characters"],
yx(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 = MockConsole::default();
cb.set_size_chars(yx(4, 40));
let mut ob = OutputBuilder::new(yx(4, 40));
ob = ob.refresh(linecol(0, 0), &["first", "second", "third"], yx(0, 0));
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(linecol(1, 0), yx(1, 0));
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(linecol(2, 0), yx(2, 0));
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.refresh(linecol(3, 0), &["second", "third", "fourth"], yx(2, 0));
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.refresh(linecol(4, 0), &["third", "fourth", "fifth"], yx(2, 0));
cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.quick_refresh(linecol(3, 0), yx(1, 0));
cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.quick_refresh(linecol(2, 0), yx(0, 0));
cb.add_input_keys(&[Key::Backspace]);
ob = ob.set_dirty();
ob = ob.refresh(linecol(1, 6), &["secondthird", "fourth", "fifth"], yx(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 = MockConsole::default();
cb.set_size_chars(yx(4, 40));
let mut ob = OutputBuilder::new(yx(4, 40));
ob = ob.refresh(
linecol(0, 0),
&["first", "this is a line of text with more than 40", "third"],
yx(0, 0),
);
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(linecol(1, 0), yx(1, 0));
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.quick_refresh(linecol(2, 0), yx(2, 0));
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.refresh(
linecol(3, 0),
&["this is a line of text with more than 40", "third", "fourth"],
yx(2, 0),
);
cb.add_input_keys(&[Key::ArrowDown]);
ob = ob.refresh(linecol(4, 0), &["third", "fourth", "quite a long line"], yx(2, 0));
cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.quick_refresh(linecol(3, 0), yx(1, 0));
cb.add_input_keys(&[Key::ArrowUp]);
ob = ob.quick_refresh(linecol(2, 0), yx(0, 0));
cb.add_input_keys(&[Key::Backspace]);
ob = ob.set_dirty();
ob = ob.refresh(
linecol(1, 51),
&["ne of text with more than 40 characterst", "", " line"],
yx(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,
);
}
#[test]
fn test_narrow_console() {
let mut cb = MockConsole::default();
cb.set_size_chars(yx(10, 25));
let mut ob = OutputBuilder::new(yx(10, 25));
ob = ob.refresh(linecol(0, 0), &[""], yx(0, 0));
run_editor("", "\n", cb, ob);
}
#[test]
fn test_very_narrow_console() {
let mut cb = MockConsole::default();
cb.set_size_chars(yx(10, 5));
let mut ob = OutputBuilder::new(yx(10, 5));
ob = ob.refresh(linecol(0, 0), &[""], yx(0, 0));
run_editor("", "\n", cb, ob);
}
}