378 lines
13 KiB
Rust
378 lines
13 KiB
Rust
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<EditorBufferState>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct EditorBufferState {
|
|
filepath: Option<PathBuf>,
|
|
}
|
|
|
|
#[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<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,
|
|
}
|
|
}
|
|
}
|
|
|
|
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<String> = target
|
|
.buffer
|
|
.iter_chars()
|
|
.collect::<String>()
|
|
.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<String> = target
|
|
.buffer
|
|
.iter_chars()
|
|
.collect::<String>()
|
|
.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<String> = target
|
|
.buffer
|
|
.iter_chars()
|
|
.collect::<String>()
|
|
.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<String> = target
|
|
.buffer
|
|
.iter_chars()
|
|
.collect::<String>()
|
|
.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<String> = target
|
|
.buffer
|
|
.iter_chars()
|
|
.collect::<String>()
|
|
.lines()
|
|
.map(|l| l.into())
|
|
.collect();
|
|
assert_eq!(expected_lines, found_lines);
|
|
assert_eq!(Point::LineColumn(8, 0), target.get_cursor_position());
|
|
}
|
|
}
|