Refactor EditorBuffer and TextBuffer to be immutable

This commit is contained in:
Matthew Gordon 2025-11-15 21:11:47 -04:00
parent 4188301e79
commit 41e9c197cd
3 changed files with 198 additions and 147 deletions

View File

@ -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<PathBuf>) -> CommandResponse {
if let Some(filepath) = filepath.as_ref().or(self.filepath.as_ref()) {
pub fn save_file(&self, filepath: Option<PathBuf>) -> 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!(
Ok(bytes_read) => CommandResult::success_with_message(
self.clone(),
format!(
"Read {bytes_read} bytes to \"{}\"",
filepath.to_string_lossy()
)),
Err(err) => CommandResponse::Failure(format!("{}", err)),
),
),
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();

View File

@ -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<EditorBufferState>,
}
#[derive(Default)]
struct EditorBufferState {
filepath: Option<PathBuf>,
}
#[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<String>) -> Self {
Self {
success: true,
response: CommandResponse::Message(msg.into()),
buffer,
}
}
fn fail_with_message(buffer: EditorBuffer, msg: impl Into<String>) -> 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<String> = 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<String> = 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<String> = 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<String> = target
.buffer
.iter_chars()

View File

@ -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<Rope>,
}
@ -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());