Compare commits
No commits in common. "41e9c197cdaefaa16d3b75ec5cf9cf858b506a6d" and "69d87ef680691554db67f47b38a17e454663affe" have entirely different histories.
41e9c197cd
...
69d87ef680
|
|
@ -7,8 +7,3 @@ edition = "2024"
|
||||||
ntest = "0.9.3"
|
ntest = "0.9.3"
|
||||||
rand = {version="0.9.2", features=["small_rng", "alloc"]}
|
rand = {version="0.9.2", features=["small_rng", "alloc"]}
|
||||||
tempfile = "3.23.0"
|
tempfile = "3.23.0"
|
||||||
|
|
||||||
[profile.test]
|
|
||||||
opt-level = 3
|
|
||||||
lto = "fat"
|
|
||||||
codegen-units = 1
|
|
||||||
|
|
@ -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};
|
use crate::{Point, TextBuffer, TextBufferReader, TextBufferWriter};
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
impl EditorBuffer {
|
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) {
|
match std::fs::File::open(&filepath) {
|
||||||
Ok(mut file) => {
|
Ok(mut file) => {
|
||||||
let mut buffer = TextBuffer::new();
|
let mut buffer = TextBuffer::new();
|
||||||
|
|
@ -15,59 +15,43 @@ impl EditorBuffer {
|
||||||
"Read {bytes_read} bytes from \"{}\"",
|
"Read {bytes_read} bytes from \"{}\"",
|
||||||
filepath.to_string_lossy()
|
filepath.to_string_lossy()
|
||||||
);
|
);
|
||||||
let state = Rc::new(EditorBufferState {
|
self.filepath = Some(filepath);
|
||||||
filepath: Some(filepath),
|
self.cursor = Point::default();
|
||||||
..*self.state
|
self.buffer = buffer;
|
||||||
});
|
CommandResponse::Success(msg)
|
||||||
let cursor = Point::default();
|
|
||||||
let buffer = buffer;
|
|
||||||
CommandResult::success_with_message(
|
|
||||||
Self {
|
|
||||||
state,
|
|
||||||
cursor,
|
|
||||||
buffer,
|
|
||||||
..self.clone()
|
|
||||||
},
|
|
||||||
msg,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Err(err) => CommandResult::fail_with_message(self.clone(), format!("{}", err)),
|
Err(err) => CommandResponse::Failure(format!("{}", err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
if err.kind() == std::io::ErrorKind::NotFound {
|
if err.kind() == std::io::ErrorKind::NotFound {
|
||||||
CommandResult::fail_with_message(
|
CommandResponse::Failure(format!(
|
||||||
self.clone(),
|
"File not found: \"{}\"",
|
||||||
format!("File not found: \"{}\"", filepath.to_string_lossy()),
|
filepath.to_string_lossy()
|
||||||
)
|
))
|
||||||
} else {
|
} else {
|
||||||
CommandResult::fail_with_message(self.clone(), format!("{}", err))
|
CommandResponse::Failure(format!("{}", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_file(&self, filepath: Option<PathBuf>) -> CommandResult {
|
pub fn save_file(&mut self, filepath: Option<PathBuf>) -> CommandResponse {
|
||||||
if let Some(filepath) = filepath.as_ref().or(self.state.filepath.as_ref()) {
|
if let Some(filepath) = filepath.as_ref().or(self.filepath.as_ref()) {
|
||||||
match std::fs::File::create(filepath) {
|
match std::fs::File::create(filepath) {
|
||||||
Ok(mut file) => {
|
Ok(mut file) => {
|
||||||
match std::io::copy(&mut TextBufferReader::new(&self.buffer), &mut file) {
|
match std::io::copy(&mut TextBufferReader::new(&self.buffer), &mut file) {
|
||||||
Ok(bytes_read) => CommandResult::success_with_message(
|
Ok(bytes_read) => CommandResponse::Success(format!(
|
||||||
self.clone(),
|
|
||||||
format!(
|
|
||||||
"Read {bytes_read} bytes to \"{}\"",
|
"Read {bytes_read} bytes to \"{}\"",
|
||||||
filepath.to_string_lossy()
|
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 {
|
} 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()
|
.iter()
|
||||||
.collect();
|
.collect();
|
||||||
let expected_text = std::fs::read_to_string(&test_file_path).unwrap();
|
let expected_text = std::fs::read_to_string(&test_file_path).unwrap();
|
||||||
let target = EditorBuffer::new();
|
let mut target = EditorBuffer::new();
|
||||||
let result = target.execute(Command::OpenFile(test_file_path));
|
assert!(matches!(
|
||||||
assert!(result.is_ok());
|
target.execute(Command::OpenFile(test_file_path)),
|
||||||
let target = result.buffer;
|
CommandResponse::Success(_)
|
||||||
|
));
|
||||||
|
|
||||||
let mut buffer_bytes = Vec::new();
|
let mut buffer_bytes = Vec::new();
|
||||||
TextBufferReader::new(&target.buffer)
|
TextBufferReader::new(&target.buffer)
|
||||||
|
|
@ -106,11 +91,9 @@ mod tests {
|
||||||
.iter()
|
.iter()
|
||||||
.collect();
|
.collect();
|
||||||
let expected_message = format!("File not found: \"{}\"", test_file_path.to_string_lossy());
|
let expected_message = format!("File not found: \"{}\"", test_file_path.to_string_lossy());
|
||||||
let target = EditorBuffer::new();
|
let mut target = EditorBuffer::new();
|
||||||
let result = target.execute(Command::OpenFile(test_file_path));
|
match target.execute(Command::OpenFile(test_file_path)) {
|
||||||
assert!(!result.is_ok());
|
CommandResponse::Failure(s) => assert_eq!(expected_message, s),
|
||||||
match result.response {
|
|
||||||
CommandResponse::Message(s) => assert_eq!(expected_message, s),
|
|
||||||
_ => panic!(),
|
_ => panic!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -123,12 +106,14 @@ mod tests {
|
||||||
]
|
]
|
||||||
.iter()
|
.iter()
|
||||||
.collect();
|
.collect();
|
||||||
let target = EditorBuffer::new();
|
let mut target = EditorBuffer::new();
|
||||||
let target = target.execute(Command::OpenFile(test_file_path.clone())).buffer;
|
target.execute(Command::OpenFile(test_file_path.clone()));
|
||||||
let temp_dir = tempdir().unwrap();
|
let temp_dir = tempdir().unwrap();
|
||||||
let tmp_file_path = temp_dir.path().join(r"Les_Trois_Mousquetaires.txt");
|
let tmp_file_path = temp_dir.path().join(r"Les_Trois_Mousquetaires.txt");
|
||||||
let result = target.execute(Command::SaveAs(tmp_file_path.clone()));
|
assert!(matches!(
|
||||||
assert!(result.is_ok());
|
dbg!(target.execute(Command::SaveAs(tmp_file_path.clone()))),
|
||||||
|
CommandResponse::Success(_)
|
||||||
|
));
|
||||||
|
|
||||||
let read_text = std::fs::read_to_string(&tmp_file_path).unwrap();
|
let read_text = std::fs::read_to_string(&tmp_file_path).unwrap();
|
||||||
let expected_text = std::fs::read_to_string(&test_file_path).unwrap();
|
let expected_text = std::fs::read_to_string(&test_file_path).unwrap();
|
||||||
|
|
|
||||||
|
|
@ -1,64 +1,22 @@
|
||||||
use std::{path::PathBuf, rc::Rc};
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::{Point, TextBuffer};
|
use crate::{Point, TextBuffer};
|
||||||
|
|
||||||
mod command;
|
|
||||||
mod io;
|
mod io;
|
||||||
|
mod command;
|
||||||
pub use command::{Command, Movement, Unit};
|
pub use command::{Command, Movement, Unit};
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default)]
|
||||||
pub struct EditorBuffer {
|
pub struct EditorBuffer {
|
||||||
buffer: TextBuffer,
|
buffer: TextBuffer,
|
||||||
cursor: Point,
|
cursor: Point,
|
||||||
state: Rc<EditorBufferState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct EditorBufferState {
|
|
||||||
filepath: Option<PathBuf>,
|
filepath: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug)]
|
||||||
pub enum CommandResponse {
|
pub enum CommandResponse {
|
||||||
None,
|
Success(String),
|
||||||
Message(String),
|
Failure(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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EditorBuffer {
|
impl EditorBuffer {
|
||||||
|
|
@ -68,7 +26,7 @@ impl EditorBuffer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute a command on the [EditorBuffer]
|
/// Execute a command on the [EditorBuffer]
|
||||||
pub fn execute(&self, command: Command) -> CommandResult {
|
pub fn execute(&mut self, command: Command) -> CommandResponse {
|
||||||
match command {
|
match command {
|
||||||
Command::OpenFile(filepath) => self.open_file(filepath),
|
Command::OpenFile(filepath) => self.open_file(filepath),
|
||||||
Command::Save => self.save_file(None),
|
Command::Save => self.save_file(None),
|
||||||
|
|
@ -82,226 +40,27 @@ impl EditorBuffer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_cursor_position(&self) -> Point {
|
fn move_cursor_to_point(&mut self, _point: Point) -> CommandResponse {
|
||||||
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 {
|
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_char(&self, c: char) -> CommandResult {
|
fn move_cursor_to_line_number(&mut self, _line_num: usize) -> CommandResponse {
|
||||||
CommandResult::ok(Self {
|
|
||||||
buffer: self.buffer.insert_char(c, self.cursor),
|
|
||||||
cursor: self.cursor.advance(),
|
|
||||||
..self.clone()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert_string(&self, _s: String) -> CommandResult {
|
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_cursor(&self, _movement: Movement) -> CommandResult {
|
fn insert_char(&mut self, _c: char) -> CommandResponse {
|
||||||
todo!()
|
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!()
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -11,31 +11,18 @@ mod writer;
|
||||||
pub use writer::TextBufferWriter;
|
pub use writer::TextBufferWriter;
|
||||||
|
|
||||||
/// A block of text, usually containing the contents of a text file.
|
/// A block of text, usually containing the contents of a text file.
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct TextBuffer {
|
pub struct TextBuffer {
|
||||||
contents: Rc<Rope>,
|
contents: Rc<Rope>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A location in a [TextBuffer]
|
/// A location in a [TextBuffer]
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
#[derive(Default, Clone, Copy)]
|
||||||
pub enum Point {
|
pub enum Point {
|
||||||
/// The end of the buffer
|
/// The end of the buffer
|
||||||
#[default]
|
#[default]
|
||||||
Start,
|
|
||||||
LineColumn(usize, usize),
|
|
||||||
End,
|
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 {
|
impl TextBuffer {
|
||||||
/// Create a new empty [TextBuffer]
|
/// Create a new empty [TextBuffer]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
|
@ -61,18 +48,21 @@ impl TextBuffer {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
self.contents.total_lines()
|
self.contents.total_lines()
|
||||||
|
+ if self
|
||||||
|
.contents
|
||||||
|
.get_char_at_index(self.contents.total_chars() - 1)
|
||||||
|
== '\n'
|
||||||
|
{
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert `text` at `point`.
|
/// Insert `text` at `point`.
|
||||||
pub fn insert_text(&mut self, text: impl Into<String>, point: Point) {
|
pub fn insert_text(&mut self, text: impl Into<String>, point: Point) {
|
||||||
match point {
|
match point {
|
||||||
Point::Start => {
|
|
||||||
self.contents = self.contents.insert_at_char_index(0, text);
|
|
||||||
}
|
|
||||||
Point::LineColumn(_, _) => {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
Point::End => {
|
Point::End => {
|
||||||
self.contents = self
|
self.contents = self
|
||||||
.contents
|
.contents
|
||||||
|
|
@ -81,28 +71,18 @@ impl TextBuffer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_char(&self, c: char, point: Point) -> Self {
|
pub fn insert_char(&mut self, c: char, point: Point) {
|
||||||
let contents = match point {
|
match point {
|
||||||
Point::Start => self.contents.insert_at_char_index(0, c),
|
Point::End => {
|
||||||
Point::LineColumn(line_num, column_num) => {
|
self.contents = self
|
||||||
self.contents
|
|
||||||
.insert_at_line_and_column(line_num - 1, column_num, c)
|
|
||||||
}
|
|
||||||
Point::End => self
|
|
||||||
.contents
|
.contents
|
||||||
.insert_at_char_index(self.contents.total_chars(), c),
|
.insert_at_char_index(self.contents.total_chars(), c)
|
||||||
};
|
}
|
||||||
Self { contents }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_at_char_index(&mut self, start: usize, length: usize) -> bool {
|
pub fn delete_at_char_index(&mut self, start: usize, length: usize) {
|
||||||
match self.contents.delete_at_char_index(start, length) {
|
self.contents = self.contents.delete_at_char_index(start, length)
|
||||||
Ok(r) => {
|
|
||||||
self.contents = r;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
Err(_) => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iter_chars(&self) -> CharIterator {
|
pub fn iter_chars(&self) -> CharIterator {
|
||||||
|
|
@ -134,68 +114,68 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn insert_char_at_end_increases_counts_as_expected() {
|
fn insert_char_at_end_increases_counts_as_expected() {
|
||||||
let target = TextBuffer::new();
|
let mut target = TextBuffer::new();
|
||||||
let target = target.insert_char('A', Point::End);
|
target.insert_char('A', Point::End);
|
||||||
assert_eq!(1, target.num_bytes());
|
assert_eq!(1, target.num_bytes());
|
||||||
assert_eq!(1, target.num_chars());
|
assert_eq!(1, target.num_chars());
|
||||||
assert_eq!(1, target.num_lines());
|
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_bytes());
|
||||||
assert_eq!(2, target.num_chars());
|
assert_eq!(2, target.num_chars());
|
||||||
assert_eq!(1, target.num_lines());
|
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_bytes());
|
||||||
assert_eq!(3, target.num_chars());
|
assert_eq!(3, target.num_chars());
|
||||||
assert_eq!(1, target.num_lines());
|
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_bytes());
|
||||||
assert_eq!(4, target.num_chars());
|
assert_eq!(4, target.num_chars());
|
||||||
assert_eq!(1, target.num_lines());
|
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_bytes());
|
||||||
assert_eq!(5, target.num_chars());
|
assert_eq!(5, target.num_chars());
|
||||||
assert_eq!(1, target.num_lines());
|
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_bytes());
|
||||||
assert_eq!(6, target.num_chars());
|
assert_eq!(6, target.num_chars());
|
||||||
assert_eq!(1, target.num_lines());
|
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_bytes());
|
||||||
assert_eq!(7, target.num_chars());
|
assert_eq!(7, target.num_chars());
|
||||||
assert_eq!(2, target.num_lines());
|
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_bytes());
|
||||||
assert_eq!(8, target.num_chars());
|
assert_eq!(8, target.num_chars());
|
||||||
assert_eq!(2, target.num_lines());
|
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_bytes());
|
||||||
assert_eq!(9, target.num_chars());
|
assert_eq!(9, target.num_chars());
|
||||||
assert_eq!(3, target.num_lines());
|
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_bytes());
|
||||||
assert_eq!(10, target.num_chars());
|
assert_eq!(10, target.num_chars());
|
||||||
assert_eq!(4, target.num_lines());
|
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_bytes());
|
||||||
assert_eq!(11, target.num_chars());
|
assert_eq!(11, target.num_chars());
|
||||||
assert_eq!(4, target.num_lines());
|
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_bytes());
|
||||||
assert_eq!(12, target.num_chars());
|
assert_eq!(12, target.num_chars());
|
||||||
assert_eq!(4, target.num_lines());
|
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!(15, target.num_bytes());
|
||||||
assert_eq!(13, target.num_chars());
|
assert_eq!(13, target.num_chars());
|
||||||
assert_eq!(4, target.num_lines());
|
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!(16, target.num_bytes());
|
||||||
assert_eq!(14, target.num_chars());
|
assert_eq!(14, target.num_chars());
|
||||||
assert_eq!(4, target.num_lines());
|
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!(17, target.num_bytes());
|
||||||
assert_eq!(15, target.num_chars());
|
assert_eq!(15, target.num_chars());
|
||||||
assert_eq!(4, target.num_lines());
|
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!(18, target.num_bytes());
|
||||||
assert_eq!(16, target.num_chars());
|
assert_eq!(16, target.num_chars());
|
||||||
assert_eq!(5, target.num_lines());
|
assert_eq!(5, target.num_lines());
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ impl std::io::Read for TextBufferReader {
|
||||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||||
let mut bytes_written = 0;
|
let mut bytes_written = 0;
|
||||||
while let Some(rope) = self.current_node.clone() {
|
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 bytes = text.as_bytes();
|
||||||
let length = bytes
|
let length = bytes
|
||||||
.len()
|
.len()
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,11 +3,6 @@ use std::rc::Rc;
|
||||||
mod fibbonacci;
|
mod fibbonacci;
|
||||||
use fibbonacci::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
|
/// [Rope](https://en.wikipedia.org/wiki/Rope_(data_structure)) data structure
|
||||||
/// implementation.
|
/// implementation.
|
||||||
///
|
///
|
||||||
|
|
@ -24,11 +19,9 @@ pub enum Rope {
|
||||||
/// or the number of characters in the string if this is a leaf.
|
/// or the number of characters in the string if this is a leaf.
|
||||||
chars_weight: usize,
|
chars_weight: usize,
|
||||||
|
|
||||||
/// Total number of line endings and number of characters in last line
|
/// Total number of line endings in the string contained in the left
|
||||||
/// in the string contained in the left subtree, or the number of line
|
/// subtree, or the number of line endings in the string if this is a leaf.
|
||||||
/// endings and number of characters in last line in the string if this
|
lines_weight: usize,
|
||||||
/// is a leaf.
|
|
||||||
line_column_weight: LineColumn,
|
|
||||||
|
|
||||||
/// The root of the left subtree
|
/// The root of the left subtree
|
||||||
left: Rc<Rope>,
|
left: Rc<Rope>,
|
||||||
|
|
@ -36,8 +29,6 @@ pub enum Rope {
|
||||||
right: Option<Rc<Rope>>,
|
right: Option<Rc<Rope>>,
|
||||||
},
|
},
|
||||||
Leaf {
|
Leaf {
|
||||||
chars_count: usize,
|
|
||||||
line_column_count: LineColumn,
|
|
||||||
text: String,
|
text: String,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -46,29 +37,13 @@ impl Rope {
|
||||||
/// Create a new Rope containing the passed text in a single node.
|
/// Create a new Rope containing the passed text in a single node.
|
||||||
pub fn new(contents: impl Into<String>) -> Rc<Self> {
|
pub fn new(contents: impl Into<String>) -> Rc<Self> {
|
||||||
let text = contents.into();
|
let text = contents.into();
|
||||||
let (chars_count, line_column_count) = text.chars().fold(
|
Rc::new(Rope::Leaf { text })
|
||||||
(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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new empty Rope
|
/// Create a new empty Rope
|
||||||
pub fn empty() -> Rc<Self> {
|
pub fn empty() -> Rc<Self> {
|
||||||
Rope::new("")
|
let text = "".into();
|
||||||
|
Rc::new(Rope::Leaf { text })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the total number of bytes in the text.
|
/// Return the total number of bytes in the text.
|
||||||
|
|
@ -84,7 +59,7 @@ impl Rope {
|
||||||
right: Some(right),
|
right: Some(right),
|
||||||
..
|
..
|
||||||
} => bytes_weight + right.total_bytes(),
|
} => bytes_weight + right.total_bytes(),
|
||||||
Rope::Leaf { text, .. } => text.len(),
|
Rope::Leaf { text } => text.len(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,14 +76,25 @@ impl Rope {
|
||||||
right: Some(right),
|
right: Some(right),
|
||||||
..
|
..
|
||||||
} => chars_weight + right.total_chars(),
|
} => 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
|
/// Return the total number of lines in the text
|
||||||
pub fn total_lines(&self) -> usize {
|
pub fn total_lines(&self) -> usize {
|
||||||
let LineColumn(l, c) = self.line_column_total();
|
match self {
|
||||||
if c > 0 { l + 1 } else { l }
|
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.
|
/// Return the character as a given character index.
|
||||||
|
|
@ -129,7 +115,7 @@ impl Rope {
|
||||||
right.get_char_at_index(index - chars_weight)
|
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,
|
index: usize,
|
||||||
text: impl Into<String>,
|
text: impl Into<String>,
|
||||||
) -> Rc<Rope> {
|
) -> Rc<Rope> {
|
||||||
let text: String = text.into();
|
|
||||||
let new_node = Rope::new(text);
|
let new_node = Rope::new(text);
|
||||||
let total_chars = self.total_chars();
|
let total_chars = self.total_chars();
|
||||||
if total_chars == 0 {
|
if index == 0 {
|
||||||
new_node
|
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 {
|
} else {
|
||||||
match self.split_at_char_index(index) {
|
panic!("Attempt to insert past end of rope.")
|
||||||
(Some(before), after) => {
|
|
||||||
Rope::join(Rope::join(before, Some(new_node)), after).rebalance()
|
|
||||||
}
|
}
|
||||||
(None, after) => Rope::join(new_node, after).rebalance(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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`.
|
/// Return a new rope with `length` characters removed after `start`.
|
||||||
pub fn delete_at_char_index(self: &Rc<Rope>, start: usize, length: usize) -> RopeResult {
|
pub fn delete_at_char_index(self: &Rc<Rope>, start: usize, length: usize) -> Rc<Rope> {
|
||||||
if let (beginning, Some(rest)) = self.split_at_char_index(start) {
|
let (beginning, rest) = self.split_at_char_index(start);
|
||||||
let (_, end) = rest.split_at_char_index(length);
|
let (_, end) = rest.split_at_char_index(length);
|
||||||
if let Some(beginning) = beginning {
|
beginning.concat(end)
|
||||||
Ok(Rope::join(beginning, end).rebalance())
|
|
||||||
} else if let Some(end) = end {
|
|
||||||
Ok(end)
|
|
||||||
} else {
|
|
||||||
Ok(Rope::new(""))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(Rc::clone(self))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Split the rope in two at character `index`.
|
/// Split the rope in two at character `index`.
|
||||||
///
|
///
|
||||||
/// The result is two Ropes—one containing the first 'i' characters of the
|
/// The result is two Ropes—one containing the first 'i' characters of the
|
||||||
/// text and the other containing the rest of the text.
|
/// text and the other containing the rest of the text.
|
||||||
pub fn split_at_char_index(
|
pub fn split_at_char_index(self: &Rc<Self>, index: usize) -> (Rc<Self>, Rc<Self>) {
|
||||||
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>>),
|
|
||||||
{
|
|
||||||
match *self.as_ref() {
|
match *self.as_ref() {
|
||||||
Rope::Branch {
|
Rope::Branch {
|
||||||
|
chars_weight,
|
||||||
ref left,
|
ref left,
|
||||||
ref right,
|
ref right,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let self_weight = weight(self);
|
if index < chars_weight {
|
||||||
if index < self_weight {
|
let (first, second) = left.split_at_char_index(index);
|
||||||
// Split left subtree
|
|
||||||
let (first, second) = left.split_at_index(index, weight, split_leaf);
|
|
||||||
(
|
(
|
||||||
first,
|
first.rebalance(),
|
||||||
second
|
Rope::join(second, right.as_ref().map(|r| r.clone())).rebalance(),
|
||||||
.map(|rope| Rope::join(rope, right.as_ref().map(Rc::clone)))
|
|
||||||
.or_else(|| right.as_ref().map(Rc::clone)),
|
|
||||||
)
|
)
|
||||||
} else if let Some(right) = right {
|
} else if let Some(right) = right {
|
||||||
if index > self_weight {
|
if index > chars_weight {
|
||||||
// Split right subtree
|
let (first, second) = right.split_at_char_index(index - chars_weight);
|
||||||
let (first, second) =
|
(
|
||||||
right.split_at_index(index - self_weight, weight, split_leaf);
|
Rope::join(left.clone(), Some(first)).rebalance(),
|
||||||
(Some(Rope::join(Rc::clone(left), first)), second)
|
second.rebalance(),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
// Split is aleady exactly between left and right subtrees
|
(left.clone(), right.clone())
|
||||||
(Some(Rc::clone(left)), Some(Rc::clone(right)))
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Split is at end
|
(left.clone(), Rope::empty())
|
||||||
(Some(Rc::clone(left)), None)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Rope::Leaf { ref text, .. } => {
|
Rope::Leaf { ref text } => {
|
||||||
if index >= weight(self) {
|
if let Some((byte_index, _)) = text.char_indices().nth(index) {
|
||||||
(Some(self.clone()), None)
|
let (first, second) = text.split_at(byte_index);
|
||||||
|
(Rope::new(first), Rope::new(second))
|
||||||
} else {
|
} else {
|
||||||
split_leaf(text, index)
|
(self.clone(), Rope::empty())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -370,7 +245,7 @@ impl Rope {
|
||||||
Rc::new(Rope::Branch {
|
Rc::new(Rope::Branch {
|
||||||
bytes_weight: left.total_bytes(),
|
bytes_weight: left.total_bytes(),
|
||||||
chars_weight: left.total_chars(),
|
chars_weight: left.total_chars(),
|
||||||
line_column_weight: left.line_column_total(),
|
lines_weight: left.total_lines(),
|
||||||
left,
|
left,
|
||||||
right,
|
right,
|
||||||
})
|
})
|
||||||
|
|
@ -380,32 +255,6 @@ impl Rope {
|
||||||
pub fn iter_nodes(self: Rc<Self>) -> NodeIterator {
|
pub fn iter_nodes(self: Rc<Self>) -> NodeIterator {
|
||||||
NodeIterator::new(self)
|
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> {
|
fn merge(leaf_nodes: &[Rc<Rope>]) -> Rc<Rope> {
|
||||||
|
|
@ -500,7 +349,7 @@ impl CharWithPointIterator {
|
||||||
|
|
||||||
fn next_string(node_iterator: &mut NodeIterator) -> Option<String> {
|
fn next_string(node_iterator: &mut NodeIterator) -> Option<String> {
|
||||||
node_iterator.next().map(|rope| {
|
node_iterator.next().map(|rope| {
|
||||||
if let Rope::Leaf { text, .. } = rope.as_ref() {
|
if let Rope::Leaf { text } = rope.as_ref() {
|
||||||
text.clone()
|
text.clone()
|
||||||
} else {
|
} else {
|
||||||
panic!("Rope NodeIterator yielded non-leaf node.")
|
panic!("Rope NodeIterator yielded non-leaf node.")
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
use super::super::{Rc, Rope};
|
use super::super::{Rc, Rope};
|
||||||
use rand::{
|
use rand::{
|
||||||
Rng, SeedableRng,
|
|
||||||
distr::{Alphanumeric, SampleString},
|
distr::{Alphanumeric, SampleString},
|
||||||
rngs::SmallRng,
|
rngs::SmallRng,
|
||||||
|
Rng, SeedableRng,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
@ -15,9 +15,9 @@ impl Command {
|
||||||
pub fn run(&self, rope: &Rc<Rope>) -> Rc<Rope> {
|
pub fn run(&self, rope: &Rc<Rope>) -> Rc<Rope> {
|
||||||
match self {
|
match self {
|
||||||
Command::InsertAtCharIndex { index, text } => rope.insert_at_char_index(*index, text),
|
Command::InsertAtCharIndex { index, text } => rope.insert_at_char_index(*index, text),
|
||||||
Command::DeleteAtCharIndex { index, length } => rope
|
Command::DeleteAtCharIndex { index, length } => {
|
||||||
.delete_at_char_index(*index, *length)
|
rope.delete_at_char_index(*index, *length)
|
||||||
.expect("delete was successful"),
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,7 +58,7 @@ pub fn generate_random_edit_sequence_with_seed(
|
||||||
let text = Alphanumeric.sample_string(&mut rng, text_len);
|
let text = Alphanumeric.sample_string(&mut rng, text_len);
|
||||||
Command::InsertAtCharIndex { index, text }
|
Command::InsertAtCharIndex { index, text }
|
||||||
} else {
|
} 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));
|
let length = rng.random_range(1..(current_text_length - index));
|
||||||
Command::DeleteAtCharIndex { index, length }
|
Command::DeleteAtCharIndex { index, length }
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ fn node_iterator_for_single_node_returns_node_and_only_node() {
|
||||||
assert_eq!(result.len(), 1);
|
assert_eq!(result.len(), 1);
|
||||||
assert_eq!(result[0].total_bytes(), 3);
|
assert_eq!(result[0].total_bytes(), 3);
|
||||||
assert_eq!(result[0].total_chars(), 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() {
|
match result[0].as_ref() {
|
||||||
Rope::Leaf { text, .. } => assert_eq!(text, "The"),
|
Rope::Leaf { text, .. } => assert_eq!(text, "The"),
|
||||||
_ => panic!(),
|
_ => panic!(),
|
||||||
|
|
@ -91,7 +91,7 @@ fn node_iterator_returns_nodes_in_correct_order() {
|
||||||
assert_eq!(result.len(), 10);
|
assert_eq!(result.len(), 10);
|
||||||
for (node, string) in result.iter().zip(strings) {
|
for (node, string) in result.iter().zip(strings) {
|
||||||
match &node.as_ref() {
|
match &node.as_ref() {
|
||||||
Rope::Leaf { text, .. } => assert_eq!(text, string),
|
Rope::Leaf { text } => assert_eq!(text, string),
|
||||||
_ => panic!(),
|
_ => panic!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -116,14 +116,8 @@ fn split_splits_at_correct_location() {
|
||||||
let full_string = small_test_rope_full_string();
|
let full_string = small_test_rope_full_string();
|
||||||
for i in 0..(full_string.chars().count()) {
|
for i in 0..(full_string.chars().count()) {
|
||||||
let (first, second) = target.split_at_char_index(i);
|
let (first, second) = target.split_at_char_index(i);
|
||||||
let first_string: String = first
|
let first_string: String = first.iter_chars().collect();
|
||||||
.expect("first part is not None")
|
let second_string: String = second.iter_chars().collect();
|
||||||
.iter_chars()
|
|
||||||
.collect();
|
|
||||||
let second_string: String = second
|
|
||||||
.expect("second part is not None")
|
|
||||||
.iter_chars()
|
|
||||||
.collect();
|
|
||||||
assert_eq!(first_string, full_string[0..i]);
|
assert_eq!(first_string, full_string[0..i]);
|
||||||
assert_eq!(second_string, full_string[i..]);
|
assert_eq!(second_string, full_string[i..]);
|
||||||
}
|
}
|
||||||
|
|
@ -138,8 +132,6 @@ fn split_splits_at_correct_location_with_multibyte_chars() {
|
||||||
.collect();
|
.collect();
|
||||||
for i in 0..(expected_chars.len()) {
|
for i in 0..(expected_chars.len()) {
|
||||||
let (first, second) = target.split_at_char_index(i);
|
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 string: String = first.iter_chars().collect();
|
||||||
let expected: String = expected_chars[0..i].iter().collect();
|
let expected: String = expected_chars[0..i].iter().collect();
|
||||||
assert_eq!(string, expected);
|
assert_eq!(string, expected);
|
||||||
|
|
@ -232,11 +224,11 @@ fn get_char_at_index() {
|
||||||
fn insert_at_char_index() {
|
fn insert_at_char_index() {
|
||||||
let target = Rope::new("The brown dog");
|
let target = Rope::new("The brown dog");
|
||||||
assert_eq!(target.iter_chars().collect::<String>(), "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");
|
let rope1 = target.insert_at_char_index(4, "quick");
|
||||||
assert_eq!(target.iter_chars().collect::<String>(), "The brown dog");
|
assert_eq!(target.iter_chars().collect::<String>(), "The brown dog");
|
||||||
assert_eq!(rope1.iter_chars().collect::<String>(), "The quickbrown 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, " ");
|
let rope2 = rope1.insert_at_char_index(9, " ");
|
||||||
assert_eq!(target.iter_chars().collect::<String>(), "The brown dog");
|
assert_eq!(target.iter_chars().collect::<String>(), "The brown dog");
|
||||||
assert_eq!(rope1.iter_chars().collect::<String>(), "The quickbrown 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>(),
|
rope2.iter_chars().collect::<String>(),
|
||||||
"The quick brown dog"
|
"The quick brown dog"
|
||||||
);
|
);
|
||||||
assert_eq!(1, rope2.total_lines());
|
assert_eq!(0, rope2.total_lines());
|
||||||
let rope3 =
|
let rope3 =
|
||||||
rope2.insert_at_char_index("The quick brown dog".len(), " jumps over the lazy fox.");
|
rope2.insert_at_char_index("The quick brown dog".len(), " jumps over the lazy fox.");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rope3.iter_chars().collect::<String>(),
|
rope3.iter_chars().collect::<String>(),
|
||||||
"The quick brown dog jumps over the lazy fox."
|
"The quick brown dog jumps over the lazy fox."
|
||||||
);
|
);
|
||||||
assert_eq!(1, rope3.total_lines());
|
assert_eq!(0, rope3.total_lines());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delete_at_char_index() {
|
fn delete_at_char_index() {
|
||||||
let target = Rope::new("The quick brown fox jumps over the lazy dog.");
|
let target = Rope::new("The quick brown fox jumps over the lazy dog.");
|
||||||
let test = target
|
let test = target.delete_at_char_index(10, 6);
|
||||||
.delete_at_char_index(10, 6)
|
|
||||||
.expect("Delete was successful");
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
target.iter_chars().collect::<String>(),
|
target.iter_chars().collect::<String>(),
|
||||||
"The quick brown fox jumps over the lazy dog."
|
"The quick brown fox jumps over the lazy dog."
|
||||||
|
|
@ -268,9 +258,7 @@ fn delete_at_char_index() {
|
||||||
test.iter_chars().collect::<String>(),
|
test.iter_chars().collect::<String>(),
|
||||||
"The quick fox jumps over the lazy dog."
|
"The quick fox jumps over the lazy dog."
|
||||||
);
|
);
|
||||||
let test = target
|
let test = target.delete_at_char_index(0, 4);
|
||||||
.delete_at_char_index(0, 4)
|
|
||||||
.expect("Delete was successful");
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
target.iter_chars().collect::<String>(),
|
target.iter_chars().collect::<String>(),
|
||||||
"The quick brown fox jumps over the lazy dog."
|
"The quick brown fox jumps over the lazy dog."
|
||||||
|
|
@ -279,9 +267,8 @@ fn delete_at_char_index() {
|
||||||
test.iter_chars().collect::<String>(),
|
test.iter_chars().collect::<String>(),
|
||||||
"quick brown fox jumps over the lazy dog."
|
"quick brown fox jumps over the lazy dog."
|
||||||
);
|
);
|
||||||
let test = target
|
let test =
|
||||||
.delete_at_char_index("The quick brown fox jumps over the lazy dog.".len() - 5, 5)
|
target.delete_at_char_index("The quick brown fox jumps over the lazy dog.".len() - 5, 5);
|
||||||
.expect("Delete was successful");
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
target.iter_chars().collect::<String>(),
|
target.iter_chars().collect::<String>(),
|
||||||
"The quick brown fox jumps over the lazy dog."
|
"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 (start_text, edits) = command_list::generate_random_edit_sequence_with_seed(1000, seed);
|
||||||
let mut target = Rope::new(start_text);
|
let mut target = Rope::new(start_text);
|
||||||
for (command, expected_text) in edits {
|
for (command, expected_text) in edits {
|
||||||
|
println!("{:?}", command);
|
||||||
target = command.run(&target);
|
target = command.run(&target);
|
||||||
assert_eq!(expected_text, target.iter_chars().collect::<String>());
|
assert_eq!(expected_text, target.iter_chars().collect::<String>());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue