use std::{path::PathBuf, rc::Rc}; use crate::{Point, TextBuffer}; mod command; mod io; pub use command::{Command, Movement, Unit}; #[derive(Default, Clone)] pub struct EditorBuffer { buffer: TextBuffer, cursor: Point, state: Rc, } #[derive(Default)] struct EditorBufferState { filepath: Option, } #[derive(Debug, PartialEq)] pub enum CommandResponse { None, Message(String), } #[must_use] pub struct CommandResult { success: bool, pub response: CommandResponse, pub buffer: EditorBuffer, } impl CommandResult { pub fn is_ok(&self) -> bool { self.success } fn ok(buffer: EditorBuffer) -> Self { Self { success: true, response: CommandResponse::None, buffer, } } fn success_with_message(buffer: EditorBuffer, msg: impl Into) -> Self { Self { success: true, response: CommandResponse::Message(msg.into()), buffer, } } fn fail_with_message(buffer: EditorBuffer, msg: impl Into) -> Self { Self { success: false, response: CommandResponse::Message(msg.into()), buffer, } } } impl EditorBuffer { /// Create new empty [EditorBuffer] pub fn new() -> Self { Self::default() } /// Execute a command on the [EditorBuffer] pub fn execute(&self, command: Command) -> CommandResult { match command { Command::OpenFile(filepath) => self.open_file(filepath), Command::Save => self.save_file(None), Command::SaveAs(filepath) => self.save_file(Some(filepath)), Command::MoveCursorTo(point) => self.move_cursor_to_point(point), Command::MoveCursorToLine(line_num) => self.move_cursor_to_line_number(line_num), Command::MoveCursor(movement) => self.move_cursor(movement), Command::InsertChar(c) => self.insert_char(c), Command::InsertString(s) => self.insert_string(s), Command::Delete(movement) => self.delete(movement), } } pub fn get_cursor_position(&self) -> Point { self.cursor } fn move_cursor_to_point(&self, point: Point) -> CommandResult { CommandResult::ok(Self { cursor: point, ..self.clone() }) } fn move_cursor_to_line_number(&self, line_num: usize) -> CommandResult { self.move_cursor_to_point(Point::LineColumn(line_num, 0)) } fn insert_char(&self, c: char) -> CommandResult { let newline = c == '\n'; CommandResult::ok(Self { buffer: self.buffer.insert_char(c, self.cursor), cursor: match self.cursor { Point::Start => { if newline { Point::LineColumn(1, 0) } else { Point::LineColumn(0, 1) } } Point::LineColumn(line, column) => { if newline { Point::LineColumn(line + 1, 0) } else { Point::LineColumn(line, column + 1) } } Point::End => Point::End, }, ..self.clone() }) } fn insert_string(&self, _s: String) -> CommandResult { todo!() } fn move_cursor(&self, _movement: Movement) -> CommandResult { todo!() } fn delete(&self, _movement: Movement) -> CommandResult { todo!() } } #[cfg(test)] mod tests { use super::*; use std::io::{Read, Seek, SeekFrom, Write}; use tempfile::NamedTempFile; fn create_simple_test_file() -> NamedTempFile { let inner = || { let mut file = NamedTempFile::new()?; writeln!(file, "Messe ocus Pangur Bán,")?; writeln!(file, "cechtar nathar fria saindan")?; writeln!(file, "bíth a menmasam fri seilgg")?; writeln!(file, "mu menma céin im saincheirdd.")?; writeln!(file)?; writeln!(file, "Caraimse fos ferr cach clú")?; writeln!(file, "oc mu lebran leir ingn")?; writeln!(file, "ni foirmtech frimm Pangur Bá")?; writeln!(file, "caraid cesin a maccdán.")?; writeln!(file)?; writeln!(file, "Orubiam scél cen scís")?; writeln!(file, "innar tegdais ar noendís")?; writeln!(file, "taithiunn dichrichide clius")?; writeln!(file, "ni fristarddam arnáthius.")?; file.seek(SeekFrom::Start(0))?; Ok::<_, std::io::Error>(file) }; inner().expect("Creating temporary file") } #[test] fn open_file_loads_file_contents() { let mut test_file = create_simple_test_file(); let mut expected_contents = String::new(); test_file .read_to_string(&mut expected_contents) .expect("Reading text file"); let target = EditorBuffer::new(); let result = target.execute(Command::OpenFile(test_file.path().into())); let found_contents: String = result.buffer.buffer.iter_chars().collect(); assert_eq!(expected_contents, found_contents); } #[test] fn cursor_at_beginning_after_file_opened() { let test_file = create_simple_test_file(); let target = EditorBuffer::new(); let result = target.execute(Command::OpenFile(test_file.path().into())); assert_eq!(Point::Start, result.buffer.get_cursor_position()); } #[test] fn move_cursor_to_point_in_file() { let test_file = create_simple_test_file(); let target = EditorBuffer::new(); let result = target.execute(Command::OpenFile(test_file.path().into())); assert!(result.is_ok()); let target = result.buffer; let result = target.execute(Command::MoveCursorTo(Point::LineColumn(0, 5))); assert!(result.is_ok()); let target = result.buffer; assert_eq!(Point::LineColumn(0, 5), target.get_cursor_position()); let result = target.execute(Command::MoveCursorTo(Point::LineColumn(3, 11))); assert!(result.is_ok()); let target = result.buffer; assert_eq!(Point::LineColumn(3, 11), target.get_cursor_position()); let result = target.execute(Command::MoveCursorTo(Point::LineColumn(3, 0))); assert!(result.is_ok()); let target = result.buffer; assert_eq!(Point::LineColumn(3, 0), target.get_cursor_position()); } #[test] fn insert_character() { let mut test_file = create_simple_test_file(); let mut file_contents = String::new(); test_file .read_to_string(&mut file_contents) .expect("Reading text file"); let test_file = test_file; let mut expected_lines: Vec<_> = file_contents.lines().collect(); expected_lines[2] = "Xbíth a menmasam fri seilgg"; let expected_lines = expected_lines; let target = EditorBuffer::new(); let result = target.execute(Command::OpenFile(test_file.path().into())); assert!(result.is_ok()); let target = result.buffer; let result = target.execute(Command::MoveCursorTo(Point::LineColumn(3, 0))); assert!(result.is_ok()); let target = result.buffer; let result = target.execute(Command::InsertChar('X')); assert!(result.is_ok()); let target = result.buffer; let found_lines: Vec = target .buffer .iter_chars() .collect::() .lines() .map(|l| l.into()) .collect(); assert_eq!(expected_lines, found_lines); let mut test_file = create_simple_test_file(); let mut file_contents = String::new(); test_file .read_to_string(&mut file_contents) .expect("Reading text file"); let test_file = test_file; let mut expected_lines: Vec<_> = file_contents.lines().collect(); expected_lines[2] = "bXíth a menmasam fri seilgg"; let expected_lines = expected_lines; let target = EditorBuffer::new(); let result = target.execute(Command::OpenFile(test_file.path().into())); assert!(result.is_ok()); let target = result.buffer; let result = target.execute(Command::MoveCursorTo(Point::LineColumn(3, 1))); assert!(result.is_ok()); let target = result.buffer; let result = target.execute(Command::InsertChar('X')); assert!(result.is_ok()); let target = result.buffer; let found_lines: Vec = target .buffer .iter_chars() .collect::() .lines() .map(|l| l.into()) .collect(); assert_eq!(expected_lines, found_lines); let mut test_file = create_simple_test_file(); let mut file_contents = String::new(); test_file .read_to_string(&mut file_contents) .expect("Reading text file"); let test_file = test_file; let mut expected_lines: Vec<_> = file_contents.lines().collect(); expected_lines[2] = "bíXth a menmasam fri seilgg"; let expected_lines = expected_lines; let target = EditorBuffer::new(); let result = target.execute(Command::OpenFile(test_file.path().into())); assert!(result.is_ok()); let target = result.buffer; let result = target.execute(Command::MoveCursorTo(Point::LineColumn(3, 2))); assert!(result.is_ok()); let target = result.buffer; let result = target.execute(Command::InsertChar('X')); assert!(result.is_ok()); let target = result.buffer; let found_lines: Vec = target .buffer .iter_chars() .collect::() .lines() .map(|l| l.into()) .collect(); assert_eq!(expected_lines, found_lines); let mut test_file = create_simple_test_file(); let mut file_contents = String::new(); test_file .read_to_string(&mut file_contents) .expect("Reading text file"); let test_file = test_file; let mut expected_lines: Vec<_> = file_contents.lines().collect(); expected_lines[2] = "bíth a menmXasam fri seilgg"; let expected_lines = expected_lines; let target = EditorBuffer::new(); let result = target.execute(Command::OpenFile(test_file.path().into())); assert!(result.is_ok()); let target = result.buffer; let result = target.execute(Command::MoveCursorTo(Point::LineColumn(3, 11))); assert!(result.is_ok()); let target = result.buffer; let result = target.execute(Command::InsertChar('X')); assert!(result.is_ok()); let target = result.buffer; let found_lines: Vec = target .buffer .iter_chars() .collect::() .lines() .map(|l| l.into()) .collect(); assert_eq!(expected_lines, found_lines); } #[test] fn insert_line() { let mut test_file = create_simple_test_file(); let mut file_contents = String::new(); test_file .read_to_string(&mut file_contents) .expect("Reading text file"); let test_file = test_file; let mut expected_lines: Vec<_> = file_contents.lines().collect(); expected_lines.insert(6, "abc 123".into()); let target = EditorBuffer::new(); let result = target.execute(Command::OpenFile(test_file.path().into())); assert!(result.is_ok()); let target = result.buffer; let result = target.execute(Command::MoveCursorToLine(7)); assert!(result.is_ok()); let target = result.buffer; let result = target.execute(Command::InsertChar('a')); assert!(result.is_ok()); let target = result.buffer; let result = target.execute(Command::InsertChar('b')); assert!(result.is_ok()); let target = result.buffer; let result = target.execute(Command::InsertChar('c')); assert!(result.is_ok()); let target = result.buffer; let result = target.execute(Command::InsertChar(' ')); assert!(result.is_ok()); let target = result.buffer; let result = target.execute(Command::InsertChar('1')); assert!(result.is_ok()); let target = result.buffer; let result = target.execute(Command::InsertChar('2')); assert!(result.is_ok()); let target = result.buffer; let result = target.execute(Command::InsertChar('3')); assert!(result.is_ok()); let target = result.buffer; let result = target.execute(Command::InsertChar('\n')); assert!(result.is_ok()); let target = result.buffer; let found_lines: Vec = target .buffer .iter_chars() .collect::() .lines() .map(|l| l.into()) .collect(); assert_eq!(expected_lines, found_lines); assert_eq!(Point::LineColumn(8, 0), target.get_cursor_position()); } }