Compare commits

..

No commits in common. "41e9c197cdaefaa16d3b75ec5cf9cf858b506a6d" and "69d87ef680691554db67f47b38a17e454663affe" have entirely different histories.

9 changed files with 165 additions and 728 deletions

View File

@ -7,8 +7,3 @@ edition = "2024"
ntest = "0.9.3"
rand = {version="0.9.2", features=["small_rng", "alloc"]}
tempfile = "3.23.0"
[profile.test]
opt-level = 3
lto = "fat"
codegen-units = 1

View File

@ -1,11 +1,11 @@
use std::{path::PathBuf, rc::Rc};
use std::path::PathBuf;
use super::{CommandResult, EditorBuffer, EditorBufferState};
use super::{CommandResponse, EditorBuffer};
use crate::{Point, TextBuffer, TextBufferReader, TextBufferWriter};
#[doc(hidden)]
impl EditorBuffer {
pub fn open_file(&self, filepath: PathBuf) -> CommandResult {
pub fn open_file(&mut self, filepath: PathBuf) -> CommandResponse {
match std::fs::File::open(&filepath) {
Ok(mut file) => {
let mut buffer = TextBuffer::new();
@ -15,59 +15,43 @@ impl EditorBuffer {
"Read {bytes_read} bytes from \"{}\"",
filepath.to_string_lossy()
);
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,
)
self.filepath = Some(filepath);
self.cursor = Point::default();
self.buffer = buffer;
CommandResponse::Success(msg)
}
Err(err) => CommandResult::fail_with_message(self.clone(), format!("{}", err)),
Err(err) => CommandResponse::Failure(format!("{}", err)),
}
}
Err(err) => {
if err.kind() == std::io::ErrorKind::NotFound {
CommandResult::fail_with_message(
self.clone(),
format!("File not found: \"{}\"", filepath.to_string_lossy()),
)
CommandResponse::Failure(format!(
"File not found: \"{}\"",
filepath.to_string_lossy()
))
} else {
CommandResult::fail_with_message(self.clone(), format!("{}", err))
CommandResponse::Failure(format!("{}", err))
}
}
}
}
pub fn save_file(&self, filepath: Option<PathBuf>) -> CommandResult {
if let Some(filepath) = filepath.as_ref().or(self.state.filepath.as_ref()) {
pub fn save_file(&mut self, filepath: Option<PathBuf>) -> CommandResponse {
if let Some(filepath) = filepath.as_ref().or(self.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) => 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))
}
Ok(bytes_read) => CommandResponse::Success(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)),
}
} else {
CommandResult::fail_with_message(self.clone(), "Attempting to same file with no name.")
CommandResponse::Failure("Attempting to same file with no name.".into())
}
}
}
@ -87,10 +71,11 @@ mod tests {
.iter()
.collect();
let expected_text = std::fs::read_to_string(&test_file_path).unwrap();
let target = EditorBuffer::new();
let result = target.execute(Command::OpenFile(test_file_path));
assert!(result.is_ok());
let target = result.buffer;
let mut target = EditorBuffer::new();
assert!(matches!(
target.execute(Command::OpenFile(test_file_path)),
CommandResponse::Success(_)
));
let mut buffer_bytes = Vec::new();
TextBufferReader::new(&target.buffer)
@ -106,11 +91,9 @@ mod tests {
.iter()
.collect();
let expected_message = format!("File not found: \"{}\"", test_file_path.to_string_lossy());
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),
let mut target = EditorBuffer::new();
match target.execute(Command::OpenFile(test_file_path)) {
CommandResponse::Failure(s) => assert_eq!(expected_message, s),
_ => panic!(),
}
}
@ -123,12 +106,14 @@ mod tests {
]
.iter()
.collect();
let target = EditorBuffer::new();
let target = target.execute(Command::OpenFile(test_file_path.clone())).buffer;
let mut target = EditorBuffer::new();
target.execute(Command::OpenFile(test_file_path.clone()));
let temp_dir = tempdir().unwrap();
let tmp_file_path = temp_dir.path().join(r"Les_Trois_Mousquetaires.txt");
let result = target.execute(Command::SaveAs(tmp_file_path.clone()));
assert!(result.is_ok());
assert!(matches!(
dbg!(target.execute(Command::SaveAs(tmp_file_path.clone()))),
CommandResponse::Success(_)
));
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,64 +1,22 @@
use std::{path::PathBuf, rc::Rc};
use std::path::PathBuf;
use crate::{Point, TextBuffer};
mod command;
mod io;
mod command;
pub use command::{Command, Movement, Unit};
#[derive(Default, Clone)]
#[derive(Default)]
pub struct EditorBuffer {
buffer: TextBuffer,
cursor: Point,
state: Rc<EditorBufferState>,
}
#[derive(Default)]
struct EditorBufferState {
filepath: Option<PathBuf>,
}
#[derive(Debug, PartialEq)]
#[derive(Debug)]
pub enum CommandResponse {
None,
Message(String),
}
#[must_use]
pub struct CommandResult {
success: bool,
response: CommandResponse,
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,
}
}
Success(String),
Failure(String),
}
impl EditorBuffer {
@ -68,7 +26,7 @@ impl EditorBuffer {
}
/// Execute a command on the [EditorBuffer]
pub fn execute(&self, command: Command) -> CommandResult {
pub fn execute(&mut self, command: Command) -> CommandResponse {
match command {
Command::OpenFile(filepath) => self.open_file(filepath),
Command::Save => self.save_file(None),
@ -82,226 +40,27 @@ impl EditorBuffer {
}
}
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 {
fn move_cursor_to_point(&mut self, _point: Point) -> CommandResponse {
todo!()
}
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(&self, _s: String) -> CommandResult {
fn move_cursor_to_line_number(&mut self, _line_num: usize) -> CommandResponse {
todo!()
}
fn move_cursor(&self, _movement: Movement) -> CommandResult {
fn insert_char(&mut self, _c: char) -> CommandResponse {
todo!()
}
fn delete(&self, _movement: Movement) -> CommandResult {
fn insert_string(&mut self, _s: String) -> CommandResponse {
todo!()
}
fn move_cursor(&mut self, _movement: Movement) -> CommandResponse {
todo!()
}
fn delete(&mut self, _movement: Movement) -> CommandResponse {
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);
}
}

View File

@ -11,31 +11,18 @@ 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>,
}
/// A location in a [TextBuffer]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[derive(Default, Clone, Copy)]
pub enum Point {
/// The end of the buffer
#[default]
Start,
LineColumn(usize, usize),
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 {
@ -61,18 +48,21 @@ impl TextBuffer {
0
} else {
self.contents.total_lines()
+ if self
.contents
.get_char_at_index(self.contents.total_chars() - 1)
== '\n'
{
0
} else {
1
}
}
}
/// Insert `text` at `point`.
pub fn insert_text(&mut self, text: impl Into<String>, point: Point) {
match point {
Point::Start => {
self.contents = self.contents.insert_at_char_index(0, text);
}
Point::LineColumn(_, _) => {
todo!()
}
Point::End => {
self.contents = self
.contents
@ -81,28 +71,18 @@ impl TextBuffer {
}
}
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
.insert_at_line_and_column(line_num - 1, column_num, c)
pub fn insert_char(&mut self, c: char, point: Point) {
match point {
Point::End => {
self.contents = self
.contents
.insert_at_char_index(self.contents.total_chars(), c)
}
Point::End => self
.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 {
match self.contents.delete_at_char_index(start, length) {
Ok(r) => {
self.contents = r;
true
}
Err(_) => false,
}
pub fn delete_at_char_index(&mut self, start: usize, length: usize) {
self.contents = self.contents.delete_at_char_index(start, length)
}
pub fn iter_chars(&self) -> CharIterator {
@ -134,68 +114,68 @@ mod tests {
#[test]
fn insert_char_at_end_increases_counts_as_expected() {
let target = TextBuffer::new();
let target = target.insert_char('A', Point::End);
let mut target = TextBuffer::new();
target.insert_char('A', Point::End);
assert_eq!(1, target.num_bytes());
assert_eq!(1, target.num_chars());
assert_eq!(1, target.num_lines());
let target = target.insert_char(' ', Point::End);
target.insert_char(' ', Point::End);
assert_eq!(2, target.num_bytes());
assert_eq!(2, target.num_chars());
assert_eq!(1, target.num_lines());
let target = target.insert_char('c', Point::End);
target.insert_char('c', Point::End);
assert_eq!(3, target.num_bytes());
assert_eq!(3, target.num_chars());
assert_eq!(1, target.num_lines());
let target = target.insert_char('a', Point::End);
target.insert_char('a', Point::End);
assert_eq!(4, target.num_bytes());
assert_eq!(4, target.num_chars());
assert_eq!(1, target.num_lines());
let target = target.insert_char('t', Point::End);
target.insert_char('t', Point::End);
assert_eq!(5, target.num_bytes());
assert_eq!(5, target.num_chars());
assert_eq!(1, target.num_lines());
let target = target.insert_char('\n', Point::End);
target.insert_char('\n', Point::End);
assert_eq!(6, target.num_bytes());
assert_eq!(6, target.num_chars());
assert_eq!(1, target.num_lines());
let target = target.insert_char('A', Point::End);
target.insert_char('A', Point::End);
assert_eq!(7, target.num_bytes());
assert_eq!(7, target.num_chars());
assert_eq!(2, target.num_lines());
let target = target.insert_char('\n', Point::End);
target.insert_char('\n', Point::End);
assert_eq!(8, target.num_bytes());
assert_eq!(8, target.num_chars());
assert_eq!(2, target.num_lines());
let target = target.insert_char('\n', Point::End);
target.insert_char('\n', Point::End);
assert_eq!(9, target.num_bytes());
assert_eq!(9, target.num_chars());
assert_eq!(3, target.num_lines());
let target = target.insert_char('*', Point::End);
target.insert_char('*', Point::End);
assert_eq!(10, target.num_bytes());
assert_eq!(10, target.num_chars());
assert_eq!(4, target.num_lines());
let target = target.insert_char('*', Point::End);
target.insert_char('*', Point::End);
assert_eq!(11, target.num_bytes());
assert_eq!(11, target.num_chars());
assert_eq!(4, target.num_lines());
let target = target.insert_char(' ', Point::End);
target.insert_char(' ', Point::End);
assert_eq!(12, target.num_bytes());
assert_eq!(12, target.num_chars());
assert_eq!(4, target.num_lines());
let target = target.insert_char('猫', Point::End);
target.insert_char('猫', Point::End);
assert_eq!(15, target.num_bytes());
assert_eq!(13, target.num_chars());
assert_eq!(4, target.num_lines());
let target = target.insert_char(' ', Point::End);
target.insert_char(' ', Point::End);
assert_eq!(16, target.num_bytes());
assert_eq!(14, target.num_chars());
assert_eq!(4, target.num_lines());
let target = target.insert_char('\n', Point::End);
target.insert_char('\n', Point::End);
assert_eq!(17, target.num_bytes());
assert_eq!(15, target.num_chars());
assert_eq!(4, target.num_lines());
let target = target.insert_char('_', Point::End);
target.insert_char('_', Point::End);
assert_eq!(18, target.num_bytes());
assert_eq!(16, target.num_chars());
assert_eq!(5, target.num_lines());

View File

@ -27,7 +27,7 @@ impl std::io::Read for TextBufferReader {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let mut bytes_written = 0;
while let Some(rope) = self.current_node.clone() {
if let Rope::Leaf { text, .. } = rope.as_ref() {
if let Rope::Leaf { text } = rope.as_ref() {
let bytes = text.as_bytes();
let length = bytes
.len()

View File

@ -1,119 +0,0 @@
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
/// Represents a block of text, where:
///
/// LineColumn.0 is the numbner of newlines in the text.
/// LineColumn.1 is the number of characters after the last newline.
///
/// So a block of text with `n` lines, ending with a newline, would be
/// represented by `LineColumn(n, 0)`. A block of text, *not* ending in a
/// newline, with `m` characters on the last line, would be represented by
/// `LineColumn(n-1, m)`.
pub struct LineColumn(pub usize, pub usize);
impl std::cmp::PartialOrd for LineColumn {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
/// Assume the two values are substrings withing the same string. If the two
/// values start on the same character, does the second one end before, after or
/// at the same place as the first?
impl std::cmp::Ord for LineColumn {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
if self.0 == other.0 {
self.1.cmp(&other.1)
} else {
self.0.cmp(&other.0)
}
}
}
/// Return the representation of a string formed by concatenating the two
/// strings. Note that, like string concatenation, this is *not*
/// commutative. a+b and b+a are different things.
impl std::ops::Add for LineColumn {
type Output = LineColumn;
fn add(self, rhs: Self) -> Self::Output {
if rhs.0 == 0 {
LineColumn(self.0, self.1 + rhs.1)
} else {
LineColumn(self.0 + rhs.0, rhs.1)
}
}
}
/// If a + b = c, then c - a = b
/// The result of removing the second value from beginning of the first.
impl std::ops::Sub for LineColumn {
type Output = LineColumn;
fn sub(self, rhs: Self) -> Self::Output {
if rhs.0 == 0 {
LineColumn(self.0, self.1 - rhs.1)
} else if self.0 == rhs.0 {
LineColumn(0, self.1 - rhs.1)
} else {
LineColumn(self.0 - rhs.0, self.1)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::{Rng, SeedableRng, rngs::SmallRng};
#[test]
fn cmp_works() {
assert_eq!(LineColumn(0, 0), LineColumn(0, 0));
assert_eq!(LineColumn(1, 0), LineColumn(1, 0));
assert_eq!(LineColumn(0, 2), LineColumn(0, 2));
assert_eq!(LineColumn(4, 16), LineColumn(4, 16));
assert_ne!(LineColumn(0, 0), LineColumn(0, 1));
assert_ne!(LineColumn(0, 0), LineColumn(1, 0));
assert_ne!(LineColumn(3, 0), LineColumn(3, 2));
assert_ne!(LineColumn(16, 21), LineColumn(17, 21));
assert!(LineColumn(0, 0) < LineColumn(0, 1));
assert!(LineColumn(0, 1) > LineColumn(0, 0));
assert!(LineColumn(12, 34) < LineColumn(23, 45));
assert!(LineColumn(12, 34) > LineColumn(1, 23));
assert!(LineColumn(12, 34) < LineColumn(12, 35));
assert!(LineColumn(12, 34) > LineColumn(12, 33));
}
#[test]
fn add_works() {
assert_eq!(LineColumn(12, 34), LineColumn(0, 0) + LineColumn(12, 34));
assert_eq!(LineColumn(12, 34), LineColumn(0, 1) + LineColumn(12, 34));
assert_eq!(LineColumn(12, 34), LineColumn(0, 2) + LineColumn(12, 34));
assert_eq!(LineColumn(12, 34), LineColumn(0, 89) + LineColumn(12, 34));
assert_eq!(LineColumn(15, 34), LineColumn(3, 0) + LineColumn(12, 34));
assert_eq!(LineColumn(15, 34), LineColumn(3, 1) + LineColumn(12, 34));
assert_eq!(LineColumn(15, 34), LineColumn(3, 2) + LineColumn(12, 34));
assert_eq!(LineColumn(15, 34), LineColumn(3, 89) + LineColumn(12, 34));
assert_eq!(LineColumn(5, 35), LineColumn(5, 23) + LineColumn(0, 12));
assert_eq!(LineColumn(5, 23), LineColumn(5, 23) + LineColumn(0, 0));
assert_eq!(LineColumn(0, 34), LineColumn(0, 15) + LineColumn(0, 19));
}
#[test]
fn subtract_works() {
assert_eq!(LineColumn(12, 34), LineColumn(12, 34) - LineColumn(0, 0));
assert_eq!(LineColumn(12, 31), LineColumn(12, 34) - LineColumn(0, 3));
assert_eq!(LineColumn(2, 34), LineColumn(12, 34) - LineColumn(10, 0));
assert_eq!(LineColumn(2, 34), LineColumn(12, 34) - LineColumn(10, 5));
let mut rng = SmallRng::seed_from_u64(0x0123456789abcdef);
for _ in 0..100 {
let a = LineColumn(rng.random_range(..1000), rng.random_range(..300));
let b = LineColumn(rng.random_range(..1000), rng.random_range(..300));
let c = a + b;
assert_eq!(b, c - a);
}
}
}

View File

@ -3,11 +3,6 @@ use std::rc::Rc;
mod fibbonacci;
use fibbonacci::fibbonacci;
mod line_column;
use line_column::LineColumn;
pub type RopeResult = Result<Rc<Rope>, Rc<Rope>>;
/// [Rope](https://en.wikipedia.org/wiki/Rope_(data_structure)) data structure
/// implementation.
///
@ -24,11 +19,9 @@ pub enum Rope {
/// or the number of characters in the string if this is a leaf.
chars_weight: usize,
/// Total number of line endings and number of characters in last line
/// in the string contained in the left subtree, or the number of line
/// endings and number of characters in last line in the string if this
/// is a leaf.
line_column_weight: LineColumn,
/// Total number of line endings in the string contained in the left
/// subtree, or the number of line endings in the string if this is a leaf.
lines_weight: usize,
/// The root of the left subtree
left: Rc<Rope>,
@ -36,8 +29,6 @@ pub enum Rope {
right: Option<Rc<Rope>>,
},
Leaf {
chars_count: usize,
line_column_count: LineColumn,
text: String,
},
}
@ -46,29 +37,13 @@ impl Rope {
/// Create a new Rope containing the passed text in a single node.
pub fn new(contents: impl Into<String>) -> Rc<Self> {
let text = contents.into();
let (chars_count, line_column_count) = text.chars().fold(
(0, LineColumn(0, 0)),
|(char_count, LineColumn(newline_count, chars_since_newline)), c| {
(
char_count + 1,
if c == '\n' {
LineColumn(newline_count + 1, 0)
} else {
LineColumn(newline_count, chars_since_newline + 1)
},
)
},
);
Rc::new(Rope::Leaf {
chars_count,
line_column_count,
text,
})
Rc::new(Rope::Leaf { text })
}
/// Create a new empty Rope
pub fn empty() -> Rc<Self> {
Rope::new("")
let text = "".into();
Rc::new(Rope::Leaf { text })
}
/// Return the total number of bytes in the text.
@ -84,7 +59,7 @@ impl Rope {
right: Some(right),
..
} => bytes_weight + right.total_bytes(),
Rope::Leaf { text, .. } => text.len(),
Rope::Leaf { text } => text.len(),
}
}
@ -101,14 +76,25 @@ impl Rope {
right: Some(right),
..
} => chars_weight + right.total_chars(),
Rope::Leaf { chars_count, .. } => *chars_count,
Rope::Leaf { text } => text.chars().count(),
}
}
/// Return the total number of lines in the text
pub fn total_lines(&self) -> usize {
let LineColumn(l, c) = self.line_column_total();
if c > 0 { l + 1 } else { l }
match self {
Rope::Branch {
lines_weight,
right: None,
..
} => *lines_weight,
Rope::Branch {
lines_weight,
right: Some(right),
..
} => lines_weight + right.total_lines(),
Rope::Leaf { text } => text.chars().filter(|&c| c == '\n').count(),
}
}
/// Return the character as a given character index.
@ -129,7 +115,7 @@ impl Rope {
right.get_char_at_index(index - chars_weight)
}
}
Rope::Leaf { text, .. } => text.chars().nth(index).unwrap(),
Rope::Leaf { text } => text.chars().nth(index).unwrap(),
}
}
@ -145,176 +131,65 @@ impl Rope {
index: usize,
text: impl Into<String>,
) -> Rc<Rope> {
let text: String = text.into();
let new_node = Rope::new(text);
let total_chars = self.total_chars();
if total_chars == 0 {
new_node
if index == 0 {
new_node.concat(self.clone())
} else if index < total_chars {
let (before, after) = self.split_at_char_index(index);
before.concat(new_node).concat(after)
} else if index == total_chars {
self.clone().concat(new_node)
} else {
match self.split_at_char_index(index) {
(Some(before), after) => {
Rope::join(Rope::join(before, Some(new_node)), after).rebalance()
}
(None, after) => Rope::join(new_node, after).rebalance(),
}
panic!("Attempt to insert past end of rope.")
}
}
/// Insert new text into the rope at a given line and column.
pub fn insert_at_line_and_column(
self: &Rc<Rope>,
line_num: usize,
column_num: usize,
text: impl Into<String>,
) -> Rc<Rope> {
let new_node = Rope::new(text);
if line_num == 0 && column_num == 0 {
Rope::join(new_node, Some(Rc::clone(self))).rebalance()
} else {
match self.split_at_line_and_column(line_num, column_num) {
(Some(before), after) => {
Rope::join(Rope::join(before, Some(new_node)), after).rebalance()
}
(None, after) => Rope::join(new_node, after).rebalance(),
}
}
}
pub fn split_at_line_and_column(
self: &Rc<Rope>,
line_num: usize,
column_num: usize,
) -> (Option<Rc<Rope>>, Option<Rc<Rope>>) {
self.split_at_index(
LineColumn(line_num, column_num),
|rope| match rope {
Rope::Branch {
line_column_weight, ..
} => *line_column_weight,
Rope::Leaf {
line_column_count, ..
} => *line_column_count,
},
|text, LineColumn(line, column)| {
let mut current_line = 0;
let mut current_column = 0;
let mut byte_to_split_at = text.len();
for (i, c) in text.char_indices() {
if c == '\n' {
current_line += 1;
current_column = 0;
} else {
current_column += 1;
}
if (current_line == line && current_column > column) || current_line > line {
byte_to_split_at = i;
break;
}
}
if byte_to_split_at == 0 {
(None, Some(Rope::new(text)))
} else if byte_to_split_at == text.len() {
(Some(Rope::new(text)), None)
} else {
let (first, second) = text.split_at(byte_to_split_at);
(Some(Rope::new(first)), Some(Rope::new(second)))
}
},
)
}
/// Return a new rope with `length` characters removed after `start`.
pub fn delete_at_char_index(self: &Rc<Rope>, start: usize, length: usize) -> RopeResult {
if let (beginning, Some(rest)) = self.split_at_char_index(start) {
let (_, end) = rest.split_at_char_index(length);
if let Some(beginning) = beginning {
Ok(Rope::join(beginning, end).rebalance())
} else if let Some(end) = end {
Ok(end)
} else {
Ok(Rope::new(""))
}
} else {
Err(Rc::clone(self))
}
pub fn delete_at_char_index(self: &Rc<Rope>, start: usize, length: usize) -> Rc<Rope> {
let (beginning, rest) = self.split_at_char_index(start);
let (_, end) = rest.split_at_char_index(length);
beginning.concat(end)
}
/// Split the rope in two at character `index`.
///
/// The result is two Ropes—one containing the first 'i' characters of the
/// text and the other containing the rest of the text.
pub fn split_at_char_index(
self: &Rc<Self>,
index: usize,
) -> (Option<Rc<Self>>, Option<Rc<Self>>) {
self.split_at_index(
index,
|rope| match rope {
Rope::Branch { chars_weight, .. } => *chars_weight,
Rope::Leaf { chars_count, .. } => *chars_count,
},
|text, i| {
if let Some((byte_index, _)) = text.char_indices().nth(i) {
let (first, second) = text.split_at(byte_index);
(Some(Rope::new(first)), Some(Rope::new(second)))
} else {
panic!()
}
},
)
}
/// Split the rope in two at character `index`.
///
/// The result is two Ropes—one containing the first 'i' characters of the
/// text and the other containing the rest of the text.
pub fn split_at_index<I, WeightF, SplitF>(
self: &Rc<Self>,
index: I,
weight: WeightF,
split_leaf: SplitF,
) -> (Option<Rc<Self>>, Option<Rc<Self>>)
where
I: std::cmp::Ord + std::ops::Sub<Output = I> + std::fmt::Debug + Copy,
WeightF: Fn(&Self) -> I,
SplitF: Fn(&str, I) -> (Option<Rc<Self>>, Option<Rc<Self>>),
{
pub fn split_at_char_index(self: &Rc<Self>, index: usize) -> (Rc<Self>, Rc<Self>) {
match *self.as_ref() {
Rope::Branch {
chars_weight,
ref left,
ref right,
..
} => {
let self_weight = weight(self);
if index < self_weight {
// Split left subtree
let (first, second) = left.split_at_index(index, weight, split_leaf);
if index < chars_weight {
let (first, second) = left.split_at_char_index(index);
(
first,
second
.map(|rope| Rope::join(rope, right.as_ref().map(Rc::clone)))
.or_else(|| right.as_ref().map(Rc::clone)),
first.rebalance(),
Rope::join(second, right.as_ref().map(|r| r.clone())).rebalance(),
)
} else if let Some(right) = right {
if index > self_weight {
// Split right subtree
let (first, second) =
right.split_at_index(index - self_weight, weight, split_leaf);
(Some(Rope::join(Rc::clone(left), first)), second)
if index > chars_weight {
let (first, second) = right.split_at_char_index(index - chars_weight);
(
Rope::join(left.clone(), Some(first)).rebalance(),
second.rebalance(),
)
} else {
// Split is aleady exactly between left and right subtrees
(Some(Rc::clone(left)), Some(Rc::clone(right)))
(left.clone(), right.clone())
}
} else {
// Split is at end
(Some(Rc::clone(left)), None)
(left.clone(), Rope::empty())
}
}
Rope::Leaf { ref text, .. } => {
if index >= weight(self) {
(Some(self.clone()), None)
Rope::Leaf { ref text } => {
if let Some((byte_index, _)) = text.char_indices().nth(index) {
let (first, second) = text.split_at(byte_index);
(Rope::new(first), Rope::new(second))
} else {
split_leaf(text, index)
(self.clone(), Rope::empty())
}
}
}
@ -370,7 +245,7 @@ impl Rope {
Rc::new(Rope::Branch {
bytes_weight: left.total_bytes(),
chars_weight: left.total_chars(),
line_column_weight: left.line_column_total(),
lines_weight: left.total_lines(),
left,
right,
})
@ -380,32 +255,6 @@ impl Rope {
pub fn iter_nodes(self: Rc<Self>) -> NodeIterator {
NodeIterator::new(self)
}
/// Return the total number of lines in the text, and the number of characters on the last line
pub fn line_column_total(&self) -> LineColumn {
match self {
Rope::Branch {
line_column_weight,
right: None,
..
} => *line_column_weight,
Rope::Branch {
line_column_weight,
right: Some(right),
..
} => *line_column_weight + right.line_column_total(),
Rope::Leaf { text, .. } => {
text.chars()
.fold(LineColumn(0, 0), |LineColumn(line, col), c| {
if c == '\n' {
LineColumn(line + 1, 0)
} else {
LineColumn(line, col + 1)
}
})
}
}
}
}
fn merge(leaf_nodes: &[Rc<Rope>]) -> Rc<Rope> {
@ -500,7 +349,7 @@ impl CharWithPointIterator {
fn next_string(node_iterator: &mut NodeIterator) -> Option<String> {
node_iterator.next().map(|rope| {
if let Rope::Leaf { text, .. } = rope.as_ref() {
if let Rope::Leaf { text } = rope.as_ref() {
text.clone()
} else {
panic!("Rope NodeIterator yielded non-leaf node.")

View File

@ -1,8 +1,8 @@
use super::super::{Rc, Rope};
use rand::{
Rng, SeedableRng,
distr::{Alphanumeric, SampleString},
rngs::SmallRng,
Rng, SeedableRng,
};
#[derive(Clone, Debug)]
@ -15,9 +15,9 @@ impl Command {
pub fn run(&self, rope: &Rc<Rope>) -> Rc<Rope> {
match self {
Command::InsertAtCharIndex { index, text } => rope.insert_at_char_index(*index, text),
Command::DeleteAtCharIndex { index, length } => rope
.delete_at_char_index(*index, *length)
.expect("delete was successful"),
Command::DeleteAtCharIndex { index, length } => {
rope.delete_at_char_index(*index, *length)
}
}
}
@ -58,7 +58,7 @@ pub fn generate_random_edit_sequence_with_seed(
let text = Alphanumeric.sample_string(&mut rng, text_len);
Command::InsertAtCharIndex { index, text }
} else {
let index = rng.random_range(0..current_text_length - 1);
let index = rng.random_range(0..current_text_length-1);
let length = rng.random_range(1..(current_text_length - index));
Command::DeleteAtCharIndex { index, length }
};

View File

@ -74,7 +74,7 @@ fn node_iterator_for_single_node_returns_node_and_only_node() {
assert_eq!(result.len(), 1);
assert_eq!(result[0].total_bytes(), 3);
assert_eq!(result[0].total_chars(), 3);
assert_eq!(result[0].total_lines(), 1);
assert_eq!(result[0].total_lines(), 0);
match result[0].as_ref() {
Rope::Leaf { text, .. } => assert_eq!(text, "The"),
_ => panic!(),
@ -91,7 +91,7 @@ fn node_iterator_returns_nodes_in_correct_order() {
assert_eq!(result.len(), 10);
for (node, string) in result.iter().zip(strings) {
match &node.as_ref() {
Rope::Leaf { text, .. } => assert_eq!(text, string),
Rope::Leaf { text } => assert_eq!(text, string),
_ => panic!(),
}
}
@ -116,14 +116,8 @@ fn split_splits_at_correct_location() {
let full_string = small_test_rope_full_string();
for i in 0..(full_string.chars().count()) {
let (first, second) = target.split_at_char_index(i);
let first_string: String = first
.expect("first part is not None")
.iter_chars()
.collect();
let second_string: String = second
.expect("second part is not None")
.iter_chars()
.collect();
let first_string: String = first.iter_chars().collect();
let second_string: String = second.iter_chars().collect();
assert_eq!(first_string, full_string[0..i]);
assert_eq!(second_string, full_string[i..]);
}
@ -138,8 +132,6 @@ fn split_splits_at_correct_location_with_multibyte_chars() {
.collect();
for i in 0..(expected_chars.len()) {
let (first, second) = target.split_at_char_index(i);
let first = first.expect("First part is not None");
let second = second.expect("Second part is not None");
let string: String = first.iter_chars().collect();
let expected: String = expected_chars[0..i].iter().collect();
assert_eq!(string, expected);
@ -232,11 +224,11 @@ fn get_char_at_index() {
fn insert_at_char_index() {
let target = Rope::new("The brown dog");
assert_eq!(target.iter_chars().collect::<String>(), "The brown dog");
assert_eq!(1, target.total_lines());
assert_eq!(0, target.total_lines());
let rope1 = target.insert_at_char_index(4, "quick");
assert_eq!(target.iter_chars().collect::<String>(), "The brown dog");
assert_eq!(rope1.iter_chars().collect::<String>(), "The quickbrown dog");
assert_eq!(1, rope1.total_lines());
assert_eq!(0, rope1.total_lines());
let rope2 = rope1.insert_at_char_index(9, " ");
assert_eq!(target.iter_chars().collect::<String>(), "The brown dog");
assert_eq!(rope1.iter_chars().collect::<String>(), "The quickbrown dog");
@ -244,22 +236,20 @@ fn insert_at_char_index() {
rope2.iter_chars().collect::<String>(),
"The quick brown dog"
);
assert_eq!(1, rope2.total_lines());
assert_eq!(0, rope2.total_lines());
let rope3 =
rope2.insert_at_char_index("The quick brown dog".len(), " jumps over the lazy fox.");
assert_eq!(
rope3.iter_chars().collect::<String>(),
"The quick brown dog jumps over the lazy fox."
);
assert_eq!(1, rope3.total_lines());
assert_eq!(0, rope3.total_lines());
}
#[test]
fn delete_at_char_index() {
let target = Rope::new("The quick brown fox jumps over the lazy dog.");
let test = target
.delete_at_char_index(10, 6)
.expect("Delete was successful");
let test = target.delete_at_char_index(10, 6);
assert_eq!(
target.iter_chars().collect::<String>(),
"The quick brown fox jumps over the lazy dog."
@ -268,9 +258,7 @@ fn delete_at_char_index() {
test.iter_chars().collect::<String>(),
"The quick fox jumps over the lazy dog."
);
let test = target
.delete_at_char_index(0, 4)
.expect("Delete was successful");
let test = target.delete_at_char_index(0, 4);
assert_eq!(
target.iter_chars().collect::<String>(),
"The quick brown fox jumps over the lazy dog."
@ -279,9 +267,8 @@ fn delete_at_char_index() {
test.iter_chars().collect::<String>(),
"quick brown fox jumps over the lazy dog."
);
let test = target
.delete_at_char_index("The quick brown fox jumps over the lazy dog.".len() - 5, 5)
.expect("Delete was successful");
let test =
target.delete_at_char_index("The quick brown fox jumps over the lazy dog.".len() - 5, 5);
assert_eq!(
target.iter_chars().collect::<String>(),
"The quick brown fox jumps over the lazy dog."
@ -499,6 +486,7 @@ fn random_insertions_and_deletions_with_seed(seed: u64) {
let (start_text, edits) = command_list::generate_random_edit_sequence_with_seed(1000, seed);
let mut target = Rope::new(start_text);
for (command, expected_text) in edits {
println!("{:?}", command);
target = command.run(&target);
assert_eq!(expected_text, target.iter_chars().collect::<String>());
}