From 41e9c197cdaefaa16d3b75ec5cf9cf858b506a6d Mon Sep 17 00:00:00 2001 From: Matthew Gordon Date: Sat, 15 Nov 2025 21:11:47 -0400 Subject: [PATCH] Refactor EditorBuffer and TextBuffer to be immutable --- core/src/editor_buffer/io.rs | 87 ++++++++------ core/src/editor_buffer/mod.rs | 208 +++++++++++++++++++--------------- core/src/text_buffer/mod.rs | 50 ++++---- 3 files changed, 198 insertions(+), 147 deletions(-) diff --git a/core/src/editor_buffer/io.rs b/core/src/editor_buffer/io.rs index 431dda4..bffa843 100644 --- a/core/src/editor_buffer/io.rs +++ b/core/src/editor_buffer/io.rs @@ -1,11 +1,11 @@ -use std::path::PathBuf; +use std::{path::PathBuf, rc::Rc}; -use super::{CommandResponse, EditorBuffer}; +use super::{CommandResult, EditorBuffer, EditorBufferState}; use crate::{Point, TextBuffer, TextBufferReader, TextBufferWriter}; #[doc(hidden)] impl EditorBuffer { - pub fn open_file(&mut self, filepath: PathBuf) -> CommandResponse { + pub fn open_file(&self, filepath: PathBuf) -> CommandResult { match std::fs::File::open(&filepath) { Ok(mut file) => { let mut buffer = TextBuffer::new(); @@ -15,43 +15,59 @@ impl EditorBuffer { "Read {bytes_read} bytes from \"{}\"", filepath.to_string_lossy() ); - self.filepath = Some(filepath); - self.cursor = Point::default(); - self.buffer = buffer; - CommandResponse::Success(msg) + let state = Rc::new(EditorBufferState { + filepath: Some(filepath), + ..*self.state + }); + let cursor = Point::default(); + let buffer = buffer; + CommandResult::success_with_message( + Self { + state, + cursor, + buffer, + ..self.clone() + }, + msg, + ) } - Err(err) => CommandResponse::Failure(format!("{}", err)), + Err(err) => CommandResult::fail_with_message(self.clone(), format!("{}", err)), } } Err(err) => { if err.kind() == std::io::ErrorKind::NotFound { - CommandResponse::Failure(format!( - "File not found: \"{}\"", - filepath.to_string_lossy() - )) + CommandResult::fail_with_message( + self.clone(), + format!("File not found: \"{}\"", filepath.to_string_lossy()), + ) } else { - CommandResponse::Failure(format!("{}", err)) + CommandResult::fail_with_message(self.clone(), format!("{}", err)) } } } } - pub fn save_file(&mut self, filepath: Option) -> CommandResponse { - if let Some(filepath) = filepath.as_ref().or(self.filepath.as_ref()) { + pub fn save_file(&self, filepath: Option) -> CommandResult { + if let Some(filepath) = filepath.as_ref().or(self.state.filepath.as_ref()) { match std::fs::File::create(filepath) { Ok(mut file) => { match std::io::copy(&mut TextBufferReader::new(&self.buffer), &mut file) { - Ok(bytes_read) => CommandResponse::Success(format!( - "Read {bytes_read} bytes to \"{}\"", - filepath.to_string_lossy() - )), - Err(err) => CommandResponse::Failure(format!("{}", err)), + Ok(bytes_read) => CommandResult::success_with_message( + self.clone(), + format!( + "Read {bytes_read} bytes to \"{}\"", + filepath.to_string_lossy() + ), + ), + Err(err) => { + CommandResult::fail_with_message(self.clone(), format!("{}", err)) + } } } - Err(err) => CommandResponse::Failure(format!("{}", err)), + Err(err) => CommandResult::fail_with_message(self.clone(), format!("{}", err)), } } else { - CommandResponse::Failure("Attempting to same file with no name.".into()) + CommandResult::fail_with_message(self.clone(), "Attempting to same file with no name.") } } } @@ -71,11 +87,10 @@ mod tests { .iter() .collect(); let expected_text = std::fs::read_to_string(&test_file_path).unwrap(); - let mut target = EditorBuffer::new(); - assert!(matches!( - target.execute(Command::OpenFile(test_file_path)), - CommandResponse::Success(_) - )); + let target = EditorBuffer::new(); + let result = target.execute(Command::OpenFile(test_file_path)); + assert!(result.is_ok()); + let target = result.buffer; let mut buffer_bytes = Vec::new(); TextBufferReader::new(&target.buffer) @@ -91,9 +106,11 @@ mod tests { .iter() .collect(); let expected_message = format!("File not found: \"{}\"", test_file_path.to_string_lossy()); - let mut target = EditorBuffer::new(); - match target.execute(Command::OpenFile(test_file_path)) { - CommandResponse::Failure(s) => assert_eq!(expected_message, s), + let target = EditorBuffer::new(); + let result = target.execute(Command::OpenFile(test_file_path)); + assert!(!result.is_ok()); + match result.response { + CommandResponse::Message(s) => assert_eq!(expected_message, s), _ => panic!(), } } @@ -106,14 +123,12 @@ mod tests { ] .iter() .collect(); - let mut target = EditorBuffer::new(); - target.execute(Command::OpenFile(test_file_path.clone())); + let target = EditorBuffer::new(); + let target = target.execute(Command::OpenFile(test_file_path.clone())).buffer; let temp_dir = tempdir().unwrap(); let tmp_file_path = temp_dir.path().join(r"Les_Trois_Mousquetaires.txt"); - assert!(matches!( - target.execute(Command::SaveAs(tmp_file_path.clone())), - CommandResponse::Success(_) - )); + let result = target.execute(Command::SaveAs(tmp_file_path.clone())); + assert!(result.is_ok()); let read_text = std::fs::read_to_string(&tmp_file_path).unwrap(); let expected_text = std::fs::read_to_string(&test_file_path).unwrap(); diff --git a/core/src/editor_buffer/mod.rs b/core/src/editor_buffer/mod.rs index 75948d7..aa2a89a 100644 --- a/core/src/editor_buffer/mod.rs +++ b/core/src/editor_buffer/mod.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::{path::PathBuf, rc::Rc}; use crate::{Point, TextBuffer}; @@ -6,26 +6,57 @@ mod command; mod io; pub use command::{Command, Movement, Unit}; -#[derive(Default)] +#[derive(Default, Clone)] pub struct EditorBuffer { buffer: TextBuffer, cursor: Point, + state: Rc, +} + +#[derive(Default)] +struct EditorBufferState { filepath: Option, } #[derive(Debug, PartialEq)] pub enum CommandResponse { - Ok, - Success(String), - Failure(String), + None, + Message(String), } -impl CommandResponse { +#[must_use] +pub struct CommandResult { + success: bool, + response: CommandResponse, + buffer: EditorBuffer, +} + +impl CommandResult { pub fn is_ok(&self) -> bool { - match self { - Self::Ok => true, - Self::Success(_) => true, - Self::Failure(_) => false, + 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, } } } @@ -37,7 +68,7 @@ impl EditorBuffer { } /// Execute a command on the [EditorBuffer] - pub fn execute(&mut self, command: Command) -> CommandResponse { + pub fn execute(&self, command: Command) -> CommandResult { match command { Command::OpenFile(filepath) => self.open_file(filepath), Command::Save => self.save_file(None), @@ -55,29 +86,34 @@ impl EditorBuffer { self.cursor } - fn move_cursor_to_point(&mut self, point: Point) -> CommandResponse { - self.cursor = point; - CommandResponse::Ok + fn move_cursor_to_point(&self, point: Point) -> CommandResult { + CommandResult::ok(Self { + cursor: point, + ..self.clone() + }) } - fn move_cursor_to_line_number(&mut self, _line_num: usize) -> CommandResponse { + fn move_cursor_to_line_number(&self, _line_num: usize) -> CommandResult { todo!() } - fn insert_char(&mut self, c: char) -> CommandResponse { - self.buffer.insert_char(c, self.cursor); - CommandResponse::Ok + fn insert_char(&self, c: char) -> CommandResult { + CommandResult::ok(Self { + buffer: self.buffer.insert_char(c, self.cursor), + cursor: self.cursor.advance(), + ..self.clone() + }) } - fn insert_string(&mut self, _s: String) -> CommandResponse { + fn insert_string(&self, _s: String) -> CommandResult { todo!() } - fn move_cursor(&mut self, _movement: Movement) -> CommandResponse { + fn move_cursor(&self, _movement: Movement) -> CommandResult { todo!() } - fn delete(&mut self, _movement: Movement) -> CommandResponse { + fn delete(&self, _movement: Movement) -> CommandResult { todo!() } } @@ -119,42 +155,38 @@ mod tests { test_file .read_to_string(&mut expected_contents) .expect("Reading text file"); - let mut target = EditorBuffer::new(); - target.execute(Command::OpenFile(test_file.path().into())); - let found_contents: String = target.buffer.iter_chars().collect(); + 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 mut target = EditorBuffer::new(); - target.execute(Command::OpenFile(test_file.path().into())); - assert_eq!(Point::Start, target.get_cursor_position()); + 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 mut target = EditorBuffer::new(); - target.execute(Command::OpenFile(test_file.path().into())); - assert!( - target - .execute(Command::MoveCursorTo(Point::LineColumn(0, 5))) - .is_ok() - ); + 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()); - assert!( - target - .execute(Command::MoveCursorTo(Point::LineColumn(3, 11))) - .is_ok() - ); + 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()); - assert!( - target - .execute(Command::MoveCursorTo(Point::LineColumn(3, 0))) - .is_ok() - ); + 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()); } @@ -169,18 +201,16 @@ mod tests { let mut expected_lines: Vec<_> = file_contents.lines().collect(); expected_lines[2] = "Xbíth a menmasam fri seilgg"; let expected_lines = expected_lines; - let mut target = EditorBuffer::new(); - assert!( - target - .execute(Command::OpenFile(test_file.path().into())) - .is_ok() - ); - assert!( - target - .execute(Command::MoveCursorTo(Point::LineColumn(3, 0))) - .is_ok() - ); - assert!(target.execute(Command::InsertChar('X')).is_ok()); + 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() @@ -199,18 +229,16 @@ mod tests { let mut expected_lines: Vec<_> = file_contents.lines().collect(); expected_lines[2] = "bXíth a menmasam fri seilgg"; let expected_lines = expected_lines; - let mut target = EditorBuffer::new(); - assert!( - target - .execute(Command::OpenFile(test_file.path().into())) - .is_ok() - ); - assert!( - target - .execute(Command::MoveCursorTo(Point::LineColumn(3, 1))) - .is_ok() - ); - assert!(target.execute(Command::InsertChar('X')).is_ok()); + 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() @@ -229,18 +257,16 @@ mod tests { let mut expected_lines: Vec<_> = file_contents.lines().collect(); expected_lines[2] = "bíXth a menmasam fri seilgg"; let expected_lines = expected_lines; - let mut target = EditorBuffer::new(); - assert!( - target - .execute(Command::OpenFile(test_file.path().into())) - .is_ok() - ); - assert!( - target - .execute(Command::MoveCursorTo(Point::LineColumn(3, 2))) - .is_ok() - ); - assert!(target.execute(Command::InsertChar('X')).is_ok()); + 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() @@ -259,18 +285,16 @@ mod tests { let mut expected_lines: Vec<_> = file_contents.lines().collect(); expected_lines[2] = "bíth a menmXasam fri seilgg"; let expected_lines = expected_lines; - let mut target = EditorBuffer::new(); - assert!( - target - .execute(Command::OpenFile(test_file.path().into())) - .is_ok() - ); - assert!( - target - .execute(Command::MoveCursorTo(Point::LineColumn(3, 11))) - .is_ok() - ); - assert!(target.execute(Command::InsertChar('X')).is_ok()); + 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() diff --git a/core/src/text_buffer/mod.rs b/core/src/text_buffer/mod.rs index ea90e50..c208bbf 100644 --- a/core/src/text_buffer/mod.rs +++ b/core/src/text_buffer/mod.rs @@ -11,6 +11,7 @@ mod writer; pub use writer::TextBufferWriter; /// A block of text, usually containing the contents of a text file. +#[derive(Clone)] pub struct TextBuffer { contents: Rc, } @@ -25,6 +26,16 @@ pub enum Point { End, } +impl Point { + pub fn advance(self) -> Self { + match self { + Self::Start => Self::LineColumn(0, 1), + Self::LineColumn(l, c) => Self::LineColumn(l, c + 1), + Self::End => Self::End, + } + } +} + impl TextBuffer { /// Create a new empty [TextBuffer] pub fn new() -> Self { @@ -70,8 +81,8 @@ impl TextBuffer { } } - pub fn insert_char(&mut self, c: char, point: Point) { - self.contents = match point { + pub fn insert_char(&self, c: char, point: Point) -> Self { + let contents = match point { Point::Start => self.contents.insert_at_char_index(0, c), Point::LineColumn(line_num, column_num) => { self.contents @@ -81,6 +92,7 @@ impl TextBuffer { .contents .insert_at_char_index(self.contents.total_chars(), c), }; + Self { contents } } pub fn delete_at_char_index(&mut self, start: usize, length: usize) -> bool { @@ -122,68 +134,68 @@ mod tests { #[test] fn insert_char_at_end_increases_counts_as_expected() { - let mut target = TextBuffer::new(); - target.insert_char('A', Point::End); + let target = TextBuffer::new(); + let target = target.insert_char('A', Point::End); assert_eq!(1, target.num_bytes()); assert_eq!(1, target.num_chars()); assert_eq!(1, target.num_lines()); - target.insert_char(' ', Point::End); + let target = target.insert_char(' ', Point::End); assert_eq!(2, target.num_bytes()); assert_eq!(2, target.num_chars()); assert_eq!(1, target.num_lines()); - target.insert_char('c', Point::End); + let target = target.insert_char('c', Point::End); assert_eq!(3, target.num_bytes()); assert_eq!(3, target.num_chars()); assert_eq!(1, target.num_lines()); - target.insert_char('a', Point::End); + let target = target.insert_char('a', Point::End); assert_eq!(4, target.num_bytes()); assert_eq!(4, target.num_chars()); assert_eq!(1, target.num_lines()); - target.insert_char('t', Point::End); + let target = target.insert_char('t', Point::End); assert_eq!(5, target.num_bytes()); assert_eq!(5, target.num_chars()); assert_eq!(1, target.num_lines()); - target.insert_char('\n', Point::End); + let target = target.insert_char('\n', Point::End); assert_eq!(6, target.num_bytes()); assert_eq!(6, target.num_chars()); assert_eq!(1, target.num_lines()); - target.insert_char('A', Point::End); + let target = target.insert_char('A', Point::End); assert_eq!(7, target.num_bytes()); assert_eq!(7, target.num_chars()); assert_eq!(2, target.num_lines()); - target.insert_char('\n', Point::End); + let target = target.insert_char('\n', Point::End); assert_eq!(8, target.num_bytes()); assert_eq!(8, target.num_chars()); assert_eq!(2, target.num_lines()); - target.insert_char('\n', Point::End); + let target = target.insert_char('\n', Point::End); assert_eq!(9, target.num_bytes()); assert_eq!(9, target.num_chars()); assert_eq!(3, target.num_lines()); - target.insert_char('*', Point::End); + let target = target.insert_char('*', Point::End); assert_eq!(10, target.num_bytes()); assert_eq!(10, target.num_chars()); assert_eq!(4, target.num_lines()); - target.insert_char('*', Point::End); + let target = target.insert_char('*', Point::End); assert_eq!(11, target.num_bytes()); assert_eq!(11, target.num_chars()); assert_eq!(4, target.num_lines()); - target.insert_char(' ', Point::End); + let target = target.insert_char(' ', Point::End); assert_eq!(12, target.num_bytes()); assert_eq!(12, target.num_chars()); assert_eq!(4, target.num_lines()); - target.insert_char('猫', Point::End); + let target = target.insert_char('猫', Point::End); assert_eq!(15, target.num_bytes()); assert_eq!(13, target.num_chars()); assert_eq!(4, target.num_lines()); - target.insert_char(' ', Point::End); + let target = target.insert_char(' ', Point::End); assert_eq!(16, target.num_bytes()); assert_eq!(14, target.num_chars()); assert_eq!(4, target.num_lines()); - target.insert_char('\n', Point::End); + let target = target.insert_char('\n', Point::End); assert_eq!(17, target.num_bytes()); assert_eq!(15, target.num_chars()); assert_eq!(4, target.num_lines()); - target.insert_char('_', Point::End); + let target = target.insert_char('_', Point::End); assert_eq!(18, target.num_bytes()); assert_eq!(16, target.num_chars()); assert_eq!(5, target.num_lines());